blob: 8b726bf93b03fb906820ba35fafc4794868f5924 [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):
Oleh Prypin82ac2402019-01-29 16:18:30 +0100466 self.ReadConfigFile()
467 config = self.ConfigFromArgs()
468 if config.startswith('//'):
469 if not self.Exists(self.ToAbsPath(config)):
470 raise MBErr('args file "%s" not found' % config)
471 vals = self.DefaultVals()
472 vals['args_file'] = config
473 else:
474 if not config in self.configs:
475 raise MBErr('Config "%s" not found in %s' %
476 (config, self.args.config_file))
477 vals = self.FlattenConfig(config)
kjellandera013a022016-11-14 05:54:22 -0800478 return vals
479
480 def ReadConfigFile(self):
481 if not self.Exists(self.args.config_file):
482 raise MBErr('config file not found at %s' % self.args.config_file)
483
484 try:
485 contents = ast.literal_eval(self.ReadFile(self.args.config_file))
486 except SyntaxError as e:
487 raise MBErr('Failed to parse config file "%s": %s' %
488 (self.args.config_file, e))
489
490 self.configs = contents['configs']
491 self.masters = contents['masters']
492 self.mixins = contents['mixins']
493
494 def ReadIsolateMap(self):
Oleh Prypinb708e932018-03-18 17:34:20 +0100495 isolate_map = self.args.isolate_map_file
496 if not self.Exists(isolate_map):
497 raise MBErr('isolate map file not found at %s' % isolate_map)
kjellandera013a022016-11-14 05:54:22 -0800498 try:
Oleh Prypinb708e932018-03-18 17:34:20 +0100499 return ast.literal_eval(self.ReadFile(isolate_map))
kjellandera013a022016-11-14 05:54:22 -0800500 except SyntaxError as e:
Oleh Prypinb708e932018-03-18 17:34:20 +0100501 raise MBErr(
502 'Failed to parse isolate map file "%s": %s' % (isolate_map, e))
kjellandera013a022016-11-14 05:54:22 -0800503
504 def ConfigFromArgs(self):
505 if self.args.config:
506 if self.args.master or self.args.builder:
507 raise MBErr('Can not specific both -c/--config and -m/--master or '
508 '-b/--builder')
509
510 return self.args.config
511
512 if not self.args.master or not self.args.builder:
513 raise MBErr('Must specify either -c/--config or '
514 '(-m/--master and -b/--builder)')
515
516 if not self.args.master in self.masters:
517 raise MBErr('Master name "%s" not found in "%s"' %
518 (self.args.master, self.args.config_file))
519
520 if not self.args.builder in self.masters[self.args.master]:
521 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' %
522 (self.args.builder, self.args.master, self.args.config_file))
523
524 config = self.masters[self.args.master][self.args.builder]
525 if isinstance(config, dict):
526 if self.args.phase is None:
527 raise MBErr('Must specify a build --phase for %s on %s' %
528 (self.args.builder, self.args.master))
529 phase = str(self.args.phase)
530 if phase not in config:
531 raise MBErr('Phase %s doesn\'t exist for %s on %s' %
532 (phase, self.args.builder, self.args.master))
533 return config[phase]
534
535 if self.args.phase is not None:
536 raise MBErr('Must not specify a build --phase for %s on %s' %
537 (self.args.builder, self.args.master))
538 return config
539
540 def FlattenConfig(self, config):
541 mixins = self.configs[config]
542 vals = self.DefaultVals()
543
544 visited = []
545 self.FlattenMixins(mixins, vals, visited)
546 return vals
547
548 def DefaultVals(self):
549 return {
550 'args_file': '',
551 'cros_passthrough': False,
552 'gn_args': '',
kjellandera013a022016-11-14 05:54:22 -0800553 }
554
555 def FlattenMixins(self, mixins, vals, visited):
556 for m in mixins:
557 if m not in self.mixins:
558 raise MBErr('Unknown mixin "%s"' % m)
559
560 visited.append(m)
561
562 mixin_vals = self.mixins[m]
563
564 if 'cros_passthrough' in mixin_vals:
565 vals['cros_passthrough'] = mixin_vals['cros_passthrough']
566 if 'gn_args' in mixin_vals:
567 if vals['gn_args']:
568 vals['gn_args'] += ' ' + mixin_vals['gn_args']
569 else:
570 vals['gn_args'] = mixin_vals['gn_args']
kjellandera013a022016-11-14 05:54:22 -0800571
572 if 'mixins' in mixin_vals:
573 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
574 return vals
575
kjellandera013a022016-11-14 05:54:22 -0800576 def RunGNGen(self, vals):
577 build_dir = self.args.path[0]
578
579 cmd = self.GNCmd('gen', build_dir, '--check')
580 gn_args = self.GNArgs(vals)
581
582 # Since GN hasn't run yet, the build directory may not even exist.
583 self.MaybeMakeDirectory(self.ToAbsPath(build_dir))
584
585 gn_args_path = self.ToAbsPath(build_dir, 'args.gn')
586 self.WriteFile(gn_args_path, gn_args, force_verbose=True)
587
588 swarming_targets = []
589 if getattr(self.args, 'swarming_targets_file', None):
590 # We need GN to generate the list of runtime dependencies for
591 # the compile targets listed (one per line) in the file so
592 # we can run them via swarming. We use gn_isolate_map.pyl to convert
593 # the compile targets to the matching GN labels.
594 path = self.args.swarming_targets_file
595 if not self.Exists(path):
596 self.WriteFailureAndRaise('"%s" does not exist' % path,
597 output_path=None)
598 contents = self.ReadFile(path)
599 swarming_targets = set(contents.splitlines())
600
601 isolate_map = self.ReadIsolateMap()
602 err, labels = self.MapTargetsToLabels(isolate_map, swarming_targets)
603 if err:
604 raise MBErr(err)
605
606 gn_runtime_deps_path = self.ToAbsPath(build_dir, 'runtime_deps')
607 self.WriteFile(gn_runtime_deps_path, '\n'.join(labels) + '\n')
608 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path)
609
610 ret, _, _ = self.Run(cmd)
611 if ret:
612 # If `gn gen` failed, we should exit early rather than trying to
613 # generate isolates. Run() will have already logged any error output.
614 self.Print('GN gen failed: %d' % ret)
615 return ret
616
617 android = 'target_os="android"' in vals['gn_args']
618 for target in swarming_targets:
619 if android:
620 # Android targets may be either android_apk or executable. The former
621 # will result in runtime_deps associated with the stamp file, while the
622 # latter will result in runtime_deps associated with the executable.
623 label = isolate_map[target]['label']
624 runtime_deps_targets = [
625 target + '.runtime_deps',
626 'obj/%s.stamp.runtime_deps' % label.replace(':', '/')]
627 elif isolate_map[target]['type'] == 'gpu_browser_test':
628 if self.platform == 'win32':
629 runtime_deps_targets = ['browser_tests.exe.runtime_deps']
630 else:
631 runtime_deps_targets = ['browser_tests.runtime_deps']
Edward Lemur20110752017-09-28 16:14:37 +0200632 elif isolate_map[target]['type'] == 'script':
633 label = isolate_map[target]['label'].split(':')[1]
kjellandera013a022016-11-14 05:54:22 -0800634 runtime_deps_targets = [
Edward Lemur20110752017-09-28 16:14:37 +0200635 '%s.runtime_deps' % label]
kjellandera013a022016-11-14 05:54:22 -0800636 if self.platform == 'win32':
Edward Lemur20110752017-09-28 16:14:37 +0200637 runtime_deps_targets += [ label + '.exe.runtime_deps' ]
kjellandera013a022016-11-14 05:54:22 -0800638 else:
Edward Lemur20110752017-09-28 16:14:37 +0200639 runtime_deps_targets += [ label + '.runtime_deps' ]
kjellandera013a022016-11-14 05:54:22 -0800640 elif self.platform == 'win32':
641 runtime_deps_targets = [target + '.exe.runtime_deps']
642 else:
643 runtime_deps_targets = [target + '.runtime_deps']
644
645 for r in runtime_deps_targets:
646 runtime_deps_path = self.ToAbsPath(build_dir, r)
647 if self.Exists(runtime_deps_path):
648 break
649 else:
650 raise MBErr('did not generate any of %s' %
651 ', '.join(runtime_deps_targets))
652
653 command, extra_files = self.GetIsolateCommand(target, vals)
654
655 runtime_deps = self.ReadFile(runtime_deps_path).splitlines()
656
657 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
658 extra_files)
659
660 return 0
661
662 def RunGNIsolate(self, vals):
663 target = self.args.target[0]
664 isolate_map = self.ReadIsolateMap()
665 err, labels = self.MapTargetsToLabels(isolate_map, [target])
666 if err:
667 raise MBErr(err)
668 label = labels[0]
669
670 build_dir = self.args.path[0]
671 command, extra_files = self.GetIsolateCommand(target, vals)
672
673 cmd = self.GNCmd('desc', build_dir, label, 'runtime_deps')
674 ret, out, _ = self.Call(cmd)
675 if ret:
676 if out:
677 self.Print(out)
678 return ret
679
680 runtime_deps = out.splitlines()
681
682 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
683 extra_files)
684
685 ret, _, _ = self.Run([
686 self.executable,
687 self.PathJoin('tools', 'swarming_client', 'isolate.py'),
688 'check',
689 '-i',
690 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
691 '-s',
692 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target))],
693 buffer_output=False)
694
695 return ret
696
697 def WriteIsolateFiles(self, build_dir, command, target, runtime_deps,
698 extra_files):
699 isolate_path = self.ToAbsPath(build_dir, target + '.isolate')
700 self.WriteFile(isolate_path,
701 pprint.pformat({
702 'variables': {
703 'command': command,
704 'files': sorted(runtime_deps + extra_files),
705 }
706 }) + '\n')
707
708 self.WriteJSON(
709 {
710 'args': [
711 '--isolated',
712 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
713 '--isolate',
714 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
715 ],
kjellander1c3548c2017-02-15 22:38:22 -0800716 'dir': self.src_dir,
kjellandera013a022016-11-14 05:54:22 -0800717 'version': 1,
718 },
719 isolate_path + 'd.gen.json',
720 )
721
722 def MapTargetsToLabels(self, isolate_map, targets):
723 labels = []
724 err = ''
725
726 def StripTestSuffixes(target):
727 for suffix in ('_apk_run', '_apk', '_run'):
728 if target.endswith(suffix):
729 return target[:-len(suffix)], suffix
730 return None, None
731
732 for target in targets:
733 if target == 'all':
734 labels.append(target)
735 elif target.startswith('//'):
736 labels.append(target)
737 else:
738 if target in isolate_map:
739 stripped_target, suffix = target, ''
740 else:
741 stripped_target, suffix = StripTestSuffixes(target)
742 if stripped_target in isolate_map:
743 if isolate_map[stripped_target]['type'] == 'unknown':
744 err += ('test target "%s" type is unknown\n' % target)
745 else:
746 labels.append(isolate_map[stripped_target]['label'] + suffix)
747 else:
748 err += ('target "%s" not found in '
749 '//testing/buildbot/gn_isolate_map.pyl\n' % target)
750
751 return err, labels
752
753 def GNCmd(self, subcommand, path, *args):
Oleh Prypinb708e932018-03-18 17:34:20 +0100754 if self.platform.startswith('linux'):
kjellandera013a022016-11-14 05:54:22 -0800755 subdir, exe = 'linux64', 'gn'
756 elif self.platform == 'darwin':
757 subdir, exe = 'mac', 'gn'
758 else:
759 subdir, exe = 'win', 'gn.exe'
760
kjellander1c3548c2017-02-15 22:38:22 -0800761 gn_path = self.PathJoin(self.src_dir, 'buildtools', subdir, exe)
kjellandera013a022016-11-14 05:54:22 -0800762 return [gn_path, subcommand, path] + list(args)
763
764
765 def GNArgs(self, vals):
766 if vals['cros_passthrough']:
767 if not 'GN_ARGS' in os.environ:
768 raise MBErr('MB is expecting GN_ARGS to be in the environment')
769 gn_args = os.environ['GN_ARGS']
770 if not re.search('target_os.*=.*"chromeos"', gn_args):
771 raise MBErr('GN_ARGS is missing target_os = "chromeos": (GN_ARGS=%s)' %
772 gn_args)
773 else:
774 gn_args = vals['gn_args']
775
776 if self.args.goma_dir:
777 gn_args += ' goma_dir="%s"' % self.args.goma_dir
778
779 android_version_code = self.args.android_version_code
780 if android_version_code:
781 gn_args += ' android_default_version_code="%s"' % android_version_code
782
783 android_version_name = self.args.android_version_name
784 if android_version_name:
785 gn_args += ' android_default_version_name="%s"' % android_version_name
786
787 # Canonicalize the arg string into a sorted, newline-separated list
788 # of key-value pairs, and de-dup the keys if need be so that only
789 # the last instance of each arg is listed.
790 gn_args = gn_helpers.ToGNString(gn_helpers.FromGNArgs(gn_args))
791
792 args_file = vals.get('args_file', None)
793 if args_file:
794 gn_args = ('import("%s")\n' % vals['args_file']) + gn_args
795 return gn_args
796
kjellandera013a022016-11-14 05:54:22 -0800797 def GetIsolateCommand(self, target, vals):
kjellandera013a022016-11-14 05:54:22 -0800798 isolate_map = self.ReadIsolateMap()
799 test_type = isolate_map[target]['type']
800
Oleh Prypinb708e932018-03-18 17:34:20 +0100801 is_android = 'target_os="android"' in vals['gn_args']
802 is_linux = self.platform.startswith('linux') and not is_android
kjellandera013a022016-11-14 05:54:22 -0800803
804 if test_type == 'nontest':
805 self.WriteFailureAndRaise('We should not be isolating %s.' % target,
806 output_path=None)
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800807 if test_type not in ('console_test_launcher', 'windowed_test_launcher',
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100808 'non_parallel_console_test_launcher', 'raw',
Edward Lemur20110752017-09-28 16:14:37 +0200809 'additional_compile_target', 'junit_test', 'script'):
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800810 self.WriteFailureAndRaise('No command line for %s found (test type %s).'
811 % (target, test_type), output_path=None)
kjellandera013a022016-11-14 05:54:22 -0800812
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800813 cmdline = []
Oleh Prypinb708e932018-03-18 17:34:20 +0100814 extra_files = [
815 '../../.vpython',
816 '../../testing/test_env.py',
817 ]
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800818
Yves Gerey2e0c6552018-10-08 21:59:25 +0200819 must_retry = False
Edward Lemur98d40362018-01-15 17:37:04 +0100820 if test_type == 'script':
821 cmdline = ['../../' + self.ToSrcRelPath(isolate_map[target]['script'])]
Oleh Prypinb708e932018-03-18 17:34:20 +0100822 elif is_android:
kjellanderf9e2a362017-03-24 12:17:33 -0700823 cmdline = ['../../build/android/test_wrapper/logdog_wrapper.py',
824 '--target', target,
Oleh Prypin5bb1afd2018-12-14 16:11:48 +0000825 '--logdog-bin-cmd', '../../bin/logdog_butler',
ehmaldonado34623ce2017-09-08 07:03:13 -0700826 '--logcat-output-file', '${ISOLATED_OUTDIR}/logcats',
827 '--store-tombstones']
kjellandera013a022016-11-14 05:54:22 -0800828 else:
Oleh Prypin739b8162018-05-17 13:28:29 +0200829 if isolate_map[target].get('use_webcam', False):
830 cmdline.append('../../tools_webrtc/ensure_webcam_is_running.py')
831 extra_files.append('../../tools_webrtc/ensure_webcam_is_running.py')
kjellandera013a022016-11-14 05:54:22 -0800832
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800833 # This needs to mirror the settings in //build/config/ui.gni:
834 # use_x11 = is_linux && !use_ozone.
835 use_x11 = is_linux and not 'use_ozone=true' in vals['gn_args']
836
837 xvfb = use_x11 and test_type == 'windowed_test_launcher'
838 if xvfb:
Oleh Prypin739b8162018-05-17 13:28:29 +0200839 cmdline.append('../../testing/xvfb.py')
840 extra_files.append('../../testing/xvfb.py')
841 else:
842 cmdline.append('../../testing/test_env.py')
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800843
Mirko Bonadei264bee82018-08-07 08:53:41 +0200844 if test_type != 'raw':
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800845 extra_files += [
846 '../../third_party/gtest-parallel/gtest-parallel',
ehmaldonadoa7507eb2017-05-10 13:40:29 -0700847 '../../third_party/gtest-parallel/gtest_parallel.py',
Henrik Kjellander90fd7d82017-05-09 08:30:10 +0200848 '../../tools_webrtc/gtest-parallel-wrapper.py',
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800849 ]
ehmaldonado55833842017-02-13 03:58:13 -0800850 sep = '\\' if self.platform == 'win32' else '/'
851 output_dir = '${ISOLATED_OUTDIR}' + sep + 'test_logs'
Edward Lemurbeffdd42017-09-27 13:07:47 +0200852 timeout = isolate_map[target].get('timeout', 900)
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100853 cmdline += [
Henrik Kjellander90fd7d82017-05-09 08:30:10 +0200854 '../../tools_webrtc/gtest-parallel-wrapper.py',
ehmaldonado55833842017-02-13 03:58:13 -0800855 '--output_dir=%s' % output_dir,
ehmaldonado76e60e92017-05-04 06:18:26 -0700856 '--gtest_color=no',
857 # We tell gtest-parallel to interrupt the test after 900 seconds,
858 # so it can exit cleanly and report results, instead of being
859 # interrupted by swarming and not reporting anything.
Edward Lemurbeffdd42017-09-27 13:07:47 +0200860 '--timeout=%s' % timeout,
ehmaldonado55833842017-02-13 03:58:13 -0800861 ]
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100862 if test_type == 'non_parallel_console_test_launcher':
863 # Still use the gtest-parallel-wrapper.py script since we need it to
864 # run tests on swarming, but don't execute tests in parallel.
865 cmdline.append('--workers=1')
Yves Gerey2e0c6552018-10-08 21:59:25 +0200866 must_retry = True
867
868 asan = 'is_asan=true' in vals['gn_args']
869 lsan = 'is_lsan=true' in vals['gn_args']
870 msan = 'is_msan=true' in vals['gn_args']
871 tsan = 'is_tsan=true' in vals['gn_args']
872 sanitizer = asan or lsan or msan or tsan
873 if must_retry and not sanitizer:
874 # Retry would hide most sanitizers detections.
875 cmdline.append('--retry_failed=3')
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100876
877 executable_prefix = '.\\' if self.platform == 'win32' else './'
878 executable_suffix = '.exe' if self.platform == 'win32' else ''
879 executable = executable_prefix + target + executable_suffix
880
881 cmdline.append(executable)
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800882
kjellander382f2b22017-04-11 04:07:01 -0700883 cmdline.extend([
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800884 '--asan=%d' % asan,
kjellander461a5602017-05-05 06:39:16 -0700885 '--lsan=%d' % lsan,
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800886 '--msan=%d' % msan,
887 '--tsan=%d' % tsan,
kjellander382f2b22017-04-11 04:07:01 -0700888 ])
kjellandera013a022016-11-14 05:54:22 -0800889
kjellander74e81262017-03-23 00:51:11 -0700890 cmdline += isolate_map[target].get('args', [])
891
kjellandera013a022016-11-14 05:54:22 -0800892 return cmdline, extra_files
893
894 def ToAbsPath(self, build_path, *comps):
kjellander1c3548c2017-02-15 22:38:22 -0800895 return self.PathJoin(self.src_dir,
kjellandera013a022016-11-14 05:54:22 -0800896 self.ToSrcRelPath(build_path),
897 *comps)
898
899 def ToSrcRelPath(self, path):
900 """Returns a relative path from the top of the repo."""
901 if path.startswith('//'):
902 return path[2:].replace('/', self.sep)
kjellander1c3548c2017-02-15 22:38:22 -0800903 return self.RelPath(path, self.src_dir)
kjellandera013a022016-11-14 05:54:22 -0800904
kjellandera013a022016-11-14 05:54:22 -0800905 def RunGNAnalyze(self, vals):
906 # Analyze runs before 'gn gen' now, so we need to run gn gen
907 # in order to ensure that we have a build directory.
908 ret = self.RunGNGen(vals)
909 if ret:
910 return ret
911
912 build_path = self.args.path[0]
913 input_path = self.args.input_path[0]
914 gn_input_path = input_path + '.gn'
915 output_path = self.args.output_path[0]
916 gn_output_path = output_path + '.gn'
917
918 inp = self.ReadInputJSON(['files', 'test_targets',
919 'additional_compile_targets'])
920 if self.args.verbose:
921 self.Print()
922 self.Print('analyze input:')
923 self.PrintJSON(inp)
924 self.Print()
925
926
927 # This shouldn't normally happen, but could due to unusual race conditions,
928 # like a try job that gets scheduled before a patch lands but runs after
929 # the patch has landed.
930 if not inp['files']:
931 self.Print('Warning: No files modified in patch, bailing out early.')
932 self.WriteJSON({
933 'status': 'No dependency',
934 'compile_targets': [],
935 'test_targets': [],
936 }, output_path)
937 return 0
938
939 gn_inp = {}
940 gn_inp['files'] = ['//' + f for f in inp['files'] if not f.startswith('//')]
941
942 isolate_map = self.ReadIsolateMap()
943 err, gn_inp['additional_compile_targets'] = self.MapTargetsToLabels(
944 isolate_map, inp['additional_compile_targets'])
945 if err:
946 raise MBErr(err)
947
948 err, gn_inp['test_targets'] = self.MapTargetsToLabels(
949 isolate_map, inp['test_targets'])
950 if err:
951 raise MBErr(err)
952 labels_to_targets = {}
953 for i, label in enumerate(gn_inp['test_targets']):
954 labels_to_targets[label] = inp['test_targets'][i]
955
956 try:
957 self.WriteJSON(gn_inp, gn_input_path)
958 cmd = self.GNCmd('analyze', build_path, gn_input_path, gn_output_path)
959 ret, _, _ = self.Run(cmd, force_verbose=True)
960 if ret:
961 return ret
962
963 gn_outp_str = self.ReadFile(gn_output_path)
964 try:
965 gn_outp = json.loads(gn_outp_str)
966 except Exception as e:
967 self.Print("Failed to parse the JSON string GN returned: %s\n%s"
968 % (repr(gn_outp_str), str(e)))
969 raise
970
971 outp = {}
972 if 'status' in gn_outp:
973 outp['status'] = gn_outp['status']
974 if 'error' in gn_outp:
975 outp['error'] = gn_outp['error']
976 if 'invalid_targets' in gn_outp:
977 outp['invalid_targets'] = gn_outp['invalid_targets']
978 if 'compile_targets' in gn_outp:
979 if 'all' in gn_outp['compile_targets']:
980 outp['compile_targets'] = ['all']
981 else:
982 outp['compile_targets'] = [
983 label.replace('//', '') for label in gn_outp['compile_targets']]
984 if 'test_targets' in gn_outp:
985 outp['test_targets'] = [
986 labels_to_targets[label] for label in gn_outp['test_targets']]
987
988 if self.args.verbose:
989 self.Print()
990 self.Print('analyze output:')
991 self.PrintJSON(outp)
992 self.Print()
993
994 self.WriteJSON(outp, output_path)
995
996 finally:
997 if self.Exists(gn_input_path):
998 self.RemoveFile(gn_input_path)
999 if self.Exists(gn_output_path):
1000 self.RemoveFile(gn_output_path)
1001
1002 return 0
1003
1004 def ReadInputJSON(self, required_keys):
1005 path = self.args.input_path[0]
1006 output_path = self.args.output_path[0]
1007 if not self.Exists(path):
1008 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
1009
1010 try:
1011 inp = json.loads(self.ReadFile(path))
1012 except Exception as e:
1013 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
1014 (path, e), output_path)
1015
1016 for k in required_keys:
1017 if not k in inp:
1018 self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
1019 output_path)
1020
1021 return inp
1022
1023 def WriteFailureAndRaise(self, msg, output_path):
1024 if output_path:
1025 self.WriteJSON({'error': msg}, output_path, force_verbose=True)
1026 raise MBErr(msg)
1027
1028 def WriteJSON(self, obj, path, force_verbose=False):
1029 try:
1030 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n',
1031 force_verbose=force_verbose)
1032 except Exception as e:
1033 raise MBErr('Error %s writing to the output path "%s"' %
1034 (e, path))
1035
1036 def CheckCompile(self, master, builder):
1037 url_template = self.args.url_template + '/{builder}/builds/_all?as_text=1'
1038 url = urllib2.quote(url_template.format(master=master, builder=builder),
1039 safe=':/()?=')
1040 try:
1041 builds = json.loads(self.Fetch(url))
1042 except Exception as e:
1043 return str(e)
1044 successes = sorted(
1045 [int(x) for x in builds.keys() if "text" in builds[x] and
1046 cmp(builds[x]["text"][:2], ["build", "successful"]) == 0],
1047 reverse=True)
1048 if not successes:
1049 return "no successful builds"
1050 build = builds[str(successes[0])]
1051 step_names = set([step["name"] for step in build["steps"]])
1052 compile_indicators = set(["compile", "compile (with patch)", "analyze"])
1053 if compile_indicators & step_names:
1054 return "compiles"
1055 return "does not compile"
1056
1057 def PrintCmd(self, cmd, env):
1058 if self.platform == 'win32':
1059 env_prefix = 'set '
1060 env_quoter = QuoteForSet
1061 shell_quoter = QuoteForCmd
1062 else:
1063 env_prefix = ''
1064 env_quoter = pipes.quote
1065 shell_quoter = pipes.quote
1066
1067 def print_env(var):
1068 if env and var in env:
1069 self.Print('%s%s=%s' % (env_prefix, var, env_quoter(env[var])))
1070
kjellandera013a022016-11-14 05:54:22 -08001071 print_env('LLVM_FORCE_HEAD_REVISION')
1072
1073 if cmd[0] == self.executable:
1074 cmd = ['python'] + cmd[1:]
1075 self.Print(*[shell_quoter(arg) for arg in cmd])
1076
1077 def PrintJSON(self, obj):
1078 self.Print(json.dumps(obj, indent=2, sort_keys=True))
1079
1080 def Build(self, target):
1081 build_dir = self.ToSrcRelPath(self.args.path[0])
Oleh Prypinb708e932018-03-18 17:34:20 +01001082 ninja_cmd = ['ninja', '-C', build_dir]
kjellandera013a022016-11-14 05:54:22 -08001083 if self.args.jobs:
1084 ninja_cmd.extend(['-j', '%d' % self.args.jobs])
1085 ninja_cmd.append(target)
1086 ret, _, _ = self.Run(ninja_cmd, force_verbose=False, buffer_output=False)
1087 return ret
1088
1089 def Run(self, cmd, env=None, force_verbose=True, buffer_output=True):
1090 # This function largely exists so it can be overridden for testing.
1091 if self.args.dryrun or self.args.verbose or force_verbose:
1092 self.PrintCmd(cmd, env)
1093 if self.args.dryrun:
1094 return 0, '', ''
1095
1096 ret, out, err = self.Call(cmd, env=env, buffer_output=buffer_output)
1097 if self.args.verbose or force_verbose:
1098 if ret:
1099 self.Print(' -> returned %d' % ret)
1100 if out:
1101 self.Print(out, end='')
1102 if err:
1103 self.Print(err, end='', file=sys.stderr)
1104 return ret, out, err
1105
1106 def Call(self, cmd, env=None, buffer_output=True):
1107 if buffer_output:
kjellander1c3548c2017-02-15 22:38:22 -08001108 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001109 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1110 env=env)
1111 out, err = p.communicate()
1112 else:
kjellander1c3548c2017-02-15 22:38:22 -08001113 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001114 env=env)
1115 p.wait()
1116 out = err = ''
1117 return p.returncode, out, err
1118
1119 def ExpandUser(self, path):
1120 # This function largely exists so it can be overridden for testing.
1121 return os.path.expanduser(path)
1122
1123 def Exists(self, path):
1124 # This function largely exists so it can be overridden for testing.
1125 return os.path.exists(path)
1126
1127 def Fetch(self, url):
1128 # This function largely exists so it can be overridden for testing.
1129 f = urllib2.urlopen(url)
1130 contents = f.read()
1131 f.close()
1132 return contents
1133
1134 def MaybeMakeDirectory(self, path):
1135 try:
1136 os.makedirs(path)
1137 except OSError, e:
1138 if e.errno != errno.EEXIST:
1139 raise
1140
1141 def PathJoin(self, *comps):
1142 # This function largely exists so it can be overriden for testing.
1143 return os.path.join(*comps)
1144
1145 def Print(self, *args, **kwargs):
1146 # This function largely exists so it can be overridden for testing.
1147 print(*args, **kwargs)
1148 if kwargs.get('stream', sys.stdout) == sys.stdout:
1149 sys.stdout.flush()
1150
1151 def ReadFile(self, path):
1152 # This function largely exists so it can be overriden for testing.
1153 with open(path) as fp:
1154 return fp.read()
1155
1156 def RelPath(self, path, start='.'):
1157 # This function largely exists so it can be overriden for testing.
1158 return os.path.relpath(path, start)
1159
1160 def RemoveFile(self, path):
1161 # This function largely exists so it can be overriden for testing.
1162 os.remove(path)
1163
1164 def RemoveDirectory(self, abs_path):
1165 if self.platform == 'win32':
1166 # In other places in chromium, we often have to retry this command
1167 # because we're worried about other processes still holding on to
1168 # file handles, but when MB is invoked, it will be early enough in the
1169 # build that their should be no other processes to interfere. We
1170 # can change this if need be.
1171 self.Run(['cmd.exe', '/c', 'rmdir', '/q', '/s', abs_path])
1172 else:
1173 shutil.rmtree(abs_path, ignore_errors=True)
1174
1175 def TempFile(self, mode='w'):
1176 # This function largely exists so it can be overriden for testing.
1177 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
1178
1179 def WriteFile(self, path, contents, force_verbose=False):
1180 # This function largely exists so it can be overriden for testing.
1181 if self.args.dryrun or self.args.verbose or force_verbose:
1182 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
1183 with open(path, 'w') as fp:
1184 return fp.write(contents)
1185
1186
1187class MBErr(Exception):
1188 pass
1189
1190
1191# See http://goo.gl/l5NPDW and http://goo.gl/4Diozm for the painful
1192# details of this next section, which handles escaping command lines
1193# so that they can be copied and pasted into a cmd window.
1194UNSAFE_FOR_SET = set('^<>&|')
1195UNSAFE_FOR_CMD = UNSAFE_FOR_SET.union(set('()%'))
1196ALL_META_CHARS = UNSAFE_FOR_CMD.union(set('"'))
1197
1198
1199def QuoteForSet(arg):
1200 if any(a in UNSAFE_FOR_SET for a in arg):
1201 arg = ''.join('^' + a if a in UNSAFE_FOR_SET else a for a in arg)
1202 return arg
1203
1204
1205def QuoteForCmd(arg):
1206 # First, escape the arg so that CommandLineToArgvW will parse it properly.
kjellandera013a022016-11-14 05:54:22 -08001207 if arg == '' or ' ' in arg or '"' in arg:
1208 quote_re = re.compile(r'(\\*)"')
1209 arg = '"%s"' % (quote_re.sub(lambda mo: 2 * mo.group(1) + '\\"', arg))
1210
1211 # Then check to see if the arg contains any metacharacters other than
1212 # double quotes; if it does, quote everything (including the double
1213 # quotes) for safety.
1214 if any(a in UNSAFE_FOR_CMD for a in arg):
1215 arg = ''.join('^' + a if a in ALL_META_CHARS else a for a in arg)
1216 return arg
1217
1218
1219if __name__ == '__main__':
1220 sys.exit(main(sys.argv[1:]))