|  | #!/usr/bin/env python3 | 
|  | # | 
|  | # ======- check-ninja-deps - build debugging script ----*- python -*--========# | 
|  | # | 
|  | # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | 
|  | # See https://llvm.org/LICENSE.txt for license information. | 
|  | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | 
|  | # | 
|  | # ==------------------------------------------------------------------------==# | 
|  |  | 
|  | """Script to find missing formal dependencies in a build.ninja file. | 
|  |  | 
|  | Suppose you have a header file that's autogenerated by (for example) Tablegen. | 
|  | If a C++ compilation step needs to include that header, then it must be | 
|  | executed after the Tablegen build step that generates the header. So the | 
|  | dependency graph in build.ninja should have the Tablegen build step as an | 
|  | ancestor of the C++ one. If it does not, then there's a latent build-failure | 
|  | bug, because depending on the order that ninja chooses to schedule its build | 
|  | steps, the C++ build step could run first, and fail because the header it needs | 
|  | does not exist yet. | 
|  |  | 
|  | But because that kind of bug can easily be latent or intermittent, you might | 
|  | not notice, if your local test build happens to succeed. What you'd like is a | 
|  | way to detect problems of this kind reliably, even if they _didn't_ cause a | 
|  | failure on your first test. | 
|  |  | 
|  | This script tries to do that. It's specific to the 'ninja' build tool, because | 
|  | ninja has useful auxiliary output modes that produce the necessary data: | 
|  |  | 
|  | - 'ninja -t graph' emits the full DAG of formal dependencies derived from | 
|  | build.ninja (in Graphviz format) | 
|  |  | 
|  | - 'ninja -t deps' dumps the database of dependencies discovered at build time | 
|  | by finding out which headers each source file actually included | 
|  |  | 
|  | By cross-checking these two sources of data against each other, you can find | 
|  | true dependencies shown by 'deps' that are not reflected as formal dependencies | 
|  | in 'graph', i.e. a generated header that is required by a given source file but | 
|  | not forced to be built first. | 
|  |  | 
|  | To run it: | 
|  |  | 
|  | - set up a build directory using ninja as the build tool (cmake -G Ninja) | 
|  |  | 
|  | - in that build directory, run ninja to perform an actual build (populating | 
|  | the dependency database) | 
|  |  | 
|  | - then, in the same build directory, run this script. No arguments are needed | 
|  | (but -C and -f are accepted, and propagated to ninja for convenience). | 
|  |  | 
|  | Requirements outside core Python: the 'pygraphviz' module, available via pip or | 
|  | as the 'python3-pygraphviz' package in Debian and Ubuntu. | 
|  |  | 
|  | """ | 
|  |  | 
|  | import sys | 
|  | import argparse | 
|  | import subprocess | 
|  | import pygraphviz | 
|  |  | 
|  |  | 
|  | def toposort(g): | 
|  | """Topologically sort a graph. | 
|  |  | 
|  | The input g is a pygraphviz graph object representing a DAG. The function | 
|  | yields the vertices of g in an arbitrary order consistent with the edges, | 
|  | so that for any edge v->w, v is output before w.""" | 
|  |  | 
|  | # Count the number of immediate predecessors *not yet output* for each | 
|  | # vertex. Initially this is simply their in-degrees. | 
|  | ideg = {v: g.in_degree(v) for v in g.nodes_iter()} | 
|  |  | 
|  | # Set of vertices which can be output next, which is true if they have no | 
|  | # immediate predecessor that has not already been output. | 
|  | ready = {v for v, d in ideg.items() if d == 0} | 
|  |  | 
|  | # Keep outputting vertices while we have any to output. | 
|  | while len(ready) > 0: | 
|  | v = next(iter(ready)) | 
|  | yield v | 
|  | ready.remove(v) | 
|  |  | 
|  | # Having output v, find each immediate successor w, and decrement its | 
|  | # 'ideg' value by 1, to indicate that one more of its predecessors has | 
|  | # now been output. | 
|  | for w in g.out_neighbors(v): | 
|  | ideg[w] -= 1 | 
|  | if ideg[w] == 0: | 
|  | # If that counter reaches zero, w is ready to output. | 
|  | ready.add(w) | 
|  |  | 
|  |  | 
|  | def ancestors(g, translate=lambda x: x): | 
|  | """Form the set of ancestors for each vertex of a graph. | 
|  |  | 
|  | The input g is a pygraphviz graph object representing a DAG. The function | 
|  | yields a sequence of pairs (vertex, set of proper ancestors). | 
|  |  | 
|  | The vertex names are all mapped through 'translate' before output. This | 
|  | allows us to produce output referring to the label rather than the | 
|  | identifier of every vertex. | 
|  | """ | 
|  |  | 
|  | # Store the set of (translated) ancestors for each vertex so far. a[v] | 
|  | # includes (the translation of) v itself. | 
|  | a = {} | 
|  |  | 
|  | for v in toposort(g): | 
|  | vm = translate(v) | 
|  |  | 
|  | # Make up a[v], based on a[predecessors of v]. | 
|  | a[v] = {vm}  # include v itself | 
|  | for w in g.in_neighbors(v): | 
|  | a[v].update(a[w]) | 
|  |  | 
|  | # Remove v itself from the set before yielding it, so that the caller | 
|  | # doesn't get the trivial dependency of v on itself. | 
|  | yield vm, a[v].difference({vm}) | 
|  |  | 
|  |  | 
|  | def main(): | 
|  | parser = argparse.ArgumentParser( | 
|  | description="Find missing formal dependencies on generated include " | 
|  | "files in a build.ninja file." | 
|  | ) | 
|  | parser.add_argument("-C", "--build-dir", help="Build directory (default cwd)") | 
|  | parser.add_argument( | 
|  | "-f", "--build-file", help="Build directory (default build.ninja)" | 
|  | ) | 
|  | args = parser.parse_args() | 
|  |  | 
|  | errs = 0 | 
|  |  | 
|  | ninja_prefix = ["ninja"] | 
|  | if args.build_dir is not None: | 
|  | ninja_prefix.extend(["-C", args.build_dir]) | 
|  | if args.build_file is not None: | 
|  | ninja_prefix.extend(["-f", args.build_file]) | 
|  |  | 
|  | # Get the formal dependency graph and decode it using pygraphviz. | 
|  | g = pygraphviz.AGraph( | 
|  | subprocess.check_output(ninja_prefix + ["-t", "graph"]).decode("UTF-8") | 
|  | ) | 
|  |  | 
|  | # Helper function to ask for the label of a vertex, which is where ninja's | 
|  | # Graphviz output keeps the actual file name of the target. | 
|  | label = lambda v: g.get_node(v).attr["label"] | 
|  |  | 
|  | # Start by making a list of build targets, i.e. generated files. These are | 
|  | # just any graph vertex with at least one predecessor. | 
|  | targets = set(label(v) for v in g.nodes_iter() if g.in_degree(v) > 0) | 
|  |  | 
|  | # Find the set of ancestors of each graph vertex. We pass in 'label' as a | 
|  | # translation function, so that this gives us the set of ancestor _files_ | 
|  | # for a given _file_ rather than arbitrary numeric vertex ids. | 
|  | deps = dict(ancestors(g, label)) | 
|  |  | 
|  | # Fetch the cached dependency data and check it against our formal ancestry | 
|  | # data. | 
|  | currtarget = None | 
|  | for line in ( | 
|  | subprocess.check_output(ninja_prefix + ["-t", "deps"]) | 
|  | .decode("UTF-8") | 
|  | .splitlines() | 
|  | ): | 
|  | # ninja -t deps output consists of stanzas of the following form, | 
|  | # separated by a blank line: | 
|  | # | 
|  | # target: [other information we don't need] | 
|  | #     some_file.cpp | 
|  | #     some_header.h | 
|  | #     other_header.h | 
|  | # | 
|  | # We parse this ad-hoc by detecting the four leading spaces in a | 
|  | # source-file line, and the colon in a target line. 'currtarget' stores | 
|  | # the last target name we saw. | 
|  | if line.startswith("    "): | 
|  | dep = line[4:] | 
|  | assert currtarget is not None, "Source file appeared before target" | 
|  |  | 
|  | # We're only interested in this dependency if it's a *generated* | 
|  | # file, i.e. it is in our set of targets. Also, we must check that | 
|  | # currtarget is actually a target we know about: the dependency | 
|  | # cache is not cleared when build.ninja changes, so it can contain | 
|  | # stale data from targets that existed only in past builds in the | 
|  | # same directory. | 
|  | if dep in targets and currtarget in deps and dep not in deps[currtarget]: | 
|  | print( | 
|  | "error:", | 
|  | currtarget, | 
|  | "requires", | 
|  | dep, | 
|  | "but has no dependency on it", | 
|  | file=sys.stderr, | 
|  | ) | 
|  | errs += 1 | 
|  | elif ":" in line: | 
|  | currtarget = line.split(":", 1)[0] | 
|  |  | 
|  | if errs: | 
|  | sys.exit("{:d} errors found".format(errs)) | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | main() |