#!/usr/bin/python # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Extract dependency tree out of emerge and make it accessible and useful.""" import json import optparse import re import shutil import subprocess import sys import tempfile import time class ParseException(Exception): def __init__(self, reason): Exception.__init__(self) self.reason = reason def __str__(self): return self.reason class SetEncoder(json.JSONEncoder): """Custom json encoder class, doesn't hate set types.""" def default(self, o): if isinstance(o, set): return list(o) return json.JSONEncoder.default(self, o) def GetDepLinesFromPortage(options, packages): """Get dependency lines out of emerge. This calls emerge -p --debug and extracts the 'digraph' lines which detail the dependencies." """ # Use a temporary directory for $ROOT, so that emerge will consider all # packages regardless of current build status. temp_dir = tempfile.mkdtemp() emerge = 'emerge' if options.board: emerge += '-' + options.board cmdline = [emerge, '-p', '--debug', '--root=' + temp_dir] if not options.build_time: cmdline.append('--root-deps=rdeps') cmdline += packages # Store output in a temp file as it is too big for a unix pipe. stderr_buffer = tempfile.TemporaryFile() depsproc = subprocess.Popen(cmdline, stderr=stderr_buffer, stdout=open('/dev/null', 'w'), bufsize=64*1024) depsproc.wait() subprocess.check_call(['sudo', 'rm', '-rf', temp_dir]) assert(depsproc.returncode==0) stderr_buffer.seek(0) lines = [] output = False for line in stderr_buffer: stripped = line.rstrip() if output: lines.append(stripped) if stripped == 'digraph:': output = True if not output: raise ParseException('Could not find digraph in output from emerge.') return lines def ParseDepLines(lines): """Parse the dependency lines into a dependency tree. This parses the digraph lines, extract the information and builds the dependency tree (doubly-linked)." """ # The digraph output looks like this: # hard-host-depends depends on # ('ebuild', '/tmp/root', 'dev-lang/swig-1.3.36', 'merge') depends on # ('ebuild', '/tmp/root', 'dev-lang/perl-5.8.8-r8', 'merge') (buildtime) # ('binary', '/tmp/root', 'sys-auth/policykit-0.9-r1', 'merge') depends on # ('binary', '/tmp/root', 'x11-misc/xbitmaps-1.1.0', 'merge') (no children) re_deps = re.compile(r'(?P\W*)\(\'(?P\w+)\',' r' \'(?P[\w/\.-]+)\',' r' \'(?P[\w\+-]+)/(?P[\w\+-]+)-' r'(?P\d+[\w\.-]*)\', \'(?P\w+)\'\)' r' (?P(depends on|\(.*\)))') re_seed_deps = re.compile(r'(?P[\w\+/-]+) depends on') # Packages that fail the previous regex should match this one and be noted as # failure. re_failed = re.compile(r'.*depends on.*') deps_map = {} current_package = None for line in lines: deps_match = re_deps.match(line) if deps_match: package_name = deps_match.group('package_name') category = deps_match.group('category') indent = deps_match.group('indent') action = deps_match.group('action') dep_type = deps_match.group('dep_type') version = deps_match.group('version') # Pretty print what we've captured. full_package_name = '%s/%s-%s' % (category, package_name, version) try: package_info = deps_map[full_package_name] except KeyError: package_info = { 'deps': set(), 'rev_deps': set(), 'name': package_name, 'category': category, 'version': version, 'full_name': full_package_name, 'action': action, } deps_map[full_package_name] = package_info if not indent: if dep_type == 'depends on': current_package = package_info else: current_package = None else: if not current_package: raise ParseException('Found a dependency without parent:\n' + line) if dep_type == 'depend on': raise ParseException('Found extra levels of dependencies:\n' + line) current_package['deps'].add(full_package_name) package_info['rev_deps'].add(current_package['full_name']) else: seed_match = re_seed_deps.match(line) if seed_match: package_name = seed_match.group('package_name') try: current_package = deps_map[package_name] except KeyError: current_package = { 'deps': set(), 'rev_deps': set(), 'name': package_name, 'category': '', 'version': '', 'full_name': package_name, 'action': 'seed', } deps_map[package_name] = current_package else: # Is this a package that failed to match our huge regex? failed_match = re_failed.match(line) if failed_match: raise ParseException('Couldn\'t understand line:\n' + line) return deps_map def main(): parser = optparse.OptionParser(usage='usage: %prog [options] package1 ...') parser.add_option('-b', '--board', help='The board to extract dependencies from.') parser.add_option('-B', '--build-time', action='store_true', dest='build_time', help='Also extract build-time dependencies.') parser.add_option('-o', '--output', default=None, help='Output file.') (options, packages) = parser.parse_args() if not packages: parser.print_usage() sys.exit(1) lines = GetDepLinesFromPortage(options, packages) deps_map = ParseDepLines(lines) output = json.dumps(deps_map, sort_keys=True, indent=2, cls=SetEncoder) if options.output: output_file = open(options.output, 'w') output_file.write(output) output_file.close() else: print output if __name__ == '__main__': main()