Update ab.

This commit is contained in:
David Given
2024-09-02 23:51:03 +02:00
parent 3c3d8d080c
commit af0ce4cf35
15 changed files with 563 additions and 570 deletions

View File

@@ -72,7 +72,7 @@ all: +all README.md
binaries: all
tests: all
README.md: $(OBJ)/scripts/+mkdocindex/+mkdocindex$(EXT)
README.md: $(OBJ)/scripts/+mkdocindex/mkdocindex$(EXT)
@echo MKDOC $@
@csplit -s -f$(OBJ)/README. README.md '/<!-- FORMATSSTART -->/' '%<!-- FORMATSEND -->%'
@(cat $(OBJ)/README.00 && $< && cat $(OBJ)/README.01) > README.md

View File

@@ -50,6 +50,5 @@ build-files = $(shell find . -name 'build.py') $(wildcard build/*.py) $(wildcard
$(OBJ)/build.mk: Makefile $(build-files)
@echo "AB"
@mkdir -p $(OBJ)
$(hide) $(PYTHON) -X pycache_prefix=$(OBJ) build/ab.py $(patsubst %,-t %,$(TARGETS)) -o $@ \
build.py || rm -f $@
$(hide) $(PYTHON) -X pycache_prefix=$(OBJ) build/ab.py -o $@ build.py \
|| rm -f $@

View File

@@ -1,136 +1,81 @@
from collections.abc import Iterable, Sequence
from os.path import *
from types import SimpleNamespace
from pathlib import Path
from typing import Iterable
import argparse
import builtins
from copy import copy
import functools
import importlib
import importlib.abc
import importlib.util
from importlib.machinery import (
SourceFileLoader,
PathFinder,
ModuleSpec,
)
import inspect
import re
import sys
import builtins
import string
import fnmatch
import traceback
import sys
defaultGlobals = {}
targets = {}
unmaterialisedTargets = set()
materialisingStack = []
outputFp = None
verbose = False
quiet = False
cwdStack = [""]
targets = {}
unmaterialisedTargets = {} # dict, not set, to get consistent ordering
materialisingStack = []
defaultGlobals = {}
sys.path += ["."]
old_import = builtins.__import__
def new_import(name, *args, **kwargs):
if name not in sys.modules:
path = name.replace(".", "/") + ".py"
if isfile(path):
sys.stderr.write(f"loading {path}\n")
loader = importlib.machinery.SourceFileLoader(name, path)
class PathFinderImpl(PathFinder):
def find_spec(self, fullname, path, target=None):
if not path:
path = ["."]
if len(path) != 1:
return None
spec = importlib.util.spec_from_loader(
name, loader, origin="built-in"
try:
path = relpath(path[0])
except ValueError:
return None
realpath = fullname.replace(".", "/")
buildpath = realpath + ".py"
if isfile(buildpath):
spec = importlib.util.spec_from_file_location(
name=fullname,
location=buildpath,
loader=BuildFileLoaderImpl(fullname=fullname, path=buildpath),
submodule_search_locations=[],
)
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
cwdStack.append(dirname(path))
spec.loader.exec_module(module)
cwdStack.pop()
return old_import(name, *args, **kwargs)
return spec
if isdir(realpath):
return ModuleSpec(fullname, None, origin=realpath, is_package=True)
return None
builtins.__import__ = new_import
class BuildFileLoaderImpl(SourceFileLoader):
def exec_module(self, module):
sourcepath = relpath(module.__file__)
if not quiet:
print("loading", sourcepath)
cwdStack.append(dirname(sourcepath))
super(SourceFileLoader, self).exec_module(module)
cwdStack.pop()
sys.meta_path.insert(0, PathFinderImpl())
class ABException(BaseException):
pass
class Invocation:
name = None
callback = None
types = None
ins = None
outs = None
binding = None
traits = None
attr = None
attrdeps = None
def __init__(self):
self.attr = SimpleNamespace()
self.attrdeps = SimpleNamespace()
self.traits = set()
def __eq__(self, other):
return self.name is other.name
def __hash__(self):
return id(self.name)
def materialise(self, replacing=False):
if self in unmaterialisedTargets:
if not replacing and (self in materialisingStack):
print("Found dependency cycle:")
for i in materialisingStack:
print(f" {i.name}")
print(f" {self.name}")
sys.exit(1)
materialisingStack.append(self)
# Perform type conversion to the declared rule parameter types.
try:
self.args = {}
for k, v in self.binding.arguments.items():
if k != "kwargs":
t = self.types.get(k, None)
if t:
v = t(v).convert(self)
self.args[k] = v
else:
for kk, vv in v.items():
t = self.types.get(kk, None)
if t:
vv = t(vv).convert(self)
self.args[kk] = vv
# Actually call the callback.
cwdStack.append(self.cwd)
self.callback(**self.args)
cwdStack.pop()
except BaseException as e:
print(f"Error materialising {self}: {self.callback}")
print(f"Arguments: {self.args}")
raise e
if self.outs is None:
raise ABException(f"{self.name} didn't set self.outs")
if self in unmaterialisedTargets:
unmaterialisedTargets.remove(self)
materialisingStack.pop()
def bubbleattr(self, attr, xs):
xs = targetsof(xs, cwd=self.cwd)
a = set()
if hasattr(self.attrdeps, attr):
a = getattr(self.attrdeps, attr)
for x in xs:
a.add(x)
setattr(self.attrdeps, attr, a)
def __repr__(self):
return "'%s'" % self.name
def error(message):
raise ABException(message)
def Rule(func):
@@ -139,303 +84,325 @@ def Rule(func):
@functools.wraps(func)
def wrapper(*, name=None, replaces=None, **kwargs):
cwd = None
if name:
if ("+" in name) and not name.startswith("+"):
(cwd, _) = name.split("+", 1)
if "cwd" in kwargs:
cwd = kwargs["cwd"]
del kwargs["cwd"]
if not cwd:
cwd = cwdStack[-1]
if replaces:
cwd = replaces.cwd
else:
cwd = cwdStack[-1]
if name:
i = Invocation()
if name.startswith("./"):
name = join(cwd, name)
elif "+" not in name:
name = join(cwd, "+" + name)
if name[0] != "+":
name = "+" + name
t = Target(cwd, join(cwd, name))
i.name = name
i.localname = name.split("+")[-1]
if name in targets:
raise ABException(f"target {i.name} has already been defined")
targets[name] = i
assert (
t.name not in targets
), f"target {t.name} has already been defined"
targets[t.name] = t
elif replaces:
i = replaces
name = i.name
t = replaces
else:
raise ABException("you must supply either 'name' or 'replaces'")
i.cwd = cwd
i.sentinel = "$(OBJ)/.sentinels/" + name + ".mark"
i.types = func.__annotations__
i.callback = func
i.traits.add(func.__name__)
t.cwd = cwd
t.types = func.__annotations__
t.callback = func
t.traits.add(func.__name__)
i.binding = sig.bind(name=name, self=i, **kwargs)
i.binding.apply_defaults()
t.binding = sig.bind(name=name, self=t, **kwargs)
t.binding.apply_defaults()
unmaterialisedTargets.add(i)
unmaterialisedTargets[t] = None
if replaces:
i.materialise(replacing=True)
return i
t.materialise(replacing=True)
return t
defaultGlobals[func.__name__] = wrapper
return wrapper
class Type:
def __init__(self, value):
self.value = value
class Target:
def __init__(self, cwd, name):
if verbose:
print("rule('%s', cwd='%s'" % (name, cwd))
self.name = name
self.localname = self.name.rsplit("+")[-1]
self.traits = set()
self.dir = join("$(OBJ)", name)
self.ins = []
self.outs = []
self.materialised = False
self.args = {}
def __eq__(self, other):
return self.name is other.name
class List(Type):
def convert(self, invocation):
value = self.value
if not value:
return []
if type(value) is str:
return [value]
return list(value)
def __hash__(self):
return id(self)
def __repr__(self):
return f"Target('{self.name}', {id(self)})"
class Targets(Type):
def convert(self, invocation):
value = self.value
if not value:
return []
if type(value) is str:
value = [value]
if type(value) is list:
value = targetsof(value, cwd=invocation.cwd)
return value
def templateexpand(selfi, s):
class Formatter(string.Formatter):
def get_field(self, name, a1, a2):
return (
eval(name, selfi.callback.__globals__, selfi.args),
False,
)
def format_field(self, value, format_spec):
if not value:
return ""
if type(value) == str:
return value
if isinstance(value, (set, tuple)):
value = list(value)
if type(value) != list:
value = [value]
return " ".join(
[selfi.templateexpand(f) for f in filenamesof(value)]
)
class Target(Type):
def convert(self, invocation):
value = self.value
return Formatter().format(s)
def materialise(self, replacing=False):
if self not in unmaterialisedTargets:
return
if not replacing and self in materialisingStack:
print("Found dependency cycle:")
for i in materialisingStack:
print(f" {i.name}")
print(f" {self.name}")
sys.exit(1)
materialisingStack.append(self)
# Perform type conversion to the declared rule parameter types.
try:
self.args = {}
for k, v in self.binding.arguments.items():
if k != "kwargs":
t = self.types.get(k, None)
if t:
v = t.convert(v, self)
self.args[k] = copy(v)
else:
for kk, vv in v.items():
t = self.types.get(kk, None)
if t:
vv = t.convert(v, self)
self.args[kk] = copy(vv)
self.args["name"] = self.name
self.args["dir"] = self.dir
self.args["self"] = self
# Actually call the callback.
cwdStack.append(self.cwd)
self.callback(
**{k: v for k, v in self.args.items() if k not in {"dir"}}
)
cwdStack.pop()
except BaseException as e:
print(f"Error materialising {self}: {self.callback}")
print(f"Arguments: {self.args}")
raise e
if self.outs is None:
raise ABException(f"{self.name} didn't set self.outs")
if self in unmaterialisedTargets:
del unmaterialisedTargets[self]
materialisingStack.pop()
self.materialised = True
def convert(value, target):
if not value:
return None
return targetof(value, cwd=invocation.cwd)
return target.targetof(value)
def targetof(self, value):
if isinstance(value, Path):
value = value.as_posix()
if isinstance(value, Target):
t = value
else:
if value[0] == "=":
value = join(self.dir, value[1:])
class TargetsMap(Type):
def convert(self, invocation):
value = self.value
if not value:
return {}
if type(value) is dict:
return {
k: targetof(v, cwd=invocation.cwd) for k, v in value.items()
}
raise ABException(f"wanted a dict of targets, got a {type(value)}")
if value.startswith("."):
# Check for local rule.
if value.startswith(".+"):
value = normpath(join(self.cwd, value[1:]))
# Check for local path.
elif value.startswith("./"):
value = normpath(join(self.cwd, value))
# Explicit directories are always raw files.
elif value.endswith("/"):
return self._filetarget(value)
# Anything starting with a variable expansion is always a raw file.
elif value.startswith("$"):
return self._filetarget(value)
# If this is not a rule lookup...
if "+" not in value:
# ...and if the value is pointing at a directory without a trailing /,
# it's a shorthand rule lookup.
if isdir(value):
value = value + "+" + basename(value)
# Otherwise it's an absolute file.
else:
return self._filetarget(value)
def flatten(*xs):
def recurse(xs):
for x in xs:
if isinstance(x, Iterable) and not isinstance(x, (str, bytes)):
yield from recurse(x)
else:
yield x
# At this point we have the fully qualified name of a rule.
return list(recurse(xs))
(path, target) = value.rsplit("+", 1)
value = join(path, "+" + target)
if value not in targets:
# Load the new build file.
path = join(path, "build.py")
loadbuildfile(path)
assert (
value in targets
), f"build file at '{path}' doesn't contain '+{target}' when trying to resolve '{value}'"
def fileinvocation(s):
i = Invocation()
i.name = s
i.outs = [s]
targets[s] = i
return i
t = targets[value]
def targetof(s, cwd=None):
if isinstance(s, Invocation):
s.materialise()
return s
if type(s) != str:
raise ABException("parameter of targetof is not a single target")
if s in targets:
t = targets[s]
t.materialise()
return t
if s.startswith("."):
if cwd == None:
raise ABException(
"relative target names can't be used in targetof without supplying cwd"
)
if s.startswith(".+"):
s = cwd + s[1:]
elif s.startswith("./"):
s = normpath(join(cwd, s))
def _filetarget(self, value):
if value in targets:
return targets[value]
elif s.endswith("/"):
return fileinvocation(s)
elif s.startswith("$"):
return fileinvocation(s)
if "+" not in s:
if isdir(s):
s = s + "+" + basename(s)
else:
return fileinvocation(s)
(path, target) = s.split("+", 2)
s = join(path, "+" + target)
loadbuildfile(join(path, "build.py"))
if not s in targets:
raise ABException(
f"build file at {path} doesn't contain +{target} when trying to resolve {s}"
)
i = targets[s]
i.materialise()
return i
t = Target(self.cwd, value)
t.outs = [value]
targets[value] = t
return t
def targetsof(*xs, cwd=None):
return flatten([targetof(x, cwd) for x in flatten(xs)])
class Targets:
def convert(value, target):
if not value:
return []
assert isinstance(
value, (list, tuple)
), "cannot convert non-list to Targets"
return [target.targetof(x) for x in flatten(value)]
def filenamesof(*xs):
s = []
for t in flatten(xs):
if type(t) == str:
t = normpath(t)
s += [t]
else:
s += [f for f in [normpath(f) for f in filenamesof(t.outs)]]
return s
class TargetsMap:
def convert(value, target):
if not value:
return {}
output = {k: target.targetof(v) for k, v in value.items()}
for k, v in output.items():
assert (
len(filenamesof([v])) == 1
), f"targets of a TargetsMap used as an argument of {target} with key '{k}' must contain precisely one output file, but was {filenamesof([v])}"
return output
def filenamesmatchingof(xs, pattern):
return fnmatch.filter(filenamesof(xs), pattern)
def loadbuildfile(filename):
filename = filename.replace("/", ".").removesuffix(".py")
builtins.__import__(filename)
def targetswithtraitsof(xs, trait):
return [target for target in targetsof(xs) if trait in target.traits]
def flatten(items):
def generate(xs):
for x in xs:
if isinstance(x, Iterable) and not isinstance(x, (str, bytes)):
yield from generate(x)
else:
yield x
return list(generate(items))
def targetnamesof(*xs):
s = []
for x in flatten(xs):
if type(x) == str:
x = normpath(x)
if x not in s:
s += [x]
else:
if x.name not in s:
s += [x.name]
return s
def targetnamesof(items):
if not isinstance(items, (list, tuple, set)):
error("argument of filenamesof is not a list/tuple/set")
return [t.name for t in items]
def filenamesof(items):
if not isinstance(items, (list, tuple, set)):
error("argument of filenamesof is not a list/tuple/set")
def generate(xs):
for x in xs:
if isinstance(x, Target):
yield from generate(x.outs)
else:
yield x
return list(generate(items))
def filenameof(x):
xs = filenamesof(x)
if len(xs) != 1:
raise ABException("expected a single item")
xs = filenamesof(x.outs)
assert (
len(xs) == 1
), f"tried to use filenameof() on {x} which does not have exactly one output: {x.outs}"
return xs[0]
def bubbledattrsof(x, attr):
x = targetsof(x)
alltargets = set()
pending = set(x) if isinstance(x, Iterable) else {x}
while pending:
t = pending.pop()
if t not in alltargets:
alltargets.add(t)
if hasattr(t.attrdeps, attr):
pending.update(getattr(t.attrdeps, attr))
values = []
for t in alltargets:
if hasattr(t.attr, attr):
values += getattr(t.attr, attr)
return values
def stripext(path):
return splitext(path)[0]
def emit(*args):
outputFp.write(" ".join(flatten(args)))
outputFp.write(" ".join(args))
outputFp.write("\n")
def templateexpand(s, invocation):
class Formatter(string.Formatter):
def get_field(self, name, a1, a2):
return (
eval(name, invocation.callback.__globals__, invocation.args),
False,
)
def emit_rule(name, ins, outs, cmds=[], label=None):
fins = filenamesof(ins)
fouts = filenamesof(outs)
nonobjs = [f for f in fouts if not f.startswith("$(OBJ)")]
def format_field(self, value, format_spec):
if type(self) == str:
return value
return " ".join(
[templateexpand(f, invocation) for f in filenamesof(value)]
)
return Formatter().format(s)
def emitter_rule(rule, ins, outs, deps=[]):
emit("")
emit(".PHONY:", rule.name)
emit(rule.name, ":", rule.sentinel)
if nonobjs:
emit("clean::")
emit("\t$(hide) rm -f", *nonobjs)
emit(
rule.sentinel,
# filenamesof(outs) if outs else [],
":",
filenamesof(ins),
filenamesof(deps),
)
emit(".PHONY:", name)
if outs:
emit(name, ":", *fouts)
if cmds:
emit(*fouts, "&:", *fins)
else:
emit(*fouts, ":", *fins)
if label:
emit("\t$(hide)", "$(ECHO)", label)
for c in cmds:
emit("\t$(hide)", c)
else:
assert len(cmds) == 0, "rules with no outputs cannot have commands"
emit(name, ":", *fins)
def emitter_endrule(rule, outs):
emit("\t$(hide) mkdir -p", dirname(rule.sentinel))
emit("\t$(hide) touch", rule.sentinel)
for f in filenamesof(outs):
emit(".SECONDARY:", f)
emit(f, ":", rule.sentinel, ";")
def emitter_label(s):
emit("\t$(hide)", "$(ECHO)", s)
def emitter_exec(cs):
for c in cs:
emit("\t$(hide)", c)
def unmake(*ss):
return [
re.sub(r"\$\(([^)]*)\)", r"$\1", s) for s in flatten(filenamesof(ss))
]
emit("")
@Rule
def simplerule(
self,
name,
ins: Targets = None,
outs: List = [],
deps: Targets = None,
commands: List = [],
ins: Targets = [],
outs: Targets = [],
deps: Targets = [],
commands=[],
label="RULE",
**kwargs,
):
self.ins = ins
self.outs = outs
self.deps = deps
emitter_rule(self, ins + deps, outs)
emitter_label(templateexpand("{label} {name}", self))
dirs = []
cs = []
@@ -447,100 +414,69 @@ def simplerule(
cs = [("mkdir -p %s" % dir) for dir in dirs]
for c in commands:
cs += [templateexpand(c, self)]
cs += [self.templateexpand(c)]
emitter_exec(cs)
emitter_endrule(self, outs)
@Rule
def normalrule(
self,
name=None,
ins: Targets = None,
deps: Targets = None,
outs: List = [],
label="RULE",
objdir=None,
commands: List = [],
**kwargs,
):
objdir = objdir or join("$(OBJ)", name)
self.attr.objdir = objdir
simplerule(
replaces=self,
ins=ins,
deps=deps,
outs=[join(objdir, f) for f in outs],
label=label,
commands=commands,
**kwargs,
emit_rule(
name=self.name,
ins=ins + deps,
outs=outs,
label=self.templateexpand("{label} {name}"),
cmds=cs,
)
@Rule
def export(self, name=None, items: TargetsMap = {}, deps: Targets = None):
cs = []
self.ins = []
self.outs = []
def export(self, name=None, items: TargetsMap = {}, deps: Targets = []):
ins = []
outs = []
for dest, src in items.items():
destf = filenameof(dest)
dir = dirname(destf)
dest = self.targetof(dest)
outs += [dest]
srcs = filenamesof(src)
if len(srcs) != 1:
raise ABException(
"a dependency of an export must have exactly one output file"
)
destf = filenameof(dest)
srcs = filenamesof([src])
assert (
len(srcs) == 1
), "a dependency of an exported file must have exactly one output file"
subrule = simplerule(
name=self.name + "/+" + destf,
name=f"{self.localname}/{destf}",
cwd=self.cwd,
ins=[srcs[0]],
outs=[destf],
commands=["cp %s %s" % (srcs[0], destf)],
label="CP",
)
subrule.materialise()
emit("clean::")
emit("\t$(hide) rm -f", destf)
self.ins += [subrule]
emitter_rule(
self,
self.ins,
self.outs,
[(d.outs if d.outs else d.sentinel) for d in deps],
simplerule(
replaces=self,
ins=outs + deps,
outs=["=sentinel"],
commands=["touch {outs[0]}"],
label="EXPORT",
)
emitter_endrule(self, self.outs)
def loadbuildfile(filename):
filename = filename.replace("/", ".").removesuffix(".py")
builtins.__import__(filename)
def load(filename):
loadbuildfile(filename)
callerglobals = inspect.stack()[1][0].f_globals
for k, v in defaultGlobals.items():
callerglobals[k] = v
def main():
parser = argparse.ArgumentParser()
parser.add_argument("-v", "--verbose", action="store_true")
parser.add_argument("-q", "--quiet", action="store_true")
parser.add_argument("-o", "--output")
parser.add_argument("files", nargs="+")
parser.add_argument("-t", "--targets", action="append")
args = parser.parse_args()
if not args.targets:
raise ABException("no targets supplied")
global verbose
verbose = args.verbose
global quiet
quiet = args.quiet
global outputFp
outputFp = open(args.output, "wt")
for k in ("Rule", "Targets", "load", "filenamesof", "stripext"):
for k in ["Rule"]:
defaultGlobals[k] = globals()[k]
global __name__
@@ -550,12 +486,9 @@ def main():
for f in args.files:
loadbuildfile(f)
for t in flatten([a.split(",") for a in args.targets]):
(path, target) = t.split("+", 2)
s = join(path, "+" + target)
if s not in targets:
raise ABException("target %s is not defined" % s)
targets[s].materialise()
while unmaterialisedTargets:
t = next(iter(unmaterialisedTargets))
t.materialise()
emit("AB_LOADED = 1\n")

View File

@@ -1,21 +1,19 @@
from os.path import basename, join
from build.ab import (
ABException,
List,
Rule,
Targets,
TargetsMap,
filenameof,
filenamesmatchingof,
filenamesof,
flatten,
normalrule,
bubbledattrsof,
simplerule,
)
from build.utils import (
filenamesmatchingof,
stripext,
targetswithtraitsof,
collectattrs,
)
from os.path import *
from types import SimpleNamespace
class Toolchain:
@@ -39,16 +37,18 @@ class HostToolchain:
def cfileimpl(self, name, srcs, deps, suffix, commands, label, kind, cflags):
outleaf = stripext(basename(filenameof(srcs[0]))) + suffix
outleaf = "=" + stripext(basename(filenameof(srcs[0]))) + suffix
normalrule(
cflags = collectattrs(targets=deps, name="caller_cflags", initial=cflags)
t = simplerule(
replaces=self,
ins=srcs,
deps=deps,
outs=[outleaf],
label=label,
commands=commands,
cflags=cflags + bubbledattrsof(deps, "caller_cflags"),
cflags=cflags,
)
@@ -58,7 +58,7 @@ def cfile(
name,
srcs: Targets = None,
deps: Targets = None,
cflags: List = [],
cflags=[],
suffix=".o",
toolchain=Toolchain,
commands=None,
@@ -77,7 +77,7 @@ def cxxfile(
name,
srcs: Targets = None,
deps: Targets = None,
cflags: List = [],
cflags=[],
suffix=".o",
toolchain=Toolchain,
commands=None,
@@ -92,7 +92,11 @@ def cxxfile(
)
def findsources(name, srcs, deps, cflags, toolchain, filerule):
def findsources(name, srcs, deps, cflags, toolchain, filerule, cwd):
headers = filenamesmatchingof(srcs, "*.h")
cflags = cflags + ["-I" + dirname(h) for h in headers]
deps = deps + headers
objs = []
for s in flatten(srcs):
objs += [
@@ -102,15 +106,16 @@ def findsources(name, srcs, deps, cflags, toolchain, filerule):
deps=deps,
cflags=cflags,
toolchain=toolchain,
cwd=cwd,
)
for f in filenamesof(s)
for f in filenamesof([s])
if f.endswith(".c")
or f.endswith(".cc")
or f.endswith(".cpp")
or f.endswith(".S")
or f.endswith(".s")
]
if any(f.endswith(".o") for f in filenamesof(s)):
if any(f.endswith(".o") for f in filenamesof([s])):
objs += [s]
return objs
@@ -121,7 +126,7 @@ def cheaders(
self,
name,
hdrs: TargetsMap = None,
caller_cflags: List = None,
caller_cflags=[],
deps: Targets = None,
):
cs = []
@@ -129,27 +134,24 @@ def cheaders(
outs = []
i = 0
for dest, src in hdrs.items():
s = filenamesof(src)
if len(s) != 1:
raise ABException(
"the target of a header must return exactly one file"
)
s = filenamesof([src])
assert (
len(s) == 1
), "the target of a header must return exactly one file"
cs += ["cp {ins[" + str(i) + "]} {outs[" + str(i) + "]}"]
outs += [dest]
outs += ["=" + dest]
i = i + 1
r = normalrule(
r = simplerule(
replaces=self,
ins=ins,
outs=outs,
commands=cs,
deps=deps,
label="CHEADERS",
caller_cflags=caller_cflags + ["-I" + self.dir],
)
r.materialise()
self.attr.caller_cflags = caller_cflags + ["-I" + r.attr.objdir]
self.bubbleattr("caller_cflags", deps)
def libraryimpl(
@@ -187,27 +189,31 @@ def libraryimpl(
deps = deps + [hr]
objs = findsources(
name,
self.localname,
srcs,
targetswithtraitsof(deps, "cheaders"),
cflags + bubbledattrsof(deps, "caller_cflags"),
cflags,
toolchain,
kind,
self.cwd,
)
normalrule(
simplerule(
replaces=self,
ins=objs,
outs=[basename(name) + ".a"],
outs=[f"={self.localname}.a"],
label=label,
commands=commands,
caller_cflags=collectattrs(
targets=deps + ([hr] if hr else []), name="caller_cflags"
),
caller_ldflags=collectattrs(
targets=deps, name="caller_ldflags", initial=caller_ldflags
),
)
self.outs = self.outs + (hr.outs if hr else [])
self.traits.add("cheaders")
self.attr.caller_ldflags = caller_ldflags
self.bubbleattr("caller_ldflags", deps)
self.bubbleattr("caller_cflags", deps)
@Rule
@@ -217,10 +223,10 @@ def clibrary(
srcs: Targets = None,
deps: Targets = None,
hdrs: TargetsMap = None,
caller_cflags: List = [],
caller_ldflags: List = [],
cflags: List = [],
ldflags: List = [],
caller_cflags=[],
caller_ldflags=[],
cflags=[],
ldflags=[],
toolchain=Toolchain,
commands=None,
label=None,
@@ -254,10 +260,10 @@ def cxxlibrary(
srcs: Targets = None,
deps: Targets = None,
hdrs: TargetsMap = None,
caller_cflags: List = [],
caller_ldflags: List = [],
cflags: List = [],
ldflags: List = [],
caller_cflags=[],
caller_ldflags=[],
cflags=[],
ldflags=[],
toolchain=Toolchain,
commands=None,
label=None,
@@ -297,18 +303,20 @@ def programimpl(
kind,
):
ars = filenamesmatchingof(deps, "*.a")
deps = deps + filenamesmatchingof(srcs, "*.h")
ldflags = ldflags + bubbledattrsof(deps, "caller_ldflags")
cfiles = findsources(name, srcs, deps, cflags, toolchain, filerule)
normalrule(
cfiles = findsources(
self.localname, srcs, deps, cflags, toolchain, filerule, self.cwd
)
simplerule(
replaces=self,
ins=cfiles + ars + ars,
outs=[basename(name) + "$(EXT)"],
outs=[f"={self.localname}$(EXT)"],
deps=deps,
label=toolchain.label + label,
commands=commands,
ldflags=ldflags,
ldflags=collectattrs(
targets=deps, name="caller_ldflags", initial=ldflags
),
)
@@ -318,8 +326,8 @@ def cprogram(
name,
srcs: Targets = None,
deps: Targets = None,
cflags: List = [],
ldflags: List = [],
cflags=[],
ldflags=[],
toolchain=Toolchain,
commands=None,
label="CLINK",
@@ -349,8 +357,8 @@ def cxxprogram(
name,
srcs: Targets = None,
deps: Targets = None,
cflags: List = [],
ldflags: List = [],
cflags=[],
ldflags=[],
toolchain=Toolchain,
commands=None,
label="CXXLINK",

View File

@@ -1,4 +1,4 @@
from build.ab import Rule, emit, Target, bubbledattrsof, filenamesof
from build.ab import Rule, emit, Target, filenamesof
from types import SimpleNamespace
import os
import subprocess
@@ -14,68 +14,46 @@ HOST_PACKAGES := $(shell $(HOST_PKG_CONFIG) --list-all | cut -d' ' -f1 | sort)
)
@Rule
def package(self, name, package=None, fallback: Target = None):
emit("ifeq ($(filter %s, $(PACKAGES)),)" % package)
def _package(self, name, package, fallback, prefix=""):
emit(f"ifeq ($(filter {package}, $({prefix}PACKAGES)),)")
if fallback:
emit(f"PACKAGE_DEPS_{package} := ", filenamesof(fallback))
emit(f"{prefix}PACKAGE_DEPS_{package} := ", *filenamesof([fallback]))
emit(
f"PACKAGE_CFLAGS_{package} :=",
bubbledattrsof(fallback, "caller_cflags"),
f"{prefix}PACKAGE_CFLAGS_{package} :=",
*fallback.args.get("caller_cflags", []),
)
emit(
f"PACKAGE_LDFLAGS_{package} := ",
bubbledattrsof(fallback, "caller_ldflags"),
f"$(filter %.a, $(PACKAGE_DEPS_{package}))",
f"{prefix}PACKAGE_LDFLAGS_{package} := ",
*fallback.args.get("caller_ldflags", []),
f"$(filter %.a, $({prefix}PACKAGE_DEPS_{package}))",
)
else:
emit(f"$(error Required package '{package}' not installed.)")
emit("else")
emit(
f"PACKAGE_CFLAGS_{package} := $(shell $(PKG_CONFIG) --cflags {package})"
f"{prefix}PACKAGE_CFLAGS_{package} := $(shell $({prefix}PKG_CONFIG) --cflags {package})"
)
emit(
f"PACKAGE_LDFLAGS_{package} := $(shell $(PKG_CONFIG) --libs {package})"
f"{prefix}PACKAGE_LDFLAGS_{package} := $(shell $({prefix}PKG_CONFIG) --libs {package})"
)
emit(f"PACKAGE_DEPS_{package} :=")
emit(f"{prefix}PACKAGE_DEPS_{package} :=")
emit("endif")
emit(f"{self.name}:")
self.attr.caller_cflags = [f"$(PACKAGE_CFLAGS_{package})"]
self.attr.caller_ldflags = [f"$(PACKAGE_LDFLAGS_{package})"]
self.args["caller_cflags"] = [f"$({prefix}PACKAGE_CFLAGS_{package})"]
self.args["caller_ldflags"] = [f"$({prefix}PACKAGE_LDFLAGS_{package})"]
self.traits.add("clibrary")
self.traits.add("cheaders")
self.ins = []
self.outs = [f"$(PACKAGE_DEPS_{package})"]
self.outs = [f"$({prefix}PACKAGE_DEPS_{package})"]
@Rule
def package(self, name, package=None, fallback: Target = None):
_package(self, name, package, fallback)
@Rule
def hostpackage(self, name, package=None, fallback: Target = None):
emit("ifeq ($(filter %s, $(HOST_PACKAGES)),)" % package)
if fallback:
emit(
f"HOST_PACKAGE_CFLAGS_{package} :=",
bubbledattrsof(fallback, "caller_cflags"),
)
emit(
f"HOST_PACKAGE_LDFLAGS_{package} := ",
bubbledattrsof(fallback, "caller_ldflags"),
)
emit(f"HOST_PACKAGE_DEP_{package} := ", fallback.name)
else:
emit(f"$(error Required host package '{package}' not installed.)")
emit("else")
emit(
f"HOST_PACKAGE_CFLAGS_{package} := $(shell $(HOST_PKG_CONFIG) --cflags {package})"
)
emit(
f"HOST_PACKAGE_LDFLAGS_{package} := $(shell $(HOST_PKG_CONFIG) --libs {package})"
)
emit(f"HOST_PACKAGE_DEP_{package} := ")
emit("endif")
self.attr.caller_cflags = [f"$(HOST_PACKAGE_CFLAGS_{package})"]
self.attr.caller_ldflags = [f"$(HOST_PACKAGE_LDFLAGS_{package})"]
self.ins = []
self.outs = [f"$(HOST_PACKAGE_DEP_{package})"]
_package(self, name, package, fallback, "HOST_")

View File

@@ -1,17 +1,9 @@
from os.path import join
from build.ab import (
Rule,
Targets,
emit,
normalrule,
filenamesof,
filenamesmatchingof,
bubbledattrsof,
targetswithtraitsof,
)
from build.ab import Rule, Targets, emit, simplerule, filenamesof
from build.utils import filenamesmatchingof, collectattrs
from build.c import cxxlibrary
from types import SimpleNamespace
from build.pkg import package
from os.path import join
import build.pkg # to get the protobuf package check
emit(
"""
@@ -22,52 +14,51 @@ endif
"""
)
lib = package(name="protobuf_lib", package="protobuf")
@Rule
def proto(self, name, srcs: Targets = None, deps: Targets = None):
normalrule(
def proto(self, name, srcs: Targets = [], deps: Targets = []):
simplerule(
replaces=self,
ins=srcs,
outs=[f"{name}.descriptor"],
outs=[f"={name}.descriptor"],
deps=deps,
commands=[
"$(PROTOC) --include_source_info --descriptor_set_out={outs[0]} {ins}"
],
label="PROTO",
protosrcs=filenamesof(srcs),
)
self.attr.protosrcs = filenamesof(srcs)
self.bubbleattr("protosrcs", deps)
@Rule
def protocc(self, name, srcs: Targets = None, deps: Targets = None):
def protocc(self, name, srcs: Targets = [], deps: Targets = []):
outs = []
protos = []
for f in filenamesmatchingof(bubbledattrsof(srcs, "protosrcs"), "*.proto"):
allsrcs = collectattrs(targets=srcs, name="protosrcs")
assert allsrcs, "no sources provided"
for f in filenamesmatchingof(allsrcs, "*.proto"):
cc = f.replace(".proto", ".pb.cc")
h = f.replace(".proto", ".pb.h")
protos += [f]
srcs += [f]
outs += [cc, h]
outs += ["=" + cc, "=" + h]
srcname = f"{name}_srcs"
objdir = join("$(OBJ)", srcname)
r = normalrule(
name=srcname,
r = simplerule(
name=f"{self.localname}_srcs",
cwd=self.cwd,
ins=protos,
outs=outs,
deps=deps,
commands=["$(PROTOC) --cpp_out={self.attr.objdir} {ins}"],
commands=["$(PROTOC) --cpp_out={dir} {ins}"],
label="PROTOCC",
)
headers = {f: join(objdir, f) for f in outs if f.endswith(".pb.h")}
headers = {f[1:]: join(r.dir, f[1:]) for f in outs if f.endswith(".pb.h")}
cxxlibrary(
replaces=self,
srcs=[r],
deps=targetswithtraitsof(deps, "cheaders") + [lib],
deps=deps,
hdrs=headers,
)

View File

@@ -1,13 +1,62 @@
from build.ab import Rule, normalrule, Target, filenameof, Targets
from os.path import basename
from build.ab import (
Rule,
Target,
Targets,
filenameof,
filenamesof,
cwdStack,
error,
simplerule,
)
from os.path import relpath, splitext, join, basename
from glob import glob
import fnmatch
import itertools
def filenamesmatchingof(xs, pattern):
return fnmatch.filter(filenamesof(xs), pattern)
def stripext(path):
return splitext(path)[0]
def targetswithtraitsof(xs, trait):
return [t for t in xs if trait in t.traits]
def collectattrs(*, targets, name, initial=[]):
s = set(initial)
for a in [t.args.get(name, []) for t in targets]:
s.update(a)
return list(s)
def itemsof(pattern, root=None, cwd=None):
if not cwd:
cwd = cwdStack[-1]
if not root:
root = "."
pattern = join(cwd, pattern)
root = join(cwd, root)
result = {}
for f in glob(pattern, recursive=True):
try:
result[relpath(f, root)] = f
except ValueError:
error(f"file '{f}' is not in root '{root}'")
return result
@Rule
def objectify(self, name, src: Target, symbol):
normalrule(
simplerule(
replaces=self,
ins=["build/_objectify.py", src],
outs=[basename(filenameof(src)) + ".h"],
outs=[f"={basename(filenameof(src))}.h"],
commands=["$(PYTHON) {ins[0]} {ins[1]} " + symbol + " > {outs}"],
label="OBJECTIFY",
)
@@ -24,19 +73,19 @@ def test(
label="TEST",
):
if command:
normalrule(
simplerule(
replaces=self,
ins=[command],
outs=["sentinel"],
outs=["=sentinel"],
commands=["{ins[0]}", "touch {outs}"],
deps=deps,
label=label,
)
else:
normalrule(
simplerule(
replaces=self,
ins=ins,
outs=["sentinel"],
outs=["=sentinel"],
commands=commands + ["touch {outs}"],
deps=deps,
label=label,

37
build/zip.py Normal file
View File

@@ -0,0 +1,37 @@
from build.ab import (
Rule,
simplerule,
TargetsMap,
filenameof,
emit,
)
emit(
"""
ZIP ?= zip
ZIPNOTE ?= zipnote
"""
)
@Rule
def zip(
self, name, flags="", items: TargetsMap = {}, extension="zip", label="ZIP"
):
cs = ["rm -f {outs[0]}"]
ins = []
for k, v in items.items():
cs += [
"cat %s | $(ZIP) -q %s {outs[0]} -" % (filenameof(v), flags),
"echo '@ -\\n@=%s\\n' | $(ZIPNOTE) -w {outs[0]}" % k,
]
ins += [v]
simplerule(
replaces=self,
ins=ins,
outs=[f"={self.localname}." + extension],
commands=cs,
label=label,
)

View File

@@ -1,4 +1,4 @@
from build.ab import normalrule, simplerule
from build.ab import simplerule, simplerule
from build.utils import objectify
from build.c import clibrary
@@ -14,10 +14,10 @@ clibrary(
},
)
normalrule(
simplerule(
name="fluxengine_iconset",
ins=["./icon.png"],
outs=["fluxengine.iconset"],
outs=["=fluxengine.iconset"],
commands=[
"mkdir -p {outs[0]}",
"sips -z 64 64 {ins[0]} --out {outs[0]}/icon_32x32@2x.png > /dev/null",
@@ -25,18 +25,18 @@ normalrule(
label="ICONSET",
)
normalrule(
simplerule(
name="fluxengine_icns",
ins=[".+fluxengine_iconset"],
outs=["fluxengine.icns"],
outs=["=fluxengine.icns"],
commands=["iconutil -c icns -o {outs[0]} {ins[0]}"],
label="ICONUTIL",
)
normalrule(
simplerule(
name="fluxengine_ico",
ins=["./icon.png"],
outs=["fluxengine.ico"],
outs=["=fluxengine.ico"],
commands=["png2ico {outs[0]} {ins[0]}"],
label="MAKEICON",
)

View File

@@ -24,5 +24,5 @@ proto(
protocc(
name="config_proto_lib",
srcs=[".+common_proto", ".+config_proto", "arch+arch_proto"],
srcs=[".+common_proto", ".+config_proto", "arch+arch_proto", "+fl2_proto"]
)

View File

@@ -1,4 +1,4 @@
from build.ab import Rule, normalrule, Targets, TargetsMap
from build.ab import Rule, simplerule, Targets, TargetsMap
from build.c import cxxprogram, HostToolchain
encoders = {}
@@ -24,10 +24,10 @@ def protoencode_single(self, name, srcs: Targets, proto, symbol):
r = encoders[proto]
r.materialise()
normalrule(
simplerule(
replaces=self,
ins=srcs,
outs=[f"{name}.cc"],
outs=[f"={name}.cc"],
deps=[r],
commands=["{deps[0]} {ins} {outs} " + symbol],
label="PROTOENCODE",
@@ -46,10 +46,10 @@ def protoencode(self, name, proto, srcs: TargetsMap, symbol):
for k, v in srcs.items()
]
normalrule(
simplerule(
replaces=self,
ins=encoded,
outs=[name + ".cc"],
outs=[f"={name}.cc"],
commands=["cat {ins} > {outs}"],
label="CONCAT",
)

View File

@@ -1,4 +1,4 @@
from build.ab import normalrule, export
from build.ab import simplerule, export
from build.c import cxxlibrary
from scripts.build import protoencode
@@ -41,11 +41,11 @@ formats = [
"zilogmcz",
]
normalrule(
simplerule(
name="table_cc",
ins=[f"./{name}.textpb" for name in formats],
deps=["scripts/mktable.sh"],
outs=["table.cc"],
outs=["=table.cc"],
commands=[
"sh scripts/mktable.sh formats " + (" ".join(formats)) + " > {outs}"
],
@@ -68,10 +68,10 @@ cxxlibrary(
export(
name="docs",
items={
f"doc/disk-{f}.md": normalrule(
f"doc/disk-{f}.md": simplerule(
name=f"{f}_doc",
ins=["scripts+mkdoc"],
outs=[f"disk-{f}.md"],
outs=[f"=disk-{f}.md"],
commands=["{ins[0]} " + f + " | tr -d '\\r' > {outs[0]}"],
label="MKDOC",
)

View File

@@ -1,4 +1,4 @@
from build.ab import emit, normalrule
from build.ab import emit, simplerule
from build.c import cxxprogram
import config
@@ -18,10 +18,10 @@ endif
extrasrcs = []
if config.windows:
extrasrcs += [
normalrule(
simplerule(
name="rc",
ins=["./windres.rc"],
outs=["rc.o"],
outs=["=rc.o"],
deps=["./manifest.xml", "extras+fluxengine_ico"],
commands=["$(WINDRES) {ins[0]} {outs[0]}"],
label="WINDRES",
@@ -72,24 +72,24 @@ cxxprogram(
)
if config.osx:
normalrule(
simplerule(
name="fluxengine_pkg",
ins=[".+fluxengine_app"],
outs=["FluxEngine.pkg"],
outs=["=FluxEngine.pkg"],
commands=[
"pkgbuild --quiet --install-location /Applications --component {ins[0]} {outs[0]}"
],
label="PKGBUILD",
)
normalrule(
simplerule(
name="fluxengine_app",
ins=[
".+gui",
"extras+fluxengine_icns",
"extras/FluxEngine.app.template/",
],
outs=["FluxEngine.app"],
outs=["=FluxEngine.app"],
commands=[
"rm -rf {outs[0]}",
"cp -a {ins[2]} {outs[0]}",

View File

@@ -1,4 +1,4 @@
from build.ab import normalrule
from build.ab import simplerule
from build.c import cxxlibrary
from scripts.build import protoencode
@@ -8,11 +8,11 @@ drivetypes = [
"apple2",
]
normalrule(
simplerule(
name="drivetypes_table_cc",
ins=[f"./{name}.textpb" for name in drivetypes],
deps=["scripts/mktable.sh"],
outs=["table.cc"],
outs=["=table.cc"],
commands=[
"sh scripts/mktable.sh drivetypes "
+ (" ".join(drivetypes))

View File

@@ -7,9 +7,7 @@ from scripts.build import protoencode_single
proto(
name="test_proto",
srcs=[
"./testproto.proto",
],
srcs=["./testproto.proto"],
)
protocc(