#!/usr/bin/python import os from pathlib import Path from threading import local import shutil import argparse def find_program(prog, required=True): path = shutil.which(prog) if path == None and required: raise Exception("Required program '%s' not found on the system" % (prog)) return File(path) ROOT = Path.cwd() BUILD = Path("bld").resolve().relative_to(ROOT) DEFDESC = lambda o, i, cmd: "%s" % (cmd) C_COMPILERS = { 'gcc': lambda: Vec( "gcc", find_program("gcc"), lambda o, i, ea: "-o %s -c %s %s" % (o, i, ea), lambda i: i.name + ".o", lambda o, i, cmd: "CC %s" % (o) ), 'clang': lambda: Vec( "clang", find_program("clang"), lambda o, i, ea: "-o %s -c %s %s" % (o, i, ea), lambda i: i.name + ".o", lambda o, i, cmd: "CC %s" % (o) ), 'cc': lambda: Vec( "cc", find_program("cc"), lambda o, i, ea: "-o %s -c %s %s" % (o, i, ea), lambda i: i.name + ".o", lambda o, i, cmd: "CC %s" % (o) ), } C_LINKERS = { 'gcc': lambda: Machine( "ld-gcc", find_program("gcc"), lambda o, i, ea: "-o %s %s %s" % (o, i, ea), lambda o, i, cmd: "LD %s" % (o) ), 'clang': lambda: Machine( "ld-clang", find_program("clang"), lambda o, i, ea: "-o %s %s %s" % (o, i, ea), lambda o, i, cmd: "LD %s" % (o) ), 'cc': lambda: Machine( "ld-cc", find_program("cc"), lambda o, i, ea: "-o %s %s %s" % (o, i, ea), lambda o, i, cmd: "LD %s" % (o) ) } def wrap_lambda(s): if callable(s): return s else: return lambda *args: s class FileMeta(type): def __call__(cls, path, *rest, **krest): # If none, return none if path == None: return None # If already a file, return it if issubclass(type(path), File): return path # Adjust name to be a path if type(path) == str: p = Path(path).resolve() if p.is_relative_to(ROOT): path = p.relative_to(ROOT) else: path = p idx = str(path) + cls.__name__ if idx in cls.fdb: return cls.fdb[idx] obj = cls.__new__(cls, path, *rest, **krest) cls.__init__(obj, path, *rest, **krest) cls.fdb[idx] = obj return obj class File(metaclass = FileMeta): fdb = dict() def __init__(self, path): # The name corresponding to # the target: this can be # a path or an abstract name self.path = path def __str__(self): return str(self.path) def resolve_inner(tgt, arg): if type(arg) == list or type(arg) == tuple: for a in arg: File.resolve_inner(tgt, a) elif type(arg) == str: tgt.append(File(arg)) elif issubclass(type(arg), File): tgt.append(arg) def resolve(*args): tgt = [] for a in args: File.resolve_inner(tgt, a) return tgt class Process(File): def __init__(self, path, machine, inputs, extra_args = None): super().__init__(path) # The generator that created this process self.machine = machine # The inputs self.inputs = inputs # The extra arguments taken self.extra_args = extra_args def gen_makefile(self): inputs = " ".join([str(x) for x in self.inputs]) extra_args = self.extra_args if self.extra_args else "" out = "%s: %s\n" % (self.path, inputs) out += "\t@$(%s) %s\n" % (self.machine.name, self.machine.mkargs("$@", "$^", extra_args)) out += "\t@echo '%s'" % (self.machine.desc("$@", "$^", extra_args)) return out def gen_ninja(self): inputs = " ".join([str(x) for x in self.inputs]) out = "build %s: %s %s" % (self.path, self.machine.name, inputs) if self.extra_args != None: out += "\n extra = %s" % (self.extra_args) return out class MachineMeta(type): def __call__(cls, name, *rest, **krest): # If already a file, return it if issubclass(type(name), Machine): return name if name in cls.mdb: return cls.mdb[name] obj = cls.__new__(cls, name, *rest, **krest) cls.__init__(obj, name, *rest, **krest) cls.mdb[name] = obj return obj class Machine(metaclass = MachineMeta): mdb = dict() def __init__(self, name, exe, mkargs, desc=DEFDESC): # The name of the machine self.name = name # Executable used in the process self.exe = File(exe) # The arguments that is used to launch the process self.mkargs = wrap_lambda(mkargs) # The description self.desc = wrap_lambda(desc) def __call__(self, name, *args, extra_args=None): return self.gen(name, *args, extra_args=extra_args) def fork(self, name, mkargs, desc=None): if not desc: desc = self.desc args = lambda o, i, ea: self.mkargs(o, i, ea) + " " + mkargs(o, i, ea) return Machine(name, self.exe, args, desc) def gen(self, name, *args, extra_args=None): inputs = File.resolve(args) path = BUILD / Path.cwd().relative_to(ROOT) / name return Process(path, self, inputs, extra_args) def gen_makefile(self): return "%s = %s" % (self.name, self.exe) def gen_ninja(self): out = "rule %s\n" % (self.name) cmd = "%s %s" % (self.exe, self.mkargs("$out", "$in", "$extra")) out += " command = %s\n" % (cmd) out += " description = %s" % (self.desc("$out", "$in", cmd)) return out class Vec(Machine): def __init__(self, name, exe, mkargs, mkout, desc=DEFDESC): super().__init__(name, exe, mkargs, desc=desc) self.mkout = wrap_lambda(mkout) def __call__(self, *args, extra_args=None): return self.gen(*args, extra_args=extra_args) def fork(self, name, mkargs, desc=None): if not desc: desc = self.desc args = lambda o, i, ea: self.mkargs(o, i, ea) + " " + wrap_lambda(mkargs)(o, i, ea) return Vec(name, self.exe, args, self.mkout, desc) def gen(self, *args, extra_args=None): procs = [] inputs = File.resolve(args) for i in inputs: path = BUILD / i.path.parent / self.mkout(i.path) procs.append(Process(path, self, [i], extra_args)) return procs def find_one(names): for name in names: program = find_program(name, required=False) if program: return name return None class System: def c_compiler(c_compilers=C_COMPILERS.keys()): c_compiler = find_one(c_compilers) return C_COMPILERS[c_compiler]() def c_linker(c_linkers=C_LINKERS.keys()): c_linker = find_one(c_linkers) return C_LINKERS[c_linker]() def subdir(dir): # Save the previous location PWD = Path.cwd() # Go into subdir sub = PWD / dir os.chdir(sub) # Set helper path CWD = Path.cwd().relative_to(ROOT) exec(open('.build').read()) # Revert the current directory; os.chdir(PWD) subdir(".") def gen_bld(): for proc in File.fdb.values(): if not issubclass(type(proc), Process): continue proc.path.parent.mkdir(parents=True, exist_ok=True) def gen_makefile(): out = "" for mach in Machine.mdb.values(): out += mach.gen_makefile() out += "\n" all = [] for proc in File.fdb.values(): if not issubclass(type(proc), Process): continue out += "\n" out += proc.gen_makefile() out += "\n" all.append(str(proc)) all = " ".join(all) out += "\n.PHONY: clean\nclean:\n\trm -f %s" % (all) return out def gen_ninja(): out = "" for mach in Machine.mdb.values(): out += mach.gen_ninja() out += "\n" out += "\n" for proc in File.fdb.values(): if not issubclass(type(proc), Process): continue out += proc.gen_ninja() out += "\n" return out parser = argparse.ArgumentParser(description="cook build system") parser.add_argument("-G", type=str, metavar="backend", help="Backend to generate for: ninja or makefile") args = parser.parse_args() backend = "ninja" if args.G: backend = args.G gen_bld() open(ROOT / BUILD / ".gitignore", "w").write("*") if backend == "ninja": open(ROOT / 'build.ninja', 'w').write(gen_ninja()) elif backend == "makefile": open(ROOT / 'makefile', 'w').write(gen_makefile()) else: print("No such backend %s" % (backend))