Skip to content

Commit

Permalink
amaranth._cli: prototype. (WIP)
Browse files Browse the repository at this point in the history
  • Loading branch information
whitequark committed Sep 5, 2023
1 parent a9d0380 commit bac83ad
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 8 deletions.
118 changes: 118 additions & 0 deletions amaranth_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""
This file is not a part of the Amaranth module tree because the CLI needs to emit Make-style
dependency files as a part of the generation process. In order for `from amaranth import *`
to work as a prelude, it has to load several of the files under `amaranth/`, which means
these will not be loaded later in the process, and not recorded as dependencies.
"""

import importlib
import argparse
import stat
import sys
import os
import re


def _build_parser():
def component(reference):
from amaranth import Elaboratable

if m := re.match(r"(\w+(?:\.\w+)*):(\w+(?:\.\w+)*)", reference, re.IGNORECASE|re.ASCII):
mod_name, qual_name = m[1], m[2]
try:
obj = importlib.import_module(mod_name)
except ImportError as e:
raise argparse.ArgumentTypeError(f"{mod_name!r} does not refer to "
"an importable Python module") from e
try:
for attr in qual_name.split("."):
obj = getattr(obj, attr)
except AttributeError as e:
raise argparse.ArgumentTypeError(f"{qual_name!r} does not refer to an object "
f"within the {mod_name!r} module") from e
if not issubclass(obj, Elaboratable):
raise argparse.ArgumentTypeError(f"'{qual_name}:{mod_name}' refers to an object that is not elaboratable")
return obj
else:
raise argparse.ArgumentTypeError(f"{reference!r} is not a Python object reference")

parser = argparse.ArgumentParser(
"amaranth", description="""
Amaranth HDL command line interface.
""")
operation = parser.add_subparsers(
metavar="OPERATION", help="operation to perform",
dest="operation", required=True)

op_generate = operation.add_parser(
"generate", help="generate code in a different language from Amaranth code")
op_generate.add_argument(
metavar="COMPONENT", help="Amaranth component to convert, e.g. `pkg.mod:Cls`",
dest="component", type=component)
gen_language = op_generate.add_subparsers(
metavar="LANGUAGE", help="language to generate code in",
dest="language", required=True)

lang_verilog = gen_language.add_parser(
"verilog", help="generate Verilog code")
lang_verilog.add_argument(
"-v", metavar="VERILOG-FILE", help="Verilog file to write",
dest="verilog_file", type=argparse.FileType("w"))
lang_verilog.add_argument(
"-d", metavar="DEP-FILE", help="Make-style dependency file to write",
dest="dep_file", type=argparse.FileType("w"))

return parser


def main(args=None):
# Hook the `open()` function to find out which files are being opened by Amaranth code.
files_being_opened = set()
special_file_opened = False
def dep_audit_hook(event, args):
nonlocal special_file_opened
if files_being_opened is not None and event == "open":
filename, mode, flags = args
if mode is None or "r" in mode or "+" in mode:
if isinstance(filename, bytes):
filename = filename.decode("utf-8")
if isinstance(filename, str) and stat.S_ISREG(os.stat(filename).st_mode):
files_being_opened.add(filename)
else:
special_file_opened = True
sys.addaudithook(dep_audit_hook)

# Parse arguments and instantiate components
args = _build_parser().parse_args(args)
if args.operation == "generate":
component = args.component()

# Capture the set of opened files, as well as the loaded Python modules.
files_opened, files_being_opened = files_being_opened, None
modules_after = list(sys.modules.values())

# Remove *.pyc files from the set of open files and replace them with their *.py equivalents.
dep_files = set()
dep_files.update(files_opened)
for module in modules_after:
if getattr(module, "__spec__", None) is None:
continue
if module.__spec__.cached in dep_files:
dep_files.discard(module.__spec__.cached)
dep_files.add(module.__spec__.origin)

if args.operation == "generate":
if args.language == "verilog":
# Generate Verilog file.
from amaranth.back import verilog
args.verilog_file.write(verilog.convert(component))

# Generate dependency file.
if args.verilog_file and args.dep_file:
args.dep_file.write(f"{args.verilog_file.name}:")
if not special_file_opened:
for file in sorted(dep_files):
args.dep_file.write(f" \\\n {file}")
args.dep_file.write("\n")
else:
args.dep_file.write(f"\n.PHONY: {args.verilog_file.name}\n")
14 changes: 6 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,24 @@ dependencies = [
]

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

[project.scripts]
amaranth = "amaranth_cli:main"
amaranth-rpc = "amaranth.rpc:main"

[tool.setuptools]
# The docstring in `amaranth_cli/__init__.py` explains why it is not under `amaranth/`.
packages = ["amaranth", "amaranth_cli"]

# Build system configuration

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

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

[tool.setuptools_scm]
local_scheme = "node-and-timestamp"

Expand Down

0 comments on commit bac83ad

Please sign in to comment.