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