blob: 9628a673ae7f7f98b5094407f7d36ec03e697355 [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.')
124 subp.set_defaults(func=self.CmdAnalyze)
125
126 subp = subps.add_parser('export',
127 help='print out the expanded configuration for'
128 'each builder as a JSON object')
129 subp.add_argument('-f', '--config-file', metavar='PATH',
130 default=self.default_config,
131 help='path to config file (default is %(default)s)')
132 subp.add_argument('-g', '--goma-dir',
133 help='path to goma directory')
134 subp.set_defaults(func=self.CmdExport)
135
136 subp = subps.add_parser('gen',
137 help='generate a new set of build files')
138 AddCommonOptions(subp)
139 subp.add_argument('--swarming-targets-file',
140 help='save runtime dependencies for targets listed '
141 'in file.')
142 subp.add_argument('path', nargs=1,
143 help='path to generate build into')
144 subp.set_defaults(func=self.CmdGen)
145
146 subp = subps.add_parser('isolate',
147 help='generate the .isolate files for a given'
148 'binary')
149 AddCommonOptions(subp)
150 subp.add_argument('path', nargs=1,
151 help='path build was generated into')
152 subp.add_argument('target', nargs=1,
153 help='ninja target to generate the isolate for')
154 subp.set_defaults(func=self.CmdIsolate)
155
156 subp = subps.add_parser('lookup',
157 help='look up the command for a given config or '
158 'builder')
159 AddCommonOptions(subp)
Oleh Prypind7e2fb32019-05-31 13:25:39 +0200160 subp.add_argument('--quiet', default=False, action='store_true',
161 help='Print out just the arguments, '
162 'do not emulate the output of the gen subcommand.')
kjellandera013a022016-11-14 05:54:22 -0800163 subp.set_defaults(func=self.CmdLookup)
164
165 subp = subps.add_parser(
166 'run',
167 help='build and run the isolated version of a '
168 'binary',
169 formatter_class=argparse.RawDescriptionHelpFormatter)
170 subp.description = (
171 'Build, isolate, and run the given binary with the command line\n'
172 'listed in the isolate. You may pass extra arguments after the\n'
173 'target; use "--" if the extra arguments need to include switches.\n'
174 '\n'
175 'Examples:\n'
176 '\n'
177 ' % tools/mb/mb.py run -m chromium.linux -b "Linux Builder" \\\n'
178 ' //out/Default content_browsertests\n'
179 '\n'
180 ' % tools/mb/mb.py run out/Default content_browsertests\n'
181 '\n'
182 ' % tools/mb/mb.py run out/Default content_browsertests -- \\\n'
183 ' --test-launcher-retry-limit=0'
184 '\n'
185 )
kjellandera013a022016-11-14 05:54:22 -0800186 AddCommonOptions(subp)
187 subp.add_argument('-j', '--jobs', dest='jobs', type=int,
188 help='Number of jobs to pass to ninja')
189 subp.add_argument('--no-build', dest='build', default=True,
190 action='store_false',
191 help='Do not build, just isolate and run')
192 subp.add_argument('path', nargs=1,
193 help=('path to generate build into (or use).'
194 ' This can be either a regular path or a '
195 'GN-style source-relative path like '
196 '//out/Default.'))
Oleh Prypinb708e932018-03-18 17:34:20 +0100197 subp.add_argument('-s', '--swarmed', action='store_true',
198 help='Run under swarming')
199 subp.add_argument('-d', '--dimension', default=[], action='append', nargs=2,
200 dest='dimensions', metavar='FOO bar',
201 help='dimension to filter on')
kjellandera013a022016-11-14 05:54:22 -0800202 subp.add_argument('target', nargs=1,
203 help='ninja target to build and run')
204 subp.add_argument('extra_args', nargs='*',
205 help=('extra args to pass to the isolate to run. Use '
206 '"--" as the first arg if you need to pass '
207 'switches'))
208 subp.set_defaults(func=self.CmdRun)
209
210 subp = subps.add_parser('validate',
211 help='validate the config file')
212 subp.add_argument('-f', '--config-file', metavar='PATH',
213 default=self.default_config,
214 help='path to config file (default is %(default)s)')
215 subp.set_defaults(func=self.CmdValidate)
216
kjellandera013a022016-11-14 05:54:22 -0800217 subp = subps.add_parser('help',
218 help='Get help on a subcommand.')
219 subp.add_argument(nargs='?', action='store', dest='subcommand',
220 help='The command to get help for.')
221 subp.set_defaults(func=self.CmdHelp)
222
223 self.args = parser.parse_args(argv)
224
225 def DumpInputFiles(self):
226
227 def DumpContentsOfFilePassedTo(arg_name, path):
228 if path and self.Exists(path):
229 self.Print("\n# To recreate the file passed to %s:" % arg_name)
230 self.Print("%% cat > %s <<EOF" % path)
231 contents = self.ReadFile(path)
232 self.Print(contents)
233 self.Print("EOF\n%\n")
234
235 if getattr(self.args, 'input_path', None):
236 DumpContentsOfFilePassedTo(
237 'argv[0] (input_path)', self.args.input_path[0])
238 if getattr(self.args, 'swarming_targets_file', None):
239 DumpContentsOfFilePassedTo(
240 '--swarming-targets-file', self.args.swarming_targets_file)
241
242 def CmdAnalyze(self):
243 vals = self.Lookup()
Oleh Prypinb708e932018-03-18 17:34:20 +0100244 return self.RunGNAnalyze(vals)
kjellandera013a022016-11-14 05:54:22 -0800245
246 def CmdExport(self):
247 self.ReadConfigFile()
248 obj = {}
249 for master, builders in self.masters.items():
250 obj[master] = {}
251 for builder in builders:
252 config = self.masters[master][builder]
253 if not config:
254 continue
255
256 if isinstance(config, dict):
257 args = {k: self.FlattenConfig(v)['gn_args']
258 for k, v in config.items()}
259 elif config.startswith('//'):
260 args = config
261 else:
262 args = self.FlattenConfig(config)['gn_args']
263 if 'error' in args:
264 continue
265
266 obj[master][builder] = args
267
268 # Dump object and trim trailing whitespace.
269 s = '\n'.join(l.rstrip() for l in
270 json.dumps(obj, sort_keys=True, indent=2).splitlines())
271 self.Print(s)
272 return 0
273
274 def CmdGen(self):
275 vals = self.Lookup()
Oleh Prypinb708e932018-03-18 17:34:20 +0100276 return self.RunGNGen(vals)
kjellandera013a022016-11-14 05:54:22 -0800277
278 def CmdHelp(self):
279 if self.args.subcommand:
280 self.ParseArgs([self.args.subcommand, '--help'])
281 else:
282 self.ParseArgs(['--help'])
283
284 def CmdIsolate(self):
285 vals = self.GetConfig()
286 if not vals:
287 return 1
Oleh Prypinb708e932018-03-18 17:34:20 +0100288 return self.RunGNIsolate(vals)
kjellandera013a022016-11-14 05:54:22 -0800289
290 def CmdLookup(self):
291 vals = self.Lookup()
Oleh Prypinb708e932018-03-18 17:34:20 +0100292 gn_args = self.GNArgs(vals)
Oleh Prypind7e2fb32019-05-31 13:25:39 +0200293 if self.args.quiet:
294 self.Print(gn_args, end='')
295 else:
296 cmd = self.GNCmd('gen', '_path_')
297 self.Print('\nWriting """\\\n%s""" to _path_/args.gn.\n' % gn_args)
298 env = None
kjellandera013a022016-11-14 05:54:22 -0800299
Oleh Prypind7e2fb32019-05-31 13:25:39 +0200300 self.PrintCmd(cmd, env)
kjellandera013a022016-11-14 05:54:22 -0800301 return 0
302
303 def CmdRun(self):
304 vals = self.GetConfig()
305 if not vals:
306 return 1
307
308 build_dir = self.args.path[0]
309 target = self.args.target[0]
310
Oleh Prypinb708e932018-03-18 17:34:20 +0100311 if self.args.build:
312 ret = self.Build(target)
kjellandera013a022016-11-14 05:54:22 -0800313 if ret:
314 return ret
Oleh Prypinb708e932018-03-18 17:34:20 +0100315 ret = self.RunGNIsolate(vals)
316 if ret:
317 return ret
kjellandera013a022016-11-14 05:54:22 -0800318
Oleh Prypinb708e932018-03-18 17:34:20 +0100319 if self.args.swarmed:
320 return self._RunUnderSwarming(build_dir, target)
321 else:
322 return self._RunLocallyIsolated(build_dir, target)
323
324 def _RunUnderSwarming(self, build_dir, target):
325 # TODO(dpranke): Look up the information for the target in
326 # the //testing/buildbot.json file, if possible, so that we
327 # can determine the isolate target, command line, and additional
328 # swarming parameters, if possible.
329 #
330 # TODO(dpranke): Also, add support for sharding and merging results.
331 dimensions = []
332 for k, v in self.args.dimensions:
333 dimensions += ['-d', k, v]
334
335 cmd = [
336 self.executable,
337 self.PathJoin('tools', 'swarming_client', 'isolate.py'),
338 'archive',
339 '-s',
340 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
341 '-I', 'isolateserver.appspot.com',
342 ]
343 ret, out, _ = self.Run(cmd, force_verbose=False)
344 if ret:
345 return ret
346
347 isolated_hash = out.splitlines()[0].split()[0]
348 cmd = [
349 self.executable,
350 self.PathJoin('tools', 'swarming_client', 'swarming.py'),
351 'run',
352 '-s', isolated_hash,
353 '-I', 'isolateserver.appspot.com',
354 '-S', 'chromium-swarm.appspot.com',
355 ] + dimensions
356 if self.args.extra_args:
357 cmd += ['--'] + self.args.extra_args
358 ret, _, _ = self.Run(cmd, force_verbose=True, buffer_output=False)
359 return ret
360
361 def _RunLocallyIsolated(self, build_dir, target):
kjellandera013a022016-11-14 05:54:22 -0800362 cmd = [
363 self.executable,
364 self.PathJoin('tools', 'swarming_client', 'isolate.py'),
365 'run',
366 '-s',
367 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
Oleh Prypinb708e932018-03-18 17:34:20 +0100368 ]
kjellandera013a022016-11-14 05:54:22 -0800369 if self.args.extra_args:
Oleh Prypinb708e932018-03-18 17:34:20 +0100370 cmd += ['--'] + self.args.extra_args
371 ret, _, _ = self.Run(cmd, force_verbose=True, buffer_output=False)
kjellandera013a022016-11-14 05:54:22 -0800372 return ret
373
374 def CmdValidate(self, print_ok=True):
375 errs = []
376
377 # Read the file to make sure it parses.
378 self.ReadConfigFile()
379
380 # Build a list of all of the configs referenced by builders.
381 all_configs = {}
382 for master in self.masters:
383 for config in self.masters[master].values():
384 if isinstance(config, dict):
385 for c in config.values():
386 all_configs[c] = master
387 else:
388 all_configs[config] = master
389
390 # Check that every referenced args file or config actually exists.
391 for config, loc in all_configs.items():
392 if config.startswith('//'):
393 if not self.Exists(self.ToAbsPath(config)):
394 errs.append('Unknown args file "%s" referenced from "%s".' %
395 (config, loc))
396 elif not config in self.configs:
397 errs.append('Unknown config "%s" referenced from "%s".' %
398 (config, loc))
399
400 # Check that every actual config is actually referenced.
401 for config in self.configs:
402 if not config in all_configs:
403 errs.append('Unused config "%s".' % config)
404
405 # Figure out the whole list of mixins, and check that every mixin
406 # listed by a config or another mixin actually exists.
407 referenced_mixins = set()
408 for config, mixins in self.configs.items():
409 for mixin in mixins:
410 if not mixin in self.mixins:
411 errs.append('Unknown mixin "%s" referenced by config "%s".' %
412 (mixin, config))
413 referenced_mixins.add(mixin)
414
415 for mixin in self.mixins:
416 for sub_mixin in self.mixins[mixin].get('mixins', []):
417 if not sub_mixin in self.mixins:
418 errs.append('Unknown mixin "%s" referenced by mixin "%s".' %
419 (sub_mixin, mixin))
420 referenced_mixins.add(sub_mixin)
421
422 # Check that every mixin defined is actually referenced somewhere.
423 for mixin in self.mixins:
424 if not mixin in referenced_mixins:
425 errs.append('Unreferenced mixin "%s".' % mixin)
426
kjellandera013a022016-11-14 05:54:22 -0800427 if errs:
428 raise MBErr(('mb config file %s has problems:' % self.args.config_file) +
429 '\n ' + '\n '.join(errs))
430
431 if print_ok:
432 self.Print('mb config file %s looks ok.' % self.args.config_file)
433 return 0
434
kjellandera013a022016-11-14 05:54:22 -0800435 def GetConfig(self):
436 build_dir = self.args.path[0]
437
438 vals = self.DefaultVals()
439 if self.args.builder or self.args.master or self.args.config:
440 vals = self.Lookup()
Oleh Prypinb708e932018-03-18 17:34:20 +0100441 # Re-run gn gen in order to ensure the config is consistent with the
442 # build dir.
443 self.RunGNGen(vals)
kjellandera013a022016-11-14 05:54:22 -0800444 return vals
445
Oleh Prypinb708e932018-03-18 17:34:20 +0100446 toolchain_path = self.PathJoin(self.ToAbsPath(build_dir),
447 'toolchain.ninja')
448 if not self.Exists(toolchain_path):
449 self.Print('Must either specify a path to an existing GN build dir '
450 'or pass in a -m/-b pair or a -c flag to specify the '
451 'configuration')
452 return {}
kjellandera013a022016-11-14 05:54:22 -0800453
Oleh Prypinb708e932018-03-18 17:34:20 +0100454 vals['gn_args'] = self.GNArgsFromDir(build_dir)
kjellandera013a022016-11-14 05:54:22 -0800455 return vals
456
457 def GNArgsFromDir(self, build_dir):
458 args_contents = ""
459 gn_args_path = self.PathJoin(self.ToAbsPath(build_dir), 'args.gn')
460 if self.Exists(gn_args_path):
461 args_contents = self.ReadFile(gn_args_path)
462 gn_args = []
463 for l in args_contents.splitlines():
464 fields = l.split(' ')
465 name = fields[0]
466 val = ' '.join(fields[2:])
467 gn_args.append('%s=%s' % (name, val))
468
469 return ' '.join(gn_args)
470
471 def Lookup(self):
Oleh Prypin82ac2402019-01-29 16:18:30 +0100472 self.ReadConfigFile()
473 config = self.ConfigFromArgs()
474 if config.startswith('//'):
475 if not self.Exists(self.ToAbsPath(config)):
476 raise MBErr('args file "%s" not found' % config)
477 vals = self.DefaultVals()
478 vals['args_file'] = config
479 else:
480 if not config in self.configs:
481 raise MBErr('Config "%s" not found in %s' %
482 (config, self.args.config_file))
483 vals = self.FlattenConfig(config)
kjellandera013a022016-11-14 05:54:22 -0800484 return vals
485
486 def ReadConfigFile(self):
487 if not self.Exists(self.args.config_file):
488 raise MBErr('config file not found at %s' % self.args.config_file)
489
490 try:
491 contents = ast.literal_eval(self.ReadFile(self.args.config_file))
492 except SyntaxError as e:
493 raise MBErr('Failed to parse config file "%s": %s' %
494 (self.args.config_file, e))
495
496 self.configs = contents['configs']
497 self.masters = contents['masters']
498 self.mixins = contents['mixins']
499
500 def ReadIsolateMap(self):
Oleh Prypinb708e932018-03-18 17:34:20 +0100501 isolate_map = self.args.isolate_map_file
502 if not self.Exists(isolate_map):
503 raise MBErr('isolate map file not found at %s' % isolate_map)
kjellandera013a022016-11-14 05:54:22 -0800504 try:
Oleh Prypinb708e932018-03-18 17:34:20 +0100505 return ast.literal_eval(self.ReadFile(isolate_map))
kjellandera013a022016-11-14 05:54:22 -0800506 except SyntaxError as e:
Oleh Prypinb708e932018-03-18 17:34:20 +0100507 raise MBErr(
508 'Failed to parse isolate map file "%s": %s' % (isolate_map, e))
kjellandera013a022016-11-14 05:54:22 -0800509
510 def ConfigFromArgs(self):
511 if self.args.config:
512 if self.args.master or self.args.builder:
513 raise MBErr('Can not specific both -c/--config and -m/--master or '
514 '-b/--builder')
515
516 return self.args.config
517
518 if not self.args.master or not self.args.builder:
519 raise MBErr('Must specify either -c/--config or '
520 '(-m/--master and -b/--builder)')
521
522 if not self.args.master in self.masters:
523 raise MBErr('Master name "%s" not found in "%s"' %
524 (self.args.master, self.args.config_file))
525
526 if not self.args.builder in self.masters[self.args.master]:
527 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' %
528 (self.args.builder, self.args.master, self.args.config_file))
529
530 config = self.masters[self.args.master][self.args.builder]
531 if isinstance(config, dict):
532 if self.args.phase is None:
533 raise MBErr('Must specify a build --phase for %s on %s' %
534 (self.args.builder, self.args.master))
535 phase = str(self.args.phase)
536 if phase not in config:
537 raise MBErr('Phase %s doesn\'t exist for %s on %s' %
538 (phase, self.args.builder, self.args.master))
539 return config[phase]
540
541 if self.args.phase is not None:
542 raise MBErr('Must not specify a build --phase for %s on %s' %
543 (self.args.builder, self.args.master))
544 return config
545
546 def FlattenConfig(self, config):
547 mixins = self.configs[config]
548 vals = self.DefaultVals()
549
550 visited = []
551 self.FlattenMixins(mixins, vals, visited)
552 return vals
553
554 def DefaultVals(self):
555 return {
556 'args_file': '',
557 'cros_passthrough': False,
558 'gn_args': '',
kjellandera013a022016-11-14 05:54:22 -0800559 }
560
561 def FlattenMixins(self, mixins, vals, visited):
562 for m in mixins:
563 if m not in self.mixins:
564 raise MBErr('Unknown mixin "%s"' % m)
565
566 visited.append(m)
567
568 mixin_vals = self.mixins[m]
569
570 if 'cros_passthrough' in mixin_vals:
571 vals['cros_passthrough'] = mixin_vals['cros_passthrough']
572 if 'gn_args' in mixin_vals:
573 if vals['gn_args']:
574 vals['gn_args'] += ' ' + mixin_vals['gn_args']
575 else:
576 vals['gn_args'] = mixin_vals['gn_args']
kjellandera013a022016-11-14 05:54:22 -0800577
578 if 'mixins' in mixin_vals:
579 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
580 return vals
581
kjellandera013a022016-11-14 05:54:22 -0800582 def RunGNGen(self, vals):
583 build_dir = self.args.path[0]
584
585 cmd = self.GNCmd('gen', build_dir, '--check')
586 gn_args = self.GNArgs(vals)
587
588 # Since GN hasn't run yet, the build directory may not even exist.
589 self.MaybeMakeDirectory(self.ToAbsPath(build_dir))
590
591 gn_args_path = self.ToAbsPath(build_dir, 'args.gn')
592 self.WriteFile(gn_args_path, gn_args, force_verbose=True)
593
594 swarming_targets = []
595 if getattr(self.args, 'swarming_targets_file', None):
596 # We need GN to generate the list of runtime dependencies for
597 # the compile targets listed (one per line) in the file so
598 # we can run them via swarming. We use gn_isolate_map.pyl to convert
599 # the compile targets to the matching GN labels.
600 path = self.args.swarming_targets_file
601 if not self.Exists(path):
602 self.WriteFailureAndRaise('"%s" does not exist' % path,
603 output_path=None)
604 contents = self.ReadFile(path)
605 swarming_targets = set(contents.splitlines())
606
607 isolate_map = self.ReadIsolateMap()
608 err, labels = self.MapTargetsToLabels(isolate_map, swarming_targets)
609 if err:
610 raise MBErr(err)
611
612 gn_runtime_deps_path = self.ToAbsPath(build_dir, 'runtime_deps')
613 self.WriteFile(gn_runtime_deps_path, '\n'.join(labels) + '\n')
614 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path)
615
616 ret, _, _ = self.Run(cmd)
617 if ret:
618 # If `gn gen` failed, we should exit early rather than trying to
619 # generate isolates. Run() will have already logged any error output.
620 self.Print('GN gen failed: %d' % ret)
621 return ret
622
623 android = 'target_os="android"' in vals['gn_args']
624 for target in swarming_targets:
625 if android:
626 # Android targets may be either android_apk or executable. The former
627 # will result in runtime_deps associated with the stamp file, while the
628 # latter will result in runtime_deps associated with the executable.
629 label = isolate_map[target]['label']
630 runtime_deps_targets = [
631 target + '.runtime_deps',
632 'obj/%s.stamp.runtime_deps' % label.replace(':', '/')]
633 elif isolate_map[target]['type'] == 'gpu_browser_test':
634 if self.platform == 'win32':
635 runtime_deps_targets = ['browser_tests.exe.runtime_deps']
636 else:
637 runtime_deps_targets = ['browser_tests.runtime_deps']
Edward Lemur20110752017-09-28 16:14:37 +0200638 elif isolate_map[target]['type'] == 'script':
639 label = isolate_map[target]['label'].split(':')[1]
kjellandera013a022016-11-14 05:54:22 -0800640 runtime_deps_targets = [
Edward Lemur20110752017-09-28 16:14:37 +0200641 '%s.runtime_deps' % label]
kjellandera013a022016-11-14 05:54:22 -0800642 if self.platform == 'win32':
Edward Lemur20110752017-09-28 16:14:37 +0200643 runtime_deps_targets += [ label + '.exe.runtime_deps' ]
kjellandera013a022016-11-14 05:54:22 -0800644 else:
Edward Lemur20110752017-09-28 16:14:37 +0200645 runtime_deps_targets += [ label + '.runtime_deps' ]
kjellandera013a022016-11-14 05:54:22 -0800646 elif self.platform == 'win32':
647 runtime_deps_targets = [target + '.exe.runtime_deps']
648 else:
649 runtime_deps_targets = [target + '.runtime_deps']
650
651 for r in runtime_deps_targets:
652 runtime_deps_path = self.ToAbsPath(build_dir, r)
653 if self.Exists(runtime_deps_path):
654 break
655 else:
656 raise MBErr('did not generate any of %s' %
657 ', '.join(runtime_deps_targets))
658
659 command, extra_files = self.GetIsolateCommand(target, vals)
660
661 runtime_deps = self.ReadFile(runtime_deps_path).splitlines()
662
663 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
664 extra_files)
665
666 return 0
667
668 def RunGNIsolate(self, vals):
669 target = self.args.target[0]
670 isolate_map = self.ReadIsolateMap()
671 err, labels = self.MapTargetsToLabels(isolate_map, [target])
672 if err:
673 raise MBErr(err)
674 label = labels[0]
675
676 build_dir = self.args.path[0]
677 command, extra_files = self.GetIsolateCommand(target, vals)
678
679 cmd = self.GNCmd('desc', build_dir, label, 'runtime_deps')
680 ret, out, _ = self.Call(cmd)
681 if ret:
682 if out:
683 self.Print(out)
684 return ret
685
686 runtime_deps = out.splitlines()
687
688 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
689 extra_files)
690
691 ret, _, _ = self.Run([
692 self.executable,
693 self.PathJoin('tools', 'swarming_client', 'isolate.py'),
694 'check',
695 '-i',
696 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
697 '-s',
698 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target))],
699 buffer_output=False)
700
701 return ret
702
703 def WriteIsolateFiles(self, build_dir, command, target, runtime_deps,
704 extra_files):
705 isolate_path = self.ToAbsPath(build_dir, target + '.isolate')
706 self.WriteFile(isolate_path,
707 pprint.pformat({
708 'variables': {
709 'command': command,
710 'files': sorted(runtime_deps + extra_files),
711 }
712 }) + '\n')
713
714 self.WriteJSON(
715 {
716 'args': [
717 '--isolated',
718 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
719 '--isolate',
720 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
721 ],
kjellander1c3548c2017-02-15 22:38:22 -0800722 'dir': self.src_dir,
kjellandera013a022016-11-14 05:54:22 -0800723 'version': 1,
724 },
725 isolate_path + 'd.gen.json',
726 )
727
728 def MapTargetsToLabels(self, isolate_map, targets):
729 labels = []
730 err = ''
731
732 def StripTestSuffixes(target):
733 for suffix in ('_apk_run', '_apk', '_run'):
734 if target.endswith(suffix):
735 return target[:-len(suffix)], suffix
736 return None, None
737
738 for target in targets:
739 if target == 'all':
740 labels.append(target)
741 elif target.startswith('//'):
742 labels.append(target)
743 else:
744 if target in isolate_map:
745 stripped_target, suffix = target, ''
746 else:
747 stripped_target, suffix = StripTestSuffixes(target)
748 if stripped_target in isolate_map:
749 if isolate_map[stripped_target]['type'] == 'unknown':
750 err += ('test target "%s" type is unknown\n' % target)
751 else:
752 labels.append(isolate_map[stripped_target]['label'] + suffix)
753 else:
754 err += ('target "%s" not found in '
755 '//testing/buildbot/gn_isolate_map.pyl\n' % target)
756
757 return err, labels
758
759 def GNCmd(self, subcommand, path, *args):
Oleh Prypinb708e932018-03-18 17:34:20 +0100760 if self.platform.startswith('linux'):
kjellandera013a022016-11-14 05:54:22 -0800761 subdir, exe = 'linux64', 'gn'
762 elif self.platform == 'darwin':
763 subdir, exe = 'mac', 'gn'
764 else:
765 subdir, exe = 'win', 'gn.exe'
766
kjellander1c3548c2017-02-15 22:38:22 -0800767 gn_path = self.PathJoin(self.src_dir, 'buildtools', subdir, exe)
kjellandera013a022016-11-14 05:54:22 -0800768 return [gn_path, subcommand, path] + list(args)
769
770
771 def GNArgs(self, vals):
772 if vals['cros_passthrough']:
773 if not 'GN_ARGS' in os.environ:
774 raise MBErr('MB is expecting GN_ARGS to be in the environment')
775 gn_args = os.environ['GN_ARGS']
776 if not re.search('target_os.*=.*"chromeos"', gn_args):
777 raise MBErr('GN_ARGS is missing target_os = "chromeos": (GN_ARGS=%s)' %
778 gn_args)
779 else:
780 gn_args = vals['gn_args']
781
782 if self.args.goma_dir:
783 gn_args += ' goma_dir="%s"' % self.args.goma_dir
784
785 android_version_code = self.args.android_version_code
786 if android_version_code:
787 gn_args += ' android_default_version_code="%s"' % android_version_code
788
789 android_version_name = self.args.android_version_name
790 if android_version_name:
791 gn_args += ' android_default_version_name="%s"' % android_version_name
792
793 # Canonicalize the arg string into a sorted, newline-separated list
794 # of key-value pairs, and de-dup the keys if need be so that only
795 # the last instance of each arg is listed.
796 gn_args = gn_helpers.ToGNString(gn_helpers.FromGNArgs(gn_args))
797
798 args_file = vals.get('args_file', None)
799 if args_file:
800 gn_args = ('import("%s")\n' % vals['args_file']) + gn_args
801 return gn_args
802
kjellandera013a022016-11-14 05:54:22 -0800803 def GetIsolateCommand(self, target, vals):
kjellandera013a022016-11-14 05:54:22 -0800804 isolate_map = self.ReadIsolateMap()
805 test_type = isolate_map[target]['type']
806
Oleh Prypinb708e932018-03-18 17:34:20 +0100807 is_android = 'target_os="android"' in vals['gn_args']
808 is_linux = self.platform.startswith('linux') and not is_android
kjellandera013a022016-11-14 05:54:22 -0800809
810 if test_type == 'nontest':
811 self.WriteFailureAndRaise('We should not be isolating %s.' % target,
812 output_path=None)
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800813 if test_type not in ('console_test_launcher', 'windowed_test_launcher',
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100814 'non_parallel_console_test_launcher', 'raw',
Edward Lemur20110752017-09-28 16:14:37 +0200815 'additional_compile_target', 'junit_test', 'script'):
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800816 self.WriteFailureAndRaise('No command line for %s found (test type %s).'
817 % (target, test_type), output_path=None)
kjellandera013a022016-11-14 05:54:22 -0800818
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800819 cmdline = []
Oleh Prypinb708e932018-03-18 17:34:20 +0100820 extra_files = [
821 '../../.vpython',
822 '../../testing/test_env.py',
823 ]
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800824
Mirko Bonadeibd33ce22019-07-10 19:21:20 +0200825 if test_type == 'raw':
826 cmdline.append('../../tools_webrtc/flags_compatibility.py')
827 extra_files.append('../../tools_webrtc/flags_compatibility.py')
828
Yves Gerey2e0c6552018-10-08 21:59:25 +0200829 must_retry = False
Edward Lemur98d40362018-01-15 17:37:04 +0100830 if test_type == 'script':
Mirko Bonadeibd33ce22019-07-10 19:21:20 +0200831 cmdline += ['../../' + self.ToSrcRelPath(isolate_map[target]['script'])]
Oleh Prypinb708e932018-03-18 17:34:20 +0100832 elif is_android:
Mirko Bonadeibd33ce22019-07-10 19:21:20 +0200833 cmdline += ['../../build/android/test_wrapper/logdog_wrapper.py',
834 '--target', target,
835 '--logdog-bin-cmd', '../../bin/logdog_butler',
836 '--logcat-output-file', '${ISOLATED_OUTDIR}/logcats',
837 '--store-tombstones']
kjellandera013a022016-11-14 05:54:22 -0800838 else:
Oleh Prypin739b8162018-05-17 13:28:29 +0200839 if isolate_map[target].get('use_webcam', False):
840 cmdline.append('../../tools_webrtc/ensure_webcam_is_running.py')
841 extra_files.append('../../tools_webrtc/ensure_webcam_is_running.py')
kjellandera013a022016-11-14 05:54:22 -0800842
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800843 # This needs to mirror the settings in //build/config/ui.gni:
844 # use_x11 = is_linux && !use_ozone.
845 use_x11 = is_linux and not 'use_ozone=true' in vals['gn_args']
846
847 xvfb = use_x11 and test_type == 'windowed_test_launcher'
848 if xvfb:
Oleh Prypin739b8162018-05-17 13:28:29 +0200849 cmdline.append('../../testing/xvfb.py')
850 extra_files.append('../../testing/xvfb.py')
851 else:
852 cmdline.append('../../testing/test_env.py')
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800853
Mirko Bonadei264bee82018-08-07 08:53:41 +0200854 if test_type != 'raw':
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800855 extra_files += [
856 '../../third_party/gtest-parallel/gtest-parallel',
ehmaldonadoa7507eb2017-05-10 13:40:29 -0700857 '../../third_party/gtest-parallel/gtest_parallel.py',
Henrik Kjellander90fd7d82017-05-09 08:30:10 +0200858 '../../tools_webrtc/gtest-parallel-wrapper.py',
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800859 ]
ehmaldonado55833842017-02-13 03:58:13 -0800860 sep = '\\' if self.platform == 'win32' else '/'
861 output_dir = '${ISOLATED_OUTDIR}' + sep + 'test_logs'
Edward Lemurbeffdd42017-09-27 13:07:47 +0200862 timeout = isolate_map[target].get('timeout', 900)
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100863 cmdline += [
Henrik Kjellander90fd7d82017-05-09 08:30:10 +0200864 '../../tools_webrtc/gtest-parallel-wrapper.py',
ehmaldonado55833842017-02-13 03:58:13 -0800865 '--output_dir=%s' % output_dir,
ehmaldonado76e60e92017-05-04 06:18:26 -0700866 '--gtest_color=no',
867 # We tell gtest-parallel to interrupt the test after 900 seconds,
868 # so it can exit cleanly and report results, instead of being
869 # interrupted by swarming and not reporting anything.
Edward Lemurbeffdd42017-09-27 13:07:47 +0200870 '--timeout=%s' % timeout,
ehmaldonado55833842017-02-13 03:58:13 -0800871 ]
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100872 if test_type == 'non_parallel_console_test_launcher':
873 # Still use the gtest-parallel-wrapper.py script since we need it to
874 # run tests on swarming, but don't execute tests in parallel.
875 cmdline.append('--workers=1')
Yves Gerey2e0c6552018-10-08 21:59:25 +0200876 must_retry = True
877
878 asan = 'is_asan=true' in vals['gn_args']
879 lsan = 'is_lsan=true' in vals['gn_args']
880 msan = 'is_msan=true' in vals['gn_args']
881 tsan = 'is_tsan=true' in vals['gn_args']
882 sanitizer = asan or lsan or msan or tsan
883 if must_retry and not sanitizer:
884 # Retry would hide most sanitizers detections.
885 cmdline.append('--retry_failed=3')
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100886
887 executable_prefix = '.\\' if self.platform == 'win32' else './'
888 executable_suffix = '.exe' if self.platform == 'win32' else ''
889 executable = executable_prefix + target + executable_suffix
890
891 cmdline.append(executable)
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800892
kjellander382f2b22017-04-11 04:07:01 -0700893 cmdline.extend([
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800894 '--asan=%d' % asan,
kjellander461a5602017-05-05 06:39:16 -0700895 '--lsan=%d' % lsan,
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800896 '--msan=%d' % msan,
897 '--tsan=%d' % tsan,
kjellander382f2b22017-04-11 04:07:01 -0700898 ])
kjellandera013a022016-11-14 05:54:22 -0800899
kjellander74e81262017-03-23 00:51:11 -0700900 cmdline += isolate_map[target].get('args', [])
901
kjellandera013a022016-11-14 05:54:22 -0800902 return cmdline, extra_files
903
904 def ToAbsPath(self, build_path, *comps):
kjellander1c3548c2017-02-15 22:38:22 -0800905 return self.PathJoin(self.src_dir,
kjellandera013a022016-11-14 05:54:22 -0800906 self.ToSrcRelPath(build_path),
907 *comps)
908
909 def ToSrcRelPath(self, path):
910 """Returns a relative path from the top of the repo."""
911 if path.startswith('//'):
912 return path[2:].replace('/', self.sep)
kjellander1c3548c2017-02-15 22:38:22 -0800913 return self.RelPath(path, self.src_dir)
kjellandera013a022016-11-14 05:54:22 -0800914
kjellandera013a022016-11-14 05:54:22 -0800915 def RunGNAnalyze(self, vals):
916 # Analyze runs before 'gn gen' now, so we need to run gn gen
917 # in order to ensure that we have a build directory.
918 ret = self.RunGNGen(vals)
919 if ret:
920 return ret
921
922 build_path = self.args.path[0]
923 input_path = self.args.input_path[0]
924 gn_input_path = input_path + '.gn'
925 output_path = self.args.output_path[0]
926 gn_output_path = output_path + '.gn'
927
928 inp = self.ReadInputJSON(['files', 'test_targets',
929 'additional_compile_targets'])
930 if self.args.verbose:
931 self.Print()
932 self.Print('analyze input:')
933 self.PrintJSON(inp)
934 self.Print()
935
936
937 # This shouldn't normally happen, but could due to unusual race conditions,
938 # like a try job that gets scheduled before a patch lands but runs after
939 # the patch has landed.
940 if not inp['files']:
941 self.Print('Warning: No files modified in patch, bailing out early.')
942 self.WriteJSON({
943 'status': 'No dependency',
944 'compile_targets': [],
945 'test_targets': [],
946 }, output_path)
947 return 0
948
949 gn_inp = {}
950 gn_inp['files'] = ['//' + f for f in inp['files'] if not f.startswith('//')]
951
952 isolate_map = self.ReadIsolateMap()
953 err, gn_inp['additional_compile_targets'] = self.MapTargetsToLabels(
954 isolate_map, inp['additional_compile_targets'])
955 if err:
956 raise MBErr(err)
957
958 err, gn_inp['test_targets'] = self.MapTargetsToLabels(
959 isolate_map, inp['test_targets'])
960 if err:
961 raise MBErr(err)
962 labels_to_targets = {}
963 for i, label in enumerate(gn_inp['test_targets']):
964 labels_to_targets[label] = inp['test_targets'][i]
965
966 try:
967 self.WriteJSON(gn_inp, gn_input_path)
968 cmd = self.GNCmd('analyze', build_path, gn_input_path, gn_output_path)
969 ret, _, _ = self.Run(cmd, force_verbose=True)
970 if ret:
971 return ret
972
973 gn_outp_str = self.ReadFile(gn_output_path)
974 try:
975 gn_outp = json.loads(gn_outp_str)
976 except Exception as e:
977 self.Print("Failed to parse the JSON string GN returned: %s\n%s"
978 % (repr(gn_outp_str), str(e)))
979 raise
980
981 outp = {}
982 if 'status' in gn_outp:
983 outp['status'] = gn_outp['status']
984 if 'error' in gn_outp:
985 outp['error'] = gn_outp['error']
986 if 'invalid_targets' in gn_outp:
987 outp['invalid_targets'] = gn_outp['invalid_targets']
988 if 'compile_targets' in gn_outp:
989 if 'all' in gn_outp['compile_targets']:
990 outp['compile_targets'] = ['all']
991 else:
992 outp['compile_targets'] = [
993 label.replace('//', '') for label in gn_outp['compile_targets']]
994 if 'test_targets' in gn_outp:
995 outp['test_targets'] = [
996 labels_to_targets[label] for label in gn_outp['test_targets']]
997
998 if self.args.verbose:
999 self.Print()
1000 self.Print('analyze output:')
1001 self.PrintJSON(outp)
1002 self.Print()
1003
1004 self.WriteJSON(outp, output_path)
1005
1006 finally:
1007 if self.Exists(gn_input_path):
1008 self.RemoveFile(gn_input_path)
1009 if self.Exists(gn_output_path):
1010 self.RemoveFile(gn_output_path)
1011
1012 return 0
1013
1014 def ReadInputJSON(self, required_keys):
1015 path = self.args.input_path[0]
1016 output_path = self.args.output_path[0]
1017 if not self.Exists(path):
1018 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
1019
1020 try:
1021 inp = json.loads(self.ReadFile(path))
1022 except Exception as e:
1023 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
1024 (path, e), output_path)
1025
1026 for k in required_keys:
1027 if not k in inp:
1028 self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
1029 output_path)
1030
1031 return inp
1032
1033 def WriteFailureAndRaise(self, msg, output_path):
1034 if output_path:
1035 self.WriteJSON({'error': msg}, output_path, force_verbose=True)
1036 raise MBErr(msg)
1037
1038 def WriteJSON(self, obj, path, force_verbose=False):
1039 try:
1040 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n',
1041 force_verbose=force_verbose)
1042 except Exception as e:
1043 raise MBErr('Error %s writing to the output path "%s"' %
1044 (e, path))
1045
1046 def CheckCompile(self, master, builder):
1047 url_template = self.args.url_template + '/{builder}/builds/_all?as_text=1'
1048 url = urllib2.quote(url_template.format(master=master, builder=builder),
1049 safe=':/()?=')
1050 try:
1051 builds = json.loads(self.Fetch(url))
1052 except Exception as e:
1053 return str(e)
1054 successes = sorted(
1055 [int(x) for x in builds.keys() if "text" in builds[x] and
1056 cmp(builds[x]["text"][:2], ["build", "successful"]) == 0],
1057 reverse=True)
1058 if not successes:
1059 return "no successful builds"
1060 build = builds[str(successes[0])]
1061 step_names = set([step["name"] for step in build["steps"]])
1062 compile_indicators = set(["compile", "compile (with patch)", "analyze"])
1063 if compile_indicators & step_names:
1064 return "compiles"
1065 return "does not compile"
1066
1067 def PrintCmd(self, cmd, env):
1068 if self.platform == 'win32':
1069 env_prefix = 'set '
1070 env_quoter = QuoteForSet
1071 shell_quoter = QuoteForCmd
1072 else:
1073 env_prefix = ''
1074 env_quoter = pipes.quote
1075 shell_quoter = pipes.quote
1076
1077 def print_env(var):
1078 if env and var in env:
1079 self.Print('%s%s=%s' % (env_prefix, var, env_quoter(env[var])))
1080
kjellandera013a022016-11-14 05:54:22 -08001081 print_env('LLVM_FORCE_HEAD_REVISION')
1082
1083 if cmd[0] == self.executable:
1084 cmd = ['python'] + cmd[1:]
1085 self.Print(*[shell_quoter(arg) for arg in cmd])
1086
1087 def PrintJSON(self, obj):
1088 self.Print(json.dumps(obj, indent=2, sort_keys=True))
1089
1090 def Build(self, target):
1091 build_dir = self.ToSrcRelPath(self.args.path[0])
Oleh Prypinb708e932018-03-18 17:34:20 +01001092 ninja_cmd = ['ninja', '-C', build_dir]
kjellandera013a022016-11-14 05:54:22 -08001093 if self.args.jobs:
1094 ninja_cmd.extend(['-j', '%d' % self.args.jobs])
1095 ninja_cmd.append(target)
1096 ret, _, _ = self.Run(ninja_cmd, force_verbose=False, buffer_output=False)
1097 return ret
1098
1099 def Run(self, cmd, env=None, force_verbose=True, buffer_output=True):
1100 # This function largely exists so it can be overridden for testing.
1101 if self.args.dryrun or self.args.verbose or force_verbose:
1102 self.PrintCmd(cmd, env)
1103 if self.args.dryrun:
1104 return 0, '', ''
1105
1106 ret, out, err = self.Call(cmd, env=env, buffer_output=buffer_output)
1107 if self.args.verbose or force_verbose:
1108 if ret:
1109 self.Print(' -> returned %d' % ret)
1110 if out:
1111 self.Print(out, end='')
1112 if err:
1113 self.Print(err, end='', file=sys.stderr)
1114 return ret, out, err
1115
1116 def Call(self, cmd, env=None, buffer_output=True):
1117 if buffer_output:
kjellander1c3548c2017-02-15 22:38:22 -08001118 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001119 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1120 env=env)
1121 out, err = p.communicate()
1122 else:
kjellander1c3548c2017-02-15 22:38:22 -08001123 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001124 env=env)
1125 p.wait()
1126 out = err = ''
1127 return p.returncode, out, err
1128
1129 def ExpandUser(self, path):
1130 # This function largely exists so it can be overridden for testing.
1131 return os.path.expanduser(path)
1132
1133 def Exists(self, path):
1134 # This function largely exists so it can be overridden for testing.
1135 return os.path.exists(path)
1136
1137 def Fetch(self, url):
1138 # This function largely exists so it can be overridden for testing.
1139 f = urllib2.urlopen(url)
1140 contents = f.read()
1141 f.close()
1142 return contents
1143
1144 def MaybeMakeDirectory(self, path):
1145 try:
1146 os.makedirs(path)
1147 except OSError, e:
1148 if e.errno != errno.EEXIST:
1149 raise
1150
1151 def PathJoin(self, *comps):
1152 # This function largely exists so it can be overriden for testing.
1153 return os.path.join(*comps)
1154
1155 def Print(self, *args, **kwargs):
1156 # This function largely exists so it can be overridden for testing.
1157 print(*args, **kwargs)
1158 if kwargs.get('stream', sys.stdout) == sys.stdout:
1159 sys.stdout.flush()
1160
1161 def ReadFile(self, path):
1162 # This function largely exists so it can be overriden for testing.
1163 with open(path) as fp:
1164 return fp.read()
1165
1166 def RelPath(self, path, start='.'):
1167 # This function largely exists so it can be overriden for testing.
1168 return os.path.relpath(path, start)
1169
1170 def RemoveFile(self, path):
1171 # This function largely exists so it can be overriden for testing.
1172 os.remove(path)
1173
1174 def RemoveDirectory(self, abs_path):
1175 if self.platform == 'win32':
1176 # In other places in chromium, we often have to retry this command
1177 # because we're worried about other processes still holding on to
1178 # file handles, but when MB is invoked, it will be early enough in the
1179 # build that their should be no other processes to interfere. We
1180 # can change this if need be.
1181 self.Run(['cmd.exe', '/c', 'rmdir', '/q', '/s', abs_path])
1182 else:
1183 shutil.rmtree(abs_path, ignore_errors=True)
1184
1185 def TempFile(self, mode='w'):
1186 # This function largely exists so it can be overriden for testing.
1187 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
1188
1189 def WriteFile(self, path, contents, force_verbose=False):
1190 # This function largely exists so it can be overriden for testing.
1191 if self.args.dryrun or self.args.verbose or force_verbose:
1192 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
1193 with open(path, 'w') as fp:
1194 return fp.write(contents)
1195
1196
1197class MBErr(Exception):
1198 pass
1199
1200
1201# See http://goo.gl/l5NPDW and http://goo.gl/4Diozm for the painful
1202# details of this next section, which handles escaping command lines
1203# so that they can be copied and pasted into a cmd window.
1204UNSAFE_FOR_SET = set('^<>&|')
1205UNSAFE_FOR_CMD = UNSAFE_FOR_SET.union(set('()%'))
1206ALL_META_CHARS = UNSAFE_FOR_CMD.union(set('"'))
1207
1208
1209def QuoteForSet(arg):
1210 if any(a in UNSAFE_FOR_SET for a in arg):
1211 arg = ''.join('^' + a if a in UNSAFE_FOR_SET else a for a in arg)
1212 return arg
1213
1214
1215def QuoteForCmd(arg):
1216 # First, escape the arg so that CommandLineToArgvW will parse it properly.
kjellandera013a022016-11-14 05:54:22 -08001217 if arg == '' or ' ' in arg or '"' in arg:
1218 quote_re = re.compile(r'(\\*)"')
1219 arg = '"%s"' % (quote_re.sub(lambda mo: 2 * mo.group(1) + '\\"', arg))
1220
1221 # Then check to see if the arg contains any metacharacters other than
1222 # double quotes; if it does, quote everything (including the double
1223 # quotes) for safety.
1224 if any(a in UNSAFE_FOR_CMD for a in arg):
1225 arg = ''.join('^' + a if a in ALL_META_CHARS else a for a in arg)
1226 return arg
1227
1228
1229if __name__ == '__main__':
1230 sys.exit(main(sys.argv[1:]))