blob: e8883673f8ca3c23ae1ae81ed3a222f8487d8ea3 [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)
160 subp.set_defaults(func=self.CmdLookup)
161
162 subp = subps.add_parser(
163 'run',
164 help='build and run the isolated version of a '
165 'binary',
166 formatter_class=argparse.RawDescriptionHelpFormatter)
167 subp.description = (
168 'Build, isolate, and run the given binary with the command line\n'
169 'listed in the isolate. You may pass extra arguments after the\n'
170 'target; use "--" if the extra arguments need to include switches.\n'
171 '\n'
172 'Examples:\n'
173 '\n'
174 ' % tools/mb/mb.py run -m chromium.linux -b "Linux Builder" \\\n'
175 ' //out/Default content_browsertests\n'
176 '\n'
177 ' % tools/mb/mb.py run out/Default content_browsertests\n'
178 '\n'
179 ' % tools/mb/mb.py run out/Default content_browsertests -- \\\n'
180 ' --test-launcher-retry-limit=0'
181 '\n'
182 )
kjellandera013a022016-11-14 05:54:22 -0800183 AddCommonOptions(subp)
184 subp.add_argument('-j', '--jobs', dest='jobs', type=int,
185 help='Number of jobs to pass to ninja')
186 subp.add_argument('--no-build', dest='build', default=True,
187 action='store_false',
188 help='Do not build, just isolate and run')
189 subp.add_argument('path', nargs=1,
190 help=('path to generate build into (or use).'
191 ' This can be either a regular path or a '
192 'GN-style source-relative path like '
193 '//out/Default.'))
Oleh Prypinb708e932018-03-18 17:34:20 +0100194 subp.add_argument('-s', '--swarmed', action='store_true',
195 help='Run under swarming')
196 subp.add_argument('-d', '--dimension', default=[], action='append', nargs=2,
197 dest='dimensions', metavar='FOO bar',
198 help='dimension to filter on')
kjellandera013a022016-11-14 05:54:22 -0800199 subp.add_argument('target', nargs=1,
200 help='ninja target to build and run')
201 subp.add_argument('extra_args', nargs='*',
202 help=('extra args to pass to the isolate to run. Use '
203 '"--" as the first arg if you need to pass '
204 'switches'))
205 subp.set_defaults(func=self.CmdRun)
206
207 subp = subps.add_parser('validate',
208 help='validate the config file')
209 subp.add_argument('-f', '--config-file', metavar='PATH',
210 default=self.default_config,
211 help='path to config file (default is %(default)s)')
212 subp.set_defaults(func=self.CmdValidate)
213
kjellandera013a022016-11-14 05:54:22 -0800214 subp = subps.add_parser('help',
215 help='Get help on a subcommand.')
216 subp.add_argument(nargs='?', action='store', dest='subcommand',
217 help='The command to get help for.')
218 subp.set_defaults(func=self.CmdHelp)
219
220 self.args = parser.parse_args(argv)
221
222 def DumpInputFiles(self):
223
224 def DumpContentsOfFilePassedTo(arg_name, path):
225 if path and self.Exists(path):
226 self.Print("\n# To recreate the file passed to %s:" % arg_name)
227 self.Print("%% cat > %s <<EOF" % path)
228 contents = self.ReadFile(path)
229 self.Print(contents)
230 self.Print("EOF\n%\n")
231
232 if getattr(self.args, 'input_path', None):
233 DumpContentsOfFilePassedTo(
234 'argv[0] (input_path)', self.args.input_path[0])
235 if getattr(self.args, 'swarming_targets_file', None):
236 DumpContentsOfFilePassedTo(
237 '--swarming-targets-file', self.args.swarming_targets_file)
238
239 def CmdAnalyze(self):
240 vals = self.Lookup()
Oleh Prypinb708e932018-03-18 17:34:20 +0100241 return self.RunGNAnalyze(vals)
kjellandera013a022016-11-14 05:54:22 -0800242
243 def CmdExport(self):
244 self.ReadConfigFile()
245 obj = {}
246 for master, builders in self.masters.items():
247 obj[master] = {}
248 for builder in builders:
249 config = self.masters[master][builder]
250 if not config:
251 continue
252
253 if isinstance(config, dict):
254 args = {k: self.FlattenConfig(v)['gn_args']
255 for k, v in config.items()}
256 elif config.startswith('//'):
257 args = config
258 else:
259 args = self.FlattenConfig(config)['gn_args']
260 if 'error' in args:
261 continue
262
263 obj[master][builder] = args
264
265 # Dump object and trim trailing whitespace.
266 s = '\n'.join(l.rstrip() for l in
267 json.dumps(obj, sort_keys=True, indent=2).splitlines())
268 self.Print(s)
269 return 0
270
271 def CmdGen(self):
272 vals = self.Lookup()
Oleh Prypinb708e932018-03-18 17:34:20 +0100273 return self.RunGNGen(vals)
kjellandera013a022016-11-14 05:54:22 -0800274
275 def CmdHelp(self):
276 if self.args.subcommand:
277 self.ParseArgs([self.args.subcommand, '--help'])
278 else:
279 self.ParseArgs(['--help'])
280
281 def CmdIsolate(self):
282 vals = self.GetConfig()
283 if not vals:
284 return 1
Oleh Prypinb708e932018-03-18 17:34:20 +0100285 return self.RunGNIsolate(vals)
kjellandera013a022016-11-14 05:54:22 -0800286
287 def CmdLookup(self):
288 vals = self.Lookup()
Oleh Prypinb708e932018-03-18 17:34:20 +0100289 cmd = self.GNCmd('gen', '_path_')
290 gn_args = self.GNArgs(vals)
291 self.Print('\nWriting """\\\n%s""" to _path_/args.gn.\n' % gn_args)
292 env = None
kjellandera013a022016-11-14 05:54:22 -0800293
294 self.PrintCmd(cmd, env)
295 return 0
296
297 def CmdRun(self):
298 vals = self.GetConfig()
299 if not vals:
300 return 1
301
302 build_dir = self.args.path[0]
303 target = self.args.target[0]
304
Oleh Prypinb708e932018-03-18 17:34:20 +0100305 if self.args.build:
306 ret = self.Build(target)
kjellandera013a022016-11-14 05:54:22 -0800307 if ret:
308 return ret
Oleh Prypinb708e932018-03-18 17:34:20 +0100309 ret = self.RunGNIsolate(vals)
310 if ret:
311 return ret
kjellandera013a022016-11-14 05:54:22 -0800312
Oleh Prypinb708e932018-03-18 17:34:20 +0100313 if self.args.swarmed:
314 return self._RunUnderSwarming(build_dir, target)
315 else:
316 return self._RunLocallyIsolated(build_dir, target)
317
318 def _RunUnderSwarming(self, build_dir, target):
319 # TODO(dpranke): Look up the information for the target in
320 # the //testing/buildbot.json file, if possible, so that we
321 # can determine the isolate target, command line, and additional
322 # swarming parameters, if possible.
323 #
324 # TODO(dpranke): Also, add support for sharding and merging results.
325 dimensions = []
326 for k, v in self.args.dimensions:
327 dimensions += ['-d', k, v]
328
329 cmd = [
330 self.executable,
331 self.PathJoin('tools', 'swarming_client', 'isolate.py'),
332 'archive',
333 '-s',
334 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
335 '-I', 'isolateserver.appspot.com',
336 ]
337 ret, out, _ = self.Run(cmd, force_verbose=False)
338 if ret:
339 return ret
340
341 isolated_hash = out.splitlines()[0].split()[0]
342 cmd = [
343 self.executable,
344 self.PathJoin('tools', 'swarming_client', 'swarming.py'),
345 'run',
346 '-s', isolated_hash,
347 '-I', 'isolateserver.appspot.com',
348 '-S', 'chromium-swarm.appspot.com',
349 ] + dimensions
350 if self.args.extra_args:
351 cmd += ['--'] + self.args.extra_args
352 ret, _, _ = self.Run(cmd, force_verbose=True, buffer_output=False)
353 return ret
354
355 def _RunLocallyIsolated(self, build_dir, target):
kjellandera013a022016-11-14 05:54:22 -0800356 cmd = [
357 self.executable,
358 self.PathJoin('tools', 'swarming_client', 'isolate.py'),
359 'run',
360 '-s',
361 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
Oleh Prypinb708e932018-03-18 17:34:20 +0100362 ]
kjellandera013a022016-11-14 05:54:22 -0800363 if self.args.extra_args:
Oleh Prypinb708e932018-03-18 17:34:20 +0100364 cmd += ['--'] + self.args.extra_args
365 ret, _, _ = self.Run(cmd, force_verbose=True, buffer_output=False)
kjellandera013a022016-11-14 05:54:22 -0800366 return ret
367
368 def CmdValidate(self, print_ok=True):
369 errs = []
370
371 # Read the file to make sure it parses.
372 self.ReadConfigFile()
373
374 # Build a list of all of the configs referenced by builders.
375 all_configs = {}
376 for master in self.masters:
377 for config in self.masters[master].values():
378 if isinstance(config, dict):
379 for c in config.values():
380 all_configs[c] = master
381 else:
382 all_configs[config] = master
383
384 # Check that every referenced args file or config actually exists.
385 for config, loc in all_configs.items():
386 if config.startswith('//'):
387 if not self.Exists(self.ToAbsPath(config)):
388 errs.append('Unknown args file "%s" referenced from "%s".' %
389 (config, loc))
390 elif not config in self.configs:
391 errs.append('Unknown config "%s" referenced from "%s".' %
392 (config, loc))
393
394 # Check that every actual config is actually referenced.
395 for config in self.configs:
396 if not config in all_configs:
397 errs.append('Unused config "%s".' % config)
398
399 # Figure out the whole list of mixins, and check that every mixin
400 # listed by a config or another mixin actually exists.
401 referenced_mixins = set()
402 for config, mixins in self.configs.items():
403 for mixin in mixins:
404 if not mixin in self.mixins:
405 errs.append('Unknown mixin "%s" referenced by config "%s".' %
406 (mixin, config))
407 referenced_mixins.add(mixin)
408
409 for mixin in self.mixins:
410 for sub_mixin in self.mixins[mixin].get('mixins', []):
411 if not sub_mixin in self.mixins:
412 errs.append('Unknown mixin "%s" referenced by mixin "%s".' %
413 (sub_mixin, mixin))
414 referenced_mixins.add(sub_mixin)
415
416 # Check that every mixin defined is actually referenced somewhere.
417 for mixin in self.mixins:
418 if not mixin in referenced_mixins:
419 errs.append('Unreferenced mixin "%s".' % mixin)
420
kjellandera013a022016-11-14 05:54:22 -0800421 if errs:
422 raise MBErr(('mb config file %s has problems:' % self.args.config_file) +
423 '\n ' + '\n '.join(errs))
424
425 if print_ok:
426 self.Print('mb config file %s looks ok.' % self.args.config_file)
427 return 0
428
kjellandera013a022016-11-14 05:54:22 -0800429 def GetConfig(self):
430 build_dir = self.args.path[0]
431
432 vals = self.DefaultVals()
433 if self.args.builder or self.args.master or self.args.config:
434 vals = self.Lookup()
Oleh Prypinb708e932018-03-18 17:34:20 +0100435 # Re-run gn gen in order to ensure the config is consistent with the
436 # build dir.
437 self.RunGNGen(vals)
kjellandera013a022016-11-14 05:54:22 -0800438 return vals
439
Oleh Prypinb708e932018-03-18 17:34:20 +0100440 toolchain_path = self.PathJoin(self.ToAbsPath(build_dir),
441 'toolchain.ninja')
442 if not self.Exists(toolchain_path):
443 self.Print('Must either specify a path to an existing GN build dir '
444 'or pass in a -m/-b pair or a -c flag to specify the '
445 'configuration')
446 return {}
kjellandera013a022016-11-14 05:54:22 -0800447
Oleh Prypinb708e932018-03-18 17:34:20 +0100448 vals['gn_args'] = self.GNArgsFromDir(build_dir)
kjellandera013a022016-11-14 05:54:22 -0800449 return vals
450
451 def GNArgsFromDir(self, build_dir):
452 args_contents = ""
453 gn_args_path = self.PathJoin(self.ToAbsPath(build_dir), 'args.gn')
454 if self.Exists(gn_args_path):
455 args_contents = self.ReadFile(gn_args_path)
456 gn_args = []
457 for l in args_contents.splitlines():
458 fields = l.split(' ')
459 name = fields[0]
460 val = ' '.join(fields[2:])
461 gn_args.append('%s=%s' % (name, val))
462
463 return ' '.join(gn_args)
464
465 def Lookup(self):
466 vals = self.ReadIOSBotConfig()
467 if not vals:
468 self.ReadConfigFile()
469 config = self.ConfigFromArgs()
470 if config.startswith('//'):
471 if not self.Exists(self.ToAbsPath(config)):
472 raise MBErr('args file "%s" not found' % config)
473 vals = self.DefaultVals()
474 vals['args_file'] = config
475 else:
476 if not config in self.configs:
477 raise MBErr('Config "%s" not found in %s' %
478 (config, self.args.config_file))
479 vals = self.FlattenConfig(config)
kjellandera013a022016-11-14 05:54:22 -0800480 return vals
481
482 def ReadIOSBotConfig(self):
483 if not self.args.master or not self.args.builder:
484 return {}
Oleh Prypinb708e932018-03-18 17:34:20 +0100485 path = self.PathJoin(self.src_dir, 'tools_webrtc', 'ios', self.args.master,
kjellander1c3548c2017-02-15 22:38:22 -0800486 self.args.builder.replace(' ', '_') + '.json')
kjellandera013a022016-11-14 05:54:22 -0800487 if not self.Exists(path):
488 return {}
489
490 contents = json.loads(self.ReadFile(path))
kjellandera013a022016-11-14 05:54:22 -0800491 gn_args = ' '.join(contents.get('gn_args', []))
492
493 vals = self.DefaultVals()
494 vals['gn_args'] = gn_args
kjellandera013a022016-11-14 05:54:22 -0800495 return vals
496
497 def ReadConfigFile(self):
498 if not self.Exists(self.args.config_file):
499 raise MBErr('config file not found at %s' % self.args.config_file)
500
501 try:
502 contents = ast.literal_eval(self.ReadFile(self.args.config_file))
503 except SyntaxError as e:
504 raise MBErr('Failed to parse config file "%s": %s' %
505 (self.args.config_file, e))
506
507 self.configs = contents['configs']
508 self.masters = contents['masters']
509 self.mixins = contents['mixins']
510
511 def ReadIsolateMap(self):
Oleh Prypinb708e932018-03-18 17:34:20 +0100512 isolate_map = self.args.isolate_map_file
513 if not self.Exists(isolate_map):
514 raise MBErr('isolate map file not found at %s' % isolate_map)
kjellandera013a022016-11-14 05:54:22 -0800515 try:
Oleh Prypinb708e932018-03-18 17:34:20 +0100516 return ast.literal_eval(self.ReadFile(isolate_map))
kjellandera013a022016-11-14 05:54:22 -0800517 except SyntaxError as e:
Oleh Prypinb708e932018-03-18 17:34:20 +0100518 raise MBErr(
519 'Failed to parse isolate map file "%s": %s' % (isolate_map, e))
kjellandera013a022016-11-14 05:54:22 -0800520
521 def ConfigFromArgs(self):
522 if self.args.config:
523 if self.args.master or self.args.builder:
524 raise MBErr('Can not specific both -c/--config and -m/--master or '
525 '-b/--builder')
526
527 return self.args.config
528
529 if not self.args.master or not self.args.builder:
530 raise MBErr('Must specify either -c/--config or '
531 '(-m/--master and -b/--builder)')
532
533 if not self.args.master in self.masters:
534 raise MBErr('Master name "%s" not found in "%s"' %
535 (self.args.master, self.args.config_file))
536
537 if not self.args.builder in self.masters[self.args.master]:
538 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' %
539 (self.args.builder, self.args.master, self.args.config_file))
540
541 config = self.masters[self.args.master][self.args.builder]
542 if isinstance(config, dict):
543 if self.args.phase is None:
544 raise MBErr('Must specify a build --phase for %s on %s' %
545 (self.args.builder, self.args.master))
546 phase = str(self.args.phase)
547 if phase not in config:
548 raise MBErr('Phase %s doesn\'t exist for %s on %s' %
549 (phase, self.args.builder, self.args.master))
550 return config[phase]
551
552 if self.args.phase is not None:
553 raise MBErr('Must not specify a build --phase for %s on %s' %
554 (self.args.builder, self.args.master))
555 return config
556
557 def FlattenConfig(self, config):
558 mixins = self.configs[config]
559 vals = self.DefaultVals()
560
561 visited = []
562 self.FlattenMixins(mixins, vals, visited)
563 return vals
564
565 def DefaultVals(self):
566 return {
567 'args_file': '',
568 'cros_passthrough': False,
569 'gn_args': '',
kjellandera013a022016-11-14 05:54:22 -0800570 }
571
572 def FlattenMixins(self, mixins, vals, visited):
573 for m in mixins:
574 if m not in self.mixins:
575 raise MBErr('Unknown mixin "%s"' % m)
576
577 visited.append(m)
578
579 mixin_vals = self.mixins[m]
580
581 if 'cros_passthrough' in mixin_vals:
582 vals['cros_passthrough'] = mixin_vals['cros_passthrough']
583 if 'gn_args' in mixin_vals:
584 if vals['gn_args']:
585 vals['gn_args'] += ' ' + mixin_vals['gn_args']
586 else:
587 vals['gn_args'] = mixin_vals['gn_args']
kjellandera013a022016-11-14 05:54:22 -0800588
589 if 'mixins' in mixin_vals:
590 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
591 return vals
592
kjellandera013a022016-11-14 05:54:22 -0800593 def RunGNGen(self, vals):
594 build_dir = self.args.path[0]
595
596 cmd = self.GNCmd('gen', build_dir, '--check')
597 gn_args = self.GNArgs(vals)
598
599 # Since GN hasn't run yet, the build directory may not even exist.
600 self.MaybeMakeDirectory(self.ToAbsPath(build_dir))
601
602 gn_args_path = self.ToAbsPath(build_dir, 'args.gn')
603 self.WriteFile(gn_args_path, gn_args, force_verbose=True)
604
605 swarming_targets = []
606 if getattr(self.args, 'swarming_targets_file', None):
607 # We need GN to generate the list of runtime dependencies for
608 # the compile targets listed (one per line) in the file so
609 # we can run them via swarming. We use gn_isolate_map.pyl to convert
610 # the compile targets to the matching GN labels.
611 path = self.args.swarming_targets_file
612 if not self.Exists(path):
613 self.WriteFailureAndRaise('"%s" does not exist' % path,
614 output_path=None)
615 contents = self.ReadFile(path)
616 swarming_targets = set(contents.splitlines())
617
618 isolate_map = self.ReadIsolateMap()
619 err, labels = self.MapTargetsToLabels(isolate_map, swarming_targets)
620 if err:
621 raise MBErr(err)
622
623 gn_runtime_deps_path = self.ToAbsPath(build_dir, 'runtime_deps')
624 self.WriteFile(gn_runtime_deps_path, '\n'.join(labels) + '\n')
625 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path)
626
627 ret, _, _ = self.Run(cmd)
628 if ret:
629 # If `gn gen` failed, we should exit early rather than trying to
630 # generate isolates. Run() will have already logged any error output.
631 self.Print('GN gen failed: %d' % ret)
632 return ret
633
634 android = 'target_os="android"' in vals['gn_args']
635 for target in swarming_targets:
636 if android:
637 # Android targets may be either android_apk or executable. The former
638 # will result in runtime_deps associated with the stamp file, while the
639 # latter will result in runtime_deps associated with the executable.
640 label = isolate_map[target]['label']
641 runtime_deps_targets = [
642 target + '.runtime_deps',
643 'obj/%s.stamp.runtime_deps' % label.replace(':', '/')]
644 elif isolate_map[target]['type'] == 'gpu_browser_test':
645 if self.platform == 'win32':
646 runtime_deps_targets = ['browser_tests.exe.runtime_deps']
647 else:
648 runtime_deps_targets = ['browser_tests.runtime_deps']
Edward Lemur20110752017-09-28 16:14:37 +0200649 elif isolate_map[target]['type'] == 'script':
650 label = isolate_map[target]['label'].split(':')[1]
kjellandera013a022016-11-14 05:54:22 -0800651 runtime_deps_targets = [
Edward Lemur20110752017-09-28 16:14:37 +0200652 '%s.runtime_deps' % label]
kjellandera013a022016-11-14 05:54:22 -0800653 if self.platform == 'win32':
Edward Lemur20110752017-09-28 16:14:37 +0200654 runtime_deps_targets += [ label + '.exe.runtime_deps' ]
kjellandera013a022016-11-14 05:54:22 -0800655 else:
Edward Lemur20110752017-09-28 16:14:37 +0200656 runtime_deps_targets += [ label + '.runtime_deps' ]
kjellandera013a022016-11-14 05:54:22 -0800657 elif self.platform == 'win32':
658 runtime_deps_targets = [target + '.exe.runtime_deps']
659 else:
660 runtime_deps_targets = [target + '.runtime_deps']
661
662 for r in runtime_deps_targets:
663 runtime_deps_path = self.ToAbsPath(build_dir, r)
664 if self.Exists(runtime_deps_path):
665 break
666 else:
667 raise MBErr('did not generate any of %s' %
668 ', '.join(runtime_deps_targets))
669
670 command, extra_files = self.GetIsolateCommand(target, vals)
671
672 runtime_deps = self.ReadFile(runtime_deps_path).splitlines()
673
674 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
675 extra_files)
676
677 return 0
678
679 def RunGNIsolate(self, vals):
680 target = self.args.target[0]
681 isolate_map = self.ReadIsolateMap()
682 err, labels = self.MapTargetsToLabels(isolate_map, [target])
683 if err:
684 raise MBErr(err)
685 label = labels[0]
686
687 build_dir = self.args.path[0]
688 command, extra_files = self.GetIsolateCommand(target, vals)
689
690 cmd = self.GNCmd('desc', build_dir, label, 'runtime_deps')
691 ret, out, _ = self.Call(cmd)
692 if ret:
693 if out:
694 self.Print(out)
695 return ret
696
697 runtime_deps = out.splitlines()
698
699 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
700 extra_files)
701
702 ret, _, _ = self.Run([
703 self.executable,
704 self.PathJoin('tools', 'swarming_client', 'isolate.py'),
705 'check',
706 '-i',
707 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
708 '-s',
709 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target))],
710 buffer_output=False)
711
712 return ret
713
714 def WriteIsolateFiles(self, build_dir, command, target, runtime_deps,
715 extra_files):
716 isolate_path = self.ToAbsPath(build_dir, target + '.isolate')
717 self.WriteFile(isolate_path,
718 pprint.pformat({
719 'variables': {
720 'command': command,
721 'files': sorted(runtime_deps + extra_files),
722 }
723 }) + '\n')
724
725 self.WriteJSON(
726 {
727 'args': [
728 '--isolated',
729 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
730 '--isolate',
731 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
732 ],
kjellander1c3548c2017-02-15 22:38:22 -0800733 'dir': self.src_dir,
kjellandera013a022016-11-14 05:54:22 -0800734 'version': 1,
735 },
736 isolate_path + 'd.gen.json',
737 )
738
739 def MapTargetsToLabels(self, isolate_map, targets):
740 labels = []
741 err = ''
742
743 def StripTestSuffixes(target):
744 for suffix in ('_apk_run', '_apk', '_run'):
745 if target.endswith(suffix):
746 return target[:-len(suffix)], suffix
747 return None, None
748
749 for target in targets:
750 if target == 'all':
751 labels.append(target)
752 elif target.startswith('//'):
753 labels.append(target)
754 else:
755 if target in isolate_map:
756 stripped_target, suffix = target, ''
757 else:
758 stripped_target, suffix = StripTestSuffixes(target)
759 if stripped_target in isolate_map:
760 if isolate_map[stripped_target]['type'] == 'unknown':
761 err += ('test target "%s" type is unknown\n' % target)
762 else:
763 labels.append(isolate_map[stripped_target]['label'] + suffix)
764 else:
765 err += ('target "%s" not found in '
766 '//testing/buildbot/gn_isolate_map.pyl\n' % target)
767
768 return err, labels
769
770 def GNCmd(self, subcommand, path, *args):
Oleh Prypinb708e932018-03-18 17:34:20 +0100771 if self.platform.startswith('linux'):
kjellandera013a022016-11-14 05:54:22 -0800772 subdir, exe = 'linux64', 'gn'
773 elif self.platform == 'darwin':
774 subdir, exe = 'mac', 'gn'
775 else:
776 subdir, exe = 'win', 'gn.exe'
777
kjellander1c3548c2017-02-15 22:38:22 -0800778 gn_path = self.PathJoin(self.src_dir, 'buildtools', subdir, exe)
kjellandera013a022016-11-14 05:54:22 -0800779 return [gn_path, subcommand, path] + list(args)
780
781
782 def GNArgs(self, vals):
783 if vals['cros_passthrough']:
784 if not 'GN_ARGS' in os.environ:
785 raise MBErr('MB is expecting GN_ARGS to be in the environment')
786 gn_args = os.environ['GN_ARGS']
787 if not re.search('target_os.*=.*"chromeos"', gn_args):
788 raise MBErr('GN_ARGS is missing target_os = "chromeos": (GN_ARGS=%s)' %
789 gn_args)
790 else:
791 gn_args = vals['gn_args']
792
793 if self.args.goma_dir:
794 gn_args += ' goma_dir="%s"' % self.args.goma_dir
795
796 android_version_code = self.args.android_version_code
797 if android_version_code:
798 gn_args += ' android_default_version_code="%s"' % android_version_code
799
800 android_version_name = self.args.android_version_name
801 if android_version_name:
802 gn_args += ' android_default_version_name="%s"' % android_version_name
803
804 # Canonicalize the arg string into a sorted, newline-separated list
805 # of key-value pairs, and de-dup the keys if need be so that only
806 # the last instance of each arg is listed.
807 gn_args = gn_helpers.ToGNString(gn_helpers.FromGNArgs(gn_args))
808
809 args_file = vals.get('args_file', None)
810 if args_file:
811 gn_args = ('import("%s")\n' % vals['args_file']) + gn_args
812 return gn_args
813
kjellandera013a022016-11-14 05:54:22 -0800814 def GetIsolateCommand(self, target, vals):
kjellandera013a022016-11-14 05:54:22 -0800815 isolate_map = self.ReadIsolateMap()
816 test_type = isolate_map[target]['type']
817
Oleh Prypinb708e932018-03-18 17:34:20 +0100818 is_android = 'target_os="android"' in vals['gn_args']
819 is_linux = self.platform.startswith('linux') and not is_android
kjellandera013a022016-11-14 05:54:22 -0800820
821 if test_type == 'nontest':
822 self.WriteFailureAndRaise('We should not be isolating %s.' % target,
823 output_path=None)
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800824 if test_type not in ('console_test_launcher', 'windowed_test_launcher',
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100825 'non_parallel_console_test_launcher', 'raw',
Edward Lemur20110752017-09-28 16:14:37 +0200826 'additional_compile_target', 'junit_test', 'script'):
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800827 self.WriteFailureAndRaise('No command line for %s found (test type %s).'
828 % (target, test_type), output_path=None)
kjellandera013a022016-11-14 05:54:22 -0800829
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800830 cmdline = []
Oleh Prypinb708e932018-03-18 17:34:20 +0100831 extra_files = [
832 '../../.vpython',
833 '../../testing/test_env.py',
834 ]
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800835
Edward Lemur98d40362018-01-15 17:37:04 +0100836 if test_type == 'script':
837 cmdline = ['../../' + self.ToSrcRelPath(isolate_map[target]['script'])]
Oleh Prypinb708e932018-03-18 17:34:20 +0100838 elif is_android:
kjellanderf9e2a362017-03-24 12:17:33 -0700839 cmdline = ['../../build/android/test_wrapper/logdog_wrapper.py',
840 '--target', target,
ehmaldonado34623ce2017-09-08 07:03:13 -0700841 '--logdog-bin-cmd', '../../bin/logdog_butler',
842 '--logcat-output-file', '${ISOLATED_OUTDIR}/logcats',
843 '--store-tombstones']
kjellandera013a022016-11-14 05:54:22 -0800844 else:
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800845 extra_files = ['../../testing/test_env.py']
kjellandera013a022016-11-14 05:54:22 -0800846
Oleh Prypincd469a42018-05-17 12:15:25 +0200847 if isolate_map[target].get('use_webcam', False):
848 cmdline.append('../../tools_webrtc/ensure_webcam_is_running.py')
849 extra_files.append('../../tools_webrtc/ensure_webcam_is_running.py')
850
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800851 # This needs to mirror the settings in //build/config/ui.gni:
852 # use_x11 = is_linux && !use_ozone.
853 use_x11 = is_linux and not 'use_ozone=true' in vals['gn_args']
854
855 xvfb = use_x11 and test_type == 'windowed_test_launcher'
856 if xvfb:
Oleh Prypincd469a42018-05-17 12:15:25 +0200857 cmdline.append('../../testing/xvfb.py')
858 extra_files.append('../../testing/xvfb.py')
859 else:
860 cmdline.append('../../testing/test_env.py')
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800861
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100862 # Memcheck is only supported for linux. Ignore in other platforms.
863 if is_linux and 'rtc_use_memcheck=true' in vals['gn_args']:
864 cmdline += [
865 'bash',
866 '../../tools_webrtc/valgrind/webrtc_tests.sh',
867 '--tool',
868 'memcheck',
869 '--target',
870 'Release',
871 '--build-dir',
872 '..',
873 '--test',
874 ]
875 elif test_type != 'raw':
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800876 extra_files += [
877 '../../third_party/gtest-parallel/gtest-parallel',
ehmaldonadoa7507eb2017-05-10 13:40:29 -0700878 '../../third_party/gtest-parallel/gtest_parallel.py',
Henrik Kjellander90fd7d82017-05-09 08:30:10 +0200879 '../../tools_webrtc/gtest-parallel-wrapper.py',
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800880 ]
ehmaldonado55833842017-02-13 03:58:13 -0800881 sep = '\\' if self.platform == 'win32' else '/'
882 output_dir = '${ISOLATED_OUTDIR}' + sep + 'test_logs'
Edward Lemurbeffdd42017-09-27 13:07:47 +0200883 timeout = isolate_map[target].get('timeout', 900)
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100884 cmdline += [
Henrik Kjellander90fd7d82017-05-09 08:30:10 +0200885 '../../tools_webrtc/gtest-parallel-wrapper.py',
ehmaldonado55833842017-02-13 03:58:13 -0800886 '--output_dir=%s' % output_dir,
ehmaldonado76e60e92017-05-04 06:18:26 -0700887 '--gtest_color=no',
888 # We tell gtest-parallel to interrupt the test after 900 seconds,
889 # so it can exit cleanly and report results, instead of being
890 # interrupted by swarming and not reporting anything.
Edward Lemurbeffdd42017-09-27 13:07:47 +0200891 '--timeout=%s' % timeout,
ehmaldonado2a280352017-05-05 04:33:57 -0700892 '--retry_failed=3',
ehmaldonado55833842017-02-13 03:58:13 -0800893 ]
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100894 if test_type == 'non_parallel_console_test_launcher':
895 # Still use the gtest-parallel-wrapper.py script since we need it to
896 # run tests on swarming, but don't execute tests in parallel.
897 cmdline.append('--workers=1')
898
899 executable_prefix = '.\\' if self.platform == 'win32' else './'
900 executable_suffix = '.exe' if self.platform == 'win32' else ''
901 executable = executable_prefix + target + executable_suffix
902
903 cmdline.append(executable)
904 if test_type != 'raw':
905 cmdline.append('--')
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800906
907 asan = 'is_asan=true' in vals['gn_args']
kjellander461a5602017-05-05 06:39:16 -0700908 lsan = 'is_lsan=true' in vals['gn_args']
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800909 msan = 'is_msan=true' in vals['gn_args']
910 tsan = 'is_tsan=true' in vals['gn_args']
911
kjellander382f2b22017-04-11 04:07:01 -0700912 cmdline.extend([
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800913 '--asan=%d' % asan,
kjellander461a5602017-05-05 06:39:16 -0700914 '--lsan=%d' % lsan,
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800915 '--msan=%d' % msan,
916 '--tsan=%d' % tsan,
kjellander382f2b22017-04-11 04:07:01 -0700917 ])
kjellandera013a022016-11-14 05:54:22 -0800918
kjellander74e81262017-03-23 00:51:11 -0700919 cmdline += isolate_map[target].get('args', [])
920
kjellandera013a022016-11-14 05:54:22 -0800921 return cmdline, extra_files
922
923 def ToAbsPath(self, build_path, *comps):
kjellander1c3548c2017-02-15 22:38:22 -0800924 return self.PathJoin(self.src_dir,
kjellandera013a022016-11-14 05:54:22 -0800925 self.ToSrcRelPath(build_path),
926 *comps)
927
928 def ToSrcRelPath(self, path):
929 """Returns a relative path from the top of the repo."""
930 if path.startswith('//'):
931 return path[2:].replace('/', self.sep)
kjellander1c3548c2017-02-15 22:38:22 -0800932 return self.RelPath(path, self.src_dir)
kjellandera013a022016-11-14 05:54:22 -0800933
kjellandera013a022016-11-14 05:54:22 -0800934 def RunGNAnalyze(self, vals):
935 # Analyze runs before 'gn gen' now, so we need to run gn gen
936 # in order to ensure that we have a build directory.
937 ret = self.RunGNGen(vals)
938 if ret:
939 return ret
940
941 build_path = self.args.path[0]
942 input_path = self.args.input_path[0]
943 gn_input_path = input_path + '.gn'
944 output_path = self.args.output_path[0]
945 gn_output_path = output_path + '.gn'
946
947 inp = self.ReadInputJSON(['files', 'test_targets',
948 'additional_compile_targets'])
949 if self.args.verbose:
950 self.Print()
951 self.Print('analyze input:')
952 self.PrintJSON(inp)
953 self.Print()
954
955
956 # This shouldn't normally happen, but could due to unusual race conditions,
957 # like a try job that gets scheduled before a patch lands but runs after
958 # the patch has landed.
959 if not inp['files']:
960 self.Print('Warning: No files modified in patch, bailing out early.')
961 self.WriteJSON({
962 'status': 'No dependency',
963 'compile_targets': [],
964 'test_targets': [],
965 }, output_path)
966 return 0
967
968 gn_inp = {}
969 gn_inp['files'] = ['//' + f for f in inp['files'] if not f.startswith('//')]
970
971 isolate_map = self.ReadIsolateMap()
972 err, gn_inp['additional_compile_targets'] = self.MapTargetsToLabels(
973 isolate_map, inp['additional_compile_targets'])
974 if err:
975 raise MBErr(err)
976
977 err, gn_inp['test_targets'] = self.MapTargetsToLabels(
978 isolate_map, inp['test_targets'])
979 if err:
980 raise MBErr(err)
981 labels_to_targets = {}
982 for i, label in enumerate(gn_inp['test_targets']):
983 labels_to_targets[label] = inp['test_targets'][i]
984
985 try:
986 self.WriteJSON(gn_inp, gn_input_path)
987 cmd = self.GNCmd('analyze', build_path, gn_input_path, gn_output_path)
988 ret, _, _ = self.Run(cmd, force_verbose=True)
989 if ret:
990 return ret
991
992 gn_outp_str = self.ReadFile(gn_output_path)
993 try:
994 gn_outp = json.loads(gn_outp_str)
995 except Exception as e:
996 self.Print("Failed to parse the JSON string GN returned: %s\n%s"
997 % (repr(gn_outp_str), str(e)))
998 raise
999
1000 outp = {}
1001 if 'status' in gn_outp:
1002 outp['status'] = gn_outp['status']
1003 if 'error' in gn_outp:
1004 outp['error'] = gn_outp['error']
1005 if 'invalid_targets' in gn_outp:
1006 outp['invalid_targets'] = gn_outp['invalid_targets']
1007 if 'compile_targets' in gn_outp:
1008 if 'all' in gn_outp['compile_targets']:
1009 outp['compile_targets'] = ['all']
1010 else:
1011 outp['compile_targets'] = [
1012 label.replace('//', '') for label in gn_outp['compile_targets']]
1013 if 'test_targets' in gn_outp:
1014 outp['test_targets'] = [
1015 labels_to_targets[label] for label in gn_outp['test_targets']]
1016
1017 if self.args.verbose:
1018 self.Print()
1019 self.Print('analyze output:')
1020 self.PrintJSON(outp)
1021 self.Print()
1022
1023 self.WriteJSON(outp, output_path)
1024
1025 finally:
1026 if self.Exists(gn_input_path):
1027 self.RemoveFile(gn_input_path)
1028 if self.Exists(gn_output_path):
1029 self.RemoveFile(gn_output_path)
1030
1031 return 0
1032
1033 def ReadInputJSON(self, required_keys):
1034 path = self.args.input_path[0]
1035 output_path = self.args.output_path[0]
1036 if not self.Exists(path):
1037 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
1038
1039 try:
1040 inp = json.loads(self.ReadFile(path))
1041 except Exception as e:
1042 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
1043 (path, e), output_path)
1044
1045 for k in required_keys:
1046 if not k in inp:
1047 self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
1048 output_path)
1049
1050 return inp
1051
1052 def WriteFailureAndRaise(self, msg, output_path):
1053 if output_path:
1054 self.WriteJSON({'error': msg}, output_path, force_verbose=True)
1055 raise MBErr(msg)
1056
1057 def WriteJSON(self, obj, path, force_verbose=False):
1058 try:
1059 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n',
1060 force_verbose=force_verbose)
1061 except Exception as e:
1062 raise MBErr('Error %s writing to the output path "%s"' %
1063 (e, path))
1064
1065 def CheckCompile(self, master, builder):
1066 url_template = self.args.url_template + '/{builder}/builds/_all?as_text=1'
1067 url = urllib2.quote(url_template.format(master=master, builder=builder),
1068 safe=':/()?=')
1069 try:
1070 builds = json.loads(self.Fetch(url))
1071 except Exception as e:
1072 return str(e)
1073 successes = sorted(
1074 [int(x) for x in builds.keys() if "text" in builds[x] and
1075 cmp(builds[x]["text"][:2], ["build", "successful"]) == 0],
1076 reverse=True)
1077 if not successes:
1078 return "no successful builds"
1079 build = builds[str(successes[0])]
1080 step_names = set([step["name"] for step in build["steps"]])
1081 compile_indicators = set(["compile", "compile (with patch)", "analyze"])
1082 if compile_indicators & step_names:
1083 return "compiles"
1084 return "does not compile"
1085
1086 def PrintCmd(self, cmd, env):
1087 if self.platform == 'win32':
1088 env_prefix = 'set '
1089 env_quoter = QuoteForSet
1090 shell_quoter = QuoteForCmd
1091 else:
1092 env_prefix = ''
1093 env_quoter = pipes.quote
1094 shell_quoter = pipes.quote
1095
1096 def print_env(var):
1097 if env and var in env:
1098 self.Print('%s%s=%s' % (env_prefix, var, env_quoter(env[var])))
1099
kjellandera013a022016-11-14 05:54:22 -08001100 print_env('LLVM_FORCE_HEAD_REVISION')
1101
1102 if cmd[0] == self.executable:
1103 cmd = ['python'] + cmd[1:]
1104 self.Print(*[shell_quoter(arg) for arg in cmd])
1105
1106 def PrintJSON(self, obj):
1107 self.Print(json.dumps(obj, indent=2, sort_keys=True))
1108
1109 def Build(self, target):
1110 build_dir = self.ToSrcRelPath(self.args.path[0])
Oleh Prypinb708e932018-03-18 17:34:20 +01001111 ninja_cmd = ['ninja', '-C', build_dir]
kjellandera013a022016-11-14 05:54:22 -08001112 if self.args.jobs:
1113 ninja_cmd.extend(['-j', '%d' % self.args.jobs])
1114 ninja_cmd.append(target)
1115 ret, _, _ = self.Run(ninja_cmd, force_verbose=False, buffer_output=False)
1116 return ret
1117
1118 def Run(self, cmd, env=None, force_verbose=True, buffer_output=True):
1119 # This function largely exists so it can be overridden for testing.
1120 if self.args.dryrun or self.args.verbose or force_verbose:
1121 self.PrintCmd(cmd, env)
1122 if self.args.dryrun:
1123 return 0, '', ''
1124
1125 ret, out, err = self.Call(cmd, env=env, buffer_output=buffer_output)
1126 if self.args.verbose or force_verbose:
1127 if ret:
1128 self.Print(' -> returned %d' % ret)
1129 if out:
1130 self.Print(out, end='')
1131 if err:
1132 self.Print(err, end='', file=sys.stderr)
1133 return ret, out, err
1134
1135 def Call(self, cmd, env=None, buffer_output=True):
1136 if buffer_output:
kjellander1c3548c2017-02-15 22:38:22 -08001137 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001138 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1139 env=env)
1140 out, err = p.communicate()
1141 else:
kjellander1c3548c2017-02-15 22:38:22 -08001142 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001143 env=env)
1144 p.wait()
1145 out = err = ''
1146 return p.returncode, out, err
1147
1148 def ExpandUser(self, path):
1149 # This function largely exists so it can be overridden for testing.
1150 return os.path.expanduser(path)
1151
1152 def Exists(self, path):
1153 # This function largely exists so it can be overridden for testing.
1154 return os.path.exists(path)
1155
1156 def Fetch(self, url):
1157 # This function largely exists so it can be overridden for testing.
1158 f = urllib2.urlopen(url)
1159 contents = f.read()
1160 f.close()
1161 return contents
1162
1163 def MaybeMakeDirectory(self, path):
1164 try:
1165 os.makedirs(path)
1166 except OSError, e:
1167 if e.errno != errno.EEXIST:
1168 raise
1169
1170 def PathJoin(self, *comps):
1171 # This function largely exists so it can be overriden for testing.
1172 return os.path.join(*comps)
1173
1174 def Print(self, *args, **kwargs):
1175 # This function largely exists so it can be overridden for testing.
1176 print(*args, **kwargs)
1177 if kwargs.get('stream', sys.stdout) == sys.stdout:
1178 sys.stdout.flush()
1179
1180 def ReadFile(self, path):
1181 # This function largely exists so it can be overriden for testing.
1182 with open(path) as fp:
1183 return fp.read()
1184
1185 def RelPath(self, path, start='.'):
1186 # This function largely exists so it can be overriden for testing.
1187 return os.path.relpath(path, start)
1188
1189 def RemoveFile(self, path):
1190 # This function largely exists so it can be overriden for testing.
1191 os.remove(path)
1192
1193 def RemoveDirectory(self, abs_path):
1194 if self.platform == 'win32':
1195 # In other places in chromium, we often have to retry this command
1196 # because we're worried about other processes still holding on to
1197 # file handles, but when MB is invoked, it will be early enough in the
1198 # build that their should be no other processes to interfere. We
1199 # can change this if need be.
1200 self.Run(['cmd.exe', '/c', 'rmdir', '/q', '/s', abs_path])
1201 else:
1202 shutil.rmtree(abs_path, ignore_errors=True)
1203
1204 def TempFile(self, mode='w'):
1205 # This function largely exists so it can be overriden for testing.
1206 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
1207
1208 def WriteFile(self, path, contents, force_verbose=False):
1209 # This function largely exists so it can be overriden for testing.
1210 if self.args.dryrun or self.args.verbose or force_verbose:
1211 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
1212 with open(path, 'w') as fp:
1213 return fp.write(contents)
1214
1215
1216class MBErr(Exception):
1217 pass
1218
1219
1220# See http://goo.gl/l5NPDW and http://goo.gl/4Diozm for the painful
1221# details of this next section, which handles escaping command lines
1222# so that they can be copied and pasted into a cmd window.
1223UNSAFE_FOR_SET = set('^<>&|')
1224UNSAFE_FOR_CMD = UNSAFE_FOR_SET.union(set('()%'))
1225ALL_META_CHARS = UNSAFE_FOR_CMD.union(set('"'))
1226
1227
1228def QuoteForSet(arg):
1229 if any(a in UNSAFE_FOR_SET for a in arg):
1230 arg = ''.join('^' + a if a in UNSAFE_FOR_SET else a for a in arg)
1231 return arg
1232
1233
1234def QuoteForCmd(arg):
1235 # First, escape the arg so that CommandLineToArgvW will parse it properly.
kjellandera013a022016-11-14 05:54:22 -08001236 if arg == '' or ' ' in arg or '"' in arg:
1237 quote_re = re.compile(r'(\\*)"')
1238 arg = '"%s"' % (quote_re.sub(lambda mo: 2 * mo.group(1) + '\\"', arg))
1239
1240 # Then check to see if the arg contains any metacharacters other than
1241 # double quotes; if it does, quote everything (including the double
1242 # quotes) for safety.
1243 if any(a in UNSAFE_FOR_CMD for a in arg):
1244 arg = ''.join('^' + a if a in ALL_META_CHARS else a for a in arg)
1245 return arg
1246
1247
1248if __name__ == '__main__':
1249 sys.exit(main(sys.argv[1:]))