341 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			341 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
#!/usr/bin/env python3
 | 
						|
 | 
						|
import argparse
 | 
						|
import collections
 | 
						|
import dataclasses
 | 
						|
import glob
 | 
						|
import itertools
 | 
						|
import os
 | 
						|
import re
 | 
						|
 | 
						|
 | 
						|
class Config:
 | 
						|
    def __init__(self, builddir, outdir):
 | 
						|
        self.builddir = builddir
 | 
						|
        self.outdir = outdir
 | 
						|
 | 
						|
        if not os.path.exists(builddir):
 | 
						|
            os.mkdir(builddir)
 | 
						|
        if not os.path.exists(outdir):
 | 
						|
            os.mkdir(outdir)
 | 
						|
 | 
						|
    toolchain_path = f"/opt/xpack-riscv-none-elf-gcc-14.2.0-3"
 | 
						|
    toolchain_prefix = "riscv-none-elf-"
 | 
						|
    hostcflags = "-g -std=c++20 -fprofile-instr-generate -fcoverage-mapping"
 | 
						|
    hostlibs = "-lgtest -lgmock -lgtest_main"
 | 
						|
    hostldflags = "-fprofile-instr-generate -fcoverage-mapping"
 | 
						|
    include_dirs = [
 | 
						|
        "hal",
 | 
						|
        "hal/uart",
 | 
						|
        "hal/lib/common",
 | 
						|
    ]
 | 
						|
    common_flags = [
 | 
						|
        "-g",
 | 
						|
        "-Wall",
 | 
						|
        "-Wextra",
 | 
						|
        "-flto",
 | 
						|
        "-march=rv32i_zicsr",
 | 
						|
        "-ffunction-sections",
 | 
						|
        "-Oz",
 | 
						|
    ]
 | 
						|
    ldflags = [
 | 
						|
        "-Oz",
 | 
						|
        "-g",
 | 
						|
        "-Wl,--gc-sections",
 | 
						|
        "-Wl,--print-memory-usage",
 | 
						|
        "-specs=nano.specs",
 | 
						|
        "-flto",
 | 
						|
        "-march=rv32i_zicsr",
 | 
						|
    ]
 | 
						|
    project_flags = [
 | 
						|
    ]
 | 
						|
 | 
						|
    @property
 | 
						|
    def include_flags(self):
 | 
						|
        return [f"-I{os.path.relpath(i, self.builddir)}" for i in self.include_dirs]
 | 
						|
 | 
						|
    @property
 | 
						|
    def cpp_flags(self):
 | 
						|
        return ["-DNDEBUG"] + self.include_flags + self.project_flags
 | 
						|
 | 
						|
    @property
 | 
						|
    def cc_flags(self):
 | 
						|
        return self.common_flags + self.cpp_flags
 | 
						|
 | 
						|
    @property
 | 
						|
    def cxx_flags(self):
 | 
						|
        return self.common_flags + [
 | 
						|
                "-std=c++20",
 | 
						|
                "-fno-rtti",
 | 
						|
                "-fno-exceptions",
 | 
						|
                "-Wno-missing-field-initializers",
 | 
						|
                ] + self.cpp_flags
 | 
						|
 | 
						|
    def relbuild(self, path):
 | 
						|
        return os.path.relpath(path, self.builddir)
 | 
						|
 | 
						|
