blob: 913409500a9ac1d33e03dbacf0388e4e46d57ebd [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 = {}
Mirko Bonadei8606b9c2021-01-12 14:29:40 +010057 self.builder_groups = {}
kjellandera013a022016-11-14 05:54:22 -080058 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')
Mirko Bonadei8606b9c2021-01-12 14:29:40 +010083 subp.add_argument('-m', '--builder-group',
Mirko Bonadei8606b9c2021-01-12 14:29:40 +010084 help='builder group name to look up config from')
kjellandera013a022016-11-14 05:54:22 -080085 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 = {}
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100255 for builder_group, builders in self.builder_groups.items():
256 obj[builder_group] = {}
kjellandera013a022016-11-14 05:54:22 -0800257 for builder in builders:
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100258 config = self.builder_groups[builder_group][builder]
kjellandera013a022016-11-14 05:54:22 -0800259 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
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100272 obj[builder_group][builder] = args
kjellandera013a022016-11-14 05:54:22 -0800273
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 = {}
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100405 for builder_group in self.builder_groups:
406 for config in self.builder_groups[builder_group].values():
kjellandera013a022016-11-14 05:54:22 -0800407 if isinstance(config, dict):
408 for c in config.values():
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100409 all_configs[c] = builder_group
kjellandera013a022016-11-14 05:54:22 -0800410 else:
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100411 all_configs[config] = builder_group
kjellandera013a022016-11-14 05:54:22 -0800412
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()
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100462 if self.args.builder or self.args.builder_group or self.args.config:
kjellandera013a022016-11-14 05:54:22 -0800463 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']
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100520 self.builder_groups = contents['builder_groups']
kjellandera013a022016-11-14 05:54:22 -0800521 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:
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100535 if self.args.builder_group or self.args.builder:
536 raise MBErr('Can not specific both -c/--config and -m/--builder-group '
537 'or -b/--builder')
kjellandera013a022016-11-14 05:54:22 -0800538
539 return self.args.config
540
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100541 if not self.args.builder_group or not self.args.builder:
kjellandera013a022016-11-14 05:54:22 -0800542 raise MBErr('Must specify either -c/--config or '
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100543 '(-m/--builder-group and -b/--builder)')
kjellandera013a022016-11-14 05:54:22 -0800544
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100545 if not self.args.builder_group in self.builder_groups:
kjellandera013a022016-11-14 05:54:22 -0800546 raise MBErr('Master name "%s" not found in "%s"' %
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100547 (self.args.builder_group, self.args.config_file))
kjellandera013a022016-11-14 05:54:22 -0800548
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100549 if not self.args.builder in self.builder_groups[self.args.builder_group]:
550 raise MBErr(
551 'Builder name "%s" not found under builder_groups[%s] in "%s"' %
552 (self.args.builder, self.args.builder_group, self.args.config_file))
kjellandera013a022016-11-14 05:54:22 -0800553
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100554 config = self.builder_groups[self.args.builder_group][self.args.builder]
kjellandera013a022016-11-14 05:54:22 -0800555 if isinstance(config, dict):
556 if self.args.phase is None:
557 raise MBErr('Must specify a build --phase for %s on %s' %
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100558 (self.args.builder, self.args.builder_group))
kjellandera013a022016-11-14 05:54:22 -0800559 phase = str(self.args.phase)
560 if phase not in config:
561 raise MBErr('Phase %s doesn\'t exist for %s on %s' %
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100562 (phase, self.args.builder, self.args.builder_group))
kjellandera013a022016-11-14 05:54:22 -0800563 return config[phase]
564
565 if self.args.phase is not None:
566 raise MBErr('Must not specify a build --phase for %s on %s' %
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100567 (self.args.builder, self.args.builder_group))
kjellandera013a022016-11-14 05:54:22 -0800568 return config
569
570 def FlattenConfig(self, config):
571 mixins = self.configs[config]
572 vals = self.DefaultVals()
573
574 visited = []
575 self.FlattenMixins(mixins, vals, visited)
576 return vals
577
578 def DefaultVals(self):
579 return {
580 'args_file': '',
581 'cros_passthrough': False,
582 'gn_args': '',
kjellandera013a022016-11-14 05:54:22 -0800583 }
584
585 def FlattenMixins(self, mixins, vals, visited):
586 for m in mixins:
587 if m not in self.mixins:
588 raise MBErr('Unknown mixin "%s"' % m)
589
590 visited.append(m)
591
592 mixin_vals = self.mixins[m]
593
594 if 'cros_passthrough' in mixin_vals:
595 vals['cros_passthrough'] = mixin_vals['cros_passthrough']
596 if 'gn_args' in mixin_vals:
597 if vals['gn_args']:
598 vals['gn_args'] += ' ' + mixin_vals['gn_args']
599 else:
600 vals['gn_args'] = mixin_vals['gn_args']
kjellandera013a022016-11-14 05:54:22 -0800601
602 if 'mixins' in mixin_vals:
603 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
604 return vals
605
kjellandera013a022016-11-14 05:54:22 -0800606 def RunGNGen(self, vals):
607 build_dir = self.args.path[0]
608
609 cmd = self.GNCmd('gen', build_dir, '--check')
610 gn_args = self.GNArgs(vals)
611
612 # Since GN hasn't run yet, the build directory may not even exist.
613 self.MaybeMakeDirectory(self.ToAbsPath(build_dir))
614
615 gn_args_path = self.ToAbsPath(build_dir, 'args.gn')
616 self.WriteFile(gn_args_path, gn_args, force_verbose=True)
617
618 swarming_targets = []
619 if getattr(self.args, 'swarming_targets_file', None):
620 # We need GN to generate the list of runtime dependencies for
621 # the compile targets listed (one per line) in the file so
622 # we can run them via swarming. We use gn_isolate_map.pyl to convert
623 # the compile targets to the matching GN labels.
624 path = self.args.swarming_targets_file
625 if not self.Exists(path):
626 self.WriteFailureAndRaise('"%s" does not exist' % path,
627 output_path=None)
628 contents = self.ReadFile(path)
629 swarming_targets = set(contents.splitlines())
630
631 isolate_map = self.ReadIsolateMap()
632 err, labels = self.MapTargetsToLabels(isolate_map, swarming_targets)
633 if err:
634 raise MBErr(err)
635
636 gn_runtime_deps_path = self.ToAbsPath(build_dir, 'runtime_deps')
637 self.WriteFile(gn_runtime_deps_path, '\n'.join(labels) + '\n')
638 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path)
639
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700640 ret, output, _ = self.Run(cmd)
kjellandera013a022016-11-14 05:54:22 -0800641 if ret:
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700642 if self.args.json_output:
643 # write errors to json.output
644 self.WriteJSON({'output': output}, self.args.json_output)
kjellandera013a022016-11-14 05:54:22 -0800645 # If `gn gen` failed, we should exit early rather than trying to
646 # generate isolates. Run() will have already logged any error output.
647 self.Print('GN gen failed: %d' % ret)
648 return ret
649
650 android = 'target_os="android"' in vals['gn_args']
651 for target in swarming_targets:
652 if android:
653 # Android targets may be either android_apk or executable. The former
654 # will result in runtime_deps associated with the stamp file, while the
655 # latter will result in runtime_deps associated with the executable.
656 label = isolate_map[target]['label']
657 runtime_deps_targets = [
658 target + '.runtime_deps',
659 'obj/%s.stamp.runtime_deps' % label.replace(':', '/')]
660 elif isolate_map[target]['type'] == 'gpu_browser_test':
661 if self.platform == 'win32':
662 runtime_deps_targets = ['browser_tests.exe.runtime_deps']
663 else:
664 runtime_deps_targets = ['browser_tests.runtime_deps']
Edward Lemur20110752017-09-28 16:14:37 +0200665 elif isolate_map[target]['type'] == 'script':
666 label = isolate_map[target]['label'].split(':')[1]
kjellandera013a022016-11-14 05:54:22 -0800667 runtime_deps_targets = [
Edward Lemur20110752017-09-28 16:14:37 +0200668 '%s.runtime_deps' % label]
kjellandera013a022016-11-14 05:54:22 -0800669 if self.platform == 'win32':
Edward Lemur20110752017-09-28 16:14:37 +0200670 runtime_deps_targets += [ label + '.exe.runtime_deps' ]
kjellandera013a022016-11-14 05:54:22 -0800671 else:
Edward Lemur20110752017-09-28 16:14:37 +0200672 runtime_deps_targets += [ label + '.runtime_deps' ]
kjellandera013a022016-11-14 05:54:22 -0800673 elif self.platform == 'win32':
674 runtime_deps_targets = [target + '.exe.runtime_deps']
675 else:
676 runtime_deps_targets = [target + '.runtime_deps']
677
678 for r in runtime_deps_targets:
679 runtime_deps_path = self.ToAbsPath(build_dir, r)
680 if self.Exists(runtime_deps_path):
681 break
682 else:
683 raise MBErr('did not generate any of %s' %
684 ', '.join(runtime_deps_targets))
685
686 command, extra_files = self.GetIsolateCommand(target, vals)
687
688 runtime_deps = self.ReadFile(runtime_deps_path).splitlines()
689
690 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
691 extra_files)
692
693 return 0
694
695 def RunGNIsolate(self, vals):
696 target = self.args.target[0]
697 isolate_map = self.ReadIsolateMap()
698 err, labels = self.MapTargetsToLabels(isolate_map, [target])
699 if err:
700 raise MBErr(err)
701 label = labels[0]
702
703 build_dir = self.args.path[0]
704 command, extra_files = self.GetIsolateCommand(target, vals)
705
706 cmd = self.GNCmd('desc', build_dir, label, 'runtime_deps')
707 ret, out, _ = self.Call(cmd)
708 if ret:
709 if out:
710 self.Print(out)
711 return ret
712
713 runtime_deps = out.splitlines()
714
715 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
716 extra_files)
717
718 ret, _, _ = self.Run([
Ye Kuangb28f0202020-03-16 10:56:20 +0900719 self.PathJoin(self.src_dir, 'tools', 'luci-go', self.isolate_exe),
kjellandera013a022016-11-14 05:54:22 -0800720 'check',
721 '-i',
Ye Kuangb28f0202020-03-16 10:56:20 +0900722 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target))],
kjellandera013a022016-11-14 05:54:22 -0800723 buffer_output=False)
724
725 return ret
726
727 def WriteIsolateFiles(self, build_dir, command, target, runtime_deps,
728 extra_files):
729 isolate_path = self.ToAbsPath(build_dir, target + '.isolate')
730 self.WriteFile(isolate_path,
731 pprint.pformat({
732 'variables': {
733 'command': command,
734 'files': sorted(runtime_deps + extra_files),
735 }
736 }) + '\n')
737
738 self.WriteJSON(
739 {
740 'args': [
741 '--isolated',
742 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
743 '--isolate',
744 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
745 ],
kjellander1c3548c2017-02-15 22:38:22 -0800746 'dir': self.src_dir,
kjellandera013a022016-11-14 05:54:22 -0800747 'version': 1,
748 },
749 isolate_path + 'd.gen.json',
750 )
751
752 def MapTargetsToLabels(self, isolate_map, targets):
753 labels = []
754 err = ''
755
756 def StripTestSuffixes(target):
757 for suffix in ('_apk_run', '_apk', '_run'):
758 if target.endswith(suffix):
759 return target[:-len(suffix)], suffix
760 return None, None
761
762 for target in targets:
763 if target == 'all':
764 labels.append(target)
765 elif target.startswith('//'):
766 labels.append(target)
767 else:
768 if target in isolate_map:
769 stripped_target, suffix = target, ''
770 else:
771 stripped_target, suffix = StripTestSuffixes(target)
772 if stripped_target in isolate_map:
773 if isolate_map[stripped_target]['type'] == 'unknown':
774 err += ('test target "%s" type is unknown\n' % target)
775 else:
776 labels.append(isolate_map[stripped_target]['label'] + suffix)
777 else:
778 err += ('target "%s" not found in '
779 '//testing/buildbot/gn_isolate_map.pyl\n' % target)
780
781 return err, labels
782
783 def GNCmd(self, subcommand, path, *args):
Oleh Prypinb708e932018-03-18 17:34:20 +0100784 if self.platform.startswith('linux'):
kjellandera013a022016-11-14 05:54:22 -0800785 subdir, exe = 'linux64', 'gn'
786 elif self.platform == 'darwin':
787 subdir, exe = 'mac', 'gn'
788 else:
789 subdir, exe = 'win', 'gn.exe'
790
kjellander1c3548c2017-02-15 22:38:22 -0800791 gn_path = self.PathJoin(self.src_dir, 'buildtools', subdir, exe)
kjellandera013a022016-11-14 05:54:22 -0800792 return [gn_path, subcommand, path] + list(args)
793
794
795 def GNArgs(self, vals):
796 if vals['cros_passthrough']:
797 if not 'GN_ARGS' in os.environ:
798 raise MBErr('MB is expecting GN_ARGS to be in the environment')
799 gn_args = os.environ['GN_ARGS']
800 if not re.search('target_os.*=.*"chromeos"', gn_args):
801 raise MBErr('GN_ARGS is missing target_os = "chromeos": (GN_ARGS=%s)' %
802 gn_args)
803 else:
804 gn_args = vals['gn_args']
805
806 if self.args.goma_dir:
807 gn_args += ' goma_dir="%s"' % self.args.goma_dir
808
809 android_version_code = self.args.android_version_code
810 if android_version_code:
811 gn_args += ' android_default_version_code="%s"' % android_version_code
812
813 android_version_name = self.args.android_version_name
814 if android_version_name:
815 gn_args += ' android_default_version_name="%s"' % android_version_name
816
817 # Canonicalize the arg string into a sorted, newline-separated list
818 # of key-value pairs, and de-dup the keys if need be so that only
819 # the last instance of each arg is listed.
820 gn_args = gn_helpers.ToGNString(gn_helpers.FromGNArgs(gn_args))
821
822 args_file = vals.get('args_file', None)
823 if args_file:
824 gn_args = ('import("%s")\n' % vals['args_file']) + gn_args
825 return gn_args
826
kjellandera013a022016-11-14 05:54:22 -0800827 def GetIsolateCommand(self, target, vals):
kjellandera013a022016-11-14 05:54:22 -0800828 isolate_map = self.ReadIsolateMap()
829 test_type = isolate_map[target]['type']
830
Oleh Prypinb708e932018-03-18 17:34:20 +0100831 is_android = 'target_os="android"' in vals['gn_args']
832 is_linux = self.platform.startswith('linux') and not is_android
kjellandera013a022016-11-14 05:54:22 -0800833
834 if test_type == 'nontest':
835 self.WriteFailureAndRaise('We should not be isolating %s.' % target,
836 output_path=None)
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800837 if test_type not in ('console_test_launcher', 'windowed_test_launcher',
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100838 'non_parallel_console_test_launcher', 'raw',
Edward Lemur20110752017-09-28 16:14:37 +0200839 'additional_compile_target', 'junit_test', 'script'):
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800840 self.WriteFailureAndRaise('No command line for %s found (test type %s).'
841 % (target, test_type), output_path=None)
kjellandera013a022016-11-14 05:54:22 -0800842
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800843 cmdline = []
Oleh Prypinb708e932018-03-18 17:34:20 +0100844 extra_files = [
845 '../../.vpython',
846 '../../testing/test_env.py',
847 ]
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800848
Yves Gerey2e0c6552018-10-08 21:59:25 +0200849 must_retry = False
Edward Lemur98d40362018-01-15 17:37:04 +0100850 if test_type == 'script':
Oleh Prypin3a51b0e2019-07-17 15:17:53 +0200851 cmdline += ['../../' + self.ToSrcRelPath(isolate_map[target]['script'])]
Oleh Prypinb708e932018-03-18 17:34:20 +0100852 elif is_android:
Oleh Prypin3a51b0e2019-07-17 15:17:53 +0200853 cmdline += ['../../build/android/test_wrapper/logdog_wrapper.py',
854 '--target', target,
855 '--logdog-bin-cmd', '../../bin/logdog_butler',
856 '--logcat-output-file', '${ISOLATED_OUTDIR}/logcats',
857 '--store-tombstones']
kjellandera013a022016-11-14 05:54:22 -0800858 else:
Patrik Höglund28b8a0b2020-03-26 20:30:50 +0100859 if test_type == 'raw':
860 cmdline.append('../../tools_webrtc/flags_compatibility.py')
861 extra_files.append('../../tools_webrtc/flags_compatibility.py')
862
Oleh Prypin739b8162018-05-17 13:28:29 +0200863 if isolate_map[target].get('use_webcam', False):
864 cmdline.append('../../tools_webrtc/ensure_webcam_is_running.py')
865 extra_files.append('../../tools_webrtc/ensure_webcam_is_running.py')
kjellandera013a022016-11-14 05:54:22 -0800866
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800867 # This needs to mirror the settings in //build/config/ui.gni:
868 # use_x11 = is_linux && !use_ozone.
869 use_x11 = is_linux and not 'use_ozone=true' in vals['gn_args']
870
871 xvfb = use_x11 and test_type == 'windowed_test_launcher'
872 if xvfb:
Oleh Prypin739b8162018-05-17 13:28:29 +0200873 cmdline.append('../../testing/xvfb.py')
874 extra_files.append('../../testing/xvfb.py')
875 else:
876 cmdline.append('../../testing/test_env.py')
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800877
Mirko Bonadei264bee82018-08-07 08:53:41 +0200878 if test_type != 'raw':
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800879 extra_files += [
880 '../../third_party/gtest-parallel/gtest-parallel',
ehmaldonadoa7507eb2017-05-10 13:40:29 -0700881 '../../third_party/gtest-parallel/gtest_parallel.py',
Henrik Kjellander90fd7d82017-05-09 08:30:10 +0200882 '../../tools_webrtc/gtest-parallel-wrapper.py',
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800883 ]
ehmaldonado55833842017-02-13 03:58:13 -0800884 sep = '\\' if self.platform == 'win32' else '/'
885 output_dir = '${ISOLATED_OUTDIR}' + sep + 'test_logs'
Edward Lemurbeffdd42017-09-27 13:07:47 +0200886 timeout = isolate_map[target].get('timeout', 900)
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100887 cmdline += [
Henrik Kjellander90fd7d82017-05-09 08:30:10 +0200888 '../../tools_webrtc/gtest-parallel-wrapper.py',
ehmaldonado55833842017-02-13 03:58:13 -0800889 '--output_dir=%s' % output_dir,
ehmaldonado76e60e92017-05-04 06:18:26 -0700890 '--gtest_color=no',
891 # We tell gtest-parallel to interrupt the test after 900 seconds,
892 # so it can exit cleanly and report results, instead of being
893 # interrupted by swarming and not reporting anything.
Edward Lemurbeffdd42017-09-27 13:07:47 +0200894 '--timeout=%s' % timeout,
ehmaldonado55833842017-02-13 03:58:13 -0800895 ]
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100896 if test_type == 'non_parallel_console_test_launcher':
897 # Still use the gtest-parallel-wrapper.py script since we need it to
898 # run tests on swarming, but don't execute tests in parallel.
899 cmdline.append('--workers=1')
Yves Gerey2e0c6552018-10-08 21:59:25 +0200900 must_retry = True
901
902 asan = 'is_asan=true' in vals['gn_args']
903 lsan = 'is_lsan=true' in vals['gn_args']
904 msan = 'is_msan=true' in vals['gn_args']
905 tsan = 'is_tsan=true' in vals['gn_args']
906 sanitizer = asan or lsan or msan or tsan
907 if must_retry and not sanitizer:
908 # Retry would hide most sanitizers detections.
909 cmdline.append('--retry_failed=3')
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100910
911 executable_prefix = '.\\' if self.platform == 'win32' else './'
912 executable_suffix = '.exe' if self.platform == 'win32' else ''
913 executable = executable_prefix + target + executable_suffix
914
915 cmdline.append(executable)
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800916
kjellander382f2b22017-04-11 04:07:01 -0700917 cmdline.extend([
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800918 '--asan=%d' % asan,
kjellander461a5602017-05-05 06:39:16 -0700919 '--lsan=%d' % lsan,
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800920 '--msan=%d' % msan,
921 '--tsan=%d' % tsan,
kjellander382f2b22017-04-11 04:07:01 -0700922 ])
kjellandera013a022016-11-14 05:54:22 -0800923
kjellander74e81262017-03-23 00:51:11 -0700924 cmdline += isolate_map[target].get('args', [])
925
kjellandera013a022016-11-14 05:54:22 -0800926 return cmdline, extra_files
927
928 def ToAbsPath(self, build_path, *comps):
kjellander1c3548c2017-02-15 22:38:22 -0800929 return self.PathJoin(self.src_dir,
kjellandera013a022016-11-14 05:54:22 -0800930 self.ToSrcRelPath(build_path),
931 *comps)
932
933 def ToSrcRelPath(self, path):
934 """Returns a relative path from the top of the repo."""
935 if path.startswith('//'):
936 return path[2:].replace('/', self.sep)
kjellander1c3548c2017-02-15 22:38:22 -0800937 return self.RelPath(path, self.src_dir)
kjellandera013a022016-11-14 05:54:22 -0800938
kjellandera013a022016-11-14 05:54:22 -0800939 def RunGNAnalyze(self, vals):
940 # Analyze runs before 'gn gen' now, so we need to run gn gen
941 # in order to ensure that we have a build directory.
942 ret = self.RunGNGen(vals)
943 if ret:
944 return ret
945
946 build_path = self.args.path[0]
947 input_path = self.args.input_path[0]
948 gn_input_path = input_path + '.gn'
949 output_path = self.args.output_path[0]
950 gn_output_path = output_path + '.gn'
951
952 inp = self.ReadInputJSON(['files', 'test_targets',
953 'additional_compile_targets'])
954 if self.args.verbose:
955 self.Print()
956 self.Print('analyze input:')
957 self.PrintJSON(inp)
958 self.Print()
959
960
961 # This shouldn't normally happen, but could due to unusual race conditions,
962 # like a try job that gets scheduled before a patch lands but runs after
963 # the patch has landed.
964 if not inp['files']:
965 self.Print('Warning: No files modified in patch, bailing out early.')
966 self.WriteJSON({
967 'status': 'No dependency',
968 'compile_targets': [],
969 'test_targets': [],
970 }, output_path)
971 return 0
972
973 gn_inp = {}
974 gn_inp['files'] = ['//' + f for f in inp['files'] if not f.startswith('//')]
975
976 isolate_map = self.ReadIsolateMap()
977 err, gn_inp['additional_compile_targets'] = self.MapTargetsToLabels(
978 isolate_map, inp['additional_compile_targets'])
979 if err:
980 raise MBErr(err)
981
982 err, gn_inp['test_targets'] = self.MapTargetsToLabels(
983 isolate_map, inp['test_targets'])
984 if err:
985 raise MBErr(err)
986 labels_to_targets = {}
987 for i, label in enumerate(gn_inp['test_targets']):
988 labels_to_targets[label] = inp['test_targets'][i]
989
990 try:
991 self.WriteJSON(gn_inp, gn_input_path)
992 cmd = self.GNCmd('analyze', build_path, gn_input_path, gn_output_path)
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700993 ret, output, _ = self.Run(cmd, force_verbose=True)
kjellandera013a022016-11-14 05:54:22 -0800994 if ret:
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700995 if self.args.json_output:
996 # write errors to json.output
997 self.WriteJSON({'output': output}, self.args.json_output)
kjellandera013a022016-11-14 05:54:22 -0800998 return ret
999
1000 gn_outp_str = self.ReadFile(gn_output_path)
1001 try:
1002 gn_outp = json.loads(gn_outp_str)
1003 except Exception as e:
1004 self.Print("Failed to parse the JSON string GN returned: %s\n%s"
1005 % (repr(gn_outp_str), str(e)))
1006 raise
1007
1008 outp = {}
1009 if 'status' in gn_outp:
1010 outp['status'] = gn_outp['status']
1011 if 'error' in gn_outp:
1012 outp['error'] = gn_outp['error']
1013 if 'invalid_targets' in gn_outp:
1014 outp['invalid_targets'] = gn_outp['invalid_targets']
1015 if 'compile_targets' in gn_outp:
1016 if 'all' in gn_outp['compile_targets']:
1017 outp['compile_targets'] = ['all']
1018 else:
1019 outp['compile_targets'] = [
1020 label.replace('//', '') for label in gn_outp['compile_targets']]
1021 if 'test_targets' in gn_outp:
1022 outp['test_targets'] = [
1023 labels_to_targets[label] for label in gn_outp['test_targets']]
1024
1025 if self.args.verbose:
1026 self.Print()
1027 self.Print('analyze output:')
1028 self.PrintJSON(outp)
1029 self.Print()
1030
1031 self.WriteJSON(outp, output_path)
1032
1033 finally:
1034 if self.Exists(gn_input_path):
1035 self.RemoveFile(gn_input_path)
1036 if self.Exists(gn_output_path):
1037 self.RemoveFile(gn_output_path)
1038
1039 return 0
1040
1041 def ReadInputJSON(self, required_keys):
1042 path = self.args.input_path[0]
1043 output_path = self.args.output_path[0]
1044 if not self.Exists(path):
1045 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
1046
1047 try:
1048 inp = json.loads(self.ReadFile(path))
1049 except Exception as e:
1050 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
1051 (path, e), output_path)
1052
1053 for k in required_keys:
1054 if not k in inp:
1055 self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
1056 output_path)
1057
1058 return inp
1059
1060 def WriteFailureAndRaise(self, msg, output_path):
1061 if output_path:
1062 self.WriteJSON({'error': msg}, output_path, force_verbose=True)
1063 raise MBErr(msg)
1064
1065 def WriteJSON(self, obj, path, force_verbose=False):
1066 try:
1067 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n',
1068 force_verbose=force_verbose)
1069 except Exception as e:
1070 raise MBErr('Error %s writing to the output path "%s"' %
1071 (e, path))
1072
kjellandera013a022016-11-14 05:54:22 -08001073 def PrintCmd(self, cmd, env):
1074 if self.platform == 'win32':
1075 env_prefix = 'set '
1076 env_quoter = QuoteForSet
1077 shell_quoter = QuoteForCmd
1078 else:
1079 env_prefix = ''
1080 env_quoter = pipes.quote
1081 shell_quoter = pipes.quote
1082
1083 def print_env(var):
1084 if env and var in env:
1085 self.Print('%s%s=%s' % (env_prefix, var, env_quoter(env[var])))
1086
kjellandera013a022016-11-14 05:54:22 -08001087 print_env('LLVM_FORCE_HEAD_REVISION')
1088
1089 if cmd[0] == self.executable:
1090 cmd = ['python'] + cmd[1:]
1091 self.Print(*[shell_quoter(arg) for arg in cmd])
1092
1093 def PrintJSON(self, obj):
1094 self.Print(json.dumps(obj, indent=2, sort_keys=True))
1095
1096 def Build(self, target):
1097 build_dir = self.ToSrcRelPath(self.args.path[0])
Oleh Prypinb708e932018-03-18 17:34:20 +01001098 ninja_cmd = ['ninja', '-C', build_dir]
kjellandera013a022016-11-14 05:54:22 -08001099 if self.args.jobs:
1100 ninja_cmd.extend(['-j', '%d' % self.args.jobs])
1101 ninja_cmd.append(target)
1102 ret, _, _ = self.Run(ninja_cmd, force_verbose=False, buffer_output=False)
1103 return ret
1104
1105 def Run(self, cmd, env=None, force_verbose=True, buffer_output=True):
1106 # This function largely exists so it can be overridden for testing.
1107 if self.args.dryrun or self.args.verbose or force_verbose:
1108 self.PrintCmd(cmd, env)
1109 if self.args.dryrun:
1110 return 0, '', ''
1111
1112 ret, out, err = self.Call(cmd, env=env, buffer_output=buffer_output)
1113 if self.args.verbose or force_verbose:
1114 if ret:
1115 self.Print(' -> returned %d' % ret)
1116 if out:
1117 self.Print(out, end='')
1118 if err:
1119 self.Print(err, end='', file=sys.stderr)
1120 return ret, out, err
1121
1122 def Call(self, cmd, env=None, buffer_output=True):
1123 if buffer_output:
kjellander1c3548c2017-02-15 22:38:22 -08001124 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001125 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1126 env=env)
1127 out, err = p.communicate()
1128 else:
kjellander1c3548c2017-02-15 22:38:22 -08001129 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001130 env=env)
1131 p.wait()
1132 out = err = ''
1133 return p.returncode, out, err
1134
1135 def ExpandUser(self, path):
1136 # This function largely exists so it can be overridden for testing.
1137 return os.path.expanduser(path)
1138
1139 def Exists(self, path):
1140 # This function largely exists so it can be overridden for testing.
1141 return os.path.exists(path)
1142
1143 def Fetch(self, url):
1144 # This function largely exists so it can be overridden for testing.
1145 f = urllib2.urlopen(url)
1146 contents = f.read()
1147 f.close()
1148 return contents
1149
1150 def MaybeMakeDirectory(self, path):
1151 try:
1152 os.makedirs(path)
1153 except OSError, e:
1154 if e.errno != errno.EEXIST:
1155 raise
1156
1157 def PathJoin(self, *comps):
1158 # This function largely exists so it can be overriden for testing.
1159 return os.path.join(*comps)
1160
1161 def Print(self, *args, **kwargs):
1162 # This function largely exists so it can be overridden for testing.
1163 print(*args, **kwargs)
1164 if kwargs.get('stream', sys.stdout) == sys.stdout:
1165 sys.stdout.flush()
1166
1167 def ReadFile(self, path):
1168 # This function largely exists so it can be overriden for testing.
1169 with open(path) as fp:
1170 return fp.read()
1171
1172 def RelPath(self, path, start='.'):
1173 # This function largely exists so it can be overriden for testing.
1174 return os.path.relpath(path, start)
1175
1176 def RemoveFile(self, path):
1177 # This function largely exists so it can be overriden for testing.
1178 os.remove(path)
1179
1180 def RemoveDirectory(self, abs_path):
1181 if self.platform == 'win32':
1182 # In other places in chromium, we often have to retry this command
1183 # because we're worried about other processes still holding on to
1184 # file handles, but when MB is invoked, it will be early enough in the
1185 # build that their should be no other processes to interfere. We
1186 # can change this if need be.
1187 self.Run(['cmd.exe', '/c', 'rmdir', '/q', '/s', abs_path])
1188 else:
1189 shutil.rmtree(abs_path, ignore_errors=True)
1190
1191 def TempFile(self, mode='w'):
1192 # This function largely exists so it can be overriden for testing.
1193 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
1194
1195 def WriteFile(self, path, contents, force_verbose=False):
1196 # This function largely exists so it can be overriden for testing.
1197 if self.args.dryrun or self.args.verbose or force_verbose:
1198 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
1199 with open(path, 'w') as fp:
1200 return fp.write(contents)
1201
1202
1203class MBErr(Exception):
1204 pass
1205
1206
1207# See http://goo.gl/l5NPDW and http://goo.gl/4Diozm for the painful
1208# details of this next section, which handles escaping command lines
1209# so that they can be copied and pasted into a cmd window.
1210UNSAFE_FOR_SET = set('^<>&|')
1211UNSAFE_FOR_CMD = UNSAFE_FOR_SET.union(set('()%'))
1212ALL_META_CHARS = UNSAFE_FOR_CMD.union(set('"'))
1213
1214
1215def QuoteForSet(arg):
1216 if any(a in UNSAFE_FOR_SET for a in arg):
1217 arg = ''.join('^' + a if a in UNSAFE_FOR_SET else a for a in arg)
1218 return arg
1219
1220
1221def QuoteForCmd(arg):
1222 # First, escape the arg so that CommandLineToArgvW will parse it properly.
kjellandera013a022016-11-14 05:54:22 -08001223 if arg == '' or ' ' in arg or '"' in arg:
1224 quote_re = re.compile(r'(\\*)"')
1225 arg = '"%s"' % (quote_re.sub(lambda mo: 2 * mo.group(1) + '\\"', arg))
1226
1227 # Then check to see if the arg contains any metacharacters other than
1228 # double quotes; if it does, quote everything (including the double
1229 # quotes) for safety.
1230 if any(a in UNSAFE_FOR_CMD for a in arg):
1231 arg = ''.join('^' + a if a in ALL_META_CHARS else a for a in arg)
1232 return arg
1233
1234
1235if __name__ == '__main__':
1236 sys.exit(main(sys.argv[1:]))