mirror of
				https://github.com/davidgiven/fluxengine.git
				synced 2025-10-24 11:11:02 -07:00 
			
		
		
		
	
		
			
				
	
	
		
			488 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			488 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from collections.abc import Iterable, Sequence
 | |
| from os.path import *
 | |
| from types import SimpleNamespace
 | |
| import argparse
 | |
| import copy
 | |
| import functools
 | |
| import importlib
 | |
| import importlib.abc
 | |
| import importlib.util
 | |
| import inspect
 | |
| import re
 | |
| import sys
 | |
| import types
 | |
| import pathlib
 | |
| import builtins
 | |
| import os
 | |
| 
 | |
| 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 ParameterList(Sequence):
 | |
|     def __init__(self, parent=[]):
 | |
|         self.data = parent
 | |
| 
 | |
|     def __getitem__(self, i):
 | |
|         return self.data[i]
 | |
| 
 | |
|     def __len__(self):
 | |
|         return len(self.data)
 | |
| 
 | |
|     def __str__(self):
 | |
|         return " ".join(self.data)
 | |
| 
 | |
|     def __add__(self, other):
 | |
|         newdata = self.data.copy() + other
 | |
|         return ParameterList(newdata)
 | |
| 
 | |
|     def __repr__(self):
 | |
|         return f"<PList: {self.data}>"
 | |
| 
 | |
| 
 | |
| class Invocation:
 | |
|     name = None
 | |
|     callback = None
 | |
|     types = None
 | |
|     ins = None
 | |
|     outs = None
 | |
|     binding = None
 | |
| 
 | |
|     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} ({id(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 __repr__(self):
 | |
|         return "<Invocation %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 = 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.types = func.__annotations__
 | |
|         i.callback = func
 | |
|         setattr(i, func.__name__, SimpleNamespace())
 | |
| 
 | |
|         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 Targets(Type):
 | |
|     def convert(self, invocation):
 | |
|         value = self.value
 | |
|         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 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):
 | |
|     if isinstance(s, Invocation):
 | |
|         s.materialise()
 | |
|         return s
 | |
| 
 | |
|     if s in targets:
 | |
|         t = targets[s]
 | |
|         t.materialise()
 | |
|         return t
 | |
| 
 | |
|     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)
 | |
|     loadbuildfile(join(path, "build.py"))
 | |
|     if not s in targets:
 | |
|         raise ABException(f"build file at {path} doesn't contain +{target}")
 | |
|     i = targets[s]
 | |
|     i.materialise()
 | |
|     return i
 | |
| 
 | |
| 
 | |
| def targetsof(*xs, cwd):
 | |
|     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 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 stripext(path):
 | |
|     return splitext(path)[0]
 | |
| 
 | |
| 
 | |
| def emit(*args):
 | |
|     outputFp.write(" ".join(flatten(args)))
 | |
|     outputFp.write("\n")
 | |
| 
 | |
| 
 | |
| def templateexpand(s, invocation):
 | |
|     class Converter:
 | |
|         def __getitem__(self, key):
 | |
|             if key == "self":
 | |
|                 return invocation
 | |
|             f = filenamesof(invocation.args[key])
 | |
|             if isinstance(f, Sequence):
 | |
|                 f = ParameterList(f)
 | |
|             return f
 | |
| 
 | |
|     return eval("f%r" % s, invocation.callback.__globals__, Converter())
 | |
| 
 | |
| 
 | |
| def emitter_rule(name, ins, outs, deps=[]):
 | |
|     emit("")
 | |
|     emit(".PHONY:", name)
 | |
|     if outs:
 | |
|         emit(name, ":", filenamesof(outs), ";")
 | |
|         emit(filenamesof(outs), "&:", filenamesof(ins), filenamesof(deps))
 | |
|     else:
 | |
|         emit(name, "&:", filenamesof(ins), filenamesof(deps))
 | |
| 
 | |
| 
 | |
| def emitter_endrule(name):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| 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 = [],
 | |
|     outs=[],
 | |
|     deps: Targets = [],
 | |
|     commands=[],
 | |
|     label="RULE",
 | |
|     **kwargs,
 | |
| ):
 | |
|     self.ins = ins
 | |
|     self.outs = outs
 | |
|     self.deps = deps
 | |
|     emitter_rule(self.name, ins + deps, outs)
 | |
|     emitter_label(templateexpand("{label} {name}", self))
 | |
| 
 | |
|     dirs = []
 | |
|     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.name)
 | |
| 
 | |
| 
 | |
| @Rule
 | |
| def normalrule(
 | |
|     self,
 | |
|     name=None,
 | |
|     ins: Targets = [],
 | |
|     deps: Targets = [],
 | |
|     outs=[],
 | |
|     label="RULE",
 | |
|     objdir=None,
 | |
|     commands=[],
 | |
|     **kwargs,
 | |
| ):
 | |
|     objdir = objdir or join("$(OBJ)", name)
 | |
| 
 | |
|     self.normalrule.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 = []):
 | |
|     cs = []
 | |
|     self.ins = items.values()
 | |
|     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"
 | |
|             )
 | |
| 
 | |
|         emitter_rule(self.name + "+" + destf, srcs, [destf])
 | |
|         emitter_label(f"CP {destf}")
 | |
|         if dir:
 | |
|             emitter_exec(["mkdir -p " + dir])
 | |
| 
 | |
|         emitter_exec(["cp %s %s" % (srcs[0], destf)])
 | |
|         self.outs += [destf]
 | |
| 
 | |
|     emitter_rule(self.name, self.outs, [], deps)
 | |
|     emit("\t@")
 | |
| 
 | |
|     if self.outs:
 | |
|         emit("clean::")
 | |
|         emit("\t$(hide) rm -f " + (" ".join(filenamesof(self.outs))))
 | |
|     self.outs += deps
 | |
| 
 | |
|     emitter_endrule(self.name)
 | |
| 
 | |
| 
 | |
| 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]):
 | |
|         if t not in targets:
 | |
|             raise ABException("target %s is not defined" % t)
 | |
|         targets[t].materialise()
 | |
|     emit("AB_LOADED = 1\n")
 | |
| 
 | |
| 
 | |
| main()
 |