def gen_rules(config):
 | 
						|
    tools = {"cxx": "g++", "cc": "gcc", "as": "as", "objcopy": "objcopy"}
 | 
						|
 | 
						|
    tc_path = config.toolchain_path
 | 
						|
    tc_prefix = config.toolchain_prefix
 | 
						|
 | 
						|
    rules = f"""
 | 
						|
rule cxx
 | 
						|
  command = $cxx -MMD -MT $out -MF $out.d {' '.join(config.cxx_flags)} -c $in -o $out
 | 
						|
  description = CXX $out
 | 
						|
  depfile = $out.d
 | 
						|
  deps = gcc
 | 
						|
 | 
						|
rule cc
 | 
						|
  command = $cc -MMD -MT $out -MF $out.d {' '.join(config.cc_flags)} -c $in -o $out
 | 
						|
  description = CC $out
 | 
						|
  depfile = $out.d
 | 
						|
  deps = gcc
 | 
						|
 | 
						|
rule as
 | 
						|
  command = $as $in -o $out
 | 
						|
  description = AS $out
 | 
						|
 | 
						|
rule link
 | 
						|
  command = $cxx {' '.join(config.ldflags)} -Wl,-T$linker_script -o $out $in $libs
 | 
						|
  description = LINK $out
 | 
						|
 | 
						|
rule objcopy
 | 
						|
  command = $objcopy -O binary $in $out
 | 
						|
  description = OBJCOPY $out
 | 
						|
 | 
						|
rule hostcxx
 | 
						|
  command = clang++ -MMD -MT $out -MF $out.d {config.hostcflags} -c $in -o $out
 | 
						|
  description = HOSTCXX $out
 | 
						|
  depfile = $out.d
 | 
						|
  deps = gcc
 | 
						|
 | 
						|
rule hostlink
 | 
						|
  command = clang++ {config.hostldflags} -o $out $in {config.hostlibs}
 | 
						|
  description = HOSTLINK $out
 | 
						|
 | 
						|
rule profdata
 | 
						|
  command = llvm-profdata merge -sparse $profraw -o $out
 | 
						|
  description = PROFDATA
 | 
						|
 | 
						|
rule cov
 | 
						|
  command = llvm-cov show --output-dir cov -format html --instr-profile $profdata $objects && touch $out
 | 
						|
  description = COV
 | 
						|
 | 
						|
rule hosttest
 | 
						|
  command = LLVM_PROFILE_FILE=$in.profraw ./$in && touch $out
 | 
						|
"""
 | 
						|
 | 
						|
    for var, tool in tools.items():
 | 
						|
        toolpath = os.path.join(tc_path, "bin", f"{tc_prefix}{tool}")
 | 
						|
        yield f"{var} = {toolpath}"
 | 
						|
 | 
						|
    for line in rules.splitlines():
 | 
						|
        yield line
 | 
						|
 | 
						|
 | 
						|
def get_suffix_rule(filename, cxx_rule="cxx", cc_rule="cc"):
 | 
						|
    suffix = filename.split(".")[-1]
 | 
						|
    return collections.defaultdict(
 | 
						|
        lambda: None,
 | 
						|
        {
 | 
						|
            "c": cc_rule,
 | 
						|
            "cc": cxx_rule,
 | 
						|
            "cpp": cxx_rule,
 | 
						|
            "s": "as",
 | 
						|
        },
 | 
						|
    )[suffix]
 | 
						|
 | 
						|
 | 
						|
def make_cxx_rule(name, cflags=()):
 | 
						|
    cflags = " ".join(cflags)
 | 
						|
    rule = f"""
 | 
						|
rule {name}
 | 
						|
  command = $cxx -MMD -MT $out -MF $out.d {cflags} -c $in -o $out
 | 
						|
  description = CXX $out
 | 
						|
  depfile = $out.d
 | 
						|
  deps = gcc
 | 
						|
"""
 | 
						|
    return rule.splitlines()
 | 
						|
 | 
						|
 | 
						|
def make_cc_rule(name, cflags=()):
 | 
						|
    cflags = " ".join(cflags)
 | 
						|
    rule = f"""
 | 
						|
rule {name}
 | 
						|
  command = $cc -MMD -MT $out -MF $out.d {cflags} -c $in -o $out
 | 
						|
  description = CC $out
 | 
						|
  depfile = $out.d
 | 
						|
  deps = gcc
 | 
						|
"""
 | 
						|
    return rule.splitlines()
 | 
						|
 | 
						|
 | 
						|
