Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / tools / isolate_driver.py
1 #!/usr/bin/env python
2 # Copyright 2014 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 """Adaptor script called through build/isolate.gypi.
7
8 Creates a wrapping .isolate which 'includes' the original one, that can be
9 consumed by tools/swarming_client/isolate.py. Path variables are determined
10 based on the current working directory. The relative_cwd in the .isolated file
11 is determined based on the .isolate file that declare the 'command' variable to
12 be used so the wrapping .isolate doesn't affect this value.
13
14 This script loads build.ninja and processes it to determine all the executables
15 referenced by the isolated target. It adds them in the wrapping .isolate file.
16
17 WARNING: The target to use for build.ninja analysis is the base name of the
18 .isolate file plus '_run'. For example, 'foo_test.isolate' would have the target
19 'foo_test_run' analysed.
20 """
21
22 import StringIO
23 import glob
24 import logging
25 import os
26 import posixpath
27 import subprocess
28 import sys
29 import time
30
31 TOOLS_DIR = os.path.dirname(os.path.abspath(__file__))
32 SWARMING_CLIENT_DIR = os.path.join(TOOLS_DIR, 'swarming_client')
33 SRC_DIR = os.path.dirname(TOOLS_DIR)
34
35 sys.path.insert(0, SWARMING_CLIENT_DIR)
36
37 import isolate_format
38
39
40 def load_ninja_recursively(build_dir, ninja_path, build_steps):
41   """Crudely extracts all the subninja and build referenced in ninja_path.
42
43   In particular, it ignores rule and variable declarations. The goal is to be
44   performant (well, as much as python can be performant) which is currently in
45   the <200ms range for a complete chromium tree. As such the code is laid out
46   for performance instead of readability.
47   """
48   logging.debug('Loading %s', ninja_path)
49   try:
50     with open(os.path.join(build_dir, ninja_path), 'rb') as f:
51       line = None
52       merge_line = ''
53       subninja = []
54       for line in f:
55         line = line.rstrip()
56         if not line:
57           continue
58
59         if line[-1] == '$':
60           # The next line needs to be merged in.
61           merge_line += line[:-1]
62           continue
63
64         if merge_line:
65           line = merge_line + line
66           merge_line = ''
67
68         statement = line[:line.find(' ')]
69         if statement == 'build':
70           # Save the dependency list as a raw string. Only the lines needed will
71           # be processed with raw_build_to_deps(). This saves a good 70ms of
72           # processing time.
73           build_target, dependencies = line[6:].split(': ', 1)
74           # Interestingly, trying to be smart and only saving the build steps
75           # with the intended extensions ('', '.stamp', '.so') slows down
76           # parsing even if 90% of the build rules can be skipped.
77           # On Windows, a single step may generate two target, so split items
78           # accordingly. It has only been seen for .exe/.exe.pdb combos.
79           for i in build_target.strip().split():
80             build_steps[i] = dependencies
81         elif statement == 'subninja':
82           subninja.append(line[9:])
83   except IOError:
84     print >> sys.stderr, 'Failed to open %s' % ninja_path
85     raise
86
87   total = 1
88   for rel_path in subninja:
89     try:
90       # Load each of the files referenced.
91       # TODO(maruel): Skip the files known to not be needed. It saves an aweful
92       # lot of processing time.
93       total += load_ninja_recursively(build_dir, rel_path, build_steps)
94     except IOError:
95       print >> sys.stderr, '... as referenced by %s' % ninja_path
96       raise
97   return total
98
99
100 def load_ninja(build_dir):
101   """Loads the tree of .ninja files in build_dir."""
102   build_steps = {}
103   total = load_ninja_recursively(build_dir, 'build.ninja', build_steps)
104   logging.info('Loaded %d ninja files, %d build steps', total, len(build_steps))
105   return build_steps
106
107
108 def using_blacklist(item):
109   """Returns True if an item should be analyzed.
110
111   Ignores many rules that are assumed to not depend on a dynamic library. If
112   the assumption doesn't hold true anymore for a file format, remove it from
113   this list. This is simply an optimization.
114   """
115   IGNORED = (
116     '.a', '.cc', '.css', '.def', '.h', '.html', '.js', '.json', '.manifest',
117     '.o', '.obj', '.pak', '.png', '.pdb', '.strings', '.txt',
118   )
119   # ninja files use native path format.
120   ext = os.path.splitext(item)[1]
121   if ext in IGNORED:
122     return False
123   # Special case Windows, keep .dll.lib but discard .lib.
124   if item.endswith('.dll.lib'):
125     return True
126   if ext == '.lib':
127     return False
128   return item not in ('', '|', '||')
129
130
131 def raw_build_to_deps(item):
132   """Converts a raw ninja build statement into the list of interesting
133   dependencies.
134   """
135   # TODO(maruel): Use a whitelist instead? .stamp, .so.TOC, .dylib.TOC,
136   # .dll.lib, .exe and empty.
137   # The first item is the build rule, e.g. 'link', 'cxx', 'phony', etc.
138   return filter(using_blacklist, item.split(' ')[1:])
139
140
141 def recurse(target, build_steps, rules_seen):
142   """Recursively returns all the interesting dependencies for root_item."""
143   out = []
144   if rules_seen is None:
145     rules_seen = set()
146   if target in rules_seen:
147     # TODO(maruel): Figure out how it happens.
148     logging.warning('Circular dependency for %s!', target)
149     return []
150   rules_seen.add(target)
151   try:
152     dependencies = raw_build_to_deps(build_steps[target])
153   except KeyError:
154     logging.info('Failed to find a build step to generate: %s', target)
155     return []
156   logging.debug('recurse(%s) -> %s', target, dependencies)
157   for dependency in dependencies:
158     out.append(dependency)
159     dependency_raw_dependencies = build_steps.get(dependency)
160     if dependency_raw_dependencies:
161       for i in raw_build_to_deps(dependency_raw_dependencies):
162         out.extend(recurse(i, build_steps, rules_seen))
163     else:
164       logging.info('Failed to find a build step to generate: %s', dependency)
165   return out
166
167
168 def post_process_deps(build_dir, dependencies):
169   """Processes the dependency list with OS specific rules."""
170   def filter_item(i):
171     if i.endswith('.so.TOC'):
172       # Remove only the suffix .TOC, not the .so!
173       return i[:-4]
174     if i.endswith('.dylib.TOC'):
175       # Remove only the suffix .TOC, not the .dylib!
176       return i[:-4]
177     if i.endswith('.dll.lib'):
178       # Remove only the suffix .lib, not the .dll!
179       return i[:-4]
180     return i
181
182   # Check for execute access. This gets rid of all the phony rules.
183   return [
184     i for i in map(filter_item, dependencies)
185     if os.access(os.path.join(build_dir, i), os.X_OK)
186   ]
187
188
189 def create_wrapper(args, isolate_index, isolated_index):
190   """Creates a wrapper .isolate that add dynamic libs.
191
192   The original .isolate is not modified.
193   """
194   cwd = os.getcwd()
195   isolate = args[isolate_index]
196   # The code assumes the .isolate file is always specified path-less in cwd. Fix
197   # if this assumption doesn't hold true.
198   assert os.path.basename(isolate) == isolate, isolate
199
200   # This will look like ../out/Debug. This is based against cwd. Note that this
201   # must equal the value provided as PRODUCT_DIR.
202   build_dir = os.path.dirname(args[isolated_index])
203
204   # This will look like chrome/unit_tests.isolate. It is based against SRC_DIR.
205   # It's used to calculate temp_isolate.
206   src_isolate = os.path.relpath(os.path.join(cwd, isolate), SRC_DIR)
207
208   # The wrapping .isolate. This will look like
209   # ../out/Debug/gen/chrome/unit_tests.isolate.
210   temp_isolate = os.path.join(build_dir, 'gen', src_isolate)
211   temp_isolate_dir = os.path.dirname(temp_isolate)
212
213   # Relative path between the new and old .isolate file.
214   isolate_relpath = os.path.relpath(
215       '.', temp_isolate_dir).replace(os.path.sep, '/')
216
217   # It's a big assumption here that the name of the isolate file matches the
218   # primary target '_run'. Fix accordingly if this doesn't hold true, e.g.
219   # complain to maruel@.
220   target = isolate[:-len('.isolate')] + '_run'
221   build_steps = load_ninja(build_dir)
222   binary_deps = post_process_deps(build_dir, recurse(target, build_steps, None))
223   logging.debug(
224       'Binary dependencies:%s', ''.join('\n  ' + i for i in binary_deps))
225
226   # Now do actual wrapping .isolate.
227   isolate_dict = {
228     'includes': [
229       posixpath.join(isolate_relpath, isolate),
230     ],
231     'variables': {
232       # Will look like ['<(PRODUCT_DIR)/lib/flibuser_prefs.so'].
233       isolate_format.KEY_TRACKED: sorted(
234           '<(PRODUCT_DIR)/%s' % i.replace(os.path.sep, '/')
235           for i in binary_deps),
236     },
237   }
238   if not os.path.isdir(temp_isolate_dir):
239     os.makedirs(temp_isolate_dir)
240   comment = (
241       '# Warning: this file was AUTOGENERATED.\n'
242       '# DO NO EDIT.\n')
243   out = StringIO.StringIO()
244   isolate_format.print_all(comment, isolate_dict, out)
245   isolate_content = out.getvalue()
246   with open(temp_isolate, 'wb') as f:
247     f.write(isolate_content)
248   logging.info('Added %d dynamic libs', len(binary_deps))
249   logging.debug('%s', isolate_content)
250   args[isolate_index] = temp_isolate
251
252
253 def main():
254   logging.basicConfig(level=logging.ERROR, format='%(levelname)7s %(message)s')
255   args = sys.argv[1:]
256   isolate = None
257   isolated = None
258   is_component = False
259   for i, arg in enumerate(args):
260     if arg == '--isolate':
261       isolate = i + 1
262     if arg == '--isolated':
263       isolated = i + 1
264     if arg == 'component=shared_library':
265       is_component = True
266   if isolate is None or isolated is None:
267     print >> sys.stderr, 'Internal failure'
268     return 1
269
270   if is_component:
271     create_wrapper(args, isolate, isolated)
272
273   swarming_client = os.path.join(SRC_DIR, 'tools', 'swarming_client')
274   sys.stdout.flush()
275   result = subprocess.call(
276       [sys.executable, os.path.join(swarming_client, 'isolate.py')] + args)
277   return result
278
279
280 if __name__ == '__main__':
281   sys.exit(main())