garageworks 0.2.0.dev299555241357__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.
Files changed (31) hide show
  1. garageworks-0.2.0.dev299555241357/PKG-INFO +10 -0
  2. garageworks-0.2.0.dev299555241357/garageworks/__init__.py +1 -0
  3. garageworks-0.2.0.dev299555241357/garageworks/bridge/__init__.py +1 -0
  4. garageworks-0.2.0.dev299555241357/garageworks/bridge/aved_bridge.py +85 -0
  5. garageworks-0.2.0.dev299555241357/garageworks/bridge/cocotb_bridge.py +93 -0
  6. garageworks-0.2.0.dev299555241357/garageworks/bridge/conv_cocotb_to_fpga.py +426 -0
  7. garageworks-0.2.0.dev299555241357/garageworks/gwscript/__init__.py +3 -0
  8. garageworks-0.2.0.dev299555241357/garageworks/gwscript/cli.py +293 -0
  9. garageworks-0.2.0.dev299555241357/garageworks/gwscript/config.py +322 -0
  10. garageworks-0.2.0.dev299555241357/garageworks/gwscript/jobs.py +282 -0
  11. garageworks-0.2.0.dev299555241357/garageworks/gwscript/lock.py +111 -0
  12. garageworks-0.2.0.dev299555241357/garageworks/gwscript/mcp_server.py +134 -0
  13. garageworks-0.2.0.dev299555241357/garageworks/gwscript/orchestrate.py +190 -0
  14. garageworks-0.2.0.dev299555241357/garageworks/gwscript/setup_wizard.py +185 -0
  15. garageworks-0.2.0.dev299555241357/garageworks/gwscript/shell.py +61 -0
  16. garageworks-0.2.0.dev299555241357/garageworks/gwscript/stages.py +397 -0
  17. garageworks-0.2.0.dev299555241357/garageworks/gwscript/status.py +429 -0
  18. garageworks-0.2.0.dev299555241357/garageworks/gwscript/sudoers.py +73 -0
  19. garageworks-0.2.0.dev299555241357/garageworks/gwscript/templates/Makefile.gw +59 -0
  20. garageworks-0.2.0.dev299555241357/garageworks/gwscript/templates/target.mk.tmpl +15 -0
  21. garageworks-0.2.0.dev299555241357/garageworks/gwscript/templates/target_bridge.py +13 -0
  22. garageworks-0.2.0.dev299555241357/garageworks/gwscript/templates/tb_base.py +12 -0
  23. garageworks-0.2.0.dev299555241357/garageworks/gwscript/timing.py +199 -0
  24. garageworks-0.2.0.dev299555241357/garageworks.egg-info/PKG-INFO +10 -0
  25. garageworks-0.2.0.dev299555241357/garageworks.egg-info/SOURCES.txt +29 -0
  26. garageworks-0.2.0.dev299555241357/garageworks.egg-info/dependency_links.txt +1 -0
  27. garageworks-0.2.0.dev299555241357/garageworks.egg-info/entry_points.txt +3 -0
  28. garageworks-0.2.0.dev299555241357/garageworks.egg-info/requires.txt +6 -0
  29. garageworks-0.2.0.dev299555241357/garageworks.egg-info/top_level.txt +1 -0
  30. garageworks-0.2.0.dev299555241357/pyproject.toml +27 -0
  31. garageworks-0.2.0.dev299555241357/setup.cfg +4 -0
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: garageworks
3
+ Version: 0.2.0.dev299555241357
4
+ Requires-Python: >=3.9
5
+ Requires-Dist: cocotb==1.9.2
6
+ Requires-Dist: cocotb-bus==0.3.0
7
+ Requires-Dist: cocotb-test==0.2.6
8
+ Requires-Dist: cocotbext-axi==0.1.26
9
+ Requires-Dist: libcst==1.8.6
10
+ Requires-Dist: pytest==8.4.2
@@ -0,0 +1 @@
1
+ # garageworks/__init__.py
@@ -0,0 +1 @@
1
+ # garageworks/bridge/__init__.py
@@ -0,0 +1,85 @@
1
+ # See LICENSE file for details.
2
+ # written by Kaz Yoshii <kazutomo.yoshii@gmail.com>
3
+
4
+ import os, sys, time
5
+ from pathlib import Path
6
+ import json
7
+ from types import SimpleNamespace
8
+ import logging
9
+
10
+ import pyaved
11
+
12
+ class AVED_Bridge:
13
+ is_fpga = True
14
+
15
+ def readAddrMap(self):
16
+ fn = os.getenv("PARAMFN")
17
+ if fn is None:
18
+ raise RuntimeError("Environment variable PARAMFN is not set")
19
+
20
+ with open(fn) as f:
21
+ data = json.load(f, object_hook=lambda d: SimpleNamespace(**d))
22
+
23
+ return data
24
+
25
+ def aved_write32(self, addr, v):
26
+ self.aved.write32(addr, v)
27
+
28
+ def aved_read32(self, addr):
29
+ v = self.aved.read32(addr)
30
+ return v
31
+
32
+ def __init__(self, base=0x1100000, dev="b1:00.0", logfn = "output.log"):
33
+ self.base = base
34
+
35
+ self.p = self.readAddrMap()
36
+
37
+ self.aved = pyaved.AVED()
38
+ self.aved.open(dev)
39
+
40
+ log_path = Path(logfn).resolve()
41
+ self.log = logging.getLogger("tblog")
42
+ self.log.setLevel(logging.INFO)
43
+ self.log.propagate = False
44
+ self.log.handlers.clear()
45
+ handler = logging.FileHandler(log_path, mode="w")
46
+ handler.setLevel(logging.INFO)
47
+ handler.setFormatter(logging.Formatter("%(message)s"))
48
+ self.log.addHandler(handler)
49
+
50
+ self.log.info("FPGA Simulation Environment Initialized!\n")
51
+
52
+ def writelog(self, txt):
53
+ print(txt, end='')
54
+ with open(self.outputfn, "a") as f:
55
+ f.write(txt)
56
+
57
+ def checkOffset(self, offset, align=8):
58
+ assert (offset % align) == 0, f"offset should be aligned with {align} bytes : {offset}"
59
+
60
+ def writeWord(self, offset, data):
61
+ self.checkOffset(offset, align=4)
62
+ self.aved_write32(self.base + offset, data)
63
+
64
+ def readWord(self, offset):
65
+ self.checkOffset(offset, align=4)
66
+ return self.aved_read32(self.base + offset)
67
+
68
+ def expectWord(self, offset, ref, msg = ""):
69
+ v = self.readWord(offset)
70
+ assert v == ref, msg
71
+
72
+ def softReset(self, maxloopcnt = 1000):
73
+ cycles = self.p.reset_cycles
74
+ self.writeWord(self.p.soft_reset_rw, 1)
75
+ reset_done = 0
76
+ loopcnt = 0
77
+ while reset_done == 0:
78
+ reset_done = self.readWord(self.p.soft_reset_rw)
79
+ if loopcnt > maxloopcnt:
80
+ raise RuntimeError("Reached maxloopcnt. Something wrong")
81
+ loopcnt += 1
82
+ return loopcnt
83
+
84
+ def setup(self):
85
+ pass
@@ -0,0 +1,93 @@
1
+ # See LICENSE file for details.
2
+ # written by Kaz Yoshii <kazutomo.yoshii@gmail.com>
3
+
4
+ import os, sys, random
5
+ from pathlib import Path
6
+ import json
7
+ from types import SimpleNamespace
8
+ import logging
9
+
10
+ import cocotb_test.simulator, pytest
11
+ import cocotb
12
+ from cocotb.clock import Clock
13
+ from cocotb.triggers import RisingEdge, Timer
14
+ from cocotb.regression import TestFactory
15
+ from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiLiteRam, AxiResp
16
+ import warnings
17
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
18
+
19
+ class COCOTB_Bridge:
20
+ is_fpga = False
21
+
22
+ def readParams(self):
23
+ fn = os.getenv("PARAMFN")
24
+ if fn is None:
25
+ raise RuntimeError("Environment variable PARAMFN is not set")
26
+
27
+ with open(fn) as f:
28
+ data = json.load(f, object_hook=lambda d: SimpleNamespace(**d))
29
+
30
+ return data
31
+
32
+ async def setup(self):
33
+ cocotb.start_soon(Clock(self.dut.s_axi_aclk, 10, units="ns").start())
34
+
35
+ bus = AxiLiteBus.from_prefix(self.dut, "S_AXI")
36
+ self.axi_master = AxiLiteMaster(
37
+ bus,
38
+ self.dut.s_axi_aclk, # lower-case clock
39
+ self.dut.s_axi_aresetn, # lower-case reset
40
+ reset_active_level=0 # because aresetn is active-low
41
+ )
42
+ await self.reset_dut()
43
+
44
+ def __init__(self, dut, logfn = "output.log"):
45
+ self.dut = dut
46
+
47
+ self.p = self.readParams()
48
+
49
+ log_path = Path(logfn).resolve()
50
+ self.log = logging.getLogger("tblog")
51
+ self.log.setLevel(logging.INFO)
52
+ self.log.propagate = False
53
+ self.log.handlers.clear()
54
+ handler = logging.FileHandler(log_path, mode="w")
55
+ handler.setLevel(logging.INFO)
56
+ handler.setFormatter(logging.Formatter("%(message)s"))
57
+ self.log.addHandler(handler)
58
+
59
+ self.log.info("Cocotb Test Initialized!\n")
60
+
61
+ async def writeWord(self, addr, data):
62
+ b = bytearray(data.to_bytes(4, byteorder="little"))
63
+ await self.axi_master.write(addr, b)
64
+
65
+ async def readWord(self, addr):
66
+ b = await self.axi_master.read(addr, 4)
67
+ v = int.from_bytes(b, byteorder="little")
68
+ return v
69
+
70
+ async def expectWord(self, addr, ref, msg = ""):
71
+ v = await self.readWord(addr)
72
+ assert v == ref, msg
73
+
74
+ async def softReset(self, maxloopcnt = 1000):
75
+ cycles = self.p.reset_cycles
76
+ await self.writeWord(self.p.soft_reset_rw, 1)
77
+ reset_done = 0
78
+ loopcnt = 0
79
+ while reset_done == 0:
80
+ reset_done = await self.readWord(self.p.soft_reset_rw)
81
+ if loopcnt > maxloopcnt:
82
+ raise RuntimeError("Reached maxloopcnt. Something wrong")
83
+ loopcnt += 1
84
+ return loopcnt
85
+
86
+ # obsolute below
87
+ async def reset_dut(self, cycles=5):
88
+ self.dut.s_axi_aresetn.value = 0 # assert (active-low)
89
+ for _ in range(cycles):
90
+ await RisingEdge(self.dut.s_axi_aclk)
91
+ self.dut.s_axi_aresetn.value = 1 # deassert
92
+ for _ in range(2):
93
+ await RisingEdge(self.dut.s_axi_aclk)
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Convert a set of AXI cocotb testbench files into FPGA testbench files.
4
+
5
+ Usage:
6
+ python conv_cocotb_to_fpga.py tb_filter.py r2bridge.py subtest1.py
7
+
8
+ Each input file `foo.py` is written as `fpga_foo.py` (default: same
9
+ directory as input, or --outdir if given). Local imports between files
10
+ in the same batch are rewritten to match (e.g. `from r2bridge import
11
+ R2Bridge` -> `from fpga_r2bridge import R2Bridge`) so the converted
12
+ files import each other correctly.
13
+
14
+ To overwrite existing output files, use the --force option.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ from pathlib import Path
21
+
22
+ import libcst as cst
23
+ import libcst.matchers as m
24
+
25
+
26
+ def fpga_name(stem: str) -> str:
27
+ """Map a module/file stem to its converted-output equivalent name."""
28
+ return f"fpga_{stem}"
29
+
30
+
31
+ class CocotbToFpgaTransformer(cst.CSTTransformer):
32
+ def __init__(
33
+ self,
34
+ fpga: str,
35
+ local_stems: frozenset[str] = frozenset(),
36
+ drop_setup: bool = True,
37
+ ):
38
+ self.sim = 'COCOTB'
39
+ self.fpga = fpga
40
+ self.local_stems = local_stems
41
+ self.drop_setup = drop_setup
42
+ self.generated_main = False
43
+ self._bridge_class_depth: list[bool] = []
44
+
45
+ def _module_name(self, node: cst.BaseExpression) -> str:
46
+ """Return dotted module name from LibCST Name/Attribute node."""
47
+ if isinstance(node, cst.Name):
48
+ return node.value
49
+ if isinstance(node, cst.Attribute):
50
+ left = self._module_name(node.value)
51
+ return f"{left}.{node.attr.value}"
52
+ return ""
53
+
54
+ # -------------------------
55
+ # Imports
56
+ # -------------------------
57
+ def leave_Import(
58
+ self,
59
+ original_node: cst.Import,
60
+ updated_node: cst.Import,
61
+ ) -> cst.Import | cst.RemovalSentinel:
62
+ kept = []
63
+ for alias in updated_node.names:
64
+ name = alias.name
65
+ # remove: import cocotb
66
+ if isinstance(name, cst.Name) and name.value == "cocotb":
67
+ continue
68
+ kept.append(alias.with_changes(comma=cst.MaybeSentinel.DEFAULT))
69
+ if not kept:
70
+ return cst.RemoveFromParent()
71
+ return updated_node.with_changes(names=kept)
72
+
73
+ def leave_ImportFrom(
74
+ self,
75
+ original_node: cst.ImportFrom,
76
+ updated_node: cst.ImportFrom,
77
+ ) -> cst.ImportFrom | cst.RemovalSentinel:
78
+ if updated_node.module is None:
79
+ return updated_node
80
+
81
+ module_code = self._module_name(updated_node.module)
82
+
83
+ # remove: from cocotb... import ...
84
+ if module_code == "cocotb" or module_code.startswith("cocotb."):
85
+ return cst.RemoveFromParent()
86
+
87
+ # e.g.,
88
+ # from garageworks.bridge.cocotb_bridge import COCOTB_Bridge
89
+ # ->
90
+ # from garageworks.bridge.aved_bridge import AVED_Bridge
91
+ if module_code == "garageworks.bridge.cocotb_bridge":
92
+ return updated_node.with_changes(
93
+ module=cst.Attribute(
94
+ value=cst.Attribute(
95
+ value=cst.Name("garageworks"),
96
+ attr=cst.Name("bridge"),
97
+ ),
98
+ attr=cst.Name(f"{self.fpga.lower()}_bridge"),
99
+ ),
100
+ names=[
101
+ cst.ImportAlias(
102
+ name=cst.Name(f"{self.fpga}_Bridge"),
103
+ comma=cst.MaybeSentinel.DEFAULT,
104
+ )
105
+ ],
106
+ )
107
+
108
+ # Batch-local import rewrite, e.g.
109
+ # from r2bridge import R2Bridge
110
+ # ->
111
+ # from fpga_r2bridge import R2Bridge
112
+ # (also covers helper modules like subtest1.py)
113
+ if module_code in self.local_stems:
114
+ return updated_node.with_changes(
115
+ module=cst.Name(fpga_name(module_code)),
116
+ )
117
+
118
+ return updated_node
119
+
120
+ # -------------------------
121
+ # async / await conversion
122
+ # -------------------------
123
+ def leave_Await(
124
+ self,
125
+ original_node: cst.Await,
126
+ updated_node: cst.Await,
127
+ ) -> cst.BaseExpression:
128
+ # await rev.readWord(...)
129
+ # ->
130
+ # rev.readWord(...)
131
+ return updated_node.expression
132
+
133
+ # -------------------------
134
+ # Function conversion
135
+ # -------------------------
136
+ def leave_FunctionDef(
137
+ self,
138
+ original_node: cst.FunctionDef,
139
+ updated_node: cst.FunctionDef,
140
+ ) -> cst.FunctionDef:
141
+ decorators = []
142
+ is_cocotb_test = False
143
+ for dec in updated_node.decorators:
144
+ if self._is_cocotb_test_decorator(dec):
145
+ is_cocotb_test = True
146
+ continue
147
+ decorators.append(dec)
148
+
149
+ # async def -> def
150
+ new_node = updated_node.with_changes(
151
+ asynchronous=None,
152
+ decorators=decorators,
153
+ )
154
+
155
+ if is_cocotb_test:
156
+ self.generated_main = True
157
+ # async def sim_simple(dut):
158
+ # ->
159
+ # def main():
160
+ new_node = new_node.with_changes(
161
+ name=cst.Name("main"),
162
+ params=cst.Parameters(),
163
+ )
164
+
165
+ # Inside a *Bridge subclass, __init__(self, cocotb_dut) drops the
166
+ # cocotb_dut param, since the FPGA-side bridge takes none.
167
+ in_bridge_class = (
168
+ self._bridge_class_depth and self._bridge_class_depth[-1]
169
+ )
170
+ if in_bridge_class and updated_node.name.value == "__init__":
171
+ params = list(updated_node.params.params)
172
+ if len(params) >= 2:
173
+ # keep self and all params after cocotb_dut
174
+ new_params_list = [params[0]] + params[2:]
175
+ new_params = updated_node.params.with_changes(
176
+ params = new_params_list
177
+ )
178
+ new_node = new_node.with_changes(params=new_params)
179
+
180
+ return new_node
181
+
182
+ def _is_cocotb_test_decorator(self, dec: cst.Decorator) -> bool:
183
+ expr = dec.decorator
184
+ # @cocotb.test
185
+ if m.matches(
186
+ expr,
187
+ m.Attribute(
188
+ value=m.Name("cocotb"),
189
+ attr=m.Name("test"),
190
+ ),
191
+ ):
192
+ return True
193
+ # @cocotb.test()
194
+ if m.matches(
195
+ expr,
196
+ m.Call(
197
+ func=m.Attribute(
198
+ value=m.Name("cocotb"),
199
+ attr=m.Name("test"),
200
+ )
201
+ ),
202
+ ):
203
+ return True
204
+ return False
205
+
206
+ # -------------------------
207
+ # Class definition: rebase project bridge classes
208
+ # e.g. class R2Bridge(COCOTB_Bridge): -> class R2Bridge(AVED_Bridge):
209
+ # -------------------------
210
+ def _is_bridge_base(self, base_value: cst.BaseExpression) -> bool:
211
+ name = self._module_name(base_value)
212
+ return name == "COCOTB_Bridge" or name.endswith("Bridge")
213
+
214
+ def visit_ClassDef(self, node: cst.ClassDef) -> None:
215
+ is_bridge_class = any(
216
+ self._is_bridge_base(base.value) for base in node.bases
217
+ )
218
+ self._bridge_class_depth.append(is_bridge_class)
219
+
220
+ def leave_ClassDef(
221
+ self,
222
+ original_node: cst.ClassDef,
223
+ updated_node: cst.ClassDef,
224
+ ) -> cst.ClassDef:
225
+ self._bridge_class_depth.pop()
226
+
227
+ new_bases = []
228
+ changed = False
229
+ for base in updated_node.bases:
230
+ if m.matches(base.value, m.Name("COCOTB_Bridge")):
231
+ new_bases.append(
232
+ base.with_changes(value=cst.Name(f"{self.fpga}_Bridge"))
233
+ )
234
+ changed = True
235
+ else:
236
+ new_bases.append(base)
237
+ if changed:
238
+ return updated_node.with_changes(bases=new_bases)
239
+ return updated_node
240
+
241
+ # -------------------------
242
+ # Class replacement (generalized to any *Bridge subclass call)
243
+ # -------------------------
244
+ def leave_Call(
245
+ self,
246
+ original_node: cst.Call,
247
+ updated_node: cst.Call,
248
+ ) -> cst.Call:
249
+ def drop_cocotb_dut(args):
250
+ args = list(args)
251
+ if (
252
+ args
253
+ and args[0].keyword is None
254
+ and isinstance(args[0].value, cst.Name)
255
+ and args[0].value.value == "cocotb_dut"
256
+ ):
257
+ return args[1:]
258
+ return args
259
+
260
+ if isinstance(updated_node.func, cst.Name):
261
+ name = updated_node.func.value
262
+
263
+ # COCOTB_Bridge(cocotb_dut) -> AVED_Bridge()
264
+ if name == "COCOTB_Bridge":
265
+ return updated_node.with_changes(
266
+ func=cst.Name(f"{self.fpga}_Bridge"),
267
+ args=[],
268
+ )
269
+
270
+ # R2Bridge(cocotb_dut, ...) -> R2Bridge(...)
271
+ if name.endswith("Bridge") and name != f"{self.fpga}_Bridge":
272
+ return updated_node.with_changes(
273
+ args=drop_cocotb_dut(updated_node.args)
274
+ )
275
+
276
+ # Inside a *Bridge subclass, super().__init__(cocotb_dut, ...) -> super().__init__(...)
277
+ in_bridge_class = (
278
+ self._bridge_class_depth and self._bridge_class_depth[-1]
279
+ )
280
+ if in_bridge_class and m.matches(
281
+ updated_node.func,
282
+ m.Attribute(
283
+ value=m.Call(func=m.Name("super")),
284
+ attr=m.Name("__init__"),
285
+ ),
286
+ ):
287
+ return updated_node.with_changes(
288
+ args=drop_cocotb_dut(updated_node.args)
289
+ )
290
+
291
+ return updated_node
292
+
293
+ # -------------------------
294
+ # Optional setup removal
295
+ # -------------------------
296
+ def leave_SimpleStatementLine(
297
+ self,
298
+ original_node: cst.SimpleStatementLine,
299
+ updated_node: cst.SimpleStatementLine,
300
+ ) -> cst.SimpleStatementLine | cst.RemovalSentinel:
301
+ if self.drop_setup and len(updated_node.body) == 1:
302
+ stmt = updated_node.body[0]
303
+ # Drop:
304
+ # await rev.setup()
305
+ # After await-removal, this becomes:
306
+ # rev.setup()
307
+ if m.matches(
308
+ stmt,
309
+ m.Expr(
310
+ value=m.Call(
311
+ func=m.Attribute(
312
+ attr=m.Name("setup"),
313
+ )
314
+ )
315
+ ),
316
+ ):
317
+ return cst.RemoveFromParent()
318
+ return updated_node
319
+
320
+ # -------------------------
321
+ # Add main entry point
322
+ # -------------------------
323
+ def leave_Module(
324
+ self,
325
+ original_node: cst.Module,
326
+ updated_node: cst.Module,
327
+ ) -> cst.Module:
328
+ if not self.generated_main:
329
+ return updated_node
330
+
331
+ main_block = cst.parse_statement(
332
+ '''
333
+ if __name__ == "__main__":
334
+ main()
335
+ '''
336
+ )
337
+ return updated_node.with_changes(
338
+ body=list(updated_node.body) + [main_block]
339
+ )
340
+
341
+
342
+ def convert_code(
343
+ source: str,
344
+ fpga: str,
345
+ local_stems: frozenset[str] = frozenset(),
346
+ drop_setup: bool = True,
347
+ ) -> str:
348
+ module = cst.parse_module(source)
349
+ transformed = module.visit(
350
+ CocotbToFpgaTransformer(
351
+ fpga=fpga,
352
+ local_stems=local_stems,
353
+ drop_setup=drop_setup,
354
+ )
355
+ )
356
+ return transformed.code
357
+
358
+
359
+ def main() -> None:
360
+ parser = argparse.ArgumentParser(
361
+ description="Convert a batch of restricted cocotb testbench files "
362
+ "to FPGA testbench files."
363
+ )
364
+ parser.add_argument(
365
+ "inputs",
366
+ nargs="+",
367
+ type=Path,
368
+ help="Input cocotb files, e.g. tb_filter.py r2bridge.py subtest1.py",
369
+ )
370
+ parser.add_argument(
371
+ "--outdir",
372
+ type=Path,
373
+ default=None,
374
+ help="Directory for converted output files "
375
+ "(default: same directory as each input file).",
376
+ )
377
+ parser.add_argument(
378
+ "--fpga",
379
+ default="AVED",
380
+ help="FPGA backend name (default: %(default)s)",
381
+ )
382
+ parser.add_argument(
383
+ "--keep-setup",
384
+ action="store_true",
385
+ help="Keep setup() calls instead of removing them.",
386
+ )
387
+ parser.add_argument(
388
+ "--force",
389
+ action="store_true",
390
+ help="Overwrite output files if they already exist.",
391
+ )
392
+ args = parser.parse_args()
393
+
394
+ # Batch-wide set of local module stems, so local imports between
395
+ # files in this same invocation get rewritten consistently
396
+ # (e.g. "r2bridge", "subtest1").
397
+ local_stems = frozenset(p.stem for p in args.inputs)
398
+
399
+ # Resolve output paths up front and check for collisions before
400
+ # writing anything.
401
+ output_paths = []
402
+ for in_path in args.inputs:
403
+ outdir = args.outdir if args.outdir is not None else in_path.parent
404
+ out_path = outdir / f"{fpga_name(in_path.stem)}{in_path.suffix}"
405
+ if out_path.exists() and not args.force:
406
+ raise FileExistsError(
407
+ f"{out_path} already exists. Use --force to overwrite."
408
+ )
409
+ output_paths.append(out_path)
410
+
411
+ for in_path, out_path in zip(args.inputs, output_paths):
412
+ source = in_path.read_text()
413
+ converted = convert_code(
414
+ source=source,
415
+ fpga=args.fpga,
416
+ local_stems=local_stems,
417
+ drop_setup=False,
418
+ # drop_setup=not args.keep_setup,
419
+ )
420
+ out_path.parent.mkdir(parents=True, exist_ok=True)
421
+ out_path.write_text(converted)
422
+ print(f"{in_path} -> {out_path}")
423
+
424
+
425
+ if __name__ == "__main__":
426
+ main()
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ __all__ = ["main"]