Files
jsix/scripts/bonnibel/module.py
Justin C. Miller 2ff7a0864b [build] Fix handling of cog header deps
The last commit was a bandaid, but this needed a real fix. Now we create
a .parse_deps.phony file in every module build dir that implicitly
depends on that module's dependencies' .parse_deps.phony files, as well
as order-only depends on any cog-parsed output for that module. This
causes the cog files to get generated first if they never have been, but
still leaves real header dependency tracking to clang's depfile
generation.
2022-01-08 15:34:47 -08:00

213 lines
6.7 KiB
Python

def resolve(path):
if path.startswith('/') or path.startswith('$'):
return path
from pathlib import Path
return str(Path(path).resolve())
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),
"location": (str, "jsix"),
"description": (str, None),
}
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 = []
child_deps = []
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]
child_deps.append(f"${{target_dir}}/{child.name}.dir/.parse_dep.phony")
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])
build.variable("asflags", ["${asflags}"] + [f"-I{i}" for i in includes])
if libs:
build.variable("libs", ["${libs}"] + libs)
inputs = []
parse_deps = []
for start in self.sources:
source = start
while source and source.action:
output = source.output
if source.action.rule:
build.newline()
build.build(
rule = source.action.rule,
outputs = output.input,
inputs = source.input,
implicit = list(map(resolve, source.deps)) +
list(source.action.deps),
variables = {"name": source.name},
)
elif source.action.implicit:
parse_deps.append(source.input)
else:
inputs.append(source.input)
source = output
parse_dep = "${module_dir}/.parse_dep.phony"
build.newline()
build.build(
rule = "touch",
outputs = [parse_dep],
implicit = child_deps,
order_only = parse_deps,
)
output = f"${{target_dir}}/{self.output}"
dump = f"${{target_dir}}/{self.output}.dump"
build.newline()
build.build(
rule = self.kind,
outputs = output,
inputs = inputs,
implicit = [parse_dep] + 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
s = Source(self.root, path, **kwargs)
self.sources.append(s)
return str(s.output)
def add_depends(self, paths, deps):
for source in self.sources:
if source.name in paths:
source.add_deps(deps)