@dataclasses.dataclass
 | 
						|
class SourceSet:
 | 
						|
    name: str
 | 
						|
    sources: list[str]
 | 
						|
    cflags: list[str]
 | 
						|
 | 
						|
    def get_objects(self, config):
 | 
						|
        for s in self.sources:
 | 
						|
            yield (config.relbuild(s), re.sub(r"\.\w+", ".o", s))
 | 
						|
 | 
						|
    def get_targets(self, config):
 | 
						|
        return list(zip(*self.get_objects(config)))[1]
 | 
						|
 | 
						|
 | 
						|
def source_set(name, sources, cflags=()):
 | 
						|
    return SourceSet(name, sources, cflags)
 | 
						|
 | 
						|
 | 
						|
def build_source_set(source_set):
 | 
						|
    def __f(config):
 | 
						|
        cxx_rule = "cxx"
 | 
						|
        cc_rule = "cc"
 | 
						|
        lines = []
 | 
						|
        if source_set.cflags:
 | 
						|
            cxx_rule = f"cxx_{name}"
 | 
						|
            lines += make_cxx_rule(cxx_rule, cflags=cflags + config.cxx_flags)
 | 
						|
            cc_rule = f"cc_{name}"
 | 
						|
            lines += make_cc_rule(cc_rule, cflags=cflags + config.cc_flags)
 | 
						|
        for line in lines:
 | 
						|
            yield line
 | 
						|
 | 
						|
        for i, o in source_set.get_objects(config):
 | 
						|
            rule = get_suffix_rule(i, cxx_rule=cxx_rule, cc_rule=cc_rule)
 | 
						|
            if rule is None:
 | 
						|
                continue
 | 
						|
            yield f"build {o}: {rule} {i}"
 | 
						|
 | 
						|
    return __f
 | 
						|
 | 
						|
 | 
						|
def build_image(source_set, elf_out, linker_script, dependencies=(), bin_out=None):
 | 
						|
    def __f(config):
 | 
						|
        # to make it builddir-relative
 | 
						|
        lscript = config.relbuild(linker_script)
 | 
						|
        elfout = config.relbuild(os.path.join(config.outdir, elf_out))
 | 
						|
 | 
						|
        for l in build_source_set(source_set)(config):
 | 
						|
            yield l
 | 
						|
 | 
						|
        objects = source_set.get_targets(config)
 | 
						|
        for dep in dependencies:
 | 
						|
            objects += dep.get_targets(config)
 | 
						|
        objects = " ".join(objects)
 | 
						|
 | 
						|
        yield f"build {elfout}: link {objects} | {lscript}"
 | 
						|
        yield f"  linker_script = {lscript}"
 | 
						|
        if bin_out is not None:
 | 
						|
            binout = config.relbuild(os.path.join(config.outdir, bin_out))
 | 
						|
            yield f"build {binout}: objcopy {elfout}"
 | 
						|
 | 
						|
    return __f
 | 
						|
 | 
						|
 | 
						|
def build_test(name, sources):
 | 
						|
    builds = [
 | 
						|
        (os.path.relpath(s, builddir), f"{name}_" + re.sub(r"\.\w+", ".o", s))
 | 
						|
        for s in sources
 | 
						|
    ]
 | 
						|
 | 
						|
    out = name
 | 
						|
 | 
						|
    for i, o in builds:
 | 
						|
        rule = "hostcxx"
 | 
						|
        yield f"build {o}: {rule} {i}"
 | 
						|
 | 
						|
    objects = " ".join(b[1] for b in builds)
 | 
						|
 | 
						|
    yield f"build {out}: hostlink {objects}"
 | 
						|
    yield f"build {out}.run: hosttest {out}"
 | 
						|
 | 
						|
 | 
						|
