mirror of
https://git.sr.ht/~kivikakk/niar
synced 2024-12-22 12:32:24 +00:00
init.
This commit is contained in:
commit
c079c08998
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
__pycache__
|
||||
/build
|
21
LICENSE
Normal file
21
LICENSE
Normal 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
9
README.md
Normal 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
26
niar/__init__.py
Normal 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
108
niar/build.py
Normal 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
197
niar/cxxrtl.py
Normal 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
7
niar/cxxrtl_platform.py
Normal 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
37
niar/logger.py
Normal 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
142
niar/project.py
Normal 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
32
niar/test_cli.py
Normal 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
24
pyproject.toml
Normal 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"
|
Loading…
Reference in a new issue