[build] Move to python build scripts per module

This change moves Bonnibel from a separate project into the jsix tree,
and alters the project configuration to be jsix-specific. (I stopped
using bonnibel for any other projects, so it's far easier to make it a
custom generator for jsix.) The build system now also uses actual python
code in `*.module` files to configure modules instead of TOML files.
Target configs (boot, kernel-mode, user-mode) now moved to separate TOML
files under `configs/` and can inherit from one another.
This commit is contained in:
Justin C. Miller
2021-08-26 01:47:58 -07:00
parent e19177d3ed
commit f79fe2e056
42 changed files with 1242 additions and 595 deletions

View File

@@ -0,0 +1,3 @@
class BonnibelError(Exception):
def __init__(self, message):
self.message = message

View File

@@ -0,0 +1,18 @@
class Action:
_action_map = {
'.c': Action_c,
'.C': Action_cxx,
'.cc': Action_cxx,
'.cpp': Action_cxx,
'.cxx': Action_cxx,
'.c++': Action_cxx,
'.s': Action_asm,
'.S': Action_asm,
'.cog': Action_cog,
}
@classmethod
def find(cls, ext):
return cls._action_map.get(ext)
class Action_c:

181
scripts/bonnibel/module.py Normal file
View File

@@ -0,0 +1,181 @@
class Module:
__fields = {
# name: (type, default)
"kind": (str, "exe"),
"output": (str, None),
"targets": (set, ()),
"deps": (set, ()),
"includes": (tuple, ()),
"sources": (tuple, ()),
"variables": (dict, ()),
"default": (bool, False),
}
def __init__(self, name, modfile, root, **kwargs):
from .source import Source
# Required fields
self.root = root
self.name = name
self.modfile = modfile
for name, data in self.__fields.items():
ctor, default = data
value = kwargs.get(name, default)
if value is not None:
value = ctor(value)
setattr(self, name, value)
for name in kwargs:
if not name in self.__fields:
raise AttributeError(f"No attribute named {name} on Module")
# Turn strings into real Source objects
self.sources = [Source(root, f) for f in self.sources]
# Filled by Module.update
self.depmods = set()
def __str__(self):
return "Module {} {}\n\t{}".format(self.kind, self.name, "\n\t".join(map(str, self.sources)))
@property
def output(self):
if self.__output is not None:
return self.__output
if self.kind == "lib":
return f"lib{self.name}.a"
else:
return f"{self.name}.elf"
@output.setter
def output(self, value):
self.__output = value
@classmethod
def update(cls, mods):
from . import BonnibelError
for mod in mods.values():
for dep in mod.deps:
if not dep in mods:
raise BonnibelError(f"module '{mod.name}' references unknown module '{dep}'")
depmod = mods[dep]
mod.depmods.add(depmod)
target_mods = [mod for mod in mods.values() if mod.targets]
for mod in target_mods:
closed = set()
children = set(mod.depmods)
while children:
child = children.pop()
closed.add(child)
child.targets |= mod.targets
children |= {m for m in child.depmods if not m in closed}
def generate(self, output):
filename = str(output / f"{self.name}.ninja")
with open(filename, "w") as buildfile:
from pathlib import Path
from ninja.ninja_syntax import Writer
build = Writer(buildfile)
build.comment("This file is automatically generated by bonnibel")
build.newline()
build.variable("module_dir", f"${{target_dir}}/{self.name}.dir")
for key, value in self.variables.items():
build.variable(key, value)
build.newline()
includes = [self.root, "${module_dir}"]
for include in self.includes:
p = Path(include)
if p.is_absolute():
if not p in includes:
includes.append(str(p.resolve()))
elif include != ".":
includes.append(str(self.root / p))
includes.append(f"${{module_dir}}/{p}")
libs = []
order_only = []
closed = set()
children = set(self.depmods)
while children:
child = children.pop()
closed.add(child)
includes += [f"${{target_dir}}/{child.name}.dir/{i}" for i in child.includes]
includes += [f"{child.root}/{i}" for i in child.includes]
if child.kind == "lib":
libs.append(f"${{target_dir}}/{child.output}")
else:
order_only.append(f"${{target_dir}}/{child.output}")
children |= {m for m in child.depmods if not m in closed}
if includes:
build.variable("ccflags", ["${ccflags}"] + [f"-I{i}" for i in includes])
if libs:
build.variable("libs", ["${libs}"] + libs)
inputs = []
implicits = []
for start in self.sources:
source = start
while source and source.action:
output = source.get_output('${module_dir}')
if source.action.rule:
build.build(
rule = source.action.rule,
outputs = output.input,
inputs = source.input,
implicit = [f'${{source_root}}/{p}' for p in source.deps or tuple()],
variables = {"name": source.name},
)
elif source.action.implicit:
implicits.append(source.input)
else:
inputs.append(source.input)
source = output
build.newline()
output = f"${{target_dir}}/{self.output}"
dump = f"${{target_dir}}/{self.output}.dump"
build.build(
rule = self.kind,
outputs = output,
inputs = inputs,
implicit = implicits + libs,
order_only = order_only,
)
build.newline()
build.build(
rule = "dump",
outputs = dump,
inputs = output,
variables = {"name": self.name},
)
if self.default:
build.newline()
build.default(output)
build.default(dump)
def add_input(self, path, **kwargs):
from .source import Source
self.sources.append(Source(self.root, path, **kwargs))

