build.run: make BuildProducts abstract, add LocalBuildProducts.
authorwhitequark <cz@m-labs.hk>
Sun, 7 Jul 2019 00:07:55 +0000 (00:07 +0000)
committerwhitequark <cz@m-labs.hk>
Sun, 7 Jul 2019 00:09:07 +0000 (00:09 +0000)
This makes it clear that we plan to have remote builds as well.

Also, document everything in build.run.

nmigen/build/plat.py
nmigen/build/run.py

index da193081f0a116af8c3435065214f112d70285e5..d7e98dc42780f36043c784b518e6447d9ae7cd9c 100644 (file)
@@ -48,7 +48,7 @@ class Platform(ResourceManager, metaclass=ABCMeta):
         if not do_build:
             return plan
 
-        products = plan.execute(build_dir)
+        products = plan.execute_local(build_dir)
         if not do_program:
             return products
 
index 204e98d146e87905d0a46d13dc9bad16f06ffb7c..47de5ddbf2b8d01c77b41b366517924afd933e4a 100644 (file)
@@ -1,5 +1,6 @@
 from collections import OrderedDict
 from contextlib import contextmanager
+from abc import ABCMeta, abstractmethod
 import os
 import sys
 import subprocess
@@ -7,21 +8,52 @@ 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 @@ class BuildPlan:
 
             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 @@ class BuildProducts:
         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()