Skip to content

Commit bac83ad

Browse files
committed
amaranth._cli: prototype. (WIP)
1 parent a9d0380 commit bac83ad

File tree

2 files changed

+124
-8
lines changed

2 files changed

+124
-8
lines changed

amaranth_cli/__init__.py

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""
2+
This file is not a part of the Amaranth module tree because the CLI needs to emit Make-style
3+
dependency files as a part of the generation process. In order for `from amaranth import *`
4+
to work as a prelude, it has to load several of the files under `amaranth/`, which means
5+
these will not be loaded later in the process, and not recorded as dependencies.
6+
"""
7+
8+
import importlib
9+
import argparse
10+
import stat
11+
import sys
12+
import os
13+
import re
14+
15+
16+
def _build_parser():
17+
def component(reference):
18+
from amaranth import Elaboratable
19+
20+
if m := re.match(r"(\w+(?:\.\w+)*):(\w+(?:\.\w+)*)", reference, re.IGNORECASE|re.ASCII):
21+
mod_name, qual_name = m[1], m[2]
22+
try:
23+
obj = importlib.import_module(mod_name)
24+
except ImportError as e:
25+
raise argparse.ArgumentTypeError(f"{mod_name!r} does not refer to "
26+
"an importable Python module") from e
27+
try:
28+
for attr in qual_name.split("."):
29+
obj = getattr(obj, attr)
30+
except AttributeError as e:
31+
raise argparse.ArgumentTypeError(f"{qual_name!r} does not refer to an object "
32+
f"within the {mod_name!r} module") from e
33+
if not issubclass(obj, Elaboratable):
34+
raise argparse.ArgumentTypeError(f"'{qual_name}:{mod_name}' refers to an object that is not elaboratable")
35+
return obj
36+
else:
37+
raise argparse.ArgumentTypeError(f"{reference!r} is not a Python object reference")
38+
39+
parser = argparse.ArgumentParser(
40+
"amaranth", description="""
41+
Amaranth HDL command line interface.
42+
""")
43+
operation = parser.add_subparsers(
44+
metavar="OPERATION", help="operation to perform",
45+
dest="operation", required=True)
46+
47+
op_generate = operation.add_parser(
48+
"generate", help="generate code in a different language from Amaranth code")
49+
op_generate.add_argument(
50+
metavar="COMPONENT", help="Amaranth component to convert, e.g. `pkg.mod:Cls`",
51+
dest="component", type=component)
52+
gen_language = op_generate.add_subparsers(
53+
metavar="LANGUAGE", help="language to generate code in",
54+
dest="language", required=True)
55+
56+
lang_verilog = gen_language.add_parser(
57+
"verilog", help="generate Verilog code")
58+
lang_verilog.add_argument(
59+
"-v", metavar="VERILOG-FILE", help="Verilog file to write",
60+
dest="verilog_file", type=argparse.FileType("w"))
61+
lang_verilog.add_argument(
62+
"-d", metavar="DEP-FILE", help="Make-style dependency file to write",
63+
dest="dep_file", type=argparse.FileType("w"))
64+
65+
return parser
66+
67+
68+
def main(args=None):
69+
# Hook the `open()` function to find out which files are being opened by Amaranth code.
70+
files_being_opened = set()
71+
special_file_opened = False
72+
def dep_audit_hook(event, args):
73+
nonlocal special_file_opened
74+
if files_being_opened is not None and event == "open":
75+
filename, mode, flags = args
76+
if mode is None or "r" in mode or "+" in mode:
77+
if isinstance(filename, bytes):
78+
filename = filename.decode("utf-8")
79+
if isinstance(filename, str) and stat.S_ISREG(os.stat(filename).st_mode):
80+
files_being_opened.add(filename)
81+
else:
82+
special_file_opened = True
83+
sys.addaudithook(dep_audit_hook)
84+
85+
# Parse arguments and instantiate components
86+
args = _build_parser().parse_args(args)
87+
if args.operation == "generate":
88+
component = args.component()
89+
90+
# Capture the set of opened files, as well as the loaded Python modules.
91+
files_opened, files_being_opened = files_being_opened, None
92+
modules_after = list(sys.modules.values())
93+
94+
# Remove *.pyc files from the set of open files and replace them with their *.py equivalents.
95+
dep_files = set()
96+
dep_files.update(files_opened)
97+
for module in modules_after:
98+
if getattr(module, "__spec__", None) is None:
99+
continue
100+
if module.__spec__.cached in dep_files:
101+
dep_files.discard(module.__spec__.cached)
102+
dep_files.add(module.__spec__.origin)
103+
104+
if args.operation == "generate":
105+
if args.language == "verilog":
106+
# Generate Verilog file.
107+
from amaranth.back import verilog
108+
args.verilog_file.write(verilog.convert(component))
109+
110+
# Generate dependency file.
111+
if args.verilog_file and args.dep_file:
112+
args.dep_file.write(f"{args.verilog_file.name}:")
113+
if not special_file_opened:
114+
for file in sorted(dep_files):
115+
args.dep_file.write(f" \\\n {file}")
116+
args.dep_file.write("\n")
117+
else:
118+
args.dep_file.write(f"\n.PHONY: {args.verilog_file.name}\n")

pyproject.toml

+6-8
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,24 @@ dependencies = [
1616
]
1717

1818
[project.optional-dependencies]
19-
# this version requirement needs to be synchronized with the one in amaranth.back.verilog!
19+
# This version requirement needs to be synchronized with the one in amaranth.back.verilog!
2020
builtin-yosys = ["amaranth-yosys>=0.10"]
2121
remote-build = ["paramiko~=2.7"]
2222

2323
[project.scripts]
24+
amaranth = "amaranth_cli:main"
2425
amaranth-rpc = "amaranth.rpc:main"
2526

27+
[tool.setuptools]
28+
# The docstring in `amaranth_cli/__init__.py` explains why it is not under `amaranth/`.
29+
packages = ["amaranth", "amaranth_cli"]
30+
2631
# Build system configuration
2732

2833
[build-system]
2934
requires = ["wheel", "setuptools>=67.0", "setuptools_scm[toml]>=6.2"]
3035
build-backend = "setuptools.build_meta"
3136

32-
[tool.setuptools]
33-
# If amaranth 0.3 is checked out with git (e.g. as a part of a persistent editable install or
34-
# a git worktree cached by tools like poetry), it can have an empty `nmigen` directory left over,
35-
# which causes a hard error because setuptools cannot determine the top-level package.
36-
# Add a workaround to improve experience for people upgrading from old checkouts.
37-
packages = ["amaranth"]
38-
3937
[tool.setuptools_scm]
4038
local_scheme = "node-and-timestamp"
4139

0 commit comments

Comments
 (0)