# HG changeset patch # User James Aley # Date 1257356417 0 # Node ID 842a773e65f2d62d7306b406cfe738ea8e84fcd6 # Parent 60053dab7e2a3c1e9685e22c66d882f73094bcb6 Adding some dependency analysis scripts diff -r 60053dab7e2a -r 842a773e65f2 jamesa/_common.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jamesa/_common.py Wed Nov 04 17:40:17 2009 +0000 @@ -0,0 +1,64 @@ +# Copyright (c) 2009 Symbian Foundation Ltd +# This component and the accompanying materials are made available +# under the terms of the License "Eclipse Public License v1.0" +# which accompanies this distribution, and is available +# at the URL "http://www.eclipse.org/legal/epl-v10.html". +# +# Initial Contributors: +# Symbian Foundation Ltd - initial contribution. +# +# Contributors: +# +# Description: +# Data structure code used by dependency analysis scripts. + +"""Common data structure code for build_graph.py and tools. +""" + +__author__ = 'James Aley' +__email__ = 'jamesa@symbian.org' +__version__ = '1.0' + +class Node: + """Node objects are similar to the Symbian notion of a Component, but + they are defined in a practical way for ROM building with less intuitive meaning. + + A Node object is identified by: + - the path to bld.inf + where by: + - the bld.inf file contains a PRJ_MMPFILES section with a least one MMP file. + """ + + def __str__(self): + """Represent node as string, using node_path + """ + return self.node_path + + def __init__(self, path): + """Initialize new Node with given path to bld.inf + """ + # path to the bld.inf file associating these mmp components + self.node_path = '' + + # list of node_path values for Node objects owning referenced from + # the MMP files + self.dependencies = [] + + # contents of this Node, likely not used algorithmically but might + # be useful later for reporting. + self.mmp_components = [] + + # the following are nodes that also satisfy the dependencies (in part), and may + # be of interest when building a ROM. + self.interesting = [] + + # dependencies that were not linked to another component in the source tree + self.unresolved = [] + + self.node_path = path + + def add_deps(self, deps): + """Add dependencies to the list, filtering duplicates + """ + self.dependencies.extend(filter(lambda x: x not in self.dependencies, deps)) + diff -r 60053dab7e2a -r 842a773e65f2 jamesa/build_graph.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jamesa/build_graph.py Wed Nov 04 17:40:17 2009 +0000 @@ -0,0 +1,343 @@ +# Copyright (c) 2009 Symbian Foundation Ltd +# This component and the accompanying materials are made available +# under the terms of the License "Eclipse Public License v1.0" +# which accompanies this distribution, and is available +# at the URL "http://www.eclipse.org/legal/epl-v10.html". +# +# Initial Contributors: +# Symbian Foundation Ltd - initial contribution. +# +# Contributors: +# +# Description: +# Generates a dependency graph of the Symbian source tree. + +"""Build a graph of component dependencies from Symbian OS source code. +The graph is serialized to a file, which can then be used by other scripts to extract data. + +The script works by recursing over the directory structure from the specified root and then +analyzing all bld.inf files to locate referenced production MMP files. These are then processed +for target and dependency information. + +You can use the supplementary scripts to then extract useful information from the generated graph +file. +""" + +from optparse import OptionParser +from _common import Node + +import re +import os +import sys +import pickle +import logging + +__author__ = 'James Aley' +__email__ = 'jamesa@symbian.org' +__version__ = '1.0' + +# Constants for various default config +_LOG_FORMAT = '%(levelname)s: %(message)s' +_MAX_PATH = 260 + +# Precompile regexes for better performance +# - Comment filtering +_RE_CLEAN_INLINE = '^(.*)//.*$' +_RE_MULTILINE_OPEN = '^(.*)/\\*.*$' +_RE_MULTILINE_CLOSE = '^.*\\*/(.*)$' +_p_clean_inline = re.compile(_RE_CLEAN_INLINE) +_p_multiline_open = re.compile(_RE_MULTILINE_OPEN) +_p_multiline_close = re.compile(_RE_MULTILINE_CLOSE) + +# - MMP file Parsing +_RE_TARGET = '^\\s*TARGET\\s+([^\\s]+).*$' +_RE_PLAIN_TARGET = '^\\s*([^\\s\\.]+)\\.?[^\\s]?\\s*' +_RE_COMPLEX_TARGET = '.*\\((.+),.+\\).*' +_RE_LIBRARY = '^\\s*[^\\s]*LIBRARY.*\\s+([^\\s]+.*)$' +_RE_START = '^\\s*START.*$' +_RE_END = '\\s*END.*$' +_p_target = re.compile(_RE_TARGET, re.I) +_p_plain_target = re.compile(_RE_PLAIN_TARGET) +_p_complex_target = re.compile(_RE_COMPLEX_TARGET) +_p_library = re.compile(_RE_LIBRARY, re.I) +_p_start = re.compile(_RE_START) +_p_end = re.compile(_RE_END) + +# - BLD.INF file parsing +_RE_PRJ_MMPFILES = '^\\s*PRJ_MMPFILES\\s*$' +_RE_OTHER_SECTION = '^\\s*PRJ_[a-z]+\\s*$' +_p_prj_mmpfiles = re.compile(_RE_PRJ_MMPFILES, re.I) +_p_other_section = re.compile(_RE_OTHER_SECTION, re.I) + +# Set up a logging instance for output +logging.basicConfig(format=_LOG_FORMAT, level=logging.WARNING, stream=sys.stdout) + +# Cache dictionary to marry Nodes to eachother +node_cache = {} + +# Dictionary representing the dependency graph. +# Each key identifies the node in the graph, where the value is the node +# object itself including the arcs to other node_path keys that it requires. +graph = {} + +def rstrip(string, suffix): + """Like Python's __str__.rstrip(chars), but it treats the chars as + a contiguous string and only strips that complete ending. + """ + if string.endswith(suffix): + string = string[:len(string) - len(suffix)] + return string + +def clean_binary_name(binary_name): + """Strips the extension off of binary names so that references to .lib + are associated with the correct binaries. + """ + match_complex_target = _p_complex_target.match(binary_name) + if match_complex_target: + binary_name = match_complex_target.groups()[0].lower().strip() + else: + match_plain_target = _p_plain_target.match(binary_name) + if match_plain_target: + binary_name = match_plain_target.groups()[0].lower().strip() + return binary_name + +def looks_like_test(path): + """Returns true if a path looks like it refers to test components. + The script does its best to filter test components, as many are missing + from the source tree and they're not interesting with respect to building + production ROM images anyway. + """ + conventions = ['tsrc', 'test'] + for convention in conventions: + # Iterate through likely test component conventions, if + # we match one, return True now + if os.path.sep + convention + os.path.sep in path.lower(): + return True + # Otherwise, nothing found, so return False + return False + +def without_comments(source_file): + """Generator function, will yield lines of the source_file object (iterable) + with commented regions removed. + """ + multiline_region = False + for line in source_file: + match_multiline_close = _p_multiline_close.match(line) + if match_multiline_close: + # Close Comments, strip to the left of the comment + multiline_region = False + line = match_multiline_close.groups()[0] + if multiline_region: + # Skip the line if we're in a commented region + continue + match_multiline_open = _p_multiline_open.match(line) + if match_multiline_open: + # Open comments, strip to the right of the comment + multiline_region = True + line = match_multiline_open.groups()[0] + match_inline = _p_clean_inline.match(line) + if match_inline: + # Strip the line to only the left of the comment + line = match_inline.groups()[0] + if line: + yield line + +def parse_mmp(mmp_path): + """Read an mmp file, return a tuple of the form: + (target, required_target_list) + """ + logging.debug('parse_mmp(%s)' % (mmp_path, )) + + mmp_file = None + try: + mmp_file = open(mmp_path) + except IOError, e: + logging.error('Unable to open: %s' % (mmp_path, )) + return + + # Iterate through MMP file lines to find the TARGET and LIBRARY statements + # Note that Symbian projects can compile to different TARGET objects depending on + # precompiler macros, so we must index all possible target names. + targets = [] + libs = [] + resource_block = False + for line in without_comments(mmp_file): + match_start = _p_start.match(line) + if match_start: + resource_block = True + match_end = _p_end.match(line) + if match_end: + resource_block = False + if resource_block: + # need to avoid resource target sections + continue + match_target = _p_target.match(line) + match_library = _p_library.match(line) + if match_target: + clean_target = clean_binary_name(match_target.groups()[0]) + targets.append(clean_target) + elif match_library: + libs_on_line = match_library.groups()[0].split() + for lib in libs_on_line: + clean_lib = clean_binary_name(lib) + libs.append(clean_lib) + mmp_file.close() + + return (targets, libs) + +def new_node(path, ref_mmps, ref_testmmps): + """Construct a new node in the graph with the provided content. + """ + logging.debug('new_node(%s, ref_mmps(%d), ref_testmmps(%d))' % (path, len(ref_mmps), len(ref_testmmps))) + node = Node(path) + + # Iterate the MMPs, read dependency and target information + for mmp in ref_mmps: + (targets, dependencies) = parse_mmp(mmp) + if len(targets) > 0: + for target in targets: + node.mmp_components.append(target) + node.add_deps(dependencies) + + # Register the components in the cache, as later we will + # join the graph nodes by referring to this cache + for c in node.mmp_components: + if c in node_cache.keys(): + existing = node_cache[c] + node_cache[c] = existing + [path] + else: + node_cache[c] = [path] + + # Add this node to the graph + graph[path] = node + +def parse_bld_inf(path): + """Parse a bld.inf file to check to see if references MMP files. + For those MMP files included, parse them to build the node object. + """ + logging.debug('parse_bld_inf(%s)' % (path, )) + + # List the files referenced from this bld.inf + ref_mmp = [] + ref_testmmp = [] + + bld_inf = None + try: + bld_inf = open(path, 'r') + except IOError, e: + logging.error('Unable to open: %s' % (path, )) + return + + # Parse the bld_inf file, adding references MMP files to appropriate lists + projects_flag = False + for line in without_comments(bld_inf): + match_projects = _p_prj_mmpfiles.match(line) + match_other_section = _p_other_section.match(line) + if match_projects: + projects_flag = True + elif match_other_section: + projects_flag = False + if projects_flag and len(line) <= _MAX_PATH: + rel_name = rstrip(line.lower().strip(), '.mmp') + bld_inf_path = os.path.dirname(path) + test_path = os.path.join(bld_inf_path, rel_name + '.mmp') + test_path = os.path.realpath(test_path) + if os.path.exists(test_path): + ref_mmp.append(test_path) + else: + logging.warning('%s refers to %s but it does not exist!' % (path, test_path)) + bld_inf.close() + + # If we found some MMP files, then this is a new node + if len(ref_mmp) > 0: + new_node(path, ref_mmp, ref_testmmp) + +def make_nodes(not_used, dir_name, file_names): + """Call back function for os.path.walk: will analyse the file names, if + there are any bld.inf files, it will open them to see if they identify a + Node object and create them as appropriate + """ + logging.debug('make_nodes(%s, %s)' % (dir_name, file_names)) + if looks_like_test(dir_name): + return + for file_name in file_names: + if file_name.lower().endswith('.inf'): + abs_path = os.path.join(dir_name, file_name) + assert(os.path.exists(abs_path)) + parse_bld_inf(abs_path) + +def connect_nodes(): + """Walk through the graph and substute the contents of the dependency + list members at each node with references to the node_path of that which + builds the referenced component. + + There will be instances where multiple graph nodes build overlapping + components. This will, in practice, mean that there are many ways of + building a suitable ROM for dependencies of one of these nodes. + """ + unresolved_deps = [] + for node_path in graph.keys(): + node = graph[node_path] + resolved = [] + for dep in node.dependencies: + if dep not in node_cache.keys(): + logging.warning('Could not resolve %s for %s' % (dep, node.node_path)) + if dep not in unresolved_deps: + unresolved_deps.append(dep) + node.unresolved.append(dep) + else: + solutions = node_cache[dep] + proposed = solutions[0] + if proposed not in resolved: + resolved.append(proposed) + node.interesting += filter(lambda x: x not in node.interesting, solutions[1:]) + node.dependencies = resolved + graph[node_path] = node + if len(unresolved_deps) > 0: + logging.warning('There were %d unresolved dependencies.' % (len(unresolved_deps), )) + +def build_graph(root): + """Walk nodes from the directory root provided looking for bld.inf files. + Graph will be built from the referened production MMP files. + """ + if not os.path.isdir(root): + logging.fatal('%s is not a directory, aborting...' % (root, )) + exit(1) + os.path.walk(root, make_nodes, None) + connect_nodes() + +def save_graph(path): + """Serialize the graph object to path. This will be a Python object pickle at + the highest available protocol version for this Python install. + """ + graph_file = None + try: + graph_file = open(path, 'wb') + except IOError, e: + logging.error('Could not write graph to file: %s' % (repr(e), )) + exit(1) + pickle.dump(graph, graph_file, pickle.HIGHEST_PROTOCOL) + graph_file.close() + +# Main: +if __name__ == '__main__': + parser = OptionParser() + parser.set_description(__doc__) + parser.add_option('-g', '--graph', dest='graph_file', + help='File name to write the graph to.', + metavar='GRAPH_FILE', default='dependencies.graph') + parser.add_option('-r', '--root', dest='graph_root', + help='Directory to recursively build a graph from, usually root of source tree.', + metavar='SOURCE_ROOT', default='.') + parser.add_option('-v', '--verbose', dest='verbose', + help='Verbose logging, will show all warnings as graph is generated. Recommend redirect!', + action='store_true', default=False) + (options, args) = parser.parse_args() + if not options.verbose: + logging.disable(logging.ERROR) + print 'Walking source from "%s"\nThis can take some time with large source trees...' % (options.graph_root, ) + build_graph(options.graph_root) + print 'Found %d components consisting of %d binaries.' % (len(graph), len(node_cache)) + print 'Wriing graph to %s' % (options.graph_file) + save_graph(options.graph_file) + print '...done!' diff -r 60053dab7e2a -r 842a773e65f2 jamesa/generate_oby.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jamesa/generate_oby.py Wed Nov 04 17:40:17 2009 +0000 @@ -0,0 +1,212 @@ +# Copyright (c) 2009 Symbian Foundation Ltd +# This component and the accompanying materials are made available +# under the terms of the License "Eclipse Public License v1.0" +# which accompanies this distribution, and is available +# at the URL "http://www.eclipse.org/legal/epl-v10.html". +# +# Initial Contributors: +# Symbian Foundation Ltd - initial contribution. +# +# Contributors: +# +# Description: +# Create a barebones OBY file from a dependency report text file. + +"""Take a report generated by get_deps.py and attempt to create an OBY +file to build a ROM with the dependency list. +""" + +from build_graph import without_comments +from optparse import OptionParser + +import os +import re +import sys +import logging + +__author__ = 'James Aley' +__email__ = 'jamesa@symbian.org' +__version__ = '1.0' + +# Logging config +_LOG_FORMAT = '%(levelname)s: %(message)s' +logging.basicConfig(format=_LOG_FORMAT, level=logging.WARNING, stream=sys.stdout) + +# Regexes for bld.inf parsing +_RE_EXPORT_SECTION = '\\s*PRJ_EXPORTS\\s*' +_RE_OTHER_SECTION = '\\s*PRJ_[a-z]+\\s*' +_RE_IBY_OBY = '\\s*([^\\s]+\\.[oi]by)\\s+.*' +_p_export_section = re.compile(_RE_EXPORT_SECTION, re.I) +_p_other_section = re.compile(_RE_OTHER_SECTION, re.I) +_p_iby_oby = re.compile(_RE_IBY_OBY, re.I) + +# OBY output templates +_OBY_HEADER = """// OBY file generated by genereate_oby.py. +// The following includes are derived from the dependency report: %s + +""" + +_OBY_INCLUDE = """ +// Required for: %s +%s +""" + +_OBY_UNRESOLVED = """ + +// The following appear to be exported by this component, +// but were not found under the include directory: +%s +""" + +_OBY_NO_EXPORTS = """ + +// The following components are required in your dependency graph, but +// they appear not to export an IBY/OBY file. Your ROM will likely not +// build until you locate the correct include files for these. +%s +""" + +_INCLUDE_TEMPLATE = '#include <%s>' + +def bld_inf_list(report_path): + """Returna list of bld.inf files from the report + """ + bld_list = [] + report_file = None + try: + report_file = open(report_path) + except IOError, e: + logging.critical('Could not open report: %s' % (repr(e), )) + exit(1) + return filter(lambda x: x and not x.isspace(), [line.strip() for line in without_comments(report_file)]) + +def get_paths(bld_inf_file): + """Returns a list of referenced OBY or IBY files from a bld.inf file. + bld_inf_file is an open file handle, which will not be closed by this + function. + """ + oby_iby = [] + export_section = False + for line in without_comments(bld_inf_file): + if export_section: + match_iby_oby = _p_iby_oby.search(line) + if match_iby_oby: + file_name = match_iby_oby.groups()[0].strip() + oby_iby.append(file_name) + else: + match_other_section = _p_other_section.search(line) + if match_other_section: + export_section = False + else: + match_export_section = _p_export_section.search(line) + if match_export_section: + export_section = True + obys = filter(lambda x: x.lower().endswith('.oby'), oby_iby) + if len(obys) > 0: + return obys + return oby_iby + +def rom_file_list(bld_inf_paths): + """Iterate through a list of bld.inf file paths and extra the references + to OBY or IBY files where appropriate (OBY takes precedence). Return a + dictionary of relevant files in the format: + { 'component_bld_inf' : [ iby_file_list] } + """ + obys_ibys = {} + for path in bld_inf_paths: + bld_inf_file = None + try: + bld_inf_file = open(path) + except IOError, e: + logging.error('Unable to open bld.inf file: %s' % (repr(e), )) + continue + rom_file_paths = get_paths(bld_inf_file) + obys_ibys[path] = rom_file_paths + bld_inf_file.close() + return obys_ibys + +def iby_map(iby_dict, dir_name, file_names): + """Searches for the specified IBY/OBY file under the include_root path. + Returns the absolute path to the IBY/OBY if it was found, otherwise a blank string. + """ + for component in iby_dict.keys(): + # Map the file names for each component IBY file to a matching + # file name under the export directory, if it exists, otherwise + # keep the existing name for now - it might be matched later. + file_names = map(lambda x: x.lower(), file_names) + component_ibys = map(lambda x: os.path.basename(x).lower() in file_names \ + and os.path.join(dir_name, os.path.basename(x)) \ + or x, \ + iby_dict[component]) + iby_dict[component] = component_ibys + +def write_oby(out_path, iby_map, input_path, include_root): + """Write an OBY file to include the required IBY and OBY files for this + ROM specification, given by iby_map. + """ + out_file = None + try: + out_file = open(out_path, 'w') + except IOError, e: + logging.critical('Unable to write OBY file: %s' % repr(e)) + exit(1) + + # Write the header with the input file name included + out_file.write(_OBY_HEADER % (input_path, )) + + exports = filter(lambda x: len(iby_map[x]) > 0, iby_map.keys()) + no_exports = filter(lambda x: len(iby_map[x]) == 0, iby_map.keys()) + + # Write the includes and missing exports + for component in exports: + iby_list = iby_map[component] + exported = filter(lambda x: x.startswith(include_root), iby_list) + # Need relative paths for include, but os.path.relpath is added + # in Python 2.6, which isn't supported by other Symbian tools + # at present :-( + exported = map(lambda x: x[len(include_root) + 1:], exported) + exported.sort() + + missing = filter(lambda x: not x.startswith(include_root), iby_list) + missing = map(lambda x: os.path.basename(x), missing) + missing.sort() + + # Write the IBY file includes for this component + out_file.write(_OBY_INCLUDE % (component, '\n'.join([_INCLUDE_TEMPLATE % (iby, ) for iby in exported]), )) + + # Write the missing IBY reports + if len(missing) > 0: + out_file.write(_OBY_UNRESOLVED % ('\n'.join(['// %s' % (missed, ) for missed in missing]), )) + + # Write report for the IBY that appear not to export any ROM include files + out_file.write(_OBY_NO_EXPORTS % ('\n'.join(['// %s' % (path,) for path in no_exports]), )) + out_file.close() + +# Main +if __name__ == '__main__': + # Options config + parser = OptionParser() + parser.set_description(__doc__) + parser.add_option('-r', '--report', dest='report_path', + help='File name for the report generated by get_deps.py', + metavar='INPUT_FILE', default='dependencies.txt') + parser.add_option('-o', '--output', dest='output_file', + help='OBY ouput file to write to.', + metavar='OUT_FILE', default='generated.oby') + parser.add_option('-i', '--include_root', dest='include_root', + help='Environment ROM inlcude root.', + metavar='INCLUDE_ROOT', + default=os.path.sep.join(['epoc32', 'rom'])) + (options, args) = parser.parse_args() + + # Read the report and get a list of bld.inf files, then convert to + # a dictionary of bld_inf -> [IBY file list] mappings. + bld_infs = bld_inf_list(options.report_path) + bld_iby_map = rom_file_list(bld_infs) + + # Walk the include tree to find the exported IBYs. + os.path.walk(options.include_root, iby_map, bld_iby_map) + + # Write the OBY file + write_oby(options.output_file, bld_iby_map, options.report_path, options.include_root) + diff -r 60053dab7e2a -r 842a773e65f2 jamesa/get_deps.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jamesa/get_deps.py Wed Nov 04 17:40:17 2009 +0000 @@ -0,0 +1,174 @@ +# Copyright (c) 2009 Symbian Foundation Ltd +# This component and the accompanying materials are made available +# under the terms of the License "Eclipse Public License v1.0" +# which accompanies this distribution, and is available +# at the URL "http://www.eclipse.org/legal/epl-v10.html". +# +# Initial Contributors: +# Symbian Foundation Ltd - initial contribution. +# +# Contributors: +# +# Description: +# Walk all nodes in the dependency graph to create a dependency report. + +"""Walk a dependency graph to find dependencies for a particular set of +components. This script uses the output of build_graph.py to trace +dependencies. + +The directory_list refer to diretories in the source tree for which +you wish to trace dependencies. The script will find all components +in the graph file under these directories and trace dependencies from +that point. +""" + +from optparse import OptionParser +from _common import Node + +import sys +import pickle +import logging + +__author__ = 'James Aley' +__email__ = 'jamesa@symbian.org' +__version__ = '1.0' + +_LOG_FORMAT = '%(levelname)s: %(message)s' +logging.basicConfig(format=_LOG_FORMAT, level=logging.WARNING, stream=sys.stdout) + +# Internalized graph object +graph = {} + +# Report formatting +_REPORT_HEADER = """// Generated by get_deps.py +// +// Dependency information for: +// +%s + +""" + +_DEPENDENCY_FORMAT = """ +// Required components: + +%s + +""" + +_MISSING_FORMAT = """ +// The following binary objects were referenced from the build files for +// components required by your specified root components. However, there +// were no build files for these objects found in the source tree parsing, +// so dependencies for them may be missing in the above listing. + +%s + +""" + +def load_graph(path): + """Return the internalized graph dictionary object. + """ + graph_file = None + graph_loaded = {} + + try: + graph_file = open(path, 'rb') + except IOError, e: + logging.critical('Unable to open graph from file: %s: %s' % (path, repr(e))) + exit(1) + try: + graph_loaded = pickle.load(graph_file) + except Exception, e: + logging.critical('File %s does not contain a valid graph: %s' % (path, repr(e))) + return graph_loaded + +def find_roots(root_dirs): + """Return a list of root nodes from the graph for tracing, based on + the specified component directories in the root_dirs list. + """ + roots = [] + for root in root_dirs: + for node in graph.keys(): + if node.startswith(root.lower()): + if node not in roots: + roots.append(node) + return roots + +def trace(root, visited = set()): + """Return a list of components required to support root. + """ + node = graph[root] + visited |= set([node.node_path]) | set(node.dependencies) + for dep in node.dependencies: + if dep not in visited: + return trace(dep, visited) + return visited + +def unresolved(deps): + """Return a set of components with unknown dependencies from a + provided list of node names. + """ + unresolved = set() + for dep in deps: + node = graph[dep] + unresolved |= set(node.unresolved) + return unresolved + +def report(out_path, roots, dependencies, missing): + """Output the dependency information to file. + """ + # open report file + out_file = None + try: + out_file = open(out_path, 'w') + except IOError, e: + logging.critical('Unable to write report: %s' % (repr(e))) + exit(1) + + # easier to read report with these sorted + roots.sort() + dependencies.sort() + missing.sort() + + # format report + formatted_header = _REPORT_HEADER % ('\n'.join(['// %s' % (line, ) for line in roots]), ) + formatted_body = _DEPENDENCY_FORMAT % ('\n'.join(dependencies)) + formatted_missing = _MISSING_FORMAT % ('\n'.join(['// %s' % (line, ) for line in missing]), ) + + # write report + out_file.write(formatted_header) + out_file.write(formatted_body) + out_file.write(formatted_missing) + + out_file.close() + +if __name__ == '__main__': + # Options config + parser = OptionParser() + parser.set_description(__doc__) + parser.set_usage('python get_deps.py [options] directory_list') + parser.add_option('-g', '--graph', dest='graph_file', + help='File name to write the graph to.', + metavar='GRAPH_FILE', default='dependencies.graph') + parser.add_option('-o', '--output', dest='output_file', + help='File to write the dependency report to.', + metavar='OUT_FILE', default='dependencies.txt') + (options, args) = parser.parse_args() + + # Intenalize the graph file + print 'Loading graph from %s' % (options.graph_file, ) + graph = load_graph(options.graph_file) + + # Extract relevant slices and merge dependencies + roots = find_roots(args) + print 'Tracing dependencies for %d components under %s' % (len(roots), ', '.join(args)) + deps = set() + for root in roots: + deps |= trace(root) + print 'Dependency graph slice yields %d of %d components.' % (len(deps), len(graph)) + unresolved_deps = unresolved(deps) + print 'Component dependencies for %d binaries are unresolved' % (len(unresolved_deps, )) + + # Write the report to the output file + report(options.output_file, roots, list(deps), list(unresolved_deps)) + print 'Report written to: %s' % (options.output_file, )