simbricks-enso-sys-py 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: simbricks-enso-sys-py
3
+ Version: 0.6.0
4
+ Summary: Ensō NIC System integration into SimBricks
5
+ Author: Team SimBricks
6
+ Author-email: team@simbricks.io
7
+ Requires-Python: >=3.12
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Classifier: Programming Language :: Python :: 3.15
13
+ Requires-Dist: simbricks-imagebuild-packer (>=0.6.0)
14
+ Requires-Dist: simbricks-orchestration (>=0.6.0)
15
+ Requires-Dist: simbricks-utils (>=0.6.0)
16
+ Project-URL: Homepage, https://www.simbricks.io/
@@ -0,0 +1,50 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 SimBricks
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ [tool.black]
24
+ line-length = 100
25
+
26
+ [build-system]
27
+ requires = ["poetry-core>=1.0.0"]
28
+ build-backend = "poetry.core.masonry.api"
29
+
30
+ [tool.poetry]
31
+ name = "simbricks-enso-sys-py"
32
+ version = "0.6.0"
33
+ description = "Ensō NIC System integration into SimBricks"
34
+ authors = [
35
+ "Team SimBricks <team@simbricks.io>"
36
+ ]
37
+ homepage = "https://www.simbricks.io/"
38
+ packages = [
39
+ { include = "simbricks" }
40
+ ]
41
+ # Guest install script, shipped as package data.
42
+ include = [
43
+ { path = "simbricks/components/enso/system/data/*", format = ["sdist", "wheel"] }
44
+ ]
45
+
46
+ [tool.poetry.dependencies]
47
+ python = ">=3.12"
48
+ simbricks-orchestration = ">=0.6.0"
49
+ simbricks-utils = ">=0.6.0"
50
+ simbricks-imagebuild-packer = ">=0.6.0"
@@ -0,0 +1,33 @@
1
+ # Copyright 2026 Max Planck Institute for Software Systems,
2
+ # National University of Singapore, Carnegie Mellon University,
3
+ # and SimBricks UG (haftungsbeschränkt)
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ from .enso import EnsoEchoServer, EnsoGen, EnsoLinuxHost, EnsoNIC
25
+ from .image import enso_image
26
+
27
+ __all__ = [
28
+ "EnsoNIC",
29
+ "EnsoLinuxHost",
30
+ "EnsoEchoServer",
31
+ "EnsoGen",
32
+ "enso_image",
33
+ ]
@@ -0,0 +1,63 @@
1
+ #!/bin/bash -eux
2
+ #
3
+ # Installs Ensō into a guest image. Run by enso_image() (see ../image.py) as
4
+ # the single packer layer inside the booted base image. Do not run it on your
5
+ # workstation: it apt-installs packages, writes to /lib/modules and installs
6
+ # into /usr/local.
7
+ #
8
+ # enso_image() exports ENSO_REPO / ENSO_BRANCH / ENSO_DIR ahead of the defaults
9
+ # below; `make guest-install` runs it with the defaults.
10
+ #
11
+ # Because packer boots the very image the driver will be loaded into, `uname -r`
12
+ # here is already the right kernel, so Ensō's own setup.sh does the whole job:
13
+ # dependencies, hugepages, the kernel module, and the meson build of the
14
+ # userspace with the buffer sizes the behavioral model expects.
15
+ #
16
+ # The one thing added afterwards is putting the module where modprobe can find
17
+ # it; setup.sh leaves it in Ensō's build tree, which would force every
18
+ # simulation run to rebuild it. EnsoLinuxHost only modprobes.
19
+ set -eux
20
+ export DEBIAN_FRONTEND=noninteractive
21
+
22
+ ENSO_REPO="${ENSO_REPO:-https://github.com/crossroadsfpga/enso}"
23
+ ENSO_BRANCH="${ENSO_BRANCH:-simbricks-24.04}"
24
+
25
+ # Must match EnsoLinuxHost.enso_parent_dir / .enso_name.
26
+ ENSO_DIR="${ENSO_DIR:-/root/enso}"
27
+
28
+ KVER=$(uname -r)
29
+
30
+ # The kernel headers come with the image's kernel, whether that is a distro one
31
+ # or a custom SimBricks build installed from a .deb. Do not apt-install them:
32
+ # there is no linux-headers package to fetch for a custom kernel.
33
+ if [ ! -d "/lib/modules/$KVER/build" ]; then
34
+ echo "no kernel build tree at /lib/modules/$KVER/build -- the image needs" >&2
35
+ echo "the headers matching its kernel to build the Ensō driver." >&2
36
+ exit 1
37
+ fi
38
+
39
+ # Only what Ensō's setup.sh does not install itself. sudo, because its scripts
40
+ # and scripts/ensogen.sh use it and the base image may not have it.
41
+ apt-get -y update
42
+ apt-get -y install build-essential git ca-certificates sudo \
43
+ libnuma-dev rsync bc libelf-dev kmod pkg-config
44
+
45
+ git clone "$ENSO_REPO" "$ENSO_DIR"
46
+ cd "$ENSO_DIR"
47
+ git checkout "$ENSO_BRANCH"
48
+
49
+ # Installs dependencies, reserves hugepages, builds and loads the kernel module,
50
+ # then meson-builds and installs the userspace. --no-quartus skips the FPGA
51
+ # toolchain check; it still fetches the bitstream, which we never program.
52
+ ./setup.sh --no-quartus
53
+
54
+ # setup.sh leaves the module in the build tree, so make it modprobe-able.
55
+ install -Dm644 "$ENSO_DIR/software/kernel/linux/intel_fpga_pcie_drv.ko" \
56
+ "/lib/modules/$KVER/extra/intel_fpga_pcie_drv.ko"
57
+ depmod -a "$KVER"
58
+
59
+ # ensogen.sh resolves its binaries through $ENSO_DIR/build, so leave the tree in
60
+ # place. Check that what the application classes invoke actually exists.
61
+ test -x "$ENSO_DIR/build/software/examples/echo"
62
+ test -x "$ENSO_DIR/build/software/examples/ensogen"
63
+ test -x "$ENSO_DIR/build/scripts/get_pcap_pkt_size"
@@ -0,0 +1,231 @@
1
+ # Copyright 2026 Max Planck Institute for Software Systems,
2
+ # National University of Singapore, Carnegie Mellon University,
3
+ # and SimBricks UG (haftungsbeschränkt)
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ from __future__ import annotations
25
+
26
+ import typing as tp
27
+
28
+ import typing_extensions as tpe
29
+
30
+ from simbricks.orchestration.system import base as sys_base
31
+ from simbricks.orchestration.system import nic
32
+ from simbricks.orchestration.system.host import app
33
+ from simbricks.orchestration.system.host import base as sys_host
34
+ from simbricks.utils import base as utils_base
35
+
36
+ if tp.TYPE_CHECKING:
37
+ from simbricks.orchestration.instantiation import base as inst_base
38
+
39
+
40
+ class EnsoNIC(nic.SimplePCIeNIC):
41
+ """
42
+ The Ensō NIC.
43
+ """
44
+
45
+ def __init__(self, s: sys_base.System) -> None:
46
+ super().__init__(s)
47
+
48
+
49
+ class EnsoLinuxHost(sys_host.LinuxHost):
50
+ """
51
+ A Linux host set up to drive an :class:`EnsoNIC`.
52
+
53
+ On top of a plain Linux host this reserves hugepages, mounts ``hugetlbfs``
54
+ and loads Ensō's kernel driver. The Ensō checkout itself is expected to be
55
+ in the disk image, put there by :func:`.image.enso_image`.
56
+ """
57
+
58
+ def __init__(self, s: sys_base.System) -> None:
59
+ super().__init__(s)
60
+ # Ensō reserves several GiB of hugepages, so give the guest headroom.
61
+ self.memory: int = 16 * 1024
62
+ self.cores: int = 2
63
+
64
+ self.enso_parent_dir: str = "/root"
65
+ self.enso_name: str = "enso"
66
+
67
+ self.nr_hugepages: int = 4096
68
+
69
+ # Ensō's kernel driver, built into the image by install-enso.sh and
70
+ # installed under /lib/modules/<kver>/extra, so it is modprobe'd by
71
+ # name.
72
+ self.enso_kmod: str = "intel_fpga_pcie_drv"
73
+ self.enso_dev_node: str = "/dev/intel_fpga_pcie_drv"
74
+
75
+ @property
76
+ def enso_dir(self) -> str:
77
+ return f"{self.enso_parent_dir}/{self.enso_name}"
78
+
79
+ def prepare_pre_cp(self, inst: inst_base.Instantiation) -> list[str]:
80
+ return [
81
+ "mount -t proc proc /proc",
82
+ "mount -t sysfs sysfs /sys",
83
+ ] + super().prepare_pre_cp(inst)
84
+
85
+ def prepare_post_cp(self, inst: inst_base.Instantiation) -> list[str]:
86
+ # NOTE: LinuxHost.prepare_post_cp is bypassed on purpose. It walks every
87
+ # PCIeHostInterface whose peer is an EthSimpleNIC/SimplePCIeNIC and
88
+ # emits
89
+ # assert com._ip is not None
90
+ # ip link set dev ethN up
91
+ # ip addr add {com._ip}/24 dev ethN
92
+ # An EnsoNIC is a SimplePCIeNIC but exposes no kernel netdev.
93
+ cmds = sys_host.BaseLinuxHost.prepare_post_cp(self, inst)
94
+ cmds += self._hugepage_cmds()
95
+ cmds += self._enso_driver_cmds()
96
+ return cmds
97
+
98
+ def _hugepage_cmds(self) -> list[str]:
99
+ """Equivalent of the hugepage section of Ensō's scripts/sw_setup.sh."""
100
+ return [
101
+ "mkdir -p /mnt/huge",
102
+ 'mount | grep -q " /mnt/huge " || mount -t hugetlbfs hugetlbfs /mnt/huge',
103
+ f"echo {self.nr_hugepages} > "
104
+ "/sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages",
105
+ ]
106
+
107
+ def _enso_driver_cmds(self) -> list[str]:
108
+ """Load Ensō's kernel driver and make sure its device node exists.
109
+
110
+ Replaces the driver loop of LinuxHost.prepare_post_cp, which we skip
111
+ (see prepare_post_cp).
112
+ """
113
+ cmds: list[str] = []
114
+ for driver in self.drivers:
115
+ if driver[0] == "/":
116
+ cmds.append(f"insmod {driver}")
117
+ else:
118
+ cmds.append(f"modprobe {driver}")
119
+
120
+ node = self.enso_dev_node
121
+ cmds += [
122
+ "modprobe uio",
123
+ f"modprobe {self.enso_kmod}",
124
+ f"test -e {node} || mknod {node} c "
125
+ f'$(grep -w {self.enso_kmod} /proc/devices | cut -f1 -d" ") 0',
126
+ f"chmod 666 {node}",
127
+ ]
128
+ return cmds
129
+
130
+ def toJSON(self) -> dict:
131
+ json_obj = super().toJSON()
132
+ json_obj["enso_parent_dir"] = self.enso_parent_dir
133
+ json_obj["enso_name"] = self.enso_name
134
+ json_obj["nr_hugepages"] = self.nr_hugepages
135
+ json_obj["enso_kmod"] = self.enso_kmod
136
+ json_obj["enso_dev_node"] = self.enso_dev_node
137
+ return json_obj
138
+
139
+ @classmethod
140
+ def fromJSON(cls, system: sys_base.System, json_obj: dict) -> tpe.Self:
141
+ instance = super().fromJSON(system, json_obj)
142
+ instance.enso_parent_dir = utils_base.get_json_attr_top(json_obj, "enso_parent_dir")
143
+ instance.enso_name = utils_base.get_json_attr_top(json_obj, "enso_name")
144
+ instance.nr_hugepages = int(utils_base.get_json_attr_top(json_obj, "nr_hugepages"))
145
+ instance.enso_kmod = utils_base.get_json_attr_top(json_obj, "enso_kmod")
146
+ instance.enso_dev_node = utils_base.get_json_attr_top(json_obj, "enso_dev_node")
147
+ return instance
148
+
149
+
150
+ class EnsoEchoServer(app.BaseLinuxApplication):
151
+ """
152
+ Ensō's bundled echo example, ``build/software/examples/echo``.
153
+ """
154
+
155
+ def __init__(self, h: EnsoLinuxHost) -> None:
156
+ utils_base.has_expected_type(h, EnsoLinuxHost)
157
+ super().__init__(h)
158
+ self.threads: int = 1
159
+ self.queues: int = 2
160
+ self.cycles: int = 0
161
+
162
+ def run_cmds(self, inst: inst_base.Instantiation) -> list[str]:
163
+ host = self.host
164
+ assert isinstance(host, EnsoLinuxHost)
165
+ return [
166
+ f"cd {host.enso_dir}",
167
+ f"./build/software/examples/echo {self.threads} {self.queues} {self.cycles}",
168
+ ]
169
+
170
+ def toJSON(self) -> dict:
171
+ json_obj = super().toJSON()
172
+ json_obj["threads"] = self.threads
173
+ json_obj["queues"] = self.queues
174
+ json_obj["cycles"] = self.cycles
175
+ return json_obj
176
+
177
+ @classmethod
178
+ def fromJSON(cls, system: sys_base.System, json_obj: dict) -> tpe.Self:
179
+ instance = super().fromJSON(system, json_obj)
180
+ instance.threads = int(utils_base.get_json_attr_top(json_obj, "threads"))
181
+ instance.queues = int(utils_base.get_json_attr_top(json_obj, "queues"))
182
+ instance.cycles = int(utils_base.get_json_attr_top(json_obj, "cycles"))
183
+ return instance
184
+
185
+
186
+ class EnsoGen(app.BaseLinuxApplication):
187
+ """
188
+ Ensō's traffic generator, ``scripts/ensogen.sh``.
189
+ """
190
+
191
+ def __init__(self, h: EnsoLinuxHost) -> None:
192
+ utils_base.has_expected_type(h, EnsoLinuxHost)
193
+ super().__init__(h)
194
+ # None selects Ensō's bundled sample capture, relative to enso_dir.
195
+ self.pcap: str | None = None
196
+ self.count: int = 10
197
+ self.rate: int = 100 # Gbps
198
+ self.pcie_addr: str | None = None
199
+
200
+ def run_cmds(self, inst: inst_base.Instantiation) -> list[str]:
201
+ host = self.host
202
+ assert isinstance(host, EnsoLinuxHost)
203
+ pcap = self.pcap
204
+ if pcap is None:
205
+ pcap = f"{host.enso_dir}/scripts/sample_pcaps/2_64_1_2.pcap"
206
+ cmd = f"./scripts/ensogen.sh {pcap} {self.rate} --count {self.count}"
207
+ if self.pcie_addr is not None:
208
+ cmd += f" --pcie-addr {self.pcie_addr}"
209
+ return [
210
+ f"cd {host.enso_dir}",
211
+ f"sleep 5",
212
+ cmd,
213
+ f"sleep 5",
214
+ ]
215
+
216
+ def toJSON(self) -> dict:
217
+ json_obj = super().toJSON()
218
+ json_obj["pcap"] = self.pcap
219
+ json_obj["count"] = self.count
220
+ json_obj["rate"] = self.rate
221
+ json_obj["pcie_addr"] = self.pcie_addr
222
+ return json_obj
223
+
224
+ @classmethod
225
+ def fromJSON(cls, system: sys_base.System, json_obj: dict) -> tpe.Self:
226
+ instance = super().fromJSON(system, json_obj)
227
+ instance.pcap = utils_base.get_json_attr_top_or_none(json_obj, "pcap")
228
+ instance.count = int(utils_base.get_json_attr_top(json_obj, "count"))
229
+ instance.rate = int(utils_base.get_json_attr_top(json_obj, "rate"))
230
+ instance.pcie_addr = utils_base.get_json_attr_top_or_none(json_obj, "pcie_addr")
231
+ return instance
@@ -0,0 +1,89 @@
1
+ # Copyright 2026 Max Planck Institute for Software Systems,
2
+ # National University of Singapore, Carnegie Mellon University,
3
+ # and SimBricks UG (haftungsbeschränkt)
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ """The Ensō guest image: the SimBricks base image with Ensō installed into it.
25
+
26
+ from simbricks.components.enso.system import enso_image
27
+
28
+ host.add_disk(enso_image(system))
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import pathlib
34
+ import shlex
35
+ import typing as tp
36
+
37
+ from simbricks.imagebuild.packer.image import PackerImage
38
+ from simbricks.orchestration.system import disk_images
39
+
40
+ if tp.TYPE_CHECKING:
41
+ from simbricks.orchestration.system import base as sys_base
42
+
43
+ _DATA = pathlib.Path(__file__).parent / "data"
44
+
45
+ ENSO_REPO = "https://github.com/crossroadsfpga/enso"
46
+ ENSO_BRANCH = "simbricks-24.04"
47
+ ENSO_DIR = "/root/enso"
48
+ DISK_SIZE = "16G"
49
+ BUILD_MEM_SIZE = "16G"
50
+ BUILD_CPUS = 4
51
+
52
+
53
+ def _script(name: str, env: dict[str, str]) -> str:
54
+ """The packaged script @name with @env exported after its shebang."""
55
+ lines = (_DATA / name).read_text().splitlines(keepends=True)
56
+ exports = "".join(f"export {k}={shlex.quote(v)}\n" for k, v in sorted(env.items()))
57
+ return lines[0] + exports + "".join(lines[1:])
58
+
59
+
60
+ def enso_image(
61
+ system: sys_base.System,
62
+ enso_repo: str = ENSO_REPO,
63
+ enso_branch: str = ENSO_BRANCH,
64
+ enso_dir: str = ENSO_DIR,
65
+ ) -> PackerImage:
66
+ """The SimBricks ``base`` image with Ensō installed into it.
67
+
68
+ Built with packer on the runner while the simulation is prepared; cached
69
+ across runs with ``simbricks-run --image-cache-dir``.
70
+
71
+ @enso_repo, @enso_branch and @enso_dir are exported into the install script
72
+ and are part of the image's content hash. @enso_dir has to match the
73
+ EnsoLinuxHost this image is attached to.
74
+
75
+ The result is an ordinary PackerImage; ``cleanup``, ``accelerator`` etc.
76
+ can be set on it.
77
+ """
78
+ image = PackerImage(system, disk_images.DistroDiskImage(system, "base"))
79
+ image.disk_size = DISK_SIZE
80
+ image.mem_size = BUILD_MEM_SIZE
81
+ image.cpus = BUILD_CPUS
82
+ image.run_script_str(
83
+ "install-enso.sh",
84
+ _script(
85
+ "install-enso.sh",
86
+ {"ENSO_REPO": enso_repo, "ENSO_BRANCH": enso_branch, "ENSO_DIR": enso_dir},
87
+ ),
88
+ )
89
+ return image