177
scripts/bonnibel/project.py Normal file
View File

@@ -0,0 +1,177 @@
from . import BonnibelError
class Project:
def __init__(self, root):
from .version import git_version
self.root = root
self.version = git_version(root)
def __str__(self):
return f"{self.name} {self.version.major}.{self.version.minor}.{self.version.patch}-{self.version.sha}"
def generate(self, root, output, modules, config, manifest):
import sys
import bonnibel
from os.path import join
from ninja.ninja_syntax import Writer
from .target import Target
targets = set()
for mod in modules.values():
targets.update({Target.load(root, t, config) for t in mod.targets})
with open(output / "build.ninja", "w") as buildfile:
build = Writer(buildfile)
build.comment("This file is automatically generated by bonnibel")
build.variable("ninja_required_version", "1.3")
build.variable("build_root", output)
build.variable("source_root", root)
build.newline()
build.include(root / "configs" / "rules.ninja")
build.newline()
build.variable("version_major", self.version.major)
build.variable("version_minor", self.version.minor)
build.variable("version_patch", self.version.patch)
build.variable("version_sha", self.version.sha)
build.newline()
for target in targets:
build.subninja(output / target.name / "target.ninja")
build.newline()
fatroot = output / "fatroot"
fatroot.mkdir(exist_ok=True)
fatroot_content = []
for line in open(manifest, 'r'):
target, name = line.split(",", 1)
target = target.strip()
name = name.strip()
if not name in modules:
raise BonnibelError(f"Manifest item '{name}' is not a known module")
mod = modules[name]
fatroot_output = f"${{build_root}}/fatroot/{mod.output}"
build.build(
rule = "strip",
outputs = [fatroot_output],
inputs = [f"${{build_root}}/{target}/{mod.output}"],
variables = {
"name": f"Installing {name}",
"debug": f"${{build_root}}/{mod.output}.debug",
})
fatroot_content.append(fatroot_output)
build.newline()
symbol_table = "${build_root}/fatroot/symbol_table.dat"
build.build(
rule = "makest",
outputs = [symbol_table],
inputs = ["${build_root}/fatroot/jsix.elf"],
)
fatroot_content.append(symbol_table)
build.newline()
bootloader = "${build_root}/fatroot/efi/boot/bootx64.efi"
build.build(
rule = "cp",
outputs = [bootloader],
inputs = ["${build_root}/boot/boot.efi"],
variables = {
"name": "Installing bootloader",
})
fatroot_content.append(bootloader)
build.newline()
build.build(
rule = "makefat",
outputs = ["${build_root}/jsix.img"],
inputs = ["${source_root}/assets/diskbase.img"],
implicit = fatroot_content,
variables = {"name": "jsix.img"},
)
build.newline()
default_assets = {
"ovmf/x64/ovmf_vars.fd": "UEFI Variables",
"debugging/jsix.elf-gdb.py": "GDB Debug Helpers",
}
for asset, name in default_assets.items():
p = root / "assets" / asset
out = f"${{build_root}}/{p.name}"
build.build(
rule = "cp",
outputs = [out],
inputs = [str(p)],
variables = {"name": name},
)
build.default([out])
build.newline()
build.rule("regen",
command = " ".join([str(root / 'configure')] + sys.argv[1:]),
description = "Regenerate build files",
generator = True,
)
build.newline()
regen_implicits = \
[f"{self.root}/configure", str(manifest)] + \
[str(mod.modfile) for mod in modules.values()]
for target in targets:
regen_implicits += target.depfiles
build.build(
rule = "regen",
outputs = ['build.ninja'],
implicit = regen_implicits,
implicit_outputs =
[f"{mod.name}.ninja" for mod in modules.values()] +
[f"{target.name}/target.ninja" for target in targets],
)
build.newline()
build.default(["${build_root}/jsix.img"])
for target in targets:
mods = [m.name for m in modules.values() if target.name in m.targets]
targetdir = output / target.name
targetdir.mkdir(exist_ok=True)
buildfilename = str(targetdir / "target.ninja")
with open(buildfilename, "w") as buildfile:
build = Writer(buildfile)
build.comment("This file is automatically generated by bonnibel")
build.newline()
build.variable("target", target.name)
build.variable("target_dir", output / target.name)
build.newline()
for name, value in target.items():
build.variable(name, value)
build.newline()
for kind in ('defs', 'run'):
for lang in ('c', 'cpp'):
deffile = str(output / target.name / f"{lang}.{kind}")
build.build(
rule = f"dump_{lang}_{kind}",
outputs = [deffile],
implicit = [buildfilename],
)
build.default(deffile)
build.newline()
for mod in mods:
build.subninja(f"{mod}.ninja")

