| #!/usr/bin/env python |
| # |
| # This tool queries a ninja build file for the test-suite to figure out details |
| # about the build like the sourcefiles involved in a target or the assembly |
| # files output when clang is invoked with -save-temps=obj. |
| # It comes with an additional mode that given two build directories invokes the |
| # diff tool for each pair of files. |
| # |
| # Examples: |
| # |
| # List .stats files for the build in the current directory (assuming |
| # -save-stats=obj in CFLAGS): |
| # $ tdiff.py --stats all |
| # |
| # Compare assembly files of the 176.gcc benchmark between two test-suite build |
| # directories (assuming -save-temps=obj in CFLAGS): |
| # $ tdiff.py -a path/dir_before -b path/dir_after --s_files 176.gcc | less |
| # |
| # Ninja query code based on ninja/src/browse.py (apache license version 2.0). |
| import sys |
| import subprocess |
| import argparse |
| import os |
| from collections import namedtuple |
| |
| |
| Node = namedtuple("Node", ["inputs", "rule", "target", "outputs"]) |
| |
| |
| def match_strip(line, prefix): |
| if not line.startswith(prefix): |
| return (False, line) |
| return (True, line[len(prefix) :]) |
| |
| |
| def parse(text): |
| text = text.strip() |
| lines = iter(text.split("\n")) |
| |
| rule = None |
| inputs = [] |
| outputs = [] |
| |
| try: |
| line = None |
| while True: |
| target = None |
| if line is None: |
| line = next(lines) |
| target = line[:-1] # strip trailing colon |
| |
| line = next(lines) |
| (match, rule) = match_strip(line, " input: ") |
| if match: |
| (match, line) = match_strip(next(lines), " ") |
| while match: |
| type = None |
| (match, line) = match_strip(line, "| ") |
| if match: |
| type = "implicit" |
| (match, line) = match_strip(line, "|| ") |
| if match: |
| type = "order-only" |
| inputs.append((line, type)) |
| (match, line) = match_strip(next(lines), " ") |
| |
| match, _ = match_strip(line, " outputs:") |
| if match: |
| (match, line) = match_strip(next(lines), " ") |
| while match: |
| outputs.append(line) |
| (match, line) = match_strip(next(lines), " ") |
| yield Node(inputs, rule, target, outputs) |
| except StopIteration: |
| pass |
| |
| if target is not None: |
| yield Node(inputs, rule, target, outputs) |
| |
| |
| def query_ninja(targets, cwd): |
| # Query ninja for a node in its build dependency tree. |
| proc = subprocess.Popen( |
| ["ninja", "-t", "query", *set(targets)], |
| cwd=cwd, |
| stdout=subprocess.PIPE, |
| universal_newlines=True, |
| ) |
| out, _ = proc.communicate() |
| if proc.returncode != 0: |
| raise Exception("Failed to query ninja for targets: %s" % (targets,)) |
| return parse(out) |
| |
| |
| def determine_max_commandline_len(): |
| """Determine maximum length of commandline possible""" |
| # See also http://www.in-ulm.de/~mascheck/various/argmax/ |
| sc_arg_max = os.sysconf("SC_ARG_MAX") |
| if sc_arg_max <= 0: |
| return 10000 # wild guess |
| env_len = 0 |
| for key, val in os.environ.items(): |
| env_len += len(key) + len(val) + 10 |
| return sc_arg_max - env_len |
| |
| |
| def get_inputs_rec(target, cwd): |
| worklist = [target] |
| |
| result = dict() |
| maxquerylen = determine_max_commandline_len() - 100 |
| while len(worklist) > 0: |
| querylist = [] |
| querylen = 0 |
| while len(worklist) > 0: |
| w = worklist.pop() |
| if w in result: |
| continue |
| querylen += 9 + len(w) |
| if querylen > maxquerylen: |
| break |
| querylist.append(w) |
| if querylist == []: |
| break |
| |
| queryres = query_ninja(querylist, cwd) |
| for res in queryres: |
| result[res.target] = res |
| for inp, typ in res.inputs: |
| if typ == "order-only": |
| continue |
| worklist.append(inp) |
| return result |
| |
| |
| def replace_ext(filename, newext): |
| # Note that os.path.splitext() does not work here: We want '.c.o' -> '.xxx' |
| dirname, basename = os.path.split(filename) |
| return dirname + "/" + basename.split(".", 1)[0] + newext |
| |
| |
| def filelist(mode, target, cwd, config): |
| tree = get_inputs_rec(config.target[0], cwd) |
| |
| if config.mode == "sources": |
| # Take leafs in the dependency tree |
| for target, depnode in tree.items(): |
| if len(depnode.inputs) == 0: |
| yield target |
| else: |
| # Take files ending in '.o' |
| for target, depnode in tree.items(): |
| if target.endswith(".o"): |
| # Determine .s/.stats ending used by -save-temps=obj or |
| # -save-stats=obj |
| if config.mode == "s_files": |
| target = replace_ext(target, ".s") |
| elif config.mode == "stats": |
| target = replace_ext(target, ".stats") |
| else: |
| assert config.mode == "objects" |
| yield target |
| |
| |
| def diff_file(dir0, dir1, target, config): |
| u_args = ["-u"] |
| if config.diff_U is not None: |
| u_args = ["-U" + config.diff_U] |
| files = ["%s/%s" % (dir0, target), "%s/%s" % (dir1, target)] |
| rescode = subprocess.call(["diff"] + u_args + files) |
| return rescode |
| |
| |
| def main(argv): |
| parser = argparse.ArgumentParser(prog=argv[0]) |
| parser.add_argument( |
| "-s", |
| "--s_files", |
| dest="mode", |
| action="store_const", |
| const="s_files", |
| help="Select assembly files", |
| ) |
| parser.add_argument( |
| "-i", |
| "--sources", |
| dest="mode", |
| action="store_const", |
| const="sources", |
| help="Select source files", |
| ) |
| parser.add_argument( |
| "-o", |
| "--objects", |
| dest="mode", |
| action="store_const", |
| const="objects", |
| help="Select object files", |
| ) |
| parser.add_argument( |
| "-S", |
| "--stats", |
| dest="mode", |
| action="store_const", |
| const="stats", |
| help="Select statistics files", |
| ) |
| parser.add_argument("-a", "--dir0", dest="dir0") |
| parser.add_argument("-b", "--dir1", dest="dir1") |
| parser.add_argument("-U", dest="diff_U") |
| parser.add_argument("target", metavar="TARGET", nargs=1) |
| config = parser.parse_args() |
| if config.mode is None: |
| parser.print_usage(sys.stderr) |
| sys.stderr.write("%s: error: Must specify a mode\n" % (argv[0],)) |
| sys.exit(1) |
| if (config.dir0 is None) != (config.dir1 is None): |
| sys.stderr.write("%s: error: Must specify dir0+dir1 (or none)") |
| sys.exit(1) |
| |
| files = filelist(config.mode, config.target[0], config.dir0, config) |
| |
| if config.dir0: |
| global_rc = 0 |
| for target in files: |
| rc = diff_file(config.dir0, config.dir1, target, config) |
| if rc != 0: |
| global_rc = rc |
| sys.exit(global_rc) |
| else: |
| # Simply print file list |
| for f in files: |
| print(f) |
| |
| |
| if __name__ == "__main__": |
| main(sys.argv) |