def make_coverage(binaries):
 | 
						|
    bins = " ".join(binaries)
 | 
						|
    profraw = " ".join(f"{x}.profraw" for x in binaries)
 | 
						|
    objects = " ".join(f"--object {x}" for x in binaries)
 | 
						|
    testruns = " ".join(f"{x}.run" for x in binaries)
 | 
						|
    yield f"build profdata: profdata | {testruns}"
 | 
						|
    yield f"  profraw = {profraw}"
 | 
						|
    yield f"build cov/index.html: cov {bins} | profdata"
 | 
						|
    yield f"  profdata = profdata"
 | 
						|
    yield f"  objects = {objects}"
 | 
						|
 | 
						|
 | 
						|
hal = source_set("hal", [
 | 
						|
        "hal/intc.cc",
 | 
						|
        "hal/interrupts.cc",
 | 
						|
        "hal/start.cc",
 | 
						|
        "hal/lib/common/xil_assert.c",
 | 
						|
        "hal/uart/xuartlite.c",
 | 
						|
        "hal/uart/xuartlite_stats.c",
 | 
						|
        "hal/uart/xuartlite_intr.c",
 | 
						|
        ])
 | 
						|
 | 
						|
bootloader = source_set("bootloader", glob.glob("./bootloader/**/*.cc", recursive=True))
 | 
						|
bootloader_image = build_image(
 | 
						|
    bootloader,
 | 
						|
    dependencies=[hal],
 | 
						|
    elf_out="bootloader.elf",
 | 
						|
    linker_script="bootloader/bootloader.ld",
 | 
						|
)
 | 
						|
 | 
						|
def app_image(app, sources=None):
 | 
						|
    if sources is None:
 | 
						|
        sources = glob.glob(f"./apps/{app}/**/*.cc", recursive=True)
 | 
						|
    return build_image(
 | 
						|
            source_set(app, sources),
 | 
						|
            linker_script="apps/app.ld",
 | 
						|
            dependencies=[hal],
 | 
						|
            elf_out=f"{app}.elf",
 | 
						|
            bin_out=f"{app}.bin",
 | 
						|
    )
 | 
						|
 | 
						|
all = [
 | 
						|
        build_source_set(hal),
 | 
						|
        bootloader_image,
 | 
						|
 | 
						|
        app_image("helloworld"),
 | 
						|
        app_image("timer"),
 | 
						|
        app_image("uart"),
 | 
						|
        app_image("async", sources=[
 | 
						|
            "apps/async/async.cc",
 | 
						|
            "apps/async/lock.cc",
 | 
						|
            "apps/async/main.cc",
 | 
						|
            "apps/async/trace.cc",
 | 
						|
            "apps/async/uart.cc",
 | 
						|
            ]),
 | 
						|
]
 | 
						|
 | 
						|
 | 
						|
def parse_args():
 | 
						|
    parser = argparse.ArgumentParser(description='Generate ninja build files.')
 | 
						|
    parser.add_argument('--version', required=True,
 | 
						|
                        help='version tag (typically from `git describe`)')
 | 
						|
    parser.add_argument('--build-dir', default='build', help='build directory')
 | 
						|
    parser.add_argument('--out-dir', default='out', help='output directory')
 | 
						|
    return parser.parse_args()
 | 
						|
 | 
						|
 | 
						|
def main():
 | 
						|
    args = parse_args()
 | 
						|
    config = Config(builddir=args.build_dir, outdir=args.out_dir)
 | 
						|
    config.project_flags.append(f'-DGIT_VERSION_TAG=\\"{args.version}\\"')
 | 
						|
    header = gen_rules(config)
 | 
						|
    lines = itertools.chain(header, *(f(config) for f in all))
 | 
						|
 | 
						|
    with open(os.path.join(config.builddir, "build.ninja"), "w") as f:
 | 
						|
        f.write("\n".join(lines))
 | 
						|
        f.write("\n")
 | 
						|
 | 
						|
    print(
 | 
						|
        f'Configure done. Build with "ninja -C {config.builddir}". Output will be in {config.outdir}/'
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    main()
 |