mirror of
https://git.sr.ht/~kivikakk/niar
synced 2024-12-22 17:32:25 +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