blob: 6287ca23663c7396c35bb55f036a5fc9e65591ee [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 = {}
Ye Kuangb28f0202020-03-16 10:56:20 +090059 self.isolate_exe = 'isolate.exe' if self.platform.startswith(
60 'win') else 'isolate'
kjellandera013a022016-11-14 05:54:22 -080061
62 def Main(self, args):
63 self.ParseArgs(args)
64 try:
65 ret = self.args.func()
66 if ret:
67 self.DumpInputFiles()
68 return ret
69 except KeyboardInterrupt:
70 self.Print('interrupted, exiting')
71 return 130
72 except Exception:
73 self.DumpInputFiles()
74 s = traceback.format_exc()
75 for l in s.splitlines():
76 self.Print(l)
77 return 1
78
79 def ParseArgs(self, argv):
80 def AddCommonOptions(subp):
81 subp.add_argument('-b', '--builder',
82 help='builder name to look up config from')
83 subp.add_argument('-m', '--master',
84 help='master name to look up config from')
85 subp.add_argument('-c', '--config',
86 help='configuration to analyze')
87 subp.add_argument('--phase',
88 help='optional phase name (used when builders '
89 'do multiple compiles with different '
90 'arguments in a single build)')
91 subp.add_argument('-f', '--config-file', metavar='PATH',
92 default=self.default_config,
93 help='path to config file '
94 '(default is %(default)s)')
95 subp.add_argument('-i', '--isolate-map-file', metavar='PATH',
96 default=self.default_isolate_map,
97 help='path to isolate map file '
98 '(default is %(default)s)')
99 subp.add_argument('-g', '--goma-dir',
100 help='path to goma directory')
kjellandera013a022016-11-14 05:54:22 -0800101 subp.add_argument('--android-version-code',
Oleh Prypinb708e932018-03-18 17:34:20 +0100102 help='Sets GN arg android_default_version_code')
kjellandera013a022016-11-14 05:54:22 -0800103 subp.add_argument('--android-version-name',
Oleh Prypinb708e932018-03-18 17:34:20 +0100104 help='Sets GN arg android_default_version_name')
kjellandera013a022016-11-14 05:54:22 -0800105 subp.add_argument('-n', '--dryrun', action='store_true',
106 help='Do a dry run (i.e., do nothing, just print '
107 'the commands that will run)')
108 subp.add_argument('-v', '--verbose', action='store_true',
109 help='verbose logging')
110
111 parser = argparse.ArgumentParser(prog='mb')
112 subps = parser.add_subparsers()
113
114 subp = subps.add_parser('analyze',
115 help='analyze whether changes to a set of files '
116 'will cause a set of binaries to be rebuilt.')
117 AddCommonOptions(subp)
118 subp.add_argument('path', nargs=1,
119 help='path build was generated into.')
120 subp.add_argument('input_path', nargs=1,
121 help='path to a file containing the input arguments '
122 'as a JSON object.')
123 subp.add_argument('output_path', nargs=1,
124 help='path to a file containing the output arguments '
125 'as a JSON object.')
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700126 subp.add_argument('--json-output',
127 help='Write errors to json.output')
kjellandera013a022016-11-14 05:54:22 -0800128 subp.set_defaults(func=self.CmdAnalyze)
129
130 subp = subps.add_parser('export',
131 help='print out the expanded configuration for'
132 'each builder as a JSON object')
133 subp.add_argument('-f', '--config-file', metavar='PATH',
134 default=self.default_config,
135 help='path to config file (default is %(default)s)')
136 subp.add_argument('-g', '--goma-dir',
137 help='path to goma directory')
138 subp.set_defaults(func=self.CmdExport)
139
140 subp = subps.add_parser('gen',
141 help='generate a new set of build files')
142 AddCommonOptions(subp)
143 subp.add_argument('--swarming-targets-file',
144 help='save runtime dependencies for targets listed '
145 'in file.')
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700146 subp.add_argument('--json-output',
147 help='Write errors to json.output')
kjellandera013a022016-11-14 05:54:22 -0800148 subp.add_argument('path', nargs=1,
149 help='path to generate build into')
150 subp.set_defaults(func=self.CmdGen)
151
152 subp = subps.add_parser('isolate',
153 help='generate the .isolate files for a given'
154 'binary')
155 AddCommonOptions(subp)
156 subp.add_argument('path', nargs=1,
157 help='path build was generated into')
158 subp.add_argument('target', nargs=1,
159 help='ninja target to generate the isolate for')
160 subp.set_defaults(func=self.CmdIsolate)
161
162 subp = subps.add_parser('lookup',
163 help='look up the command for a given config or '
164 'builder')
165 AddCommonOptions(subp)
Oleh Prypind7e2fb32019-05-31 13:25:39 +0200166 subp.add_argument('--quiet', default=False, action='store_true',
167 help='Print out just the arguments, '
168 'do not emulate the output of the gen subcommand.')
kjellandera013a022016-11-14 05:54:22 -0800169 subp.set_defaults(func=self.CmdLookup)
170
171 subp = subps.add_parser(
172 'run',
173 help='build and run the isolated version of a '
174 'binary',
175 formatter_class=argparse.RawDescriptionHelpFormatter)
176 subp.description = (
177 'Build, isolate, and run the given binary with the command line\n'
178 'listed in the isolate. You may pass extra arguments after the\n'
179 'target; use "--" if the extra arguments need to include switches.\n'
180 '\n'
181 'Examples:\n'
182 '\n'
183 ' % tools/mb/mb.py run -m chromium.linux -b "Linux Builder" \\\n'
184 ' //out/Default content_browsertests\n'
185 '\n'
186 ' % tools/mb/mb.py run out/Default content_browsertests\n'
187 '\n'
188 ' % tools/mb/mb.py run out/Default content_browsertests -- \\\n'
189 ' --test-launcher-retry-limit=0'
190 '\n'
191 )
kjellandera013a022016-11-14 05:54:22 -0800192 AddCommonOptions(subp)
193 subp.add_argument('-j', '--jobs', dest='jobs', type=int,
194 help='Number of jobs to pass to ninja')
195 subp.add_argument('--no-build', dest='build', default=True,
196 action='store_false',
197 help='Do not build, just isolate and run')
198 subp.add_argument('path', nargs=1,
199 help=('path to generate build into (or use).'
200 ' This can be either a regular path or a '
201 'GN-style source-relative path like '
202 '//out/Default.'))
Oleh Prypinb708e932018-03-18 17:34:20 +0100203 subp.add_argument('-s', '--swarmed', action='store_true',
204 help='Run under swarming')
205 subp.add_argument('-d', '--dimension', default=[], action='append', nargs=2,
206 dest='dimensions', metavar='FOO bar',
207 help='dimension to filter on')
kjellandera013a022016-11-14 05:54:22 -0800208 subp.add_argument('target', nargs=1,
209 help='ninja target to build and run')
210 subp.add_argument('extra_args', nargs='*',
211 help=('extra args to pass to the isolate to run. Use '
212 '"--" as the first arg if you need to pass '
213 'switches'))
214 subp.set_defaults(func=self.CmdRun)
215
216 subp = subps.add_parser('validate',
217 help='validate the config file')
218 subp.add_argument('-f', '--config-file', metavar='PATH',
219 default=self.default_config,
220 help='path to config file (default is %(default)s)')
221 subp.set_defaults(func=self.CmdValidate)
222
kjellandera013a022016-11-14 05:54:22 -0800223 subp = subps.add_parser('help',
224 help='Get help on a subcommand.')
225 subp.add_argument(nargs='?', action='store', dest='subcommand',
226 help='The command to get help for.')
227 subp.set_defaults(func=self.CmdHelp)
228
229 self.args = parser.parse_args(argv)
230
231 def DumpInputFiles(self):
232
233 def DumpContentsOfFilePassedTo(arg_name, path):
234 if path and self.Exists(path):
235 self.Print("\n# To recreate the file passed to %s:" % arg_name)
236 self.Print("%% cat > %s <<EOF" % path)
237 contents = self.ReadFile(path)
238 self.Print(contents)
239 self.Print("EOF\n%\n")
240
241 if getattr(self.args, 'input_path', None):
242 DumpContentsOfFilePassedTo(
243 'argv[0] (input_path)', self.args.input_path[0])
244 if getattr(self.args, 'swarming_targets_file', None):
245 DumpContentsOfFilePassedTo(
246 '--swarming-targets-file', self.args.swarming_targets_file)
247
248 def CmdAnalyze(self):
249 vals = self.Lookup()
Oleh Prypinb708e932018-03-18 17:34:20 +0100250 return self.RunGNAnalyze(vals)
kjellandera013a022016-11-14 05:54:22 -0800251
252 def CmdExport(self):
253 self.ReadConfigFile()
254 obj = {}
255 for master, builders in self.masters.items():
256 obj[master] = {}
257 for builder in builders:
258 config = self.masters[master][builder]
259 if not config:
260 continue
261
262 if isinstance(config, dict):
263 args = {k: self.FlattenConfig(v)['gn_args']
264 for k, v in config.items()}
265 elif config.startswith('//'):
266 args = config
267 else:
268 args = self.FlattenConfig(config)['gn_args']
269 if 'error' in args:
270 continue
271
272 obj[master][builder] = args
273
274 # Dump object and trim trailing whitespace.
275 s = '\n'.join(l.rstrip() for l in
276 json.dumps(obj, sort_keys=True, indent=2).splitlines())
277 self.Print(s)
278 return 0
279
280 def CmdGen(self):
281 vals = self.Lookup()
Oleh Prypinb708e932018-03-18 17:34:20 +0100282 return self.RunGNGen(vals)
kjellandera013a022016-11-14 05:54:22 -0800283
284 def CmdHelp(self):
285 if self.args.subcommand:
286 self.ParseArgs([self.args.subcommand, '--help'])
287 else:
288 self.ParseArgs(['--help'])
289
290 def CmdIsolate(self):
291 vals = self.GetConfig()
292 if not vals:
293 return 1
Oleh Prypinb708e932018-03-18 17:34:20 +0100294 return self.RunGNIsolate(vals)
kjellandera013a022016-11-14 05:54:22 -0800295
296 def CmdLookup(self):
297 vals = self.Lookup()
Oleh Prypinb708e932018-03-18 17:34:20 +0100298 gn_args = self.GNArgs(vals)
Oleh Prypind7e2fb32019-05-31 13:25:39 +0200299 if self.args.quiet:
300 self.Print(gn_args, end='')
301 else:
302 cmd = self.GNCmd('gen', '_path_')
303 self.Print('\nWriting """\\\n%s""" to _path_/args.gn.\n' % gn_args)
304 env = None
kjellandera013a022016-11-14 05:54:22 -0800305
Oleh Prypind7e2fb32019-05-31 13:25:39 +0200306 self.PrintCmd(cmd, env)
kjellandera013a022016-11-14 05:54:22 -0800307 return 0
308
309 def CmdRun(self):
310 vals = self.GetConfig()
311 if not vals:
312 return 1
313
314 build_dir = self.args.path[0]
315 target = self.args.target[0]
316
Oleh Prypinb708e932018-03-18 17:34:20 +0100317 if self.args.build:
318 ret = self.Build(target)
kjellandera013a022016-11-14 05:54:22 -0800319 if ret:
320 return ret
Oleh Prypinb708e932018-03-18 17:34:20 +0100321 ret = self.RunGNIsolate(vals)
322 if ret:
323 return ret
kjellandera013a022016-11-14 05:54:22 -0800324
Oleh Prypinb708e932018-03-18 17:34:20 +0100325 if self.args.swarmed:
326 return self._RunUnderSwarming(build_dir, target)
327 else:
328 return self._RunLocallyIsolated(build_dir, target)
329
330 def _RunUnderSwarming(self, build_dir, target):
331 # TODO(dpranke): Look up the information for the target in
332 # the //testing/buildbot.json file, if possible, so that we
333 # can determine the isolate target, command line, and additional
334 # swarming parameters, if possible.
335 #
336 # TODO(dpranke): Also, add support for sharding and merging results.
337 dimensions = []
338 for k, v in self.args.dimensions:
339 dimensions += ['-d', k, v]
340
Ye Kuange55f0c32020-03-19 16:44:08 +0900341 archive_json_path = self.ToSrcRelPath(
342 '%s/%s.archive.json' % (build_dir, target))
Oleh Prypinb708e932018-03-18 17:34:20 +0100343 cmd = [
Ye Kuangb28f0202020-03-16 10:56:20 +0900344 self.PathJoin(self.src_dir, 'tools', 'luci-go', self.isolate_exe),
Oleh Prypinb708e932018-03-18 17:34:20 +0100345 'archive',
Ye Kuange55f0c32020-03-19 16:44:08 +0900346 '-i',
347 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
Oleh Prypinb708e932018-03-18 17:34:20 +0100348 '-s',
349 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
350 '-I', 'isolateserver.appspot.com',
Ye Kuange55f0c32020-03-19 16:44:08 +0900351 '-dump-json', archive_json_path,
Oleh Prypinb708e932018-03-18 17:34:20 +0100352 ]
Ye Kuange55f0c32020-03-19 16:44:08 +0900353 ret, _, _ = self.Run(cmd, force_verbose=False)
Oleh Prypinb708e932018-03-18 17:34:20 +0100354 if ret:
355 return ret
356
Ye Kuange55f0c32020-03-19 16:44:08 +0900357 try:
358 archive_hashes = json.loads(self.ReadFile(archive_json_path))
359 except Exception:
360 self.Print(
361 'Failed to read JSON file "%s"' % archive_json_path, file=sys.stderr)
362 return 1
363 try:
364 isolated_hash = archive_hashes[target]
365 except Exception:
366 self.Print(
367 'Cannot find hash for "%s" in "%s", file content: %s' %
368 (target, archive_json_path, archive_hashes),
369 file=sys.stderr)
370 return 1
371
Oleh Prypinb708e932018-03-18 17:34:20 +0100372 cmd = [
373 self.executable,
374 self.PathJoin('tools', 'swarming_client', 'swarming.py'),
375 'run',
376 '-s', isolated_hash,
377 '-I', 'isolateserver.appspot.com',
378 '-S', 'chromium-swarm.appspot.com',
379 ] + dimensions
380 if self.args.extra_args:
381 cmd += ['--'] + self.args.extra_args
382 ret, _, _ = self.Run(cmd, force_verbose=True, buffer_output=False)
383 return ret
384
385 def _RunLocallyIsolated(self, build_dir, target):
kjellandera013a022016-11-14 05:54:22 -0800386 cmd = [
Ye Kuangb28f0202020-03-16 10:56:20 +0900387 self.PathJoin(self.src_dir, 'tools', 'luci-go', self.isolate_exe),
kjellandera013a022016-11-14 05:54:22 -0800388 'run',
Ye Kuangb28f0202020-03-16 10:56:20 +0900389 '-i',
390 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
Oleh Prypinb708e932018-03-18 17:34:20 +0100391 ]
kjellandera013a022016-11-14 05:54:22 -0800392 if self.args.extra_args:
Oleh Prypinb708e932018-03-18 17:34:20 +0100393 cmd += ['--'] + self.args.extra_args
394 ret, _, _ = self.Run(cmd, force_verbose=True, buffer_output=False)
kjellandera013a022016-11-14 05:54:22 -0800395 return ret
396
397 def CmdValidate(self, print_ok=True):
398 errs = []
399
400 # Read the file to make sure it parses.
401 self.ReadConfigFile()
402
403 # Build a list of all of the configs referenced by builders.
404 all_configs = {}
405 for master in self.masters:
406 for config in self.masters[master].values():
407 if isinstance(config, dict):
408 for c in config.values():
409 all_configs[c] = master
410 else:
411 all_configs[config] = master
412
413 # Check that every referenced args file or config actually exists.
414 for config, loc in all_configs.items():
415 if config.startswith('//'):
416 if not self.Exists(self.ToAbsPath(config)):
417 errs.append('Unknown args file "%s" referenced from "%s".' %
418 (config, loc))
419 elif not config in self.configs:
420 errs.append('Unknown config "%s" referenced from "%s".' %
421 (config, loc))
422
423 # Check that every actual config is actually referenced.
424 for config in self.configs:
425 if not config in all_configs:
426 errs.append('Unused config "%s".' % config)
427
428 # Figure out the whole list of mixins, and check that every mixin
429 # listed by a config or another mixin actually exists.
430 referenced_mixins = set()
431 for config, mixins in self.configs.items():
432 for mixin in mixins:
433 if not mixin in self.mixins:
434 errs.append('Unknown mixin "%s" referenced by config "%s".' %
435 (mixin, config))
436 referenced_mixins.add(mixin)
437
438 for mixin in self.mixins:
439 for sub_mixin in self.mixins[mixin].get('mixins', []):
440 if not sub_mixin in self.mixins:
441 errs.append('Unknown mixin "%s" referenced by mixin "%s".' %
442 (sub_mixin, mixin))
443 referenced_mixins.add(sub_mixin)
444
445 # Check that every mixin defined is actually referenced somewhere.
446 for mixin in self.mixins:
447 if not mixin in referenced_mixins:
448 errs.append('Unreferenced mixin "%s".' % mixin)
449
kjellandera013a022016-11-14 05:54:22 -0800450 if errs:
451 raise MBErr(('mb config file %s has problems:' % self.args.config_file) +
452 '\n ' + '\n '.join(errs))
453
454 if print_ok:
455 self.Print('mb config file %s looks ok.' % self.args.config_file)
456 return 0
457
kjellandera013a022016-11-14 05:54:22 -0800458 def GetConfig(self):
459 build_dir = self.args.path[0]
460
461 vals = self.DefaultVals()
462 if self.args.builder or self.args.master or self.args.config:
463 vals = self.Lookup()
Oleh Prypinb708e932018-03-18 17:34:20 +0100464 # Re-run gn gen in order to ensure the config is consistent with the
465 # build dir.
466 self.RunGNGen(vals)
kjellandera013a022016-11-14 05:54:22 -0800467 return vals
468
Oleh Prypinb708e932018-03-18 17:34:20 +0100469 toolchain_path = self.PathJoin(self.ToAbsPath(build_dir),
470 'toolchain.ninja')
471 if not self.Exists(toolchain_path):
472 self.Print('Must either specify a path to an existing GN build dir '
473 'or pass in a -m/-b pair or a -c flag to specify the '
474 'configuration')
475 return {}
kjellandera013a022016-11-14 05:54:22 -0800476
Oleh Prypinb708e932018-03-18 17:34:20 +0100477 vals['gn_args'] = self.GNArgsFromDir(build_dir)
kjellandera013a022016-11-14 05:54:22 -0800478 return vals
479
480 def GNArgsFromDir(self, build_dir):
481 args_contents = ""
482 gn_args_path = self.PathJoin(self.ToAbsPath(build_dir), 'args.gn')
483 if self.Exists(gn_args_path):
484 args_contents = self.ReadFile(gn_args_path)
485 gn_args = []
486 for l in args_contents.splitlines():
487 fields = l.split(' ')
488 name = fields[0]
489 val = ' '.join(fields[2:])
490 gn_args.append('%s=%s' % (name, val))
491
492 return ' '.join(gn_args)
493
494 def Lookup(self):
Oleh Prypin82ac2402019-01-29 16:18:30 +0100495 self.ReadConfigFile()
496 config = self.ConfigFromArgs()
497 if config.startswith('//'):
498 if not self.Exists(self.ToAbsPath(config)):
499 raise MBErr('args file "%s" not found' % config)
500 vals = self.DefaultVals()
501 vals['args_file'] = config
502 else:
503 if not config in self.configs:
504 raise MBErr('Config "%s" not found in %s' %
505 (config, self.args.config_file))
506 vals = self.FlattenConfig(config)
kjellandera013a022016-11-14 05:54:22 -0800507 return vals
508
509 def ReadConfigFile(self):
510 if not self.Exists(self.args.config_file):
511 raise MBErr('config file not found at %s' % self.args.config_file)
512
513 try:
514 contents = ast.literal_eval(self.ReadFile(self.args.config_file))
515 except SyntaxError as e:
516 raise MBErr('Failed to parse config file "%s": %s' %
517 (self.args.config_file, e))
518
519 self.configs = contents['configs']
520 self.masters = contents['masters']
521 self.mixins = contents['mixins']
522
523 def ReadIsolateMap(self):
Oleh Prypinb708e932018-03-18 17:34:20 +0100524 isolate_map = self.args.isolate_map_file
525 if not self.Exists(isolate_map):
526 raise MBErr('isolate map file not found at %s' % isolate_map)
kjellandera013a022016-11-14 05:54:22 -0800527 try:
Oleh Prypinb708e932018-03-18 17:34:20 +0100528 return ast.literal_eval(self.ReadFile(isolate_map))
kjellandera013a022016-11-14 05:54:22 -0800529 except SyntaxError as e:
Oleh Prypinb708e932018-03-18 17:34:20 +0100530 raise MBErr(
531 'Failed to parse isolate map file "%s": %s' % (isolate_map, e))
kjellandera013a022016-11-14 05:54:22 -0800532
533 def ConfigFromArgs(self):
534 if self.args.config:
535 if self.args.master or self.args.builder:
536 raise MBErr('Can not specific both -c/--config and -m/--master or '
537 '-b/--builder')
538
539 return self.args.config
540
541 if not self.args.master or not self.args.builder:
542 raise MBErr('Must specify either -c/--config or '
543 '(-m/--master and -b/--builder)')
544
545 if not self.args.master in self.masters:
546 raise MBErr('Master name "%s" not found in "%s"' %
547 (self.args.master, self.args.config_file))
548
549 if not self.args.builder in self.masters[self.args.master]:
550 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' %
551 (self.args.builder, self.args.master, self.args.config_file))
552
553 config = self.masters[self.args.master][self.args.builder]
554 if isinstance(config, dict):
555 if self.args.phase is None:
556 raise MBErr('Must specify a build --phase for %s on %s' %
557 (self.args.builder, self.args.master))
558 phase = str(self.args.phase)
559 if phase not in config:
560 raise MBErr('Phase %s doesn\'t exist for %s on %s' %
561 (phase, self.args.builder, self.args.master))
562 return config[phase]
563
564 if self.args.phase is not None:
565 raise MBErr('Must not specify a build --phase for %s on %s' %
566 (self.args.builder, self.args.master))
567 return config
568
569 def FlattenConfig(self, config):
570 mixins = self.configs[config]
571 vals = self.DefaultVals()
572
573 visited = []
574 self.FlattenMixins(mixins, vals, visited)
575 return vals
576
577 def DefaultVals(self):
578 return {
579 'args_file': '',
580 'cros_passthrough': False,
581 'gn_args': '',
kjellandera013a022016-11-14 05:54:22 -0800582 }
583
584 def FlattenMixins(self, mixins, vals, visited):
585 for m in mixins:
586 if m not in self.mixins:
587 raise MBErr('Unknown mixin "%s"' % m)
588
589 visited.append(m)
590
591 mixin_vals = self.mixins[m]
592
593 if 'cros_passthrough' in mixin_vals:
594 vals['cros_passthrough'] = mixin_vals['cros_passthrough']
595 if 'gn_args' in mixin_vals:
596 if vals['gn_args']:
597 vals['gn_args'] += ' ' + mixin_vals['gn_args']
598 else:
599 vals['gn_args'] = mixin_vals['gn_args']
kjellandera013a022016-11-14 05:54:22 -0800600
601 if 'mixins' in mixin_vals:
602 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
603 return vals
604
kjellandera013a022016-11-14 05:54:22 -0800605 def RunGNGen(self, vals):
606 build_dir = self.args.path[0]
607
608 cmd = self.GNCmd('gen', build_dir, '--check')
609 gn_args = self.GNArgs(vals)
610
611 # Since GN hasn't run yet, the build directory may not even exist.
612 self.MaybeMakeDirectory(self.ToAbsPath(build_dir))
613
614 gn_args_path = self.ToAbsPath(build_dir, 'args.gn')
615 self.WriteFile(gn_args_path, gn_args, force_verbose=True)
616
617 swarming_targets = []
618 if getattr(self.args, 'swarming_targets_file', None):
619 # We need GN to generate the list of runtime dependencies for
620 # the compile targets listed (one per line) in the file so
621 # we can run them via swarming. We use gn_isolate_map.pyl to convert
622 # the compile targets to the matching GN labels.
623 path = self.args.swarming_targets_file
624 if not self.Exists(path):
625 self.WriteFailureAndRaise('"%s" does not exist' % path,
626 output_path=None)
627 contents = self.ReadFile(path)
628 swarming_targets = set(contents.splitlines())
629
630 isolate_map = self.ReadIsolateMap()
631 err, labels = self.MapTargetsToLabels(isolate_map, swarming_targets)
632 if err:
633 raise MBErr(err)
634
635 gn_runtime_deps_path = self.ToAbsPath(build_dir, 'runtime_deps')
636 self.WriteFile(gn_runtime_deps_path, '\n'.join(labels) + '\n')
637 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path)
638
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700639 ret, output, _ = self.Run(cmd)
kjellandera013a022016-11-14 05:54:22 -0800640 if ret:
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700641 if self.args.json_output:
642 # write errors to json.output
643 self.WriteJSON({'output': output}, self.args.json_output)
kjellandera013a022016-11-14 05:54:22 -0800644 # If `gn gen` failed, we should exit early rather than trying to
645 # generate isolates. Run() will have already logged any error output.
646 self.Print('GN gen failed: %d' % ret)
647 return ret
648
649 android = 'target_os="android"' in vals['gn_args']
650 for target in swarming_targets:
651 if android:
652 # Android targets may be either android_apk or executable. The former
653 # will result in runtime_deps associated with the stamp file, while the
654 # latter will result in runtime_deps associated with the executable.
655 label = isolate_map[target]['label']
656 runtime_deps_targets = [
657 target + '.runtime_deps',
658 'obj/%s.stamp.runtime_deps' % label.replace(':', '/')]
659 elif isolate_map[target]['type'] == 'gpu_browser_test':
660 if self.platform == 'win32':
661 runtime_deps_targets = ['browser_tests.exe.runtime_deps']
662 else:
663 runtime_deps_targets = ['browser_tests.runtime_deps']
Edward Lemur20110752017-09-28 16:14:37 +0200664 elif isolate_map[target]['type'] == 'script':
665 label = isolate_map[target]['label'].split(':')[1]
kjellandera013a022016-11-14 05:54:22 -0800666 runtime_deps_targets = [
Edward Lemur20110752017-09-28 16:14:37 +0200667 '%s.runtime_deps' % label]
kjellandera013a022016-11-14 05:54:22 -0800668 if self.platform == 'win32':
Edward Lemur20110752017-09-28 16:14:37 +0200669 runtime_deps_targets += [ label + '.exe.runtime_deps' ]
kjellandera013a022016-11-14 05:54:22 -0800670 else:
Edward Lemur20110752017-09-28 16:14:37 +0200671 runtime_deps_targets += [ label + '.runtime_deps' ]
kjellandera013a022016-11-14 05:54:22 -0800672 elif self.platform == 'win32':
673 runtime_deps_targets = [target + '.exe.runtime_deps']
674 else:
675 runtime_deps_targets = [target + '.runtime_deps']
676
677 for r in runtime_deps_targets:
678 runtime_deps_path = self.ToAbsPath(build_dir, r)
679 if self.Exists(runtime_deps_path):
680 break
681 else:
682 raise MBErr('did not generate any of %s' %
683 ', '.join(runtime_deps_targets))
684
685 command, extra_files = self.GetIsolateCommand(target, vals)
686
687 runtime_deps = self.ReadFile(runtime_deps_path).splitlines()
688
689 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
690 extra_files)
691
692 return 0
693
694 def RunGNIsolate(self, vals):
695 target = self.args.target[0]
696 isolate_map = self.ReadIsolateMap()
697 err, labels = self.MapTargetsToLabels(isolate_map, [target])
698 if err:
699 raise MBErr(err)
700 label = labels[0]
701
702 build_dir = self.args.path[0]
703 command, extra_files = self.GetIsolateCommand(target, vals)
704
705 cmd = self.GNCmd('desc', build_dir, label, 'runtime_deps')
706 ret, out, _ = self.Call(cmd)
707 if ret:
708 if out:
709 self.Print(out)
710 return ret
711
712 runtime_deps = out.splitlines()
713
714 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
715 extra_files)
716
717 ret, _, _ = self.Run([
Ye Kuangb28f0202020-03-16 10:56:20 +0900718 self.PathJoin(self.src_dir, 'tools', 'luci-go', self.isolate_exe),
kjellandera013a022016-11-14 05:54:22 -0800719 'check',
720 '-i',
Ye Kuangb28f0202020-03-16 10:56:20 +0900721 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target))],
kjellandera013a022016-11-14 05:54:22 -0800722 buffer_output=False)
723
724 return ret
725
726 def WriteIsolateFiles(self, build_dir, command, target, runtime_deps,
727 extra_files):
728 isolate_path = self.ToAbsPath(build_dir, target + '.isolate')
729 self.WriteFile(isolate_path,
730 pprint.pformat({
731 'variables': {
732 'command': command,
733 'files': sorted(runtime_deps + extra_files),
734 }
735 }) + '\n')
736
737 self.WriteJSON(
738 {
739 'args': [
740 '--isolated',
741 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
742 '--isolate',
743 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
744 ],
kjellander1c3548c2017-02-15 22:38:22 -0800745 'dir': self.src_dir,
kjellandera013a022016-11-14 05:54:22 -0800746 'version': 1,
747 },
748 isolate_path + 'd.gen.json',
749 )
750
751 def MapTargetsToLabels(self, isolate_map, targets):
752 labels = []
753 err = ''
754
755 def StripTestSuffixes(target):
756 for suffix in ('_apk_run', '_apk', '_run'):
757 if target.endswith(suffix):
758 return target[:-len(suffix)], suffix
759 return None, None
760
761 for target in targets:
762 if target == 'all':
763 labels.append(target)
764 elif target.startswith('//'):
765 labels.append(target)
766 else:
767 if target in isolate_map:
768 stripped_target, suffix = target, ''
769 else:
770 stripped_target, suffix = StripTestSuffixes(target)
771 if stripped_target in isolate_map:
772 if isolate_map[stripped_target]['type'] == 'unknown':
773 err += ('test target "%s" type is unknown\n' % target)
774 else:
775 labels.append(isolate_map[stripped_target]['label'] + suffix)
776 else:
777 err += ('target "%s" not found in '
778 '//testing/buildbot/gn_isolate_map.pyl\n' % target)
779
780 return err, labels
781
782 def GNCmd(self, subcommand, path, *args):
Oleh Prypinb708e932018-03-18 17:34:20 +0100783 if self.platform.startswith('linux'):
kjellandera013a022016-11-14 05:54:22 -0800784 subdir, exe = 'linux64', 'gn'
785 elif self.platform == 'darwin':
786 subdir, exe = 'mac', 'gn'
787 else:
788 subdir, exe = 'win', 'gn.exe'
789
kjellander1c3548c2017-02-15 22:38:22 -0800790 gn_path = self.PathJoin(self.src_dir, 'buildtools', subdir, exe)
kjellandera013a022016-11-14 05:54:22 -0800791 return [gn_path, subcommand, path] + list(args)
792
793
794 def GNArgs(self, vals):
795 if vals['cros_passthrough']:
796 if not 'GN_ARGS' in os.environ:
797 raise MBErr('MB is expecting GN_ARGS to be in the environment')
798 gn_args = os.environ['GN_ARGS']
799 if not re.search('target_os.*=.*"chromeos"', gn_args):
800 raise MBErr('GN_ARGS is missing target_os = "chromeos": (GN_ARGS=%s)' %
801 gn_args)
802 else:
803 gn_args = vals['gn_args']
804
805 if self.args.goma_dir:
806 gn_args += ' goma_dir="%s"' % self.args.goma_dir
807
808 android_version_code = self.args.android_version_code
809 if android_version_code:
810 gn_args += ' android_default_version_code="%s"' % android_version_code
811
812 android_version_name = self.args.android_version_name
813 if android_version_name:
814 gn_args += ' android_default_version_name="%s"' % android_version_name
815
816 # Canonicalize the arg string into a sorted, newline-separated list
817 # of key-value pairs, and de-dup the keys if need be so that only
818 # the last instance of each arg is listed.
819 gn_args = gn_helpers.ToGNString(gn_helpers.FromGNArgs(gn_args))
820
821 args_file = vals.get('args_file', None)
822 if args_file:
823 gn_args = ('import("%s")\n' % vals['args_file']) + gn_args
824 return gn_args
825
kjellandera013a022016-11-14 05:54:22 -0800826 def GetIsolateCommand(self, target, vals):
kjellandera013a022016-11-14 05:54:22 -0800827 isolate_map = self.ReadIsolateMap()
828 test_type = isolate_map[target]['type']
829
Oleh Prypinb708e932018-03-18 17:34:20 +0100830 is_android = 'target_os="android"' in vals['gn_args']
831 is_linux = self.platform.startswith('linux') and not is_android
kjellandera013a022016-11-14 05:54:22 -0800832
833 if test_type == 'nontest':
834 self.WriteFailureAndRaise('We should not be isolating %s.' % target,
835 output_path=None)
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800836 if test_type not in ('console_test_launcher', 'windowed_test_launcher',
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100837 'non_parallel_console_test_launcher', 'raw',
Edward Lemur20110752017-09-28 16:14:37 +0200838 'additional_compile_target', 'junit_test', 'script'):
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800839 self.WriteFailureAndRaise('No command line for %s found (test type %s).'
840 % (target, test_type), output_path=None)
kjellandera013a022016-11-14 05:54:22 -0800841
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800842 cmdline = []
Oleh Prypinb708e932018-03-18 17:34:20 +0100843 extra_files = [
844 '../../.vpython',
845 '../../testing/test_env.py',
846 ]
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800847
Yves Gerey2e0c6552018-10-08 21:59:25 +0200848 must_retry = False
Edward Lemur98d40362018-01-15 17:37:04 +0100849 if test_type == 'script':
Oleh Prypin3a51b0e2019-07-17 15:17:53 +0200850 cmdline += ['../../' + self.ToSrcRelPath(isolate_map[target]['script'])]
Oleh Prypinb708e932018-03-18 17:34:20 +0100851 elif is_android:
Oleh Prypin3a51b0e2019-07-17 15:17:53 +0200852 cmdline += ['../../build/android/test_wrapper/logdog_wrapper.py',
853 '--target', target,
854 '--logdog-bin-cmd', '../../bin/logdog_butler',
855 '--logcat-output-file', '${ISOLATED_OUTDIR}/logcats',
856 '--store-tombstones']
kjellandera013a022016-11-14 05:54:22 -0800857 else:
Oleh Prypin3a51b0e2019-07-17 15:17:53 +0200858 if test_type == 'raw':
859 cmdline.append('../../tools_webrtc/flags_compatibility.py')
860 extra_files.append('../../tools_webrtc/flags_compatibility.py')
861
Oleh Prypin739b8162018-05-17 13:28:29 +0200862 if isolate_map[target].get('use_webcam', False):
863 cmdline.append('../../tools_webrtc/ensure_webcam_is_running.py')
864 extra_files.append('../../tools_webrtc/ensure_webcam_is_running.py')
kjellandera013a022016-11-14 05:54:22 -0800865
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800866 # This needs to mirror the settings in //build/config/ui.gni:
867 # use_x11 = is_linux && !use_ozone.
868 use_x11 = is_linux and not 'use_ozone=true' in vals['gn_args']
869
870 xvfb = use_x11 and test_type == 'windowed_test_launcher'
871 if xvfb:
Oleh Prypin739b8162018-05-17 13:28:29 +0200872 cmdline.append('../../testing/xvfb.py')
873 extra_files.append('../../testing/xvfb.py')
874 else:
875 cmdline.append('../../testing/test_env.py')
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800876
Mirko Bonadei264bee82018-08-07 08:53:41 +0200877 if test_type != 'raw':
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800878 extra_files += [
879 '../../third_party/gtest-parallel/gtest-parallel',
ehmaldonadoa7507eb2017-05-10 13:40:29 -0700880 '../../third_party/gtest-parallel/gtest_parallel.py',
Henrik Kjellander90fd7d82017-05-09 08:30:10 +0200881 '../../tools_webrtc/gtest-parallel-wrapper.py',
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800882 ]
ehmaldonado55833842017-02-13 03:58:13 -0800883 sep = '\\' if self.platform == 'win32' else '/'
884 output_dir = '${ISOLATED_OUTDIR}' + sep + 'test_logs'
Edward Lemurbeffdd42017-09-27 13:07:47 +0200885 timeout = isolate_map[target].get('timeout', 900)
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100886 cmdline += [
Henrik Kjellander90fd7d82017-05-09 08:30:10 +0200887 '../../tools_webrtc/gtest-parallel-wrapper.py',
ehmaldonado55833842017-02-13 03:58:13 -0800888 '--output_dir=%s' % output_dir,
ehmaldonado76e60e92017-05-04 06:18:26 -0700889 '--gtest_color=no',
890 # We tell gtest-parallel to interrupt the test after 900 seconds,
891 # so it can exit cleanly and report results, instead of being
892 # interrupted by swarming and not reporting anything.
Edward Lemurbeffdd42017-09-27 13:07:47 +0200893 '--timeout=%s' % timeout,
ehmaldonado55833842017-02-13 03:58:13 -0800894 ]
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100895 if test_type == 'non_parallel_console_test_launcher':
896 # Still use the gtest-parallel-wrapper.py script since we need it to
897 # run tests on swarming, but don't execute tests in parallel.
898 cmdline.append('--workers=1')
Yves Gerey2e0c6552018-10-08 21:59:25 +0200899 must_retry = True
900
901 asan = 'is_asan=true' in vals['gn_args']
902 lsan = 'is_lsan=true' in vals['gn_args']
903 msan = 'is_msan=true' in vals['gn_args']
904 tsan = 'is_tsan=true' in vals['gn_args']
905 sanitizer = asan or lsan or msan or tsan
906 if must_retry and not sanitizer:
907 # Retry would hide most sanitizers detections.
908 cmdline.append('--retry_failed=3')
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100909
910 executable_prefix = '.\\' if self.platform == 'win32' else './'
911 executable_suffix = '.exe' if self.platform == 'win32' else ''
912 executable = executable_prefix + target + executable_suffix
913
914 cmdline.append(executable)
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800915
kjellander382f2b22017-04-11 04:07:01 -0700916 cmdline.extend([
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800917 '--asan=%d' % asan,
kjellander461a5602017-05-05 06:39:16 -0700918 '--lsan=%d' % lsan,
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800919 '--msan=%d' % msan,
920 '--tsan=%d' % tsan,
kjellander382f2b22017-04-11 04:07:01 -0700921 ])
kjellandera013a022016-11-14 05:54:22 -0800922
kjellander74e81262017-03-23 00:51:11 -0700923 cmdline += isolate_map[target].get('args', [])
924
kjellandera013a022016-11-14 05:54:22 -0800925 return cmdline, extra_files
926
927 def ToAbsPath(self, build_path, *comps):
kjellander1c3548c2017-02-15 22:38:22 -0800928 return self.PathJoin(self.src_dir,
kjellandera013a022016-11-14 05:54:22 -0800929 self.ToSrcRelPath(build_path),
930 *comps)
931
932 def ToSrcRelPath(self, path):
933 """Returns a relative path from the top of the repo."""
934 if path.startswith('//'):
935 return path[2:].replace('/', self.sep)
kjellander1c3548c2017-02-15 22:38:22 -0800936 return self.RelPath(path, self.src_dir)
kjellandera013a022016-11-14 05:54:22 -0800937
kjellandera013a022016-11-14 05:54:22 -0800938 def RunGNAnalyze(self, vals):
939 # Analyze runs before 'gn gen' now, so we need to run gn gen
940 # in order to ensure that we have a build directory.
941 ret = self.RunGNGen(vals)
942 if ret:
943 return ret
944
945 build_path = self.args.path[0]
946 input_path = self.args.input_path[0]
947 gn_input_path = input_path + '.gn'
948 output_path = self.args.output_path[0]
949 gn_output_path = output_path + '.gn'
950
951 inp = self.ReadInputJSON(['files', 'test_targets',
952 'additional_compile_targets'])
953 if self.args.verbose:
954 self.Print()
955 self.Print('analyze input:')
956 self.PrintJSON(inp)
957 self.Print()
958
959
960 # This shouldn't normally happen, but could due to unusual race conditions,
961 # like a try job that gets scheduled before a patch lands but runs after
962 # the patch has landed.
963 if not inp['files']:
964 self.Print('Warning: No files modified in patch, bailing out early.')
965 self.WriteJSON({
966 'status': 'No dependency',
967 'compile_targets': [],
968 'test_targets': [],
969 }, output_path)
970 return 0
971
972 gn_inp = {}
973 gn_inp['files'] = ['//' + f for f in inp['files'] if not f.startswith('//')]
974
975 isolate_map = self.ReadIsolateMap()
976 err, gn_inp['additional_compile_targets'] = self.MapTargetsToLabels(
977 isolate_map, inp['additional_compile_targets'])
978 if err:
979 raise MBErr(err)
980
981 err, gn_inp['test_targets'] = self.MapTargetsToLabels(
982 isolate_map, inp['test_targets'])
983 if err:
984 raise MBErr(err)
985 labels_to_targets = {}
986 for i, label in enumerate(gn_inp['test_targets']):
987 labels_to_targets[label] = inp['test_targets'][i]
988
989 try:
990 self.WriteJSON(gn_inp, gn_input_path)
991 cmd = self.GNCmd('analyze', build_path, gn_input_path, gn_output_path)
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700992 ret, output, _ = self.Run(cmd, force_verbose=True)
kjellandera013a022016-11-14 05:54:22 -0800993 if ret:
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700994 if self.args.json_output:
995 # write errors to json.output
996 self.WriteJSON({'output': output}, self.args.json_output)
kjellandera013a022016-11-14 05:54:22 -0800997 return ret
998
999 gn_outp_str = self.ReadFile(gn_output_path)
1000 try:
1001 gn_outp = json.loads(gn_outp_str)
1002 except Exception as e:
1003 self.Print("Failed to parse the JSON string GN returned: %s\n%s"
1004 % (repr(gn_outp_str), str(e)))
1005 raise
1006
1007 outp = {}
1008 if 'status' in gn_outp:
1009 outp['status'] = gn_outp['status']
1010 if 'error' in gn_outp:
1011 outp['error'] = gn_outp['error']
1012 if 'invalid_targets' in gn_outp:
1013 outp['invalid_targets'] = gn_outp['invalid_targets']
1014 if 'compile_targets' in gn_outp:
1015 if 'all' in gn_outp['compile_targets']:
1016 outp['compile_targets'] = ['all']
1017 else:
1018 outp['compile_targets'] = [
1019 label.replace('//', '') for label in gn_outp['compile_targets']]
1020 if 'test_targets' in gn_outp:
1021 outp['test_targets'] = [
1022 labels_to_targets[label] for label in gn_outp['test_targets']]
1023
1024 if self.args.verbose:
1025 self.Print()
1026 self.Print('analyze output:')
1027 self.PrintJSON(outp)
1028 self.Print()
1029
1030 self.WriteJSON(outp, output_path)
1031
1032 finally:
1033 if self.Exists(gn_input_path):
1034 self.RemoveFile(gn_input_path)
1035 if self.Exists(gn_output_path):
1036 self.RemoveFile(gn_output_path)
1037
1038 return 0
1039
1040 def ReadInputJSON(self, required_keys):
1041 path = self.args.input_path[0]
1042 output_path = self.args.output_path[0]
1043 if not self.Exists(path):
1044 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
1045
1046 try:
1047 inp = json.loads(self.ReadFile(path))
1048 except Exception as e:
1049 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
1050 (path, e), output_path)
1051
1052 for k in required_keys:
1053 if not k in inp:
1054 self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
1055 output_path)
1056
1057 return inp
1058
1059 def WriteFailureAndRaise(self, msg, output_path):
1060 if output_path:
1061 self.WriteJSON({'error': msg}, output_path, force_verbose=True)
1062 raise MBErr(msg)
1063
1064 def WriteJSON(self, obj, path, force_verbose=False):
1065 try:
1066 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n',
1067 force_verbose=force_verbose)
1068 except Exception as e:
1069 raise MBErr('Error %s writing to the output path "%s"' %
1070 (e, path))
1071
1072 def CheckCompile(self, master, builder):
1073 url_template = self.args.url_template + '/{builder}/builds/_all?as_text=1'
1074 url = urllib2.quote(url_template.format(master=master, builder=builder),
1075 safe=':/()?=')
1076 try:
1077 builds = json.loads(self.Fetch(url))
1078 except Exception as e:
1079 return str(e)
1080 successes = sorted(
1081 [int(x) for x in builds.keys() if "text" in builds[x] and
1082 cmp(builds[x]["text"][:2], ["build", "successful"]) == 0],
1083 reverse=True)
1084 if not successes:
1085 return "no successful builds"
1086 build = builds[str(successes[0])]
1087 step_names = set([step["name"] for step in build["steps"]])
1088 compile_indicators = set(["compile", "compile (with patch)", "analyze"])
1089 if compile_indicators & step_names:
1090 return "compiles"
1091 return "does not compile"
1092
1093 def PrintCmd(self, cmd, env):
1094 if self.platform == 'win32':
1095 env_prefix = 'set '
1096 env_quoter = QuoteForSet
1097 shell_quoter = QuoteForCmd
1098 else:
1099 env_prefix = ''
1100 env_quoter = pipes.quote
1101 shell_quoter = pipes.quote
1102
1103 def print_env(var):
1104 if env and var in env:
1105 self.Print('%s%s=%s' % (env_prefix, var, env_quoter(env[var])))
1106
kjellandera013a022016-11-14 05:54:22 -08001107 print_env('LLVM_FORCE_HEAD_REVISION')
1108
1109 if cmd[0] == self.executable:
1110 cmd = ['python'] + cmd[1:]
1111 self.Print(*[shell_quoter(arg) for arg in cmd])
1112
1113 def PrintJSON(self, obj):
1114 self.Print(json.dumps(obj, indent=2, sort_keys=True))
1115
1116 def Build(self, target):
1117 build_dir = self.ToSrcRelPath(self.args.path[0])
Oleh Prypinb708e932018-03-18 17:34:20 +01001118 ninja_cmd = ['ninja', '-C', build_dir]
kjellandera013a022016-11-14 05:54:22 -08001119 if self.args.jobs:
1120 ninja_cmd.extend(['-j', '%d' % self.args.jobs])
1121 ninja_cmd.append(target)
1122 ret, _, _ = self.Run(ninja_cmd, force_verbose=False, buffer_output=False)
1123 return ret
1124
1125 def Run(self, cmd, env=None, force_verbose=True, buffer_output=True):
1126 # This function largely exists so it can be overridden for testing.
1127 if self.args.dryrun or self.args.verbose or force_verbose:
1128 self.PrintCmd(cmd, env)
1129 if self.args.dryrun:
1130 return 0, '', ''
1131
1132 ret, out, err = self.Call(cmd, env=env, buffer_output=buffer_output)
1133 if self.args.verbose or force_verbose:
1134 if ret:
1135 self.Print(' -> returned %d' % ret)
1136 if out:
1137 self.Print(out, end='')
1138 if err:
1139 self.Print(err, end='', file=sys.stderr)
1140 return ret, out, err
1141
1142 def Call(self, cmd, env=None, buffer_output=True):
1143 if buffer_output:
kjellander1c3548c2017-02-15 22:38:22 -08001144 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001145 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1146 env=env)
1147 out, err = p.communicate()
1148 else:
kjellander1c3548c2017-02-15 22:38:22 -08001149 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001150 env=env)
1151 p.wait()
1152 out = err = ''
1153 return p.returncode, out, err
1154
1155 def ExpandUser(self, path):
1156 # This function largely exists so it can be overridden for testing.
1157 return os.path.expanduser(path)
1158
1159 def Exists(self, path):
1160 # This function largely exists so it can be overridden for testing.
1161 return os.path.exists(path)
1162
1163 def Fetch(self, url):
1164 # This function largely exists so it can be overridden for testing.
1165 f = urllib2.urlopen(url)
1166 contents = f.read()
1167 f.close()
1168 return contents
1169
1170 def MaybeMakeDirectory(self, path):
1171 try:
1172 os.makedirs(path)
1173 except OSError, e:
1174 if e.errno != errno.EEXIST:
1175 raise
1176
1177 def PathJoin(self, *comps):
1178 # This function largely exists so it can be overriden for testing.
1179 return os.path.join(*comps)
1180
1181 def Print(self, *args, **kwargs):
1182 # This function largely exists so it can be overridden for testing.
1183 print(*args, **kwargs)
1184 if kwargs.get('stream', sys.stdout) == sys.stdout:
1185 sys.stdout.flush()
1186
1187 def ReadFile(self, path):
1188 # This function largely exists so it can be overriden for testing.
1189 with open(path) as fp:
1190 return fp.read()
1191
1192 def RelPath(self, path, start='.'):
1193 # This function largely exists so it can be overriden for testing.
1194 return os.path.relpath(path, start)
1195
1196 def RemoveFile(self, path):
1197 # This function largely exists so it can be overriden for testing.
1198 os.remove(path)
1199
1200 def RemoveDirectory(self, abs_path):
1201 if self.platform == 'win32':
1202 # In other places in chromium, we often have to retry this command
1203 # because we're worried about other processes still holding on to
1204 # file handles, but when MB is invoked, it will be early enough in the
1205 # build that their should be no other processes to interfere. We
1206 # can change this if need be.
1207 self.Run(['cmd.exe', '/c', 'rmdir', '/q', '/s', abs_path])
1208 else:
1209 shutil.rmtree(abs_path, ignore_errors=True)
1210
1211 def TempFile(self, mode='w'):
1212 # This function largely exists so it can be overriden for testing.
1213 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
1214
1215 def WriteFile(self, path, contents, force_verbose=False):
1216 # This function largely exists so it can be overriden for testing.
1217 if self.args.dryrun or self.args.verbose or force_verbose:
1218 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
1219 with open(path, 'w') as fp:
1220 return fp.write(contents)
1221
1222
1223class MBErr(Exception):
1224 pass
1225
1226
1227# See http://goo.gl/l5NPDW and http://goo.gl/4Diozm for the painful
1228# details of this next section, which handles escaping command lines
1229# so that they can be copied and pasted into a cmd window.
1230UNSAFE_FOR_SET = set('^<>&|')
1231UNSAFE_FOR_CMD = UNSAFE_FOR_SET.union(set('()%'))
1232ALL_META_CHARS = UNSAFE_FOR_CMD.union(set('"'))
1233
1234
1235def QuoteForSet(arg):
1236 if any(a in UNSAFE_FOR_SET for a in arg):
1237 arg = ''.join('^' + a if a in UNSAFE_FOR_SET else a for a in arg)
1238 return arg
1239
1240
1241def QuoteForCmd(arg):
1242 # First, escape the arg so that CommandLineToArgvW will parse it properly.
kjellandera013a022016-11-14 05:54:22 -08001243 if arg == '' or ' ' in arg or '"' in arg:
1244 quote_re = re.compile(r'(\\*)"')
1245 arg = '"%s"' % (quote_re.sub(lambda mo: 2 * mo.group(1) + '\\"', arg))
1246
1247 # Then check to see if the arg contains any metacharacters other than
1248 # double quotes; if it does, quote everything (including the double
1249 # quotes) for safety.
1250 if any(a in UNSAFE_FOR_CMD for a in arg):
1251 arg = ''.join('^' + a if a in ALL_META_CHARS else a for a in arg)
1252 return arg
1253
1254
1255if __name__ == '__main__':
1256 sys.exit(main(sys.argv[1:]))