Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F142022243
D6616.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
10 KB
Referenced Files
None
Subscribers
None
D6616.diff
View Options
Index: share/mk/graph-mkincludes
===================================================================
--- /dev/null
+++ share/mk/graph-mkincludes
@@ -0,0 +1,383 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2016 Jonathan Anderson. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+"""
+Parser for the include graph of BSD makefiles.
+
+Reads BSD makefiles and constructs a graph of file inclusions. This graph
+incorporates knowledge of the order in which these files are included and
+can show how metadata such as variables propagate through the graph.
+
+Usage:
+ graph-makeincludes [options] <filename> [<filename> ...]
+ graph-makeincludes -h | --help
+ graph-makeincludes --version
+
+Options:
+ -h --help Show this screen.
+ --version Show version.
+ --filter-singletons Filter out nodes with no edges in or out
+ --filter-unconnected Filter out nodes not connected to nodes with metadata
+ --metadata=<metadata> Metadata to search for [default: var:CC,var:LOCALBASE]
+ -o <file>, --output=<file> Output file, or '-' for stdout [default: -].
+ filename Input file(s), in BSD make form
+"""
+
+import collections
+import docopt
+import itertools
+import os
+import re
+import sys
+
+
+class File(object):
+ """
+ A file in the Makefile graph contains zero or more `.include` lines
+ and also keeps track of where it is `.include`d.
+ """
+ def __init__(self):
+ self.lines = collections.defaultdict(Line)
+ self.edges_out = set()
+
+class Line(object):
+ """
+ A `.include` line includes a source makefile into the current file.
+ """
+ def __init__(self):
+ self.edges_in = set()
+ self.definitions = set()
+ self.metadata = collections.defaultdict(bool)
+
+
+def parse(filenames, metadata):
+ """
+ Look through a set of Makefiles to find "interesting" lines
+ (e.g., those that include other Makefiles or define key variables).
+ """
+
+ parse.include = re.compile('^\.[a-z]?include [<"](.*)[>"]')
+ files = collections.defaultdict(File)
+
+ for filename in filenames:
+ (dirname, basename) = os.path.split(filename)
+ line_number = 0
+
+ for line in open(filename):
+ line_number += 1
+
+ # This lambda ensures we retrieve the file/line only as
+ # needed, avoiding spurious object / node creation.
+ get_node = lambda: files[filename].lines[line_number]
+
+ match = parse.include.match(line)
+ if match:
+ node = get_node()
+
+ (included,) = match.groups()
+ included = included.replace('${.CURDIR}/', '')
+ if not included.startswith('$'):
+ included = os.path.join(dirname, included)
+
+ included = os.path.normpath(included)
+
+ node.edges_in.add(included)
+ files[included].edges_out.add(node)
+
+ for (name, pattern) in metadata.items():
+ if pattern.match(line):
+ node = get_node()
+ node.definitions.add(name)
+ node.metadata[name] = True
+
+ return files
+
+
+def filter_singletons(files):
+ """
+ Filter out singleton files (files that neither include nor are included
+ by other files).
+ """
+
+ keep = set()
+
+ for (name,f) in files.items():
+ if len(f.edges_out) > 0:
+ keep.add(name)
+ continue
+
+ for l in f.lines.values():
+ if len(l.edges_in) > 0:
+ keep.add(name)
+ break
+
+ return { name:f for name,f in files.items() if name in keep }
+
+
+def filter_unconnected(files):
+ have_metadata = set()
+
+ for (name, f) in files.items():
+ for l in f.lines.values():
+ if any(l.metadata.values()):
+ have_metadata.add(name)
+ break
+
+ keep = set()
+ def keep_ancestors(f):
+ for l in f.lines.values():
+ for e in l.edges_in:
+ if e in keep or e not in files:
+ continue
+
+ keep.add(e)
+ keep_ancestors(files[e])
+
+ def keep_descendents(f):
+ for e in f.edges_out:
+ if e in keep or e not in files:
+ continue
+
+ keep.add(e)
+ keep_descendents(files[e])
+
+ for name in have_metadata:
+ f = files[name]
+ if name in keep:
+ continue
+
+ keep.add(name)
+ keep_ancestors(f)
+ keep_descendents(f)
+
+ return { name:files[name] for name in keep }
+
+
+def propagate_metadata(files, metadata):
+ """
+ Propagate all metadata through the graph, repeating until we can
+ walk though the entire graph without doing any propagation.
+ """
+
+ def propagate(source, dest):
+ """
+ Propagate the presence of metadata from one node to another.
+ """
+ for name in metadata:
+ if source.metadata[name] and not dest.metadata[name]:
+ dest.metadata[name] = True
+ propagate.new_taint = True
+
+ propagate.new_taint = True
+
+ while propagate.new_taint:
+ propagate.new_taint = False
+
+ for (filename,f) in files.items():
+ previous = None
+
+ # Propagate from line to line within the file:
+ for line_number in sorted(f.lines.keys()):
+ node = f.lines[line_number]
+ if previous: propagate(previous, node)
+ previous = node
+
+ # Propagate from the last line to its include sites:
+ if previous:
+ last = previous
+ for recipient in f.edges_out:
+ propagate(last, recipient)
+
+
+def attribute_list(attrs):
+ """
+ Convert a Python dictionary to a GraphViz dot attribute list.
+ """
+ return '[ %s ]' % ', '.join(
+ [ '%s="%s"' % (k,v) for k,v in attrs.items() if v is not None]
+ )
+
+
+# Parse arguments.
+args = docopt.docopt(__doc__)
+
+filenames = args['<filename>']
+print('Parsing %d files' % len(filenames))
+
+outname = args['--output']
+outfile = open(outname, 'w') if outname != '-' else sys.stdout
+
+# Set up metadata: filter out specials we don't care about, add variables.
+metadata = {}
+for m in args['--metadata'].split(','):
+ if m.startswith('rule:'):
+ name = m[5:]
+ metadata[name] = re.compile('^%s:' % name)
+
+ elif m.startswith('var:'):
+ name = m[4:]
+ metadata[name] = re.compile('^%s((=)|(\t.*=))' % name)
+
+ else:
+ sys.stderr.write("Error: unknown metadata type: '%s'\n" % m)
+ sys.exit(1)
+
+pastels = [ '/pastel28/%d' % x for x in range(1,9) ]
+colours = dict(zip(metadata.keys(), itertools.cycle(pastels)))
+
+
+#
+# Parse the makefiles and propagate any metadata that we care about.
+#
+files = parse(filenames, metadata)
+propagate_metadata(files, metadata)
+
+if args['--filter-singletons']:
+ files = filter_singletons(files)
+
+if args['--filter-unconnected']:
+ files = filter_unconnected(files)
+
+
+#
+# Write the actual graph, starting with a legend:
+#
+outfile.write('''
+digraph {
+ rankdir = TB;
+
+ subgraph cluster_legend {
+ label = "Legend";
+ rankdir = TB;
+ edge [ style = invis; ];
+''')
+
+prev = None
+
+for name in sorted(metadata):
+ outfile.write(' "%s" %s;\n' % (name, attribute_list({
+ 'style': 'filled',
+ 'fillcolor': colours[name],
+ })))
+
+ if prev: outfile.write(' "%s" -> "%s";\n' % (prev, name))
+ prev = name
+
+outfile.write('\n }\n')
+
+#
+# First, define each file as a cluster of lines (or just a box):
+#
+for (filename,f) in files.items():
+ safe_name = filename
+ for unsafe in [ '+', '.', '-', '$', '/' ]:
+ safe_name = safe_name.replace(unsafe, '_')
+
+ lines = sorted(f.lines.items(), key = lambda (l,_): l)
+
+ #
+ # If a file has no .include lines, it is only a source of include
+ # material rather than a recipient of it. Represent such files
+ # just a box.
+ #
+ if len(lines) == 0:
+ attrs = attribute_list({
+ 'label': filename,
+ 'shape': 'rectangle',
+ })
+
+ outfile.write(' "%s" %s;\n' % (filename, attrs))
+ continue
+
+
+ #
+ # If the file does contain .include lines, show these lines as
+ # nodes within a boxed cluster.
+ #
+ outfile.write(' subgraph cluster_%s {\n' % safe_name)
+ outfile.write(' rankdir = TB;\n')
+ outfile.write(' label = "%s";\n' % filename)
+
+ previous = None
+ prev_meta = False
+
+ for (line_number, node) in lines:
+ name = '%s:%d' % (filename, line_number)
+
+ fills = [ colours[meta] for (meta,val) in node.metadata.items() if val ]
+ fill = ':'.join(fills) if len(fills) > 0 else 'white'
+
+ shape = None
+ if len(node.definitions) > 0:
+ if prev_meta:
+ attrs['color'] = 'orange'
+ shape = 'tripleoctagon'
+ else:
+ shape = 'doublecircle'
+
+ attrs = {
+ 'fillcolor': fill,
+ 'label': line_number,
+ 'shape': shape,
+ 'style': 'filled',
+ }
+
+ outfile.write(' "%s" %s;\n' % (name, attribute_list(attrs)))
+
+ if previous:
+ attrs = { 'style': 'dotted' }
+ outfile.write(' "%s" -> "%s" %s;\n' % (
+ previous, name, attribute_list(attrs)))
+
+ previous = name
+ prev_meta = any(node.metadata.values()) if previous else False
+
+ outfile.write(' }\n')
+
+#
+# Now iterate through all nodes again to join the clusters up.
+# We have to do things this way so that we don't end up defining nodes
+# inside the wrong clusters.
+#
+for (filename,f) in files.items():
+ for (line_number, node) in f.lines.items():
+ name = '%s:%d' % (filename, line_number)
+
+ for source_filename in node.edges_in:
+ if source_filename not in files:
+ continue
+
+ source_file = files[source_filename]
+ source_lines = sorted(source_file.lines.keys())
+
+ if len(source_lines) == 0:
+ source = source_filename
+ else:
+ final_line = source_lines[-1]
+ source = '%s:%d' % (source_filename, final_line)
+
+ outfile.write(' "%s" -> "%s";\n' % (source, name))
+
+outfile.write('}\n')
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Jan 16, 1:38 AM (16 h, 57 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
27656718
Default Alt Text
D6616.diff (10 KB)
Attached To
Mode
D6616: Add script to parse makefile include graph.
Attached
Detach File
Event Timeline
Log In to Comment