mirror of
https://github.com/justinian/jsix.git
synced 2025-12-09 16:04:32 -08:00
[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:
3
scripts/bonnibel/__init__.py
Normal file
3
scripts/bonnibel/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
class BonnibelError(Exception):
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
18
scripts/bonnibel/action.py
Normal file
18
scripts/bonnibel/action.py
Normal 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
181
scripts/bonnibel/module.py
Normal 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
177
scripts/bonnibel/project.py
Normal 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")
|
||||
78
scripts/bonnibel/source.py
Normal file
78
scripts/bonnibel/source.py
Normal 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))
|
||||
50
scripts/bonnibel/target.py
Normal file
50
scripts/bonnibel/target.py
Normal 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)
|
||||
41
scripts/bonnibel/version.py
Normal file
41
scripts/bonnibel/version.py
Normal 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)
|
||||
|
||||
Reference in New Issue
Block a user