1
0
Fork 0
mirror of https://git.sr.ht/~kivikakk/niar synced 2024-12-22 07:02:23 +00:00
This commit is contained in:
Asherah Connor 2024-06-13 21:51:30 +03:00
commit c079c08998
11 changed files with 605 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
__pycache__
/build

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
Copyright (C) 2024 Asherah Connor
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

9
README.md Normal file
View file

@ -0,0 +1,9 @@
# niar
A small framework for building projects with [Amaranth].
See the [template project] for usage.
[Amaranth]: https://amaranth-lang.org/
[template project]: https://github.com/kivikakk/niar-template

26
niar/__init__.py Normal file
View file

@ -0,0 +1,26 @@
from argparse import ArgumentParser
from . import build, cxxrtl
from .cxxrtl_platform import CxxrtlPlatform
from .project import Project
__all__ = ["Project", "cli", "CxxrtlPlatform"]
def cli(np: Project):
parser = ArgumentParser(prog=np.name)
subparsers = parser.add_subparsers(required=True)
build.add_arguments(
np,
subparsers.add_parser(
"build", help="build the design, and optionally program it"
),
)
if getattr(np, "cxxrtl_targets"):
cxxrtl.add_arguments(
np, subparsers.add_parser("cxxrtl", help="run the C++ simulator tests")
)
args = parser.parse_args()
args.func(args)

108
niar/build.py Normal file
View file

@ -0,0 +1,108 @@
import importlib
import inspect
import logging
import re
from functools import partial
from typing import Optional
from amaranth.build import Platform
from .logger import logger, logtime
from .project import Project
__all__ = ["add_arguments"]
def add_arguments(np: Project, parser):
parser.set_defaults(func=partial(main, np))
match sorted(t.__name__ for t in np.targets):
case []:
raise RuntimeError("no buildable targets defined")
case [first, *rest]:
parser.add_argument(
"-b",
"--board",
choices=[first, *rest],
help="which board to build for",
required=bool(rest),
**({"default": first} if not rest else {}),
)
parser.add_argument(
"-p",
"--program",
action="store_true",
help="program the design onto the board after building",
)
parser.add_argument(
"-v",
"--verilog",
action="store_true",
help="output debug Verilog",
)
def main(np: Project, args):
logger.info("building %s for %s", np.name, args.board)
platform = np.target_by_name(args.board)
design = construct_top(np, platform)
with logtime(logging.DEBUG, "elaboration"):
plan = platform.prepare(
design,
np.name,
debug_verilog=args.verilog,
yosys_opts="-g",
)
fn = f"{np.name}.il"
size = len(plan.files[fn])
logger.debug(f"{fn!r}: {size:,} bytes")
with logtime(logging.DEBUG, "synthesis/pnr"):
products = plan.execute_local("build")
if args.program:
with logtime(logging.DEBUG, "programming"):
platform.toolchain_program(products, np.name)
heading = re.compile(r"^\d+\.\d+\. Printing statistics\.$", flags=re.MULTILINE)
next_heading = re.compile(r"^\d+\.\d+\. ", flags=re.MULTILINE)
log_file_between(logging.INFO, f"build/{np.name}.rpt", heading, next_heading)
logger.info("Device utilisation:")
heading = re.compile(r"^Info: Device utilisation:$", flags=re.MULTILINE)
next_heading = re.compile(r"^Info: Placed ", flags=re.MULTILINE)
log_file_between(
logging.INFO, f"build/{np.name}.tim", heading, next_heading, prefix="Info: "
)
def construct_top(np: Project, platform: Platform, **kwargs):
sig = inspect.signature(np.top)
if "platform" in sig.parameters:
kwargs["platform"] = platform
return np.top(**kwargs)
def log_file_between(
level: int,
path: str,
start: re.Pattern,
end: re.Pattern,
*,
prefix: Optional[str] = None,
):
with open(path, "r") as f:
for line in f:
if start.match(line):
break
else:
return
for line in f:
if end.match(line):
return
line = line.rstrip()
if prefix is not None:
line = line.removeprefix(prefix)
logger.log(level, line)

197
niar/cxxrtl.py Normal file
View file

