mirror of
				https://github.com/davidgiven/fluxengine.git
				synced 2025-10-24 11:11:02 -07:00 
			
		
		
		
	
		
			
				
	
	
		
			563 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			563 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from collections.abc import Iterable, Sequence
 | |
| from os.path import *
 | |
| from types import SimpleNamespace
 | |
| import argparse
 | |
| import functools
 | |
| import importlib
 | |
| import importlib.abc
 | |
| import importlib.util
 | |
| import inspect
 | |
| import re
 | |
| import sys
 | |
| import builtins
 | |
| import string
 | |
| import fnmatch
 | |
| import traceback
 | |
| 
 | |
| defaultGlobals = {}
 | |
| targets = {}
 | |
| unmaterialisedTargets = set()
 | |
| materialisingStack = []
 | |
| outputFp = None
 | |
| cwdStack = [""]
 | |
| 
 | |
| 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)
 | |
| 
 | |
|             spec = importlib.util.spec_from_loader(
 | |
|                 name, loader, origin="built-in"
 | |
|             )
 | |
|             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)
 | |
| 
 | |
| 
 | |
| builtins.__import__ = new_import
 | |
| 
 | |
| 
 | |
| 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 Rule(func):
 | |
|     sig = inspect.signature(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 not cwd:
 | |
|             cwd = cwdStack[-1]
 | |
| 
 | |
|         if name:
 | |
|             i = Invocation()
 | |
|             if name.startswith("./"):
 | |
|                 name = join(cwd, name)
 | |
|             elif "+" not in name:
 | |
|                 name = 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
 | |
|         elif replaces:
 | |
|             i = replaces
 | |
|             name = i.name
 | |
|         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__)
 | |
| 
 | |
|         i.binding = sig.bind(name=name, self=i, **kwargs)
 | |
|         i.binding.apply_defaults()
 | |
| 
 | |
|         unmaterialisedTargets.add(i)
 | |
|         if replaces:
 | |
|             i.materialise(replacing=True)
 | |
|         return i
 | |
| 
 | |
|     defaultGlobals[func.__name__] = wrapper
 | |
|     return wrapper
 | |
| 
 | |
| 
 | |
| class Type:
 | |
|     def __init__(self, value):
 | |
|         self.value = value
 | |
| 
 | |
| 
 | |
| class List(Type):
 | |
|     def convert(self, invocation):
 | |
|         value = self.value
 | |
|         if not value:
 | |
|             return []
 | |
|         if type(value) is str:
 | |
|             return [value]
 | |
|         return list(value)
 | |
| 
 | |
| 
 | |
| 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
 | |
| 
 | |
| 
 | |
| class Target(Type):
 | |
|     def convert(self, invocation):
 | |
|         value = self.value
 | |
|         if not value:
 | |
|             return None
 | |
|         return targetof(value, cwd=invocation.cwd)
 | |
| 
 | |
| 
 | |
| 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)}")
 | |
| 
 | |
| 
 | |
| 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
 | |
| 
 | |
|     return list(recurse(xs))
 | |
| 
 | |
| 
 | |
| def fileinvocation(s):
 | |
|     i = Invocation()
 | |
|     i.name = s
 | |
|     i.outs = [s]
 | |
|     targets[s] = i
 | |
|     return i
 | |
| 
 | |
| 
 | |
| 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))
 | |
| 
 | |
|     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
 | |
| 
 | |
| 
 | |
| def targetsof(*xs, cwd=None):
 | |
|     return flatten([targetof(x, cwd) for x in flatten(xs)])
 | |
| 
 | |
| 
 | |
| 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
 | |
| 
 | |
| 
 | |
| def filenamesmatchingof(xs, pattern):
 | |
|     return fnmatch.filter(filenamesof(xs), pattern)
 | |
| 
 | |
| 
 | |
| def targetswithtraitsof(xs, trait):
 | |
|     return [target for target in targetsof(xs) if trait in target.traits]
 | |
| 
 | |
| 
 | |
| 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 filenameof(x):
 | |
|     xs = filenamesof(x)
 | |
|     if len(xs) != 1:
 | |
|         raise ABException("expected a single item")
 | |
|     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("\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 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)
 | |
| 
 | |
|     emit(
 | |
|         rule.sentinel,
 | |
|         # filenamesof(outs) if outs else [],
 | |
|         ":",
 | |
|         filenamesof(ins),
 | |
|         filenamesof(deps),
 | |
|     )
 | |
| 
 | |
| 
 | |
| 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))
 | |
|     ]
 | |
| 
 | |
| 
 | |
| @Rule
 | |
| def simplerule(
 | |
|     self,
 | |
|     name,
 | |
|     ins: Targets = None,
 | |
|     outs: List = [],
 | |
|     deps: Targets = None,
 | |
|     commands: List = [],
 | |
|     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 = []
 | |
|     for out in filenamesof(outs):
 | |
|         dir = dirname(out)
 | |
|         if dir and dir not in dirs:
 | |
|             dirs += [dir]
 | |
| 
 | |
|         cs = [("mkdir -p %s" % dir) for dir in dirs]
 | |
| 
 | |
|     for c in commands:
 | |
|         cs += [templateexpand(c, self)]
 | |
| 
 | |
|     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,
 | |
|     )
 | |
| 
 | |
| 
 | |
| @Rule
 | |
| def export(self, name=None, items: TargetsMap = {}, deps: Targets = None):
 | |
|     cs = []
 | |
|     self.ins = []
 | |
|     self.outs = []
 | |
|     for dest, src in items.items():
 | |
|         destf = filenameof(dest)
 | |
|         dir = dirname(destf)
 | |
| 
 | |
|         srcs = filenamesof(src)
 | |
|         if len(srcs) != 1:
 | |
|             raise ABException(
 | |
|                 "a dependency of an export must have exactly one output file"
 | |
|             )
 | |
| 
 | |
|         subrule = simplerule(
 | |
|             name=self.name + "/+" + destf,
 | |
|             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],
 | |
|     )
 | |
|     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("-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 outputFp
 | |
|     outputFp = open(args.output, "wt")
 | |
| 
 | |
|     for k in ("Rule", "Targets", "load", "filenamesof", "stripext"):
 | |
|         defaultGlobals[k] = globals()[k]
 | |
| 
 | |
|     global __name__
 | |
|     sys.modules["build.ab"] = sys.modules[__name__]
 | |
|     __name__ = "build.ab"
 | |
| 
 | |
|     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()
 | |
|     emit("AB_LOADED = 1\n")
 | |
| 
 | |
| 
 | |
| main()
 |