View File

@@ -0,0 +1,78 @@
class Action:
name = property(lambda self: self.__name)
implicit = property(lambda self: False)
rule = property(lambda self: None)
def __init__(self, name):
self.__name = name
def output_of(self, path):
return None
class Compile(Action):
rule = property(lambda self: f'compile_{self.name}')
def __init__(self, name, suffix = ".o"):
super().__init__(name)
self.__suffix = suffix
def output_of(self, path):
return str(path) + self.__suffix
class Parse(Action):
rule = property(lambda self: f'parse_{self.name}')
def output_of(self, path):
suffix = "." + self.name
if path.suffix == suffix:
return path.with_suffix('')
return path
class Link(Action): pass
class Header(Action):
implicit = property(lambda self: True)
class Source:
Actions = {
'.c': Compile('c'),
'.cpp': Compile('cxx'),
'.s': Compile('asm'),
'.cog': Parse('cog'),
'.o': Link('o'),
'.h': Header('h'),
}
def __init__(self, root, path, output=None, deps=tuple()):
from pathlib import Path
self.__root = Path(root)
self.__path = Path(path)
self.__output = output
self.__deps = deps
def __str__(self):
return "{} {}:{}:{}".format(self.action, self.output, self.name, self.input)
@property
def action(self):
suffix = self.__path.suffix
return self.Actions.get(suffix)
def get_output(self, output_root):
if not self.action:
return None
path = self.__output
if path is None:
path = self.action.output_of(self.__path)
return path and Source(output_root, path)
deps = property(lambda self: self.__deps)
name = property(lambda self: str(self.__path))
input = property(lambda self: str(self.__root / self.__path))

View File

@@ -0,0 +1,50 @@
class Target(dict):
__targets = {}
@classmethod
def load(cls, root, name, config=None):
import toml
if (name, config) in cls.__targets:
return cls.__targets[(name, config)]
configs = root / "configs"
dicts = []
depfiles = []
basename = name
if config:
basename += f"-{config}"
while basename is not None:
filename = str(configs / (basename + ".toml"))
depfiles.append(filename)
desc = toml.load(filename)
basename = desc.get("extends")
dicts.append(desc.get("variables", dict()))
t = Target(name, config, depfiles)
for d in reversed(dicts):
for k, v in d.items():
if isinstance(v, (list, tuple)):
t[k] = t.get(k, list()) + list(v)
elif isinstance(v, dict):
t[k] = t.get(k, dict())
t[k].update(v)
else:
t[k] = v
cls.__targets[(name, config)] = t
return t
def __init__(self, name, config, depfiles):
self.__name = name
self.__config = config
self.__depfiles = tuple(depfiles)
def __hash__(self):
return hash((self.__name, self.__config))
name = property(lambda self: self.__name)
config = property(lambda self: self.__config)
depfiles = property(lambda self: self.__depfiles)

View File

@@ -0,0 +1,41 @@
from collections import namedtuple as _namedtuple
version = _namedtuple('version', [
'major',
'minor',
'patch',
'sha',
'dirty',
])
def _run_git(root, *args):
from subprocess import run
git = run(['git', '-C', root] + list(args),
check=True, capture_output=True)
return git.stdout.decode('utf-8').strip()
def git_version(root):
full_version = _run_git(root, 'describe', '--dirty')
full_sha = _run_git(root, 'rev-parse', 'HEAD')
dirty = False
parts1 = full_version.split('-')
if parts1[-1] == "dirty":
dirty = True
parts1 = parts1[:-1]
if parts1[0][0] == 'v':
parts1[0] = parts1[0][1:]
parts2 = parts1[0].split('.')
return version(
parts2[0],
parts2[1],
parts2[2],
full_sha[:7],
dirty)