Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: m-labs/nmigen
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 1ee21d200749
Choose a base ref
...
head repository: m-labs/nmigen
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: ba64eb2037de
Choose a head ref
  • 1 commit
  • 2 files changed
  • 1 contributor

Commits on Jul 7, 2019

  1. build.run: make BuildProducts abstract, add LocalBuildProducts.

    This makes it clear that we plan to have remote builds as well.
    
    Also, document everything in build.run.
    whitequark committed Jul 7, 2019
    Copy the full SHA
    ba64eb2 View commit details
Showing with 73 additions and 20 deletions.
  1. +1 −1 nmigen/build/plat.py
  2. +72 −19 nmigen/build/run.py
2 changes: 1 addition & 1 deletion nmigen/build/plat.py
Original file line number Diff line number Diff line change
@@ -48,7 +48,7 @@ def build(self, fragment, name="top",
if not do_build:
return plan

products = plan.execute(build_dir)
products = plan.execute_local(build_dir)
if not do_program:
return products

91 changes: 72 additions & 19 deletions nmigen/build/run.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,59 @@
from collections import OrderedDict
from contextlib import contextmanager
from abc import ABCMeta, abstractmethod
import os
import sys
import subprocess
import tempfile
import zipfile


__all__ = ["BuildPlan", "BuildProducts"]
__all__ = ["BuildPlan", "BuildProducts", "LocalBuildProducts"]


class BuildPlan:
def __init__(self, script):
"""A build plan.
Parameters
----------
script : str
The base name (without extension) of the script that will be executed.
"""
self.script = script
self.files = OrderedDict()

def add_file(self, filename, content):
"""
Add ``content``, which can be a :class:`str`` or :class:`bytes`, to the build plan
as ``filename``. The file name can be a relative path with directories separated by
forward slashes (``/``).
"""
assert isinstance(filename, str) and filename not in self.files
# Just to make sure we don't accidentally overwrite anything.
assert not os.path.normpath(filename).startswith("..")
self.files[filename] = content

def execute(self, root="build", run_script=True):
def archive(self, file):
"""
Archive files from the build plan into ``file``, which can be either a filename, or
a file-like object. The produced archive is deterministic: exact same files will
always produce exact same archive.
"""
with zipfile.ZipFile(file, "w") as archive:
# Write archive members in deterministic order and with deterministic timestamp.
for filename in sorted(self.files):
archive.writestr(zipfile.ZipInfo(filename), self.files[filename])

def execute_local(self, root="build", run_script=True):
"""
Execute build plan using the local strategy. Files from the build plan are placed in
the build root directory ``root``, and, if ``run_script`` is ``True``, the script
appropriate for the platform (``{script}.bat`` on Windows, ``{script}.sh`` elsewhere) is
executed in the build root.
Returns :class:`LocalBuildProducts`.
"""
os.makedirs(root, exist_ok=True)
cwd = os.getcwd()
try:
@@ -38,35 +70,43 @@ def execute(self, root="build", run_script=True):

if run_script:
if sys.platform.startswith("win32"):
subprocess.run(["cmd", "/c", "{}.bat".format(self.script)], check=True)
subprocess.check_call(["cmd", "/c", "{}.bat".format(self.script)])
else:
subprocess.run(["sh", "{}.sh".format(self.script)], check=True)
subprocess.check_call(["sh", "{}.sh".format(self.script)])

return BuildProducts(os.getcwd())
return LocalBuildProducts(os.getcwd())

finally:
os.chdir(cwd)

def archive(self, file):
with zipfile.ZipFile(file, "w") as archive:
# Write archive members in deterministic order and with deterministic timestamp.
for filename in sorted(self.files):
archive.writestr(zipfile.ZipInfo(filename), self.files[filename])

def execute(self):
"""
Execute build plan using the default strategy. Use one of the ``execute_*`` methods
explicitly to have more control over the strategy.
"""
return self.execute_local()

class BuildProducts:
def __init__(self, root):
# We provide no guarantees that files will be available on the local filesystem (i.e. in
# any way other than through `products.get()`), so downstream code must never rely on this.
self.__root = root

class BuildProducts(metaclass=ABCMeta):
@abstractmethod
def get(self, filename, mode="b"):
assert mode in "bt"
with open(os.path.join(self.__root, filename), "r" + mode) as f:
return f.read()
"""
Extract ``filename`` from build products, and return it as a :class:`bytes` (if ``mode``
is ``"b"``) or a :class:`str` (if ``mode`` is ``"t"``).
"""
assert mode in ("b", "t")

@contextmanager
def extract(self, *filenames):
"""
Extract ``filenames`` from build products, place them in an OS-specific temporary file
location, with the extension preserved, and delete them afterwards. This method is used
as a context manager, e.g.: ::
with products.extract("bitstream.bin", "programmer.cfg") \
as bitstream_filename, config_filename:
subprocess.check_call(["program", "-c", config_filename, bitstream_filename])
"""
files = []
try:
for filename in filenames:
@@ -88,3 +128,16 @@ def extract(self, *filenames):
finally:
for file in files:
os.unlink(file.name)


class LocalBuildProducts(BuildProducts):
def __init__(self, root):
# We provide no guarantees that files will be available on the local filesystem (i.e. in
# any way other than through `products.get()`) in general, so downstream code must never
# rely on this, even when we happen to use a local build most of the time.
self.__root = root

def get(self, filename, mode="b"):
super().get(filename, mode)
with open(os.path.join(self.__root, filename), "r" + mode) as f:
return f.read()