OLD | NEW |
| (Empty) |
1 #!/usr/bin/python | |
2 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 """Extract dependency tree out of emerge and make it accessible and useful.""" | |
7 | |
8 import json | |
9 import optparse | |
10 import re | |
11 import shutil | |
12 import subprocess | |
13 import sys | |
14 import tempfile | |
15 import time | |
16 | |
17 class ParseException(Exception): | |
18 def __init__(self, reason): | |
19 Exception.__init__(self) | |
20 self.reason = reason | |
21 | |
22 def __str__(self): | |
23 return self.reason | |
24 | |
25 | |
26 class SetEncoder(json.JSONEncoder): | |
27 """Custom json encoder class, doesn't hate set types.""" | |
28 def default(self, o): | |
29 if isinstance(o, set): | |
30 return list(o) | |
31 return json.JSONEncoder.default(self, o) | |
32 | |
33 | |
34 def GetDepLinesFromPortage(options, packages): | |
35 """Get dependency lines out of emerge. | |
36 | |
37 This calls emerge -p --debug and extracts the 'digraph' lines which detail | |
38 the dependencies." | |
39 """ | |
40 # Use a temporary directory for $ROOT, so that emerge will consider all | |
41 # packages regardless of current build status. | |
42 temp_dir = tempfile.mkdtemp() | |
43 | |
44 emerge = 'emerge' | |
45 if options.board: | |
46 emerge += '-' + options.board | |
47 cmdline = [emerge, '-p', '--debug', '--root=' + temp_dir] | |
48 if not options.build_time: | |
49 cmdline.append('--root-deps=rdeps') | |
50 cmdline += packages | |
51 | |
52 # Store output in a temp file as it is too big for a unix pipe. | |
53 stderr_buffer = tempfile.TemporaryFile() | |
54 | |
55 depsproc = subprocess.Popen(cmdline, stderr=stderr_buffer, | |
56 stdout=open('/dev/null', 'w'), bufsize=64*1024) | |
57 depsproc.wait() | |
58 | |
59 subprocess.check_call(['sudo', 'rm', '-rf', temp_dir]) | |
60 | |
61 assert(depsproc.returncode==0) | |
62 | |
63 stderr_buffer.seek(0) | |
64 lines = [] | |
65 output = False | |
66 for line in stderr_buffer: | |
67 stripped = line.rstrip() | |
68 if output: | |
69 lines.append(stripped) | |
70 if stripped == 'digraph:': | |
71 output = True | |
72 | |
73 if not output: | |
74 raise ParseException('Could not find digraph in output from emerge.') | |
75 | |
76 return lines | |
77 | |
78 | |
79 def ParseDepLines(lines): | |
80 """Parse the dependency lines into a dependency tree. | |
81 | |
82 This parses the digraph lines, extract the information and builds the | |
83 dependency tree (doubly-linked)." | |
84 """ | |
85 # The digraph output looks like this: | |
86 | |
87 # hard-host-depends depends on | |
88 # ('ebuild', '/tmp/root', 'dev-lang/swig-1.3.36', 'merge') depends on | |
89 # ('ebuild', '/tmp/root', 'dev-lang/perl-5.8.8-r8', 'merge') (buildtime) | |
90 # ('binary', '/tmp/root', 'sys-auth/policykit-0.9-r1', 'merge') depends on | |
91 # ('binary', '/tmp/root', 'x11-misc/xbitmaps-1.1.0', 'merge') (no children) | |
92 | |
93 re_deps = re.compile(r'(?P<indent>\W*)\(\'(?P<package_type>\w+)\',' | |
94 r' \'(?P<destination>[\w/\.-]+)\',' | |
95 r' \'(?P<category>[\w\+-]+)/(?P<package_name>[\w\+-]+)-' | |
96 r'(?P<version>\d+[\w\.-]*)\', \'(?P<action>\w+)\'\)' | |
97 r' (?P<dep_type>(depends on|\(.*\)))') | |
98 re_seed_deps = re.compile(r'(?P<package_name>[\w\+/-]+) depends on') | |
99 # Packages that fail the previous regex should match this one and be noted as | |
100 # failure. | |
101 re_failed = re.compile(r'.*depends on.*') | |
102 | |
103 deps_map = {} | |
104 | |
105 current_package = None | |
106 for line in lines: | |
107 deps_match = re_deps.match(line) | |
108 if deps_match: | |
109 package_name = deps_match.group('package_name') | |
110 category = deps_match.group('category') | |
111 indent = deps_match.group('indent') | |
112 action = deps_match.group('action') | |
113 dep_type = deps_match.group('dep_type') | |
114 version = deps_match.group('version') | |
115 | |
116 # Pretty print what we've captured. | |
117 full_package_name = '%s/%s-%s' % (category, package_name, version) | |
118 | |
119 try: | |
120 package_info = deps_map[full_package_name] | |
121 except KeyError: | |
122 package_info = { | |
123 'deps': set(), | |
124 'rev_deps': set(), | |
125 'name': package_name, | |
126 'category': category, | |
127 'version': version, | |
128 'full_name': full_package_name, | |
129 'action': action, | |
130 } | |
131 deps_map[full_package_name] = package_info | |
132 | |
133 if not indent: | |
134 if dep_type == 'depends on': | |
135 current_package = package_info | |
136 else: | |
137 current_package = None | |
138 else: | |
139 if not current_package: | |
140 raise ParseException('Found a dependency without parent:\n' + line) | |
141 if dep_type == 'depend on': | |
142 raise ParseException('Found extra levels of dependencies:\n' + line) | |
143 current_package['deps'].add(full_package_name) | |
144 package_info['rev_deps'].add(current_package['full_name']) | |
145 | |
146 else: | |
147 seed_match = re_seed_deps.match(line) | |
148 if seed_match: | |
149 package_name = seed_match.group('package_name') | |
150 | |
151 try: | |
152 current_package = deps_map[package_name] | |
153 except KeyError: | |
154 current_package = { | |
155 'deps': set(), | |
156 'rev_deps': set(), | |
157 'name': package_name, | |
158 'category': '', | |
159 'version': '', | |
160 'full_name': package_name, | |
161 'action': 'seed', | |
162 } | |
163 deps_map[package_name] = current_package | |
164 | |
165 else: | |
166 # Is this a package that failed to match our huge regex? | |
167 failed_match = re_failed.match(line) | |
168 if failed_match: | |
169 raise ParseException('Couldn\'t understand line:\n' + line) | |
170 | |
171 return deps_map | |
172 | |
173 | |
174 def main(): | |
175 parser = optparse.OptionParser(usage='usage: %prog [options] package1 ...') | |
176 parser.add_option('-b', '--board', | |
177 help='The board to extract dependencies from.') | |
178 parser.add_option('-B', '--build-time', action='store_true', | |
179 dest='build_time', | |
180 help='Also extract build-time dependencies.') | |
181 parser.add_option('-o', '--output', default=None, | |
182 help='Output file.') | |
183 (options, packages) = parser.parse_args() | |
184 if not packages: | |
185 parser.print_usage() | |
186 sys.exit(1) | |
187 | |
188 lines = GetDepLinesFromPortage(options, packages) | |
189 deps_map = ParseDepLines(lines) | |
190 output = json.dumps(deps_map, sort_keys=True, indent=2, cls=SetEncoder) | |
191 if options.output: | |
192 output_file = open(options.output, 'w') | |
193 output_file.write(output) | |
194 output_file.close() | |
195 else: | |
196 print output | |
197 | |
198 | |
199 if __name__ == '__main__': | |
200 main() | |
OLD | NEW |