| #!/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() |