blob: d2270c656bae727a9049f7fe06eeddc4dad9da61 [file] [log] [blame]
#!/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)