Allow for dependency chaining in *.module files by returning the expected output from a module.add_input() call.
190 lines
6.0 KiB
Python
190 lines
6.0 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),
|
|
}
|
|
|
|
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])
|
|
build.variable("asflags", ["${asflags}"] + [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.output
|
|
|
|
if source.action.rule:
|
|
build.build(
|
|
rule = source.action.rule,
|
|
outputs = output.input,
|
|
inputs = source.input,
|
|
implicit = list(map(resolve, source.deps)),
|
|
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
|
|
s = Source(self.root, path, **kwargs)
|
|
self.sources.append(s)
|
|
return str(s.output)
|