blob: f3e644128aa47fa52fe4062c4e5cab7933f5e2c7 [file] [log] [blame]
kjellandera013a022016-11-14 05:54:22 -08001#!/usr/bin/env python
2# Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
3#
4# Use of this source code is governed by a BSD-style license
5# that can be found in the LICENSE file in the root of the source
6# tree. An additional intellectual property rights grant can be found
7# in the file PATENTS. All contributing project authors may
8# be found in the AUTHORS file in the root of the source tree.
9
Oleh Prypinb708e932018-03-18 17:34:20 +010010"""MB - the Meta-Build wrapper around GN.
kjellandera013a022016-11-14 05:54:22 -080011
Oleh Prypinb708e932018-03-18 17:34:20 +010012MB is a wrapper script for GN that can be used to generate build files
kjellandera013a022016-11-14 05:54:22 -080013for sets of canned configurations and analyze them.
14"""
15
16from __future__ import print_function
17
18import argparse
19import ast
20import errno
21import json
22import os
23import pipes
24import pprint
25import re
26import shutil
27import sys
28import subprocess
29import tempfile
30import traceback
31import urllib2
32
33from collections import OrderedDict
34
Henrik Kjellanderb2d55772016-12-18 22:14:50 +010035SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
kjellander1c3548c2017-02-15 22:38:22 -080036SRC_DIR = os.path.dirname(os.path.dirname(SCRIPT_DIR))
37sys.path = [os.path.join(SRC_DIR, 'build')] + sys.path
kjellandera013a022016-11-14 05:54:22 -080038
39import gn_helpers
40
41
42def main(args):
43 mbw = MetaBuildWrapper()
44 return mbw.Main(args)
45
46
47class MetaBuildWrapper(object):
48 def __init__(self):
kjellander1c3548c2017-02-15 22:38:22 -080049 self.src_dir = SRC_DIR
Henrik Kjellanderb2d55772016-12-18 22:14:50 +010050 self.default_config = os.path.join(SCRIPT_DIR, 'mb_config.pyl')
Mirko Bonadeib63a8ac2017-01-25 09:36:50 +010051 self.default_isolate_map = os.path.join(SCRIPT_DIR, 'gn_isolate_map.pyl')
kjellandera013a022016-11-14 05:54:22 -080052 self.executable = sys.executable
53 self.platform = sys.platform
54 self.sep = os.sep
55 self.args = argparse.Namespace()
56 self.configs = {}
57 self.masters = {}
58 self.mixins = {}
59
60 def Main(self, args):
61 self.ParseArgs(args)
62 try:
63 ret = self.args.func()
64 if ret:
65 self.DumpInputFiles()
66 return ret
67 except KeyboardInterrupt:
68 self.Print('interrupted, exiting')
69 return 130
70 except Exception:
71 self.DumpInputFiles()
72 s = traceback.format_exc()
73 for l in s.splitlines():
74 self.Print(l)
75 return 1
76
77 def ParseArgs(self, argv):
78 def AddCommonOptions(subp):
79 subp.add_argument('-b', '--builder',
80 help='builder name to look up config from')
81 subp.add_argument('-m', '--master',
82 help='master name to look up config from')
83 subp.add_argument('-c', '--config',
84 help='configuration to analyze')
85 subp.add_argument('--phase',
86 help='optional phase name (used when builders '
87 'do multiple compiles with different '
88 'arguments in a single build)')
89 subp.add_argument('-f', '--config-file', metavar='PATH',
90 default=self.default_config,
91 help='path to config file '
92 '(default is %(default)s)')
93 subp.add_argument('-i', '--isolate-map-file', metavar='PATH',
94 default=self.default_isolate_map,
95 help='path to isolate map file '
96 '(default is %(default)s)')
97 subp.add_argument('-g', '--goma-dir',
98 help='path to goma directory')
kjellandera013a022016-11-14 05:54:22 -080099 subp.add_argument('--android-version-code',
Oleh Prypinb708e932018-03-18 17:34:20 +0100100 help='Sets GN arg android_default_version_code')
kjellandera013a022016-11-14 05:54:22 -0800101 subp.add_argument('--android-version-name',
Oleh Prypinb708e932018-03-18 17:34:20 +0100102 help='Sets GN arg android_default_version_name')
kjellandera013a022016-11-14 05:54:22 -0800103 subp.add_argument('-n', '--dryrun', action='store_true',
104 help='Do a dry run (i.e., do nothing, just print '
105 'the commands that will run)')
106 subp.add_argument('-v', '--verbose', action='store_true',
107 help='verbose logging')
108
109 parser = argparse.ArgumentParser(prog='mb')
110 subps = parser.add_subparsers()
111
112 subp = subps.add_parser('analyze',
113 help='analyze whether changes to a set of files '
114 'will cause a set of binaries to be rebuilt.')
115 AddCommonOptions(subp)
116 subp.add_argument('path', nargs=1,
117 help='path build was generated into.')
118 subp.add_argument('input_path', nargs=1,
119 help='path to a file containing the input arguments '
120 'as a JSON object.')
121 subp.add_argument('output_path', nargs=1,
122 help='path to a file containing the output arguments '
123 'as a JSON object.')
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700124 subp.add_argument('--json-output',
125 help='Write errors to json.output')
kjellandera013a022016-11-14 05:54:22 -0800126 subp.set_defaults(func=self.CmdAnalyze)
127
128 subp = subps.add_parser('export',
129 help='print out the expanded configuration for'
130 'each builder as a JSON object')
131 subp.add_argument('-f', '--config-file', metavar='PATH',
132 default=self.default_config,
133 help='path to config file (default is %(default)s)')
134 subp.add_argument('-g', '--goma-dir',
135 help='path to goma directory')
136 subp.set_defaults(func=self.CmdExport)
137
138 subp = subps.add_parser('gen',
139 help='generate a new set of build files')
140 AddCommonOptions(subp)
141 subp.add_argument('--swarming-targets-file',
142 help='save runtime dependencies for targets listed '
143 'in file.')
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700144 subp.add_argument('--json-output',
145 help='Write errors to json.output')
kjellandera013a022016-11-14 05:54:22 -0800146 subp.add_argument('path', nargs=1,
147 help='path to generate build into')
148 subp.set_defaults(func=self.CmdGen)
149
150 subp = subps.add_parser('isolate',
151 help='generate the .isolate files for a given'
152 'binary')
153 AddCommonOptions(subp)
154 subp.add_argument('path', nargs=1,
155 help='path build was generated into')
156 subp.add_argument('target', nargs=1,
157 help='ninja target to generate the isolate for')
158 subp.set_defaults(func=self.CmdIsolate)
159
160 subp = subps.add_parser('lookup',
161 help='look up the command for a given config or '
162 'builder')
163 AddCommonOptions(subp)
Oleh Prypind7e2fb32019-05-31 13:25:39 +0200164 subp.add_argument('--quiet', default=False, action='store_true',
165 help='Print out just the arguments, '
166 'do not emulate the output of the gen subcommand.')
kjellandera013a022016-11-14 05:54:22 -0800167 subp.set_defaults(func=self.CmdLookup)
168
169 subp = subps.add_parser(
170 'run',
171 help='build and run the isolated version of a '
172 'binary',
173 formatter_class=argparse.RawDescriptionHelpFormatter)
174 subp.description = (
175 'Build, isolate, and run the given binary with the command line\n'
176 'listed in the isolate. You may pass extra arguments after the\n'
177 'target; use "--" if the extra arguments need to include switches.\n'
178 '\n'
179 'Examples:\n'
180 '\n'
181 ' % tools/mb/mb.py run -m chromium.linux -b "Linux Builder" \\\n'
182 ' //out/Default content_browsertests\n'
183 '\n'
184 ' % tools/mb/mb.py run out/Default content_browsertests\n'
185 '\n'
186 ' % tools/mb/mb.py run out/Default content_browsertests -- \\\n'
187 ' --test-launcher-retry-limit=0'
188 '\n'
189 )
kjellandera013a022016-11-14 05:54:22 -0800190 AddCommonOptions(subp)
191 subp.add_argument('-j', '--jobs', dest='jobs', type=int,
192 help='Number of jobs to pass to ninja')
193 subp.add_argument('--no-build', dest='build', default=True,
194 action='store_false',
195 help='Do not build, just isolate and run')
196 subp.add_argument('path', nargs=1,
197 help=('path to generate build into (or use).'
198 ' This can be either a regular path or a '
199 'GN-style source-relative path like '
200 '//out/Default.'))
Oleh Prypinb708e932018-03-18 17:34:20 +0100201 subp.add_argument('-s', '--swarmed', action='store_true',
202 help='Run under swarming')
203 subp.add_argument('-d', '--dimension', default=[], action='append', nargs=2,
204 dest='dimensions', metavar='FOO bar',
205 help='dimension to filter on')
kjellandera013a022016-11-14 05:54:22 -0800206 subp.add_argument('target', nargs=1,
207 help='ninja target to build and run')
208 subp.add_argument('extra_args', nargs='*',
209 help=('extra args to pass to the isolate to run. Use '
210 '"--" as the first arg if you need to pass '
211 'switches'))
212 subp.set_defaults(func=self.CmdRun)
213
214 subp = subps.add_parser('validate',
215 help='validate the config file')
216 subp.add_argument('-f', '--config-file', metavar='PATH',
217 default=self.default_config,
218 help='path to config file (default is %(default)s)')
219 subp.set_defaults(func=self.CmdValidate)
220
kjellandera013a022016-11-14 05:54:22 -0800221 subp = subps.add_parser('help',
222 help='Get help on a subcommand.')
223 subp.add_argument(nargs='?', action='store', dest='subcommand',
224 help='The command to get help for.')
225 subp.set_defaults(func=self.CmdHelp)
226
227 self.args = parser.parse_args(argv)
228
229 def DumpInputFiles(self):
230
231 def DumpContentsOfFilePassedTo(arg_name, path):
232 if path and self.Exists(path):
233 self.Print("\n# To recreate the file passed to %s:" % arg_name)
234 self.Print("%% cat > %s <<EOF" % path)
235 contents = self.ReadFile(path)
236 self.Print(contents)
237 self.Print("EOF\n%\n")
238
239 if getattr(self.args, 'input_path', None):
240 DumpContentsOfFilePassedTo(
241 'argv[0] (input_path)', self.args.input_path[0])
242 if getattr(self.args, 'swarming_targets_file', None):
243 DumpContentsOfFilePassedTo(
244 '--swarming-targets-file', self.args.swarming_targets_file)
245
246 def CmdAnalyze(self):
247 vals = self.Lookup()
Oleh Prypinb708e932018-03-18 17:34:20 +0100248 return self.RunGNAnalyze(vals)
kjellandera013a022016-11-14 05:54:22 -0800249
250 def CmdExport(self):
251 self.ReadConfigFile()
252 obj = {}
253 for master, builders in self.masters.items():
254 obj[master] = {}
255 for builder in builders:
256 config = self.masters[master][builder]
257 if not config:
258 continue
259
260 if isinstance(config, dict):
261 args = {k: self.FlattenConfig(v)['gn_args']
262 for k, v in config.items()}
263 elif config.startswith('//'):
264 args = config
265 else:
266 args = self.FlattenConfig(config)['gn_args']
267 if 'error' in args:
268 continue
269
270 obj[master][builder] = args
271
272 # Dump object and trim trailing whitespace.
273 s = '\n'.join(l.rstrip() for l in
274 json.dumps(obj, sort_keys=True, indent=2).splitlines())
275 self.Print(s)
276 return 0
277
278 def CmdGen(self):
279 vals = self.Lookup()
Oleh Prypinb708e932018-03-18 17:34:20 +0100280 return self.RunGNGen(vals)
kjellandera013a022016-11-14 05:54:22 -0800281
282 def CmdHelp(self):
283 if self.args.subcommand:
284 self.ParseArgs([self.args.subcommand, '--help'])
285 else:
286 self.ParseArgs(['--help'])
287
288 def CmdIsolate(self):
289 vals = self.GetConfig()
290 if not vals:
291 return 1
Oleh Prypinb708e932018-03-18 17:34:20 +0100292 return self.RunGNIsolate(vals)
kjellandera013a022016-11-14 05:54:22 -0800293
294 def CmdLookup(self):
295 vals = self.Lookup()
Oleh Prypinb708e932018-03-18 17:34:20 +0100296 gn_args = self.GNArgs(vals)
Oleh Prypind7e2fb32019-05-31 13:25:39 +0200297 if self.args.quiet:
298 self.Print(gn_args, end='')
299 else:
300 cmd = self.GNCmd('gen', '_path_')
301 self.Print('\nWriting """\\\n%s""" to _path_/args.gn.\n' % gn_args)
302 env = None
kjellandera013a022016-11-14 05:54:22 -0800303
Oleh Prypind7e2fb32019-05-31 13:25:39 +0200304 self.PrintCmd(cmd, env)
kjellandera013a022016-11-14 05:54:22 -0800305 return 0
306
307 def CmdRun(self):
308 vals = self.GetConfig()
309 if not vals:
310 return 1
311
312 build_dir = self.args.path[0]
313 target = self.args.target[0]
314
Oleh Prypinb708e932018-03-18 17:34:20 +0100315 if self.args.build:
316 ret = self.Build(target)
kjellandera013a022016-11-14 05:54:22 -0800317 if ret:
318 return ret
Oleh Prypinb708e932018-03-18 17:34:20 +0100319 ret = self.RunGNIsolate(vals)
320 if ret:
321 return ret
kjellandera013a022016-11-14 05:54:22 -0800322
Oleh Prypinb708e932018-03-18 17:34:20 +0100323 if self.args.swarmed:
324 return self._RunUnderSwarming(build_dir, target)
325 else:
326 return self._RunLocallyIsolated(build_dir, target)
327
328 def _RunUnderSwarming(self, build_dir, target):
329 # TODO(dpranke): Look up the information for the target in
330 # the //testing/buildbot.json file, if possible, so that we
331 # can determine the isolate target, command line, and additional
332 # swarming parameters, if possible.
333 #
334 # TODO(dpranke): Also, add support for sharding and merging results.
335 dimensions = []
336 for k, v in self.args.dimensions:
337 dimensions += ['-d', k, v]
338
339 cmd = [
340 self.executable,
341 self.PathJoin('tools', 'swarming_client', 'isolate.py'),
342 'archive',
343 '-s',
344 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
345 '-I', 'isolateserver.appspot.com',
346 ]
347 ret, out, _ = self.Run(cmd, force_verbose=False)
348 if ret:
349 return ret
350
351 isolated_hash = out.splitlines()[0].split()[0]
352 cmd = [
353 self.executable,
354 self.PathJoin('tools', 'swarming_client', 'swarming.py'),
355 'run',
356 '-s', isolated_hash,
357 '-I', 'isolateserver.appspot.com',
358 '-S', 'chromium-swarm.appspot.com',
359 ] + dimensions
360 if self.args.extra_args:
361 cmd += ['--'] + self.args.extra_args
362 ret, _, _ = self.Run(cmd, force_verbose=True, buffer_output=False)
363 return ret
364
365 def _RunLocallyIsolated(self, build_dir, target):
kjellandera013a022016-11-14 05:54:22 -0800366 cmd = [
367 self.executable,
368 self.PathJoin('tools', 'swarming_client', 'isolate.py'),
369 'run',
370 '-s',
371 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
Oleh Prypinb708e932018-03-18 17:34:20 +0100372 ]
kjellandera013a022016-11-14 05:54:22 -0800373 if self.args.extra_args:
Oleh Prypinb708e932018-03-18 17:34:20 +0100374 cmd += ['--'] + self.args.extra_args
375 ret, _, _ = self.Run(cmd, force_verbose=True, buffer_output=False)
kjellandera013a022016-11-14 05:54:22 -0800376 return ret
377
378 def CmdValidate(self, print_ok=True):
379 errs = []
380
381 # Read the file to make sure it parses.
382 self.ReadConfigFile()
383
384 # Build a list of all of the configs referenced by builders.
385 all_configs = {}
386 for master in self.masters:
387 for config in self.masters[master].values():
388 if isinstance(config, dict):
389 for c in config.values():
390 all_configs[c] = master
391 else:
392 all_configs[config] = master
393
394 # Check that every referenced args file or config actually exists.
395 for config, loc in all_configs.items():
396 if config.startswith('//'):
397 if not self.Exists(self.ToAbsPath(config)):
398 errs.append('Unknown args file "%s" referenced from "%s".' %
399 (config, loc))
400 elif not config in self.configs:
401 errs.append('Unknown config "%s" referenced from "%s".' %
402 (config, loc))
403
404 # Check that every actual config is actually referenced.
405 for config in self.configs:
406 if not config in all_configs:
407 errs.append('Unused config "%s".' % config)
408
409 # Figure out the whole list of mixins, and check that every mixin
410 # listed by a config or another mixin actually exists.
411 referenced_mixins = set()
412 for config, mixins in self.configs.items():
413 for mixin in mixins:
414 if not mixin in self.mixins:
415 errs.append('Unknown mixin "%s" referenced by config "%s".' %
416 (mixin, config))
417 referenced_mixins.add(mixin)
418
419 for mixin in self.mixins:
420 for sub_mixin in self.mixins[mixin].get('mixins', []):
421 if not sub_mixin in self.mixins:
422 errs.append('Unknown mixin "%s" referenced by mixin "%s".' %
423 (sub_mixin, mixin))
424 referenced_mixins.add(sub_mixin)
425
426 # Check that every mixin defined is actually referenced somewhere.
427 for mixin in self.mixins:
428 if not mixin in referenced_mixins:
429 errs.append('Unreferenced mixin "%s".' % mixin)
430
kjellandera013a022016-11-14 05:54:22 -0800431 if errs:
432 raise MBErr(('mb config file %s has problems:' % self.args.config_file) +
433 '\n ' + '\n '.join(errs))
434
435 if print_ok:
436 self.Print('mb config file %s looks ok.' % self.args.config_file)
437 return 0
438
kjellandera013a022016-11-14 05:54:22 -0800439 def GetConfig(self):
440 build_dir = self.args.path[0]
441
442 vals = self.DefaultVals()
443 if self.args.builder or self.args.master or self.args.config:
444 vals = self.Lookup()
Oleh Prypinb708e932018-03-18 17:34:20 +0100445 # Re-run gn gen in order to ensure the config is consistent with the
446 # build dir.
447 self.RunGNGen(vals)
kjellandera013a022016-11-14 05:54:22 -0800448 return vals
449
Oleh Prypinb708e932018-03-18 17:34:20 +0100450 toolchain_path = self.PathJoin(self.ToAbsPath(build_dir),
451 'toolchain.ninja')
452 if not self.Exists(toolchain_path):
453 self.Print('Must either specify a path to an existing GN build dir '
454 'or pass in a -m/-b pair or a -c flag to specify the '
455 'configuration')
456 return {}
kjellandera013a022016-11-14 05:54:22 -0800457
Oleh Prypinb708e932018-03-18 17:34:20 +0100458 vals['gn_args'] = self.GNArgsFromDir(build_dir)
kjellandera013a022016-11-14 05:54:22 -0800459 return vals
460
461 def GNArgsFromDir(self, build_dir):
462 args_contents = ""
463 gn_args_path = self.PathJoin(self.ToAbsPath(build_dir), 'args.gn')
464 if self.Exists(gn_args_path):
465 args_contents = self.ReadFile(gn_args_path)
466 gn_args = []
467 for l in args_contents.splitlines():
468 fields = l.split(' ')
469 name = fields[0]
470 val = ' '.join(fields[2:])
471 gn_args.append('%s=%s' % (name, val))
472
473 return ' '.join(gn_args)
474
475 def Lookup(self):
Oleh Prypin82ac2402019-01-29 16:18:30 +0100476 self.ReadConfigFile()
477 config = self.ConfigFromArgs()
478 if config.startswith('//'):
479 if not self.Exists(self.ToAbsPath(config)):
480 raise MBErr('args file "%s" not found' % config)
481 vals = self.DefaultVals()
482 vals['args_file'] = config
483 else:
484 if not config in self.configs:
485 raise MBErr('Config "%s" not found in %s' %
486 (config, self.args.config_file))
487 vals = self.FlattenConfig(config)
kjellandera013a022016-11-14 05:54:22 -0800488 return vals
489
490 def ReadConfigFile(self):
491 if not self.Exists(self.args.config_file):
492 raise MBErr('config file not found at %s' % self.args.config_file)
493
494 try:
495 contents = ast.literal_eval(self.ReadFile(self.args.config_file))
496 except SyntaxError as e:
497 raise MBErr('Failed to parse config file "%s": %s' %
498 (self.args.config_file, e))
499
500 self.configs = contents['configs']
501 self.masters = contents['masters']
502 self.mixins = contents['mixins']
503
504 def ReadIsolateMap(self):
Oleh Prypinb708e932018-03-18 17:34:20 +0100505 isolate_map = self.args.isolate_map_file
506 if not self.Exists(isolate_map):
507 raise MBErr('isolate map file not found at %s' % isolate_map)
kjellandera013a022016-11-14 05:54:22 -0800508 try:
Oleh Prypinb708e932018-03-18 17:34:20 +0100509 return ast.literal_eval(self.ReadFile(isolate_map))
kjellandera013a022016-11-14 05:54:22 -0800510 except SyntaxError as e:
Oleh Prypinb708e932018-03-18 17:34:20 +0100511 raise MBErr(
512 'Failed to parse isolate map file "%s": %s' % (isolate_map, e))
kjellandera013a022016-11-14 05:54:22 -0800513
514 def ConfigFromArgs(self):
515 if self.args.config:
516 if self.args.master or self.args.builder:
517 raise MBErr('Can not specific both -c/--config and -m/--master or '
518 '-b/--builder')
519
520 return self.args.config
521
522 if not self.args.master or not self.args.builder:
523 raise MBErr('Must specify either -c/--config or '
524 '(-m/--master and -b/--builder)')
525
526 if not self.args.master in self.masters:
527 raise MBErr('Master name "%s" not found in "%s"' %
528 (self.args.master, self.args.config_file))
529
530 if not self.args.builder in self.masters[self.args.master]:
531 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' %
532 (self.args.builder, self.args.master, self.args.config_file))
533
534 config = self.masters[self.args.master][self.args.builder]
535 if isinstance(config, dict):
536 if self.args.phase is None:
537 raise MBErr('Must specify a build --phase for %s on %s' %
538 (self.args.builder, self.args.master))
539 phase = str(self.args.phase)
540 if phase not in config:
541 raise MBErr('Phase %s doesn\'t exist for %s on %s' %
542 (phase, self.args.builder, self.args.master))
543 return config[phase]
544
545 if self.args.phase is not None:
546 raise MBErr('Must not specify a build --phase for %s on %s' %
547 (self.args.builder, self.args.master))
548 return config
549
550 def FlattenConfig(self, config):
551 mixins = self.configs[config]
552 vals = self.DefaultVals()
553
554 visited = []
555 self.FlattenMixins(mixins, vals, visited)
556 return vals
557
558 def DefaultVals(self):
559 return {
560 'args_file': '',
561 'cros_passthrough': False,
562 'gn_args': '',
kjellandera013a022016-11-14 05:54:22 -0800563 }
564
565 def FlattenMixins(self, mixins, vals, visited):
566 for m in mixins:
567 if m not in self.mixins:
568 raise MBErr('Unknown mixin "%s"' % m)
569
570 visited.append(m)
571
572 mixin_vals = self.mixins[m]
573
574 if 'cros_passthrough' in mixin_vals:
575 vals['cros_passthrough'] = mixin_vals['cros_passthrough']
576 if 'gn_args' in mixin_vals:
577 if vals['gn_args']:
578 vals['gn_args'] += ' ' + mixin_vals['gn_args']
579 else:
580 vals['gn_args'] = mixin_vals['gn_args']
kjellandera013a022016-11-14 05:54:22 -0800581
582 if 'mixins' in mixin_vals:
583 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
584 return vals
585
kjellandera013a022016-11-14 05:54:22 -0800586 def RunGNGen(self, vals):
587 build_dir = self.args.path[0]
588
589 cmd = self.GNCmd('gen', build_dir, '--check')
590 gn_args = self.GNArgs(vals)
591
592 # Since GN hasn't run yet, the build directory may not even exist.
593 self.MaybeMakeDirectory(self.ToAbsPath(build_dir))
594
595 gn_args_path = self.ToAbsPath(build_dir, 'args.gn')
596 self.WriteFile(gn_args_path, gn_args, force_verbose=True)
597
598 swarming_targets = []
599 if getattr(self.args, 'swarming_targets_file', None):
600 # We need GN to generate the list of runtime dependencies for
601 # the compile targets listed (one per line) in the file so
602 # we can run them via swarming. We use gn_isolate_map.pyl to convert
603 # the compile targets to the matching GN labels.
604 path = self.args.swarming_targets_file
605 if not self.Exists(path):
606 self.WriteFailureAndRaise('"%s" does not exist' % path,
607 output_path=None)
608 contents = self.ReadFile(path)
609 swarming_targets = set(contents.splitlines())
610
611 isolate_map = self.ReadIsolateMap()
612 err, labels = self.MapTargetsToLabels(isolate_map, swarming_targets)
613 if err:
614 raise MBErr(err)
615
616 gn_runtime_deps_path = self.ToAbsPath(build_dir, 'runtime_deps')
617 self.WriteFile(gn_runtime_deps_path, '\n'.join(labels) + '\n')
618 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path)
619
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700620 ret, output, _ = self.Run(cmd)
kjellandera013a022016-11-14 05:54:22 -0800621 if ret:
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700622 if self.args.json_output:
623 # write errors to json.output
624 self.WriteJSON({'output': output}, self.args.json_output)
kjellandera013a022016-11-14 05:54:22 -0800625 # If `gn gen` failed, we should exit early rather than trying to
626 # generate isolates. Run() will have already logged any error output.
627 self.Print('GN gen failed: %d' % ret)
628 return ret
629
630 android = 'target_os="android"' in vals['gn_args']
631 for target in swarming_targets:
632 if android:
633 # Android targets may be either android_apk or executable. The former
634 # will result in runtime_deps associated with the stamp file, while the
635 # latter will result in runtime_deps associated with the executable.
636 label = isolate_map[target]['label']
637 runtime_deps_targets = [
638 target + '.runtime_deps',
639 'obj/%s.stamp.runtime_deps' % label.replace(':', '/')]
640 elif isolate_map[target]['type'] == 'gpu_browser_test':
641 if self.platform == 'win32':
642 runtime_deps_targets = ['browser_tests.exe.runtime_deps']
643 else:
644 runtime_deps_targets = ['browser_tests.runtime_deps']
Edward Lemur20110752017-09-28 16:14:37 +0200645 elif isolate_map[target]['type'] == 'script':
646 label = isolate_map[target]['label'].split(':')[1]
kjellandera013a022016-11-14 05:54:22 -0800647 runtime_deps_targets = [
Edward Lemur20110752017-09-28 16:14:37 +0200648 '%s.runtime_deps' % label]
kjellandera013a022016-11-14 05:54:22 -0800649 if self.platform == 'win32':
Edward Lemur20110752017-09-28 16:14:37 +0200650 runtime_deps_targets += [ label + '.exe.runtime_deps' ]
kjellandera013a022016-11-14 05:54:22 -0800651 else:
Edward Lemur20110752017-09-28 16:14:37 +0200652 runtime_deps_targets += [ label + '.runtime_deps' ]
kjellandera013a022016-11-14 05:54:22 -0800653 elif self.platform == 'win32':
654 runtime_deps_targets = [target + '.exe.runtime_deps']
655 else:
656 runtime_deps_targets = [target + '.runtime_deps']
657
658 for r in runtime_deps_targets:
659 runtime_deps_path = self.ToAbsPath(build_dir, r)
660 if self.Exists(runtime_deps_path):
661 break
662 else:
663 raise MBErr('did not generate any of %s' %
664 ', '.join(runtime_deps_targets))
665
666 command, extra_files = self.GetIsolateCommand(target, vals)
667
668 runtime_deps = self.ReadFile(runtime_deps_path).splitlines()
669
670 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
671 extra_files)
672
673 return 0
674
675 def RunGNIsolate(self, vals):
676 target = self.args.target[0]
677 isolate_map = self.ReadIsolateMap()
678 err, labels = self.MapTargetsToLabels(isolate_map, [target])
679 if err:
680 raise MBErr(err)
681 label = labels[0]
682
683 build_dir = self.args.path[0]
684 command, extra_files = self.GetIsolateCommand(target, vals)
685
686 cmd = self.GNCmd('desc', build_dir, label, 'runtime_deps')
687 ret, out, _ = self.Call(cmd)
688 if ret:
689 if out:
690 self.Print(out)
691 return ret
692
693 runtime_deps = out.splitlines()
694
695 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
696 extra_files)
697
698 ret, _, _ = self.Run([
699 self.executable,
700 self.PathJoin('tools', 'swarming_client', 'isolate.py'),
701 'check',
702 '-i',
703 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
704 '-s',
705 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target))],
706 buffer_output=False)
707
708 return ret
709
710 def WriteIsolateFiles(self, build_dir, command, target, runtime_deps,
711 extra_files):
712 isolate_path = self.ToAbsPath(build_dir, target + '.isolate')
713 self.WriteFile(isolate_path,
714 pprint.pformat({
715 'variables': {
716 'command': command,
717 'files': sorted(runtime_deps + extra_files),
718 }
719 }) + '\n')
720
721 self.WriteJSON(
722 {
723 'args': [
724 '--isolated',
725 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
726 '--isolate',
727 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
728 ],
kjellander1c3548c2017-02-15 22:38:22 -0800729 'dir': self.src_dir,
kjellandera013a022016-11-14 05:54:22 -0800730 'version': 1,
731 },
732 isolate_path + 'd.gen.json',
733 )
734
735 def MapTargetsToLabels(self, isolate_map, targets):
736 labels = []
737 err = ''
738
739 def StripTestSuffixes(target):
740 for suffix in ('_apk_run', '_apk', '_run'):
741 if target.endswith(suffix):
742 return target[:-len(suffix)], suffix
743 return None, None
744
745 for target in targets:
746 if target == 'all':
747 labels.append(target)
748 elif target.startswith('//'):
749 labels.append(target)
750 else:
751 if target in isolate_map:
752 stripped_target, suffix = target, ''
753 else:
754 stripped_target, suffix = StripTestSuffixes(target)
755 if stripped_target in isolate_map:
756 if isolate_map[stripped_target]['type'] == 'unknown':
757 err += ('test target "%s" type is unknown\n' % target)
758 else:
759 labels.append(isolate_map[stripped_target]['label'] + suffix)
760 else:
761 err += ('target "%s" not found in '
762 '//testing/buildbot/gn_isolate_map.pyl\n' % target)
763
764 return err, labels
765
766 def GNCmd(self, subcommand, path, *args):
Oleh Prypinb708e932018-03-18 17:34:20 +0100767 if self.platform.startswith('linux'):
kjellandera013a022016-11-14 05:54:22 -0800768 subdir, exe = 'linux64', 'gn'
769 elif self.platform == 'darwin':
770 subdir, exe = 'mac', 'gn'
771 else:
772 subdir, exe = 'win', 'gn.exe'
773
kjellander1c3548c2017-02-15 22:38:22 -0800774 gn_path = self.PathJoin(self.src_dir, 'buildtools', subdir, exe)
kjellandera013a022016-11-14 05:54:22 -0800775 return [gn_path, subcommand, path] + list(args)
776
777
778 def GNArgs(self, vals):
779 if vals['cros_passthrough']:
780 if not 'GN_ARGS' in os.environ:
781 raise MBErr('MB is expecting GN_ARGS to be in the environment')
782 gn_args = os.environ['GN_ARGS']
783 if not re.search('target_os.*=.*"chromeos"', gn_args):
784 raise MBErr('GN_ARGS is missing target_os = "chromeos": (GN_ARGS=%s)' %
785 gn_args)
786 else:
787 gn_args = vals['gn_args']
788
789 if self.args.goma_dir:
790 gn_args += ' goma_dir="%s"' % self.args.goma_dir
791
792 android_version_code = self.args.android_version_code
793 if android_version_code:
794 gn_args += ' android_default_version_code="%s"' % android_version_code
795
796 android_version_name = self.args.android_version_name
797 if android_version_name:
798 gn_args += ' android_default_version_name="%s"' % android_version_name
799
800 # Canonicalize the arg string into a sorted, newline-separated list
801 # of key-value pairs, and de-dup the keys if need be so that only
802 # the last instance of each arg is listed.
803 gn_args = gn_helpers.ToGNString(gn_helpers.FromGNArgs(gn_args))
804
805 args_file = vals.get('args_file', None)
806 if args_file:
807 gn_args = ('import("%s")\n' % vals['args_file']) + gn_args
808 return gn_args
809
kjellandera013a022016-11-14 05:54:22 -0800810 def GetIsolateCommand(self, target, vals):
kjellandera013a022016-11-14 05:54:22 -0800811 isolate_map = self.ReadIsolateMap()
812 test_type = isolate_map[target]['type']
813
Oleh Prypinb708e932018-03-18 17:34:20 +0100814 is_android = 'target_os="android"' in vals['gn_args']
815 is_linux = self.platform.startswith('linux') and not is_android
kjellandera013a022016-11-14 05:54:22 -0800816
817 if test_type == 'nontest':
818 self.WriteFailureAndRaise('We should not be isolating %s.' % target,
819 output_path=None)
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800820 if test_type not in ('console_test_launcher', 'windowed_test_launcher',
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100821 'non_parallel_console_test_launcher', 'raw',
Edward Lemur20110752017-09-28 16:14:37 +0200822 'additional_compile_target', 'junit_test', 'script'):
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800823 self.WriteFailureAndRaise('No command line for %s found (test type %s).'
824 % (target, test_type), output_path=None)
kjellandera013a022016-11-14 05:54:22 -0800825
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800826 cmdline = []
Oleh Prypinb708e932018-03-18 17:34:20 +0100827 extra_files = [
828 '../../.vpython',
829 '../../testing/test_env.py',
830 ]
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800831
Yves Gerey2e0c6552018-10-08 21:59:25 +0200832 must_retry = False
Edward Lemur98d40362018-01-15 17:37:04 +0100833 if test_type == 'script':
Oleh Prypin3a51b0e2019-07-17 15:17:53 +0200834 cmdline += ['../../' + self.ToSrcRelPath(isolate_map[target]['script'])]
Oleh Prypinb708e932018-03-18 17:34:20 +0100835 elif is_android:
Oleh Prypin3a51b0e2019-07-17 15:17:53 +0200836 cmdline += ['../../build/android/test_wrapper/logdog_wrapper.py',
837 '--target', target,
838 '--logdog-bin-cmd', '../../bin/logdog_butler',
839 '--logcat-output-file', '${ISOLATED_OUTDIR}/logcats',
840 '--store-tombstones']
kjellandera013a022016-11-14 05:54:22 -0800841 else:
Oleh Prypin3a51b0e2019-07-17 15:17:53 +0200842 if test_type == 'raw':
843 cmdline.append('../../tools_webrtc/flags_compatibility.py')
844 extra_files.append('../../tools_webrtc/flags_compatibility.py')
845
Oleh Prypin739b8162018-05-17 13:28:29 +0200846 if isolate_map[target].get('use_webcam', False):
847 cmdline.append('../../tools_webrtc/ensure_webcam_is_running.py')
848 extra_files.append('../../tools_webrtc/ensure_webcam_is_running.py')
kjellandera013a022016-11-14 05:54:22 -0800849
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800850 # This needs to mirror the settings in //build/config/ui.gni:
851 # use_x11 = is_linux && !use_ozone.
852 use_x11 = is_linux and not 'use_ozone=true' in vals['gn_args']
853
854 xvfb = use_x11 and test_type == 'windowed_test_launcher'
855 if xvfb:
Oleh Prypin739b8162018-05-17 13:28:29 +0200856 cmdline.append('../../testing/xvfb.py')
857 extra_files.append('../../testing/xvfb.py')
858 else:
859 cmdline.append('../../testing/test_env.py')
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800860
Mirko Bonadei264bee82018-08-07 08:53:41 +0200861 if test_type != 'raw':
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800862 extra_files += [
863 '../../third_party/gtest-parallel/gtest-parallel',
ehmaldonadoa7507eb2017-05-10 13:40:29 -0700864 '../../third_party/gtest-parallel/gtest_parallel.py',
Henrik Kjellander90fd7d82017-05-09 08:30:10 +0200865 '../../tools_webrtc/gtest-parallel-wrapper.py',
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800866 ]
ehmaldonado55833842017-02-13 03:58:13 -0800867 sep = '\\' if self.platform == 'win32' else '/'
868 output_dir = '${ISOLATED_OUTDIR}' + sep + 'test_logs'
Edward Lemurbeffdd42017-09-27 13:07:47 +0200869 timeout = isolate_map[target].get('timeout', 900)
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100870 cmdline += [
Henrik Kjellander90fd7d82017-05-09 08:30:10 +0200871 '../../tools_webrtc/gtest-parallel-wrapper.py',
ehmaldonado55833842017-02-13 03:58:13 -0800872 '--output_dir=%s' % output_dir,
ehmaldonado76e60e92017-05-04 06:18:26 -0700873 '--gtest_color=no',
874 # We tell gtest-parallel to interrupt the test after 900 seconds,
875 # so it can exit cleanly and report results, instead of being
876 # interrupted by swarming and not reporting anything.
Edward Lemurbeffdd42017-09-27 13:07:47 +0200877 '--timeout=%s' % timeout,
ehmaldonado55833842017-02-13 03:58:13 -0800878 ]
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100879 if test_type == 'non_parallel_console_test_launcher':
880 # Still use the gtest-parallel-wrapper.py script since we need it to
881 # run tests on swarming, but don't execute tests in parallel.
882 cmdline.append('--workers=1')
Yves Gerey2e0c6552018-10-08 21:59:25 +0200883 must_retry = True
884
885 asan = 'is_asan=true' in vals['gn_args']
886 lsan = 'is_lsan=true' in vals['gn_args']
887 msan = 'is_msan=true' in vals['gn_args']
888 tsan = 'is_tsan=true' in vals['gn_args']
889 sanitizer = asan or lsan or msan or tsan
890 if must_retry and not sanitizer:
891 # Retry would hide most sanitizers detections.
892 cmdline.append('--retry_failed=3')
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100893
894 executable_prefix = '.\\' if self.platform == 'win32' else './'
895 executable_suffix = '.exe' if self.platform == 'win32' else ''
896 executable = executable_prefix + target + executable_suffix
897
898 cmdline.append(executable)
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800899
kjellander382f2b22017-04-11 04:07:01 -0700900 cmdline.extend([
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800901 '--asan=%d' % asan,
kjellander461a5602017-05-05 06:39:16 -0700902 '--lsan=%d' % lsan,
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800903 '--msan=%d' % msan,
904 '--tsan=%d' % tsan,
kjellander382f2b22017-04-11 04:07:01 -0700905 ])
kjellandera013a022016-11-14 05:54:22 -0800906
kjellander74e81262017-03-23 00:51:11 -0700907 cmdline += isolate_map[target].get('args', [])
908
kjellandera013a022016-11-14 05:54:22 -0800909 return cmdline, extra_files
910
911 def ToAbsPath(self, build_path, *comps):
kjellander1c3548c2017-02-15 22:38:22 -0800912 return self.PathJoin(self.src_dir,
kjellandera013a022016-11-14 05:54:22 -0800913 self.ToSrcRelPath(build_path),
914 *comps)
915
916 def ToSrcRelPath(self, path):
917 """Returns a relative path from the top of the repo."""
918 if path.startswith('//'):
919 return path[2:].replace('/', self.sep)
kjellander1c3548c2017-02-15 22:38:22 -0800920 return self.RelPath(path, self.src_dir)
kjellandera013a022016-11-14 05:54:22 -0800921
kjellandera013a022016-11-14 05:54:22 -0800922 def RunGNAnalyze(self, vals):
923 # Analyze runs before 'gn gen' now, so we need to run gn gen
924 # in order to ensure that we have a build directory.
925 ret = self.RunGNGen(vals)
926 if ret:
927 return ret
928
929 build_path = self.args.path[0]
930 input_path = self.args.input_path[0]
931 gn_input_path = input_path + '.gn'
932 output_path = self.args.output_path[0]
933 gn_output_path = output_path + '.gn'
934
935 inp = self.ReadInputJSON(['files', 'test_targets',
936 'additional_compile_targets'])
937 if self.args.verbose:
938 self.Print()
939 self.Print('analyze input:')
940 self.PrintJSON(inp)
941 self.Print()
942
943
944 # This shouldn't normally happen, but could due to unusual race conditions,
945 # like a try job that gets scheduled before a patch lands but runs after
946 # the patch has landed.
947 if not inp['files']:
948 self.Print('Warning: No files modified in patch, bailing out early.')
949 self.WriteJSON({
950 'status': 'No dependency',
951 'compile_targets': [],
952 'test_targets': [],
953 }, output_path)
954 return 0
955
956 gn_inp = {}
957 gn_inp['files'] = ['//' + f for f in inp['files'] if not f.startswith('//')]
958
959 isolate_map = self.ReadIsolateMap()
960 err, gn_inp['additional_compile_targets'] = self.MapTargetsToLabels(
961 isolate_map, inp['additional_compile_targets'])
962 if err:
963 raise MBErr(err)
964
965 err, gn_inp['test_targets'] = self.MapTargetsToLabels(
966 isolate_map, inp['test_targets'])
967 if err:
968 raise MBErr(err)
969 labels_to_targets = {}
970 for i, label in enumerate(gn_inp['test_targets']):
971 labels_to_targets[label] = inp['test_targets'][i]
972
973 try:
974 self.WriteJSON(gn_inp, gn_input_path)
975 cmd = self.GNCmd('analyze', build_path, gn_input_path, gn_output_path)
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700976 ret, output, _ = self.Run(cmd, force_verbose=True)
kjellandera013a022016-11-14 05:54:22 -0800977 if ret:
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700978 if self.args.json_output:
979 # write errors to json.output
980 self.WriteJSON({'output': output}, self.args.json_output)
kjellandera013a022016-11-14 05:54:22 -0800981 return ret
982
983 gn_outp_str = self.ReadFile(gn_output_path)
984 try:
985 gn_outp = json.loads(gn_outp_str)
986 except Exception as e:
987 self.Print("Failed to parse the JSON string GN returned: %s\n%s"
988 % (repr(gn_outp_str), str(e)))
989 raise
990
991 outp = {}
992 if 'status' in gn_outp:
993 outp['status'] = gn_outp['status']
994 if 'error' in gn_outp:
995 outp['error'] = gn_outp['error']
996 if 'invalid_targets' in gn_outp:
997 outp['invalid_targets'] = gn_outp['invalid_targets']
998 if 'compile_targets' in gn_outp:
999 if 'all' in gn_outp['compile_targets']:
1000 outp['compile_targets'] = ['all']
1001 else:
1002 outp['compile_targets'] = [
1003 label.replace('//', '') for label in gn_outp['compile_targets']]
1004 if 'test_targets' in gn_outp:
1005 outp['test_targets'] = [
1006 labels_to_targets[label] for label in gn_outp['test_targets']]
1007
1008 if self.args.verbose:
1009 self.Print()
1010 self.Print('analyze output:')
1011 self.PrintJSON(outp)
1012 self.Print()
1013
1014 self.WriteJSON(outp, output_path)
1015
1016 finally:
1017 if self.Exists(gn_input_path):
1018 self.RemoveFile(gn_input_path)
1019 if self.Exists(gn_output_path):
1020 self.RemoveFile(gn_output_path)
1021
1022 return 0
1023
1024 def ReadInputJSON(self, required_keys):
1025 path = self.args.input_path[0]
1026 output_path = self.args.output_path[0]
1027 if not self.Exists(path):
1028 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
1029
1030 try:
1031 inp = json.loads(self.ReadFile(path))
1032 except Exception as e:
1033 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
1034 (path, e), output_path)
1035
1036 for k in required_keys:
1037 if not k in inp:
1038 self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
1039 output_path)
1040
1041 return inp
1042
1043 def WriteFailureAndRaise(self, msg, output_path):
1044 if output_path:
1045 self.WriteJSON({'error': msg}, output_path, force_verbose=True)
1046 raise MBErr(msg)
1047
1048 def WriteJSON(self, obj, path, force_verbose=False):
1049 try:
1050 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n',
1051 force_verbose=force_verbose)
1052 except Exception as e:
1053 raise MBErr('Error %s writing to the output path "%s"' %
1054 (e, path))
1055
1056 def CheckCompile(self, master, builder):
1057 url_template = self.args.url_template + '/{builder}/builds/_all?as_text=1'
1058 url = urllib2.quote(url_template.format(master=master, builder=builder),
1059 safe=':/()?=')
1060 try:
1061 builds = json.loads(self.Fetch(url))
1062 except Exception as e:
1063 return str(e)
1064 successes = sorted(
1065 [int(x) for x in builds.keys() if "text" in builds[x] and
1066 cmp(builds[x]["text"][:2], ["build", "successful"]) == 0],
1067 reverse=True)
1068 if not successes:
1069 return "no successful builds"
1070 build = builds[str(successes[0])]
1071 step_names = set([step["name"] for step in build["steps"]])
1072 compile_indicators = set(["compile", "compile (with patch)", "analyze"])
1073 if compile_indicators & step_names:
1074 return "compiles"
1075 return "does not compile"
1076
1077 def PrintCmd(self, cmd, env):
1078 if self.platform == 'win32':
1079 env_prefix = 'set '
1080 env_quoter = QuoteForSet
1081 shell_quoter = QuoteForCmd
1082 else:
1083 env_prefix = ''
1084 env_quoter = pipes.quote
1085 shell_quoter = pipes.quote
1086
1087 def print_env(var):
1088 if env and var in env:
1089 self.Print('%s%s=%s' % (env_prefix, var, env_quoter(env[var])))
1090
kjellandera013a022016-11-14 05:54:22 -08001091 print_env('LLVM_FORCE_HEAD_REVISION')
1092
1093 if cmd[0] == self.executable:
1094 cmd = ['python'] + cmd[1:]
1095 self.Print(*[shell_quoter(arg) for arg in cmd])
1096
1097 def PrintJSON(self, obj):
1098 self.Print(json.dumps(obj, indent=2, sort_keys=True))
1099
1100 def Build(self, target):
1101 build_dir = self.ToSrcRelPath(self.args.path[0])
Oleh Prypinb708e932018-03-18 17:34:20 +01001102 ninja_cmd = ['ninja', '-C', build_dir]
kjellandera013a022016-11-14 05:54:22 -08001103 if self.args.jobs:
1104 ninja_cmd.extend(['-j', '%d' % self.args.jobs])
1105 ninja_cmd.append(target)
1106 ret, _, _ = self.Run(ninja_cmd, force_verbose=False, buffer_output=False)
1107 return ret
1108
1109 def Run(self, cmd, env=None, force_verbose=True, buffer_output=True):
1110 # This function largely exists so it can be overridden for testing.
1111 if self.args.dryrun or self.args.verbose or force_verbose:
1112 self.PrintCmd(cmd, env)
1113 if self.args.dryrun:
1114 return 0, '', ''
1115
1116 ret, out, err = self.Call(cmd, env=env, buffer_output=buffer_output)
1117 if self.args.verbose or force_verbose:
1118 if ret:
1119 self.Print(' -> returned %d' % ret)
1120 if out:
1121 self.Print(out, end='')
1122 if err:
1123 self.Print(err, end='', file=sys.stderr)
1124 return ret, out, err
1125
1126 def Call(self, cmd, env=None, buffer_output=True):
1127 if buffer_output:
kjellander1c3548c2017-02-15 22:38:22 -08001128 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001129 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1130 env=env)
1131 out, err = p.communicate()
1132 else:
kjellander1c3548c2017-02-15 22:38:22 -08001133 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001134 env=env)
1135 p.wait()
1136 out = err = ''
1137 return p.returncode, out, err
1138
1139 def ExpandUser(self, path):
1140 # This function largely exists so it can be overridden for testing.
1141 return os.path.expanduser(path)
1142
1143 def Exists(self, path):
1144 # This function largely exists so it can be overridden for testing.
1145 return os.path.exists(path)
1146
1147 def Fetch(self, url):
1148 # This function largely exists so it can be overridden for testing.
1149 f = urllib2.urlopen(url)
1150 contents = f.read()
1151 f.close()
1152 return contents
1153
1154 def MaybeMakeDirectory(self, path):
1155 try:
1156 os.makedirs(path)
1157 except OSError, e:
1158 if e.errno != errno.EEXIST:
1159 raise
1160
1161 def PathJoin(self, *comps):
1162 # This function largely exists so it can be overriden for testing.
1163 return os.path.join(*comps)
1164
1165 def Print(self, *args, **kwargs):
1166 # This function largely exists so it can be overridden for testing.
1167 print(*args, **kwargs)
1168 if kwargs.get('stream', sys.stdout) == sys.stdout:
1169 sys.stdout.flush()
1170
1171 def ReadFile(self, path):
1172 # This function largely exists so it can be overriden for testing.
1173 with open(path) as fp:
1174 return fp.read()
1175
1176 def RelPath(self, path, start='.'):
1177 # This function largely exists so it can be overriden for testing.
1178 return os.path.relpath(path, start)
1179
1180 def RemoveFile(self, path):
1181 # This function largely exists so it can be overriden for testing.
1182 os.remove(path)
1183
1184 def RemoveDirectory(self, abs_path):
1185 if self.platform == 'win32':
1186 # In other places in chromium, we often have to retry this command
1187 # because we're worried about other processes still holding on to
1188 # file handles, but when MB is invoked, it will be early enough in the
1189 # build that their should be no other processes to interfere. We
1190 # can change this if need be.
1191 self.Run(['cmd.exe', '/c', 'rmdir', '/q', '/s', abs_path])
1192 else:
1193 shutil.rmtree(abs_path, ignore_errors=True)
1194
1195 def TempFile(self, mode='w'):
1196 # This function largely exists so it can be overriden for testing.
1197 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
1198
1199 def WriteFile(self, path, contents, force_verbose=False):
1200 # This function largely exists so it can be overriden for testing.
1201 if self.args.dryrun or self.args.verbose or force_verbose:
1202 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
1203 with open(path, 'w') as fp:
1204 return fp.write(contents)
1205
1206
1207class MBErr(Exception):
1208 pass
1209
1210
1211# See http://goo.gl/l5NPDW and http://goo.gl/4Diozm for the painful
1212# details of this next section, which handles escaping command lines
1213# so that they can be copied and pasted into a cmd window.
1214UNSAFE_FOR_SET = set('^<>&|')
1215UNSAFE_FOR_CMD = UNSAFE_FOR_SET.union(set('()%'))
1216ALL_META_CHARS = UNSAFE_FOR_CMD.union(set('"'))
1217
1218
1219def QuoteForSet(arg):
1220 if any(a in UNSAFE_FOR_SET for a in arg):
1221 arg = ''.join('^' + a if a in UNSAFE_FOR_SET else a for a in arg)
1222 return arg
1223
1224
1225def QuoteForCmd(arg):
1226 # First, escape the arg so that CommandLineToArgvW will parse it properly.
kjellandera013a022016-11-14 05:54:22 -08001227 if arg == '' or ' ' in arg or '"' in arg:
1228 quote_re = re.compile(r'(\\*)"')
1229 arg = '"%s"' % (quote_re.sub(lambda mo: 2 * mo.group(1) + '\\"', arg))
1230
1231 # Then check to see if the arg contains any metacharacters other than
1232 # double quotes; if it does, quote everything (including the double
1233 # quotes) for safety.
1234 if any(a in UNSAFE_FOR_CMD for a in arg):
1235 arg = ''.join('^' + a if a in ALL_META_CHARS else a for a in arg)
1236 return arg
1237
1238
1239if __name__ == '__main__':
1240 sys.exit(main(sys.argv[1:]))