Repository: amaranth-lang/amaranth
Pull request #1010 (2024-01-02 16:14:13)

build.run: add BuildPlan.execute_docker()
One usecase for this is using amaranth with vivado on MacOs.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1010?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) Report
Attention: `18 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`0849e1a`)](https://app.codecov.io/gh/amaranth-lang/amaranth/commit/0849e1af0bbecc629b1331ee257c58bf931b241a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) 86.61% compared to head [(`204b04f`)](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1010?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) 86.28%.

| [Files](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1010?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) | Patch % | Lines |
|---|---|---|
| [amaranth/build/run.py](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1010?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvYnVpbGQvcnVuLnB5) | 5.26% | [18 Missing :warning: ](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1010?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1010      +/-   ##
==========================================
- Coverage   86.61%   86.28%   -0.34%     
==========================================
  Files          41       41              
  Lines        7039     7058      +19     
  Branches     1676     1538     -138     
==========================================
- Hits         6097     6090       -7     
- Misses        781      802      +21     
- Partials      161      166       +5     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1010?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).


Patch:
diff --git a/amaranth/build/run.py b/amaranth/build/run.py
index 39d5ed5..4cc96de 100644
--- a/amaranth/build/run.py
+++ b/amaranth/build/run.py
@@ -5,6 +5,7 @@ import os
 import sys
 import subprocess
 import tempfile
+import warnings
 import zipfile
 import hashlib
 import pathlib
@@ -64,15 +65,10 @@ class BuildPlan:
             for filename in sorted(self.files):
                 archive.writestr(zipfile.ZipInfo(filename), self.files[filename])
 
-    def execute_local(self, root="build", *, run_script=True, env=None):
-        """
-        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. If ``env`` is not ``None``, the environment is replaced
-        with ``env``.
+    def extract(self, root="build"):
+        """Extracts the files from the build plan into the local build directory ``root``.
 
-        Returns :class:`LocalBuildProducts`.
+        Returns :class:`pathlib.Path`
         """
         os.makedirs(root, exist_ok=True)
         cwd = os.getcwd()
@@ -92,23 +88,66 @@ class BuildPlan:
                     content = content.encode("utf-8")
                 with open(filename, "wb") as f:
                     f.write(content)
-
-            if run_script:
-                if sys.platform.startswith("win32"):
-                    # Without "call", "cmd /c {}.bat" will return 0.
-                    # See https://stackoverflow.com/a/30736987 for a detailed explanation of why.
-                    # Running the script manually from a command prompt is unaffected.
-                    subprocess.check_call(["cmd", "/c", f"call {self.script}.bat"],
-                                          env=os.environ if env is None else env)
-                else:
-                    subprocess.check_call(["sh", f"{self.script}.sh"],
-                                          env=os.environ if env is None else env)
-
-            return LocalBuildProducts(os.getcwd())
-
+            return pathlib.Path(os.getcwd())
         finally:
             os.chdir(cwd)
 
+    def execute_local(self, root="build", *, run_script=None, env=None):
+        """
+        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. If ``env`` is not ``None``, the environment is replaced
+        with ``env``.
+
+        The ``run_script`` argument is deprecated. If you only want to extract the files
+        into a local folder, use the ``extract`` method.
+
+        Returns :class:`LocalBuildProducts`.
+        """
+        build_dir = self.extract(root)
+        if run_script is None or run_script:
+            if sys.platform.startswith("win32"):
+                # Without "call", "cmd /c {}.bat" will return 0.
+                # See https://stackoverflow.com/a/30736987 for a detailed explanation of why.
+                # Running the script manually from a command prompt is unaffected.
+                subprocess.check_call(["cmd", "/c", f"call {self.script}.bat"],
+                                        env=os.environ if env is None else env)
+            else:
+                subprocess.check_call(["sh", f"{self.script}.sh"],
+                                        env=os.environ if env is None else env)
+        # TODO(amaranth-0.5): remove
+        if run_script is not None:
+            warnings.warn("The `run_script` argument is deprecated. If you only want to "
+                            "extract the files from the BuildPlan, use the .extract() method",
+                            DeprecationWarning, stacklevel=2)
+
+
+
+        return LocalBuildProducts(build_dir)
+
+
+    def execute_local_docker(self, image, *, root="build", docker_args=[]):
+        """
+        Execute build plan inside a Docker container. Files from the build plan are placed in the 
+        build root directory ``root`` on the local filesystem. This directory is bind mounted to 
+        ``/build`` in a container and the script ``{script}.sh`` is executed inside it.
+        ``docker_args`` is a list containing additional arguments to docker.
+
+        Returns :class:`LocalBuildProducts`.
+        """
+        build_dir = self.extract(root)
+        subprocess.check_call([
+            "docker", "run", *docker_args,
+            "--rm", # remove the container after running
+            "--mount", f"type=bind,source={build_dir},target=/build",
+            "--workdir", "/build",
+            image,
+            "sh", f"{self.script}.sh",
+        ])
+        return LocalBuildProducts(build_dir)
+
+
     def execute_remote_ssh(self, *, connect_to={}, root, run_script=True):
         """
         Execute build plan using the remote SSH strategy. Files from the build
diff --git a/amaranth/hdl/ast.py b/amaranth/hdl/ast.py
index 70e4763..c7822aa 100644
--- a/amaranth/hdl/ast.py
+++ b/amaranth/hdl/ast.py
@@ -1382,6 +1382,8 @@ class Array(MutableSequence):
         self._mutable  = True
 
     def __getitem__(self, index):
+        if isinstance(index, ValueCastable):
+            index = Value.cast(index)
         if isinstance(index, Value):
             if self._mutable:
                 self._proxy_at = tracer.get_src_loc()
diff --git a/docs/changes.rst b/docs/changes.rst
index 8dbb71a..ce9f87b 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -15,6 +15,7 @@ Language changes
 
 .. currentmodule:: amaranth.hdl
 
+* Deprecated: argument `run_script=` in :meth:`BuildPlan.execute_local`
 * Added: `class:ast.Slice` objects have been made const-castable.
 * Removed: (deprecated in 0.4) :meth:`Const.normalize`. (`RFC 5`_)
 * Removed: (deprecated in 0.4) :class:`ast.Sample`, :class:`ast.Past`, :class:`ast.Stable`, :class:`ast.Rose`, :class:`ast.Fell`.
@@ -35,6 +36,8 @@ Platform integration changes
 
 .. currentmodule:: amaranth.vendor
 
+* Added: :meth:`BuildPlan.execute_local_docker`.
+* Added: :meth:`BuildPlan.extract`.
 * Added: ``build.sh``  begins with ``#!/bin/sh``.
 * Removed: (deprecated in 0.4) :mod:`vendor.intel`, :mod:`vendor.lattice_ecp5`, :mod:`vendor.lattice_ice40`, :mod:`vendor.lattice_machxo2_3l`, :mod:`vendor.quicklogic`, :mod:`vendor.xilinx`. (`RFC 18`_)
 


Test patch:
diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py
index b97b2a1..2d9ef2a 100644
--- a/tests/test_hdl_ast.py
+++ b/tests/test_hdl_ast.py
@@ -1005,6 +1005,18 @@ class ArrayTestCase(FHDLTestCase):
                 r"^Array can no longer be mutated after it was indexed with a value at "):
             a.insert(1, 2)
 
+    def test_index_value_castable(self):
+        class MyValue(ValueCastable):
+            @ValueCastable.lowermethod
+            def as_value(self):
+                return Signal()
+            
+            def shape():
+                return unsigned(1)
+        
+        a = Array([1,2,3])
+        a[MyValue()]
+
     def test_repr(self):
         a = Array([1,2,3])
         self.assertEqual(repr(a), "(array mutable [1, 2, 3])")