OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 # Copyright (c) 2012 The Chromium 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 """Merges multiple OS-specific gyp dependency lists into one that works on all | |
7 of them. | |
8 | |
9 | |
10 The logic is relatively simple. Takes the current conditions, add more | |
11 condition, find the strict subset. Done. | |
12 """ | |
13 | |
14 import copy | |
15 import logging | |
16 import optparse | |
17 import re | |
18 import sys | |
19 | |
20 import trace_inputs | |
21 | |
22 | |
23 def union(lhs, rhs): | |
24 """Merges two compatible datastructures composed of dict/list/set.""" | |
25 assert lhs is not None or rhs is not None | |
26 if lhs is None: | |
27 return copy.deepcopy(rhs) | |
28 if rhs is None: | |
29 return copy.deepcopy(lhs) | |
30 assert type(lhs) == type(rhs), (lhs, rhs) | |
31 if isinstance(lhs, dict): | |
32 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs)) | |
33 elif isinstance(lhs, set): | |
34 # Do not go inside the set. | |
35 return lhs.union(rhs) | |
36 elif isinstance(lhs, list): | |
37 # Do not go inside the list. | |
38 return lhs + rhs | |
39 assert False, type(lhs) | |
40 | |
41 | |
42 def process_variables(for_os, variables): | |
43 """Extracts files and dirs from the |variables| dict. | |
44 | |
45 Returns a list of exactly two items. Each item is a dict that maps a string | |
46 to a set (of strings). | |
47 | |
48 In the first item, the keys are file names, and the values are sets of OS | |
49 names, like "win" or "mac". In the second item, the keys are directory names, | |
50 and the values are sets of OS names too. | |
51 """ | |
52 VALID_VARIABLES = ['isolate_files', 'isolate_dirs'] | |
53 | |
54 # Verify strictness. | |
55 assert isinstance(variables, dict), variables | |
56 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys() | |
57 for items in variables.itervalues(): | |
58 assert isinstance(items, list), items | |
59 assert all(isinstance(i, basestring) for i in items), items | |
60 | |
61 # Returns [files, dirs] | |
62 return [ | |
63 dict((name, set([for_os])) for name in variables.get(var, [])) | |
64 for var in VALID_VARIABLES | |
65 ] | |
66 | |
67 | |
68 def eval_content(content): | |
69 """Evaluates a GYP file and return the value defined in it.""" | |
70 globs = {'__builtins__': None} | |
71 locs = {} | |
72 value = eval(content, globs, locs) | |
73 assert locs == {}, locs | |
74 assert globs == {'__builtins__': None}, globs | |
75 return value | |
76 | |
77 | |
78 def _process_inner(for_os, inner, old_files, old_dirs, old_os): | |
79 """Processes the variables inside a condition. | |
80 | |
81 Only meant to be called by parse_gyp_dict(). | |
82 | |
83 Args: | |
84 - for_os: OS where the references are tracked for. | |
85 - inner: Inner dictionary to process. | |
86 - old_files: Previous list of files to union with. | |
87 - old_dirs: Previous list of directories to union with. | |
88 - old_os: Previous list of OSes referenced to union with. | |
89 | |
90 Returns: | |
91 - A tuple of (files, dirs, os) where each list is a union of the new | |
92 dependencies found for this OS, as referenced by for_os, and the previous | |
93 list. | |
94 """ | |
95 assert isinstance(inner, dict), inner | |
96 assert set(['variables']).issuperset(set(inner)), inner.keys() | |
97 new_files, new_dirs = process_variables(for_os, inner.get('variables', {})) | |
98 if new_files or new_dirs: | |
99 old_os = old_os.union([for_os.lstrip('!')]) | |
100 return union(old_files, new_files), union(old_dirs, new_dirs), old_os | |
101 | |
102 | |
103 def parse_gyp_dict(value): | |
104 """Parses a gyp dict as returned by eval_content(). | |
105 | |
106 |value| is the loaded dictionary that was defined in the gyp file. | |
107 | |
108 Returns a 3-tuple, where the first two items are the same as the items | |
109 returned by process_variable() in the same order, and the last item is a set | |
110 of strings of all OSs seen in the input dict. | |
111 | |
112 The expected format is strict, anything diverting from the format below will | |
113 fail: | |
114 { | |
115 'variables': { | |
116 'isolate_files': [ | |
117 ... | |
118 ], | |
119 'isolate_dirs: [ | |
120 ... | |
121 ], | |
122 }, | |
123 'conditions': [ | |
124 ['OS=="<os>"', { | |
125 'variables': { | |
126 ... | |
127 }, | |
128 }, { # else | |
129 'variables': { | |
130 ... | |
131 }, | |
132 }], | |
133 ... | |
134 ], | |
135 } | |
136 """ | |
137 assert isinstance(value, dict), value | |
138 VALID_ROOTS = ['variables', 'conditions'] | |
139 assert set(VALID_ROOTS).issuperset(set(value)), value.keys() | |
140 | |
141 # Global level variables. | |
142 oses = set() | |
143 files, dirs = process_variables(None, value.get('variables', {})) | |
144 | |
145 # OS specific variables. | |
146 conditions = value.get('conditions', []) | |
147 assert isinstance(conditions, list), conditions | |
148 for condition in conditions: | |
149 assert isinstance(condition, list), condition | |
150 assert 2 <= len(condition) <= 3, condition | |
151 m = re.match(r'OS==\"([a-z]+)\"', condition[0]) | |
152 assert m, condition[0] | |
153 condition_os = m.group(1) | |
154 | |
155 files, dirs, oses = _process_inner( | |
156 condition_os, condition[1], files, dirs, oses) | |
157 | |
158 if len(condition) == 3: | |
159 files, dirs, oses = _process_inner( | |
160 '!' + condition_os, condition[2], files, dirs, oses) | |
161 | |
162 # TODO(maruel): _expand_negative() should be called here, because otherwise | |
163 # the OSes the negative condition represents is lost once the gyps are merged. | |
164 # This cause an invalid expansion in reduce_inputs() call. | |
165 return files, dirs, oses | |
166 | |
167 | |
168 def parse_gyp_dicts(gyps): | |
169 """Parses each gyp file and returns the merged results. | |
170 | |
171 It only loads what parse_gyp_dict() can process. | |
172 | |
173 Return values: | |
174 files: dict(filename, set(OS where this filename is a dependency)) | |
175 dirs: dict(dirame, set(OS where this dirname is a dependency)) | |
176 oses: set(all the OSes referenced) | |
177 """ | |
178 files = {} | |
179 dirs = {} | |
180 oses = set() | |
181 for gyp in gyps: | |
182 with open(gyp, 'rb') as gyp_file: | |
183 content = gyp_file.read() | |
184 gyp_files, gyp_dirs, gyp_oses = parse_gyp_dict(eval_content(content)) | |
185 files = union(gyp_files, files) | |
186 dirs = union(gyp_dirs, dirs) | |
187 oses |= gyp_oses | |
188 return files, dirs, oses | |
189 | |
190 | |
191 def _expand_negative(items, oses): | |
192 """Converts all '!foo' value in the set by oses.difference('foo').""" | |
193 assert None not in oses and len(oses) >= 2, oses | |
194 for name in items: | |
195 if None in items[name]: | |
196 # Shortcut any item having None in their set. An item listed in None means | |
197 # the item is a dependency on all OSes. As such, there is no need to list | |
198 # any OS. | |
199 items[name] = set([None]) | |
200 continue | |
201 for neg in [o for o in items[name] if o.startswith('!')]: | |
202 # Replace it with the inverse. | |
203 items[name] = items[name].union(oses.difference([neg[1:]])) | |
204 items[name].remove(neg) | |
205 if items[name] == oses: | |
206 items[name] = set([None]) | |
207 | |
208 | |
209 def _compact_negative(items, oses): | |
210 """Converts all oses.difference('foo') to '!foo'. | |
211 | |
212 It is doing the reverse of _expand_negative(). | |
213 """ | |
214 assert None not in oses and len(oses) >= 3, oses | |
215 for name in items: | |
216 missing = oses.difference(items[name]) | |
217 if len(missing) == 1: | |
218 # Replace it with a negative. | |
219 items[name] = set(['!' + tuple(missing)[0]]) | |
220 | |
221 | |
222 def reduce_inputs(files, dirs, oses): | |
223 """Reduces the variables to their strictest minimum.""" | |
224 # Construct the inverse map first. | |
225 # Look at each individual file and directory, map where they are used and | |
226 # reconstruct the inverse dictionary. | |
227 # First, expands all '!' builders into the reverse. | |
228 # TODO(maruel): This is too late to call _expand_negative(). The exact list | |
229 # negative OSes condition it represents is lost at that point. | |
230 _expand_negative(files, oses) | |
231 _expand_negative(dirs, oses) | |
232 | |
233 # Do not convert back to negative if only 2 OSes were merged. It is easier to | |
234 # read this way. | |
235 if len(oses) > 2: | |
236 _compact_negative(files, oses) | |
237 _compact_negative(dirs, oses) | |
238 | |
239 return files, dirs | |
240 | |
241 | |
242 def convert_to_gyp(files, dirs): | |
243 """Regenerates back a gyp-like configuration dict from files and dirs | |
244 mappings. | |
245 | |
246 Sort the lists. | |
247 """ | |
248 # First, inverse the mapping to make it dict first. | |
249 config = {} | |
250 def to_cond(items, name): | |
251 for item, oses in items.iteritems(): | |
252 for cond_os in oses: | |
253 condition_values = config.setdefault( | |
254 None if cond_os is None else cond_os.lstrip('!'), | |
255 [{}, {}]) | |
256 # If condition is negative, use index 1, else use index 0. | |
257 condition_value = condition_values[int((cond_os or '').startswith('!'))] | |
258 # The list of items (files or dirs). Append the new item and keep the | |
259 # list sorted. | |
260 l = condition_value.setdefault('variables', {}).setdefault(name, []) | |
261 l.append(item) | |
262 l.sort() | |
263 | |
264 to_cond(files, 'isolate_files') | |
265 to_cond(dirs, 'isolate_dirs') | |
266 | |
267 out = {} | |
268 for o in sorted(config): | |
269 d = config[o] | |
270 if o is None: | |
271 assert not d[1] | |
272 out = union(out, d[0]) | |
273 else: | |
274 c = out.setdefault('conditions', []) | |
275 if d[1]: | |
276 c.append(['OS=="%s"' % o] + d) | |
277 else: | |
278 c.append(['OS=="%s"' % o] + d[0:1]) | |
279 return out | |
280 | |
281 | |
282 def main(): | |
283 parser = optparse.OptionParser( | |
284 usage='%prog <options> [file1] [file2] ...') | |
285 parser.add_option( | |
286 '-v', '--verbose', action='count', default=0, help='Use multiple times') | |
287 | |
288 options, args = parser.parse_args() | |
289 level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)] | |
290 logging.basicConfig( | |
291 level=level, | |
292 format='%(levelname)5s %(module)15s(%(lineno)3d):%(message)s') | |
293 | |
294 trace_inputs.pretty_print( | |
295 convert_to_gyp(*reduce_inputs(*parse_gyp_dicts(args))), | |
296 sys.stdout) | |
297 return 0 | |
298 | |
299 | |
300 if __name__ == '__main__': | |
301 sys.exit(main()) | |
OLD | NEW |