diff options
Diffstat (limited to 'build.py')
| -rwxr-xr-x | build.py | 306 |
1 files changed, 306 insertions, 0 deletions
diff --git a/build.py b/build.py new file mode 100755 index 00000000..1c6640c6 --- /dev/null +++ b/build.py @@ -0,0 +1,306 @@ +#!/usr/bin/python2.7 +import os, sys, platform, shutil +import re, threading, time, json +from os import path +from hashlib import sha1 +from multiprocessing import cpu_count + + +config_file = "build.config.py" +cache_dir = ".buildcache" +object_dir = path.join(cache_dir, "obj") +cache_file = path.join(cache_dir, "cache.json") +max_workers = cpu_count() + + +config = { + "compiler" : "gcc", + "output" : "a.out", + "source" : [ "src" ], + "include" : [], + "cflags" : [], + "lflags" : [], + "run" : "./{output}" +} + + +Hint, Warn, Error = range(3) +log_prefix = { + Hint: "\x1b[32mHint:\x1b[0m", + Warn: "\x1b[33mWarn:\x1b[0m", + Error: "\x1b[31;1mError:\x1b[0m" +} + + +log_lock = threading.Lock() + +def log(msg, mode=Hint): + log_lock.acquire() + print log_prefix[mode], msg + log_lock.release() + + +def error(msg): + log(msg, mode=Error) + os._exit(1) + + +def load_config(filename): + """ loads the given config file into the `config` global dict """ + if not path.exists(filename): + error("config file does not exist: '%s'" % filename) + + d = { + "opt": sys.argv, + "platform": platform.system(), + "error": error, + "log": log, + "Hint": Hint, + "Warn": Warn, + "Error": Error + } + execfile(filename, d) + config.update(d) + + if len(config["source"]) == 0: + error("no source directories specified in config") + + +def load_cache(cache_file): + if not path.exists(cache_file): + return { "hashes": [], "cmd": "" } + with open(cache_file) as fp: + log("loaded cache") + return json.load(fp) + + +def update_cache(cache_file, obj): + with open(cache_file, "wb") as fp: + json.dump(obj, fp, indent=2) + log("updated cache") + + +def resolve_file(filename, dir): + """ finds the actual location of an included file """ + f = path.join(dir, filename) + if path.exists(f): + return short_name(f) + + for dir in config["include"]: + f = path.join(dir, filename) + if path.exists(f): + return short_name(f) + + +file_info_cache = {} + +def get_file_info(filename): + """ returns a dict of file info for the given file """ + if filename in file_info_cache: + return file_info_cache[filename] + + hash = sha1() + includes = [] + + with open(filename) as fp: + for line in fp.readlines(): + # get includes + if "#include" in line: + match = re.match('^\s*#include\s+"(.*?)"', line) + if match: + includes.append( match.group(1) ) + # update hash + hash.update(line) + hash.update("\n") + + res = { "hash": hash.hexdigest(), "includes": includes } + file_info_cache[filename] = res + return res + + +def short_name(filename): + """ returns the filename relative to the current path """ + n = len(path.abspath(".")) + return path.abspath(filename)[n+1:] + + +def get_deep_hash(filename): + """ creates a hash from the file and all its includes """ + h = sha1() + processed = set() + files = [ resolve_file(filename, ".") ] + + while len(files) > 0: + f = files.pop() + info = get_file_info(f) + processed.add(f) + + # update hash + h.update(info["hash"]) + + # add includes + for x in info["includes"]: + resolved = resolve_file(x, path.dirname(f)) + if resolved: + if resolved not in processed: + files.append(resolved) + else: + log("could not resolve file '%s'" % x, mode=Warn) + + return h.hexdigest() + + +def build_deep_hash_dict(cfiles): + """ returns a dict mapping each cfile to its hash """ + res = {} + for f in cfiles: + res[f] = get_deep_hash(f) + return res + + +def get_cfiles(): + """ returns all .h and .c files in source directories """ + res = [] + for dir in config["source"]: + for root, dirs, files in os.walk(dir): + for file in files: + if file.endswith((".c", ".h")): + f = path.join(root, file) + res.append( short_name(f) ) + return res + + +def build_compile_cmd(): + """ creates the command used to compile files """ + lst = [ + config["compiler"], + " ".join(map(lambda x: "-I" + x, config["include"])), + " ".join(config["cflags"]), + "-c", "{infile}", "-o", "{outfile}" + ] + return " ".join(lst) + + +def obj_name(filename): + """ creates the object file name for a given filename """ + filename = re.sub("[^\w]+", "_", filename) + return filename[:-2] + "_" + sha1(filename).hexdigest()[:8] + ".o" + + +def compile(cmd, filename): + """ compiles the given file into an object file using the cmd """ + log("compiling '%s'" % filename) + + outfile = path.join(object_dir, obj_name(filename)) + + res = os.system(cmd.format(infile=filename, outfile=outfile)) + if res != 0: + error("failed to compile '%s'" % filename) + + +def link(): + """ links objects and outputs the final binary """ + log("linking") + lst = [ + config["compiler"], + "-o", config["output"], + path.join(object_dir, "*"), + " ".join(config["lflags"]) + ] + cmd = " ".join(lst) + res = os.system(cmd) + if res != 0: + error("failed to link") + + +def parallel(func, workers=4): + """ runs func on multiple threads and waits for them all to finish """ + threads = [] + for i in range(workers): + t = threading.Thread(target=func) + threads.append(t) + t.start() + for t in threads: + t.join() + + + +if __name__ == "__main__": + + start_time = time.time() + + load_config(config_file) + run_at_exit = False + output_dir = path.join(".", path.dirname(config["output"])) + cache = load_cache(cache_file) + cmd = build_compile_cmd() + + if "run" in sys.argv: + run_at_exit = True + + if cache["cmd"] != cmd: + sys.argv.append("clean") + + if "clean" in sys.argv: + log("performing clean build") + shutil.rmtree(cache_dir, ignore_errors=True) + cache = load_cache(cache_file) + + + if not path.exists(object_dir): + os.makedirs(object_dir) + + if not path.exists(output_dir): + os.makedirs(output_dir) + + + if "pre" in config: + config["pre"]() + + + cfiles = get_cfiles() + hashes = build_deep_hash_dict(cfiles) + + + # delete object files for cfiles that no longer exist + obj_files = set(map(obj_name, cfiles)) + for f in os.listdir(object_dir): + if f not in obj_files: + os.remove(path.join(object_dir, f)) + + + # build list of all .c files that need compiling + pending = [] + for f in cfiles: + if f.endswith(".c"): + if f not in cache["hashes"] or cache["hashes"][f] != hashes[f]: + pending.append(f) + + + # compile files until there are none left + def worker(): + while True: + try: + f = pending.pop() + except: + break + compile(cmd, f) + + + parallel(worker, workers=max_workers) + + + link() + update_cache(cache_file, { "hashes": hashes, "cmd": cmd }) + + if "post" in config: + config["post"]() + + + log("done [%.2fs]" % (time.time() - start_time)) + + + if run_at_exit: + log("running") + cmd = config["run"].format(output=config["output"]) + os.system(cmd) |