@ -0,0 +1,197 @@
import json
import logging
import subprocess
from enum import Enum
from functools import partial
from pathlib import Path
from typing import Any
from amaranth import Elaboratable
from amaranth._toolchain.yosys import YosysBinary, find_yosys
from amaranth.back import rtlil
from .build import construct_top
from .cxxrtl_platform import CxxrtlPlatform
from .logger import logger, logtime
from .project import Project
__all__ = ["add_arguments"]
CXXFLAGS = [
"-std=c++17",
"-g",
"-pedantic",
"-Wall",
"-Wextra",
"-Wno-zero-length-array",
"-Wno-unused-parameter",
]
class _Optimize(Enum):
none = "none"
rtl = "rtl"
def __str__(self):
return self.value
@property
def opt_rtl(self) -> bool:
return self in (self.rtl,)
def add_arguments(np: Project, parser):
parser.set_defaults(func=partial(main, np))
match sorted(t.__name__ for t in np.cxxrtl_targets or []):
case []:
raise RuntimeError("no cxxrtl targets defined")
case [first, *rest]:
parser.add_argument(
"-t",
"--target",
choices=[first, *rest],
help="which CXXRTL target to build",
required=bool(rest),
**({"default": first} if not rest else {}),
)
parser.add_argument(
"-c",
"--compile",
action="store_true",
help="compile only; don't run",
)
parser.add_argument(
"-O",
"--optimize",
type=_Optimize,
choices=_Optimize,
help="build with optimizations (default: rtl)",
default=_Optimize.rtl,
)
parser.add_argument(
"-d",
"--debug",
action="store_true",
help="generate source-level debug information",
)
parser.add_argument(
"-v",
"--vcd",
action="store",
type=str,
help="output a VCD file",
)
def main(np: Project, args):
yosys = find_yosys(lambda ver: ver >= (0, 10))
platform = np.cxxrtl_target_by_name(args.target)
design = construct_top(np, platform)
cxxrtl_cc_path = np.path.build(f"{np.name}.cc")
with logtime(logging.DEBUG, "elaboration"):
_cxxrtl_convert_with_header(
yosys,
cxxrtl_cc_path,
design,
np.name,
platform,
black_boxes={},
)
cc_o_paths = {
cxxrtl_cc_path: np.path.build(f"{np.name}.o"),
}
for path in np.path("cxxrtl").glob("*.cc"):
cc_o_paths[path] = np.path.build(f"{path.stem}.o")
cxxflags = CXXFLAGS + [
f"-DCLOCK_HZ={int(platform.default_clk_frequency)}",
*(["-O3"] if args.optimize.opt_rtl else ["-O0"]),
*(["-g"] if args.debug else []),
]
procs = []
compile_commands = {}
for cc_path, o_path in cc_o_paths.items():
cmd = [
"c++",
*cxxflags,
f"-I{np.path("build")}",
f"-I{yosys.data_dir() / "include" / "backends" / "cxxrtl" / "runtime"}",
"-c",
str(cc_path),
"-o",
str(o_path),
]
compile_commands[o_path] = cmd
logger.debug(" ".join(str(e) for e in cmd))
procs.append((cc_path, subprocess.Popen(cmd)))
with open(np.path.build("compile_commands.json"), "w") as f:
json.dump(
[
{
"directory": str(np.path()),
"file": str(file),
"arguments": arguments,
}
for file, arguments in compile_commands.items()
],
f,
)
failed = []
for cc_path, p in procs:
if p.wait() != 0:
failed.append(cc_path)
if failed:
logger.error("Failed to build paths:")
for p in failed:
logger.error(f"- {p}")
raise RuntimeError("failed compile step")
exe_o_path = np.path.build("cxxrtl")
cmd = [
"c++",
*cxxflags,
*cc_o_paths.values(),
"-o",
exe_o_path,
]
logger.debug(" ".join(str(e) for e in cmd))
subprocess.run(cmd, check=True)
if not args.compile:
cmd = [exe_o_path]
if args.vcd:
cmd += ["--vcd", args.vcd]
logger.debug(" ".join(str(e) for e in cmd))
subprocess.run(cmd, check=True)
def _cxxrtl_convert_with_header(
yosys: YosysBinary,
cc_out: Path,
design: Elaboratable,
name: str,
platform: CxxrtlPlatform,
*,
black_boxes: dict[Any, str],
):
if cc_out.is_absolute():
try:
cc_out = cc_out.relative_to(Path.cwd())
except ValueError:
raise AssertionError(
"cc_out must be relative to cwd for builtin-yosys to write to it"
)
rtlil_text = rtlil.convert(design, name=name, platform=platform)
script = []
for box_source in black_boxes.values():
script.append(f"read_rtlil <<rtlil\n{box_source}\nrtlil")
script.append(f"read_rtlil <<rtlil\n{rtlil_text}\nrtlil")
script.append(f"write_cxxrtl -header {cc_out}")
yosys.run(["-q", "-"], "\n".join(script))

7
niar/cxxrtl_platform.py Normal file
View file

@ -0,0 +1,7 @@
from abc import ABCMeta, abstractmethod
__all__ = ["CxxrtlPlatform"]
class CxxrtlPlatform(metaclass=ABCMeta):
default_clk_frequency = property(abstractmethod(lambda _: None))

37
niar/logger.py Normal file
View file

@ -0,0 +1,37 @@
import logging
from contextlib import contextmanager
from datetime import datetime
from typing import Optional
__all__ = ["logger", "logtime", "enable", "disable"]
logging.basicConfig(
format="[%(asctime)s] %(name)s: %(levelname)s: %(message)s",
level=logging.DEBUG,
)
logger = logging.getLogger("niar")
@contextmanager
def logtime(level: int, activity: str, /, fail_level: Optional[int] = None):
global logger
start = datetime.now()
logger.log(level, "starting %s", activity)
try:
yield
except:
finish = datetime.now()
logger.log(fail_level or level, "%s failed in %s", activity, finish - start)
raise
else:
finish = datetime.now()
logger.log(level, "%s finished in %s", activity, finish - start)
def disable():
logger.disabled = True
def enable():
logger.disabled = False

142
niar/project.py Normal file
View file

@ -0,0 +1,142 @@
import sys
from pathlib import Path
from typing import Optional
from amaranth import Elaboratable
from amaranth.build import Platform
from .cxxrtl_platform import CxxrtlPlatform
__all__ = ["Project"]
class Prop:
def __init__(
self,
name: str,
*,
description: str,
required: bool,
isinstance: Optional[type] = None,
issubclass: Optional[type] = None,
issubclass_list: Optional[type] = None,
):
self.name = name
self.description = description
self.required = required
self.isinstance = isinstance
self.issubclass = issubclass
self.issubclass_list = issubclass_list
def validate(self, project):
assert not (
self.issubclass and self.issubclass_list
), "may only define one of issubclass and issubclass_list"
if self.required:
assert hasattr(project, self.name), (
f"{project.__module__}.{project.__class__.__qualname__} is missing "
f"property {self.name!r} ({self.description})"
)
elif not hasattr(project, self.name):
return
attr = getattr(project, self.name)
if self.isinstance:
assert isinstance(attr, self.isinstance), (
f"{project.__module__}.{project.__class__.__qualname__} property "
f"{self.name!r} ({self.description}) should an instance of "
f"{self.isinstance!r}, but is {attr!r}"
)
if self.issubclass:
assert issubclass(attr, self.issubclass), (
f"{project.__module__}.{project.__class__.__qualname__} property "
f"{self.name!r} ({self.description}) should be a subclass of "
f"{self.issubclass!r}, but is {attr!r}"
)
if self.issubclass_list:
assert isinstance(attr, list)
for elem in attr:
assert issubclass(elem, self.issubclass_list), (
f"{project.__module__}.{project.__class__.__qualname__} property "
f"{self.name!r} ({self.description}) should be a list of subclasses of "
f"{self.issubclass!r}, but has element {attr!r}"
)
class Project:
name: str
top: type[Elaboratable]
targets: list[type[Platform]]
cxxrtl_targets: Optional[list[type[CxxrtlPlatform]]]
origin: Path
PROPS = [
Prop(
"name",
description="a keyword-like identifier for the project",
required=True,
isinstance=str,
),
Prop(
"top",
description="a reference to the default top-level elaboratable to be built",
required=True,
issubclass=Elaboratable,
),
Prop(
"targets",
description="a list of platform classes the elaboratable is targetted for",
required=True,
issubclass_list=Platform,
),
Prop(
"cxxrtl_targets",
description="a list of niar.CxxrtlPlatform classes the elaboratable is targetted for",
required=False,
issubclass_list=CxxrtlPlatform,
),
]
def __init_subclass__(cls):
# We expect to be called from project-root/module/__init.py__ or similar;
# self.origin is project-root.
cls.origin = Path(sys._getframe(1).f_code.co_filename).parent.parent.absolute()
extras = cls.__dict__.keys() - {"__module__", "__doc__", "origin"}
for prop in cls.PROPS:
prop.validate(cls)
extras -= {prop.name}
assert extras == set(), f"unknown project properties: {extras}"
def target_by_name(self, name: str) -> Platform:
for t in self.targets:
if t.__name__ == name:
return t()
raise KeyError(f"unknown target {name!r}")
def cxxrtl_target_by_name(self, name: str) -> CxxrtlPlatform:
for t in self.cxxrtl_targets or []:
if t.__name__ == name:
return t()
raise KeyError(f"unknown CXXRTL target {name!r}")
def main(self):
from . import cli
cli(self)
@property
def path(self):
return ProjectPath(self)
class ProjectPath:
def __init__(self, np: Project):
self.np = np
def __call__(self, *components):
return self.np.origin.joinpath(*components)
def build(self, *components):
return self("build", *components)

32
niar/test_cli.py Normal file
View file

@ -0,0 +1,32 @@
import unittest
from argparse import ArgumentParser
from amaranth import Elaboratable, Module
from amaranth_boards.icebreaker import ICEBreakerPlatform
from . import Project, build, logger
calling_test = False
class FixtureTop(Elaboratable):
def elaborate(self, platform):
return Module()
class FixtureProject(Project):
name = "fixture"
top = FixtureTop
targets = [ICEBreakerPlatform]
class TestCLI(unittest.TestCase):
def setUp(self):
logger.disable()
self.addCleanup(logger.enable)
def test_build_works(self):
parser = ArgumentParser()
build.add_arguments(FixtureProject(), parser)
args, _argv = parser.parse_known_args()
args.func(args)

24
pyproject.toml Normal file
View file

@ -0,0 +1,24 @@
[project]
name = "niar"
version = "0.1"
description = "A small framework for building projects with Amaranth"
authors = [
{name = "Asherah Connor", email = "ashe@kivikakk.ee"},
]
dependencies = [
"amaranth >= 0.4.5, < 0.6",
]
requires-python = ">=3.8"
license = {text = "BSD-2-Clause"}
[project.optional-dependencies]
build = [
"amaranth-boards", # for test
]
[project.urls]
Homepage = "https://github.com/kivikakk/niar"
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"