blob: 4aff74621f2208848d16f8e98391de097a8e74f1 [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:
Mirko Bonadei989e6e72021-01-29 14:34:52 +0100326 cmd, _ = self.GetSwarmingCommand(self.args.target[0], vals)
327 return self._RunUnderSwarming(build_dir, target, cmd)
Oleh Prypinb708e932018-03-18 17:34:20 +0100328 else:
329 return self._RunLocallyIsolated(build_dir, target)
330
Mirko Bonadei989e6e72021-01-29 14:34:52 +0100331 def _RunUnderSwarming(self, build_dir, target, isolate_cmd):
332 cas_instance = 'chromium-swarm'
333 swarming_server = 'chromium-swarm.appspot.com'
Oleh Prypinb708e932018-03-18 17:34:20 +0100334 # TODO(dpranke): Look up the information for the target in
335 # the //testing/buildbot.json file, if possible, so that we
336 # can determine the isolate target, command line, and additional
337 # swarming parameters, if possible.
338 #
339 # TODO(dpranke): Also, add support for sharding and merging results.
340 dimensions = []
341 for k, v in self.args.dimensions:
Mirko Bonadei989e6e72021-01-29 14:34:52 +0100342 dimensions += ['-d', '%s=%s' % (k, v)]
Oleh Prypinb708e932018-03-18 17:34:20 +0100343
Ye Kuange55f0c32020-03-19 16:44:08 +0900344 archive_json_path = self.ToSrcRelPath(
345 '%s/%s.archive.json' % (build_dir, target))
Oleh Prypinb708e932018-03-18 17:34:20 +0100346 cmd = [
Ye Kuangb28f0202020-03-16 10:56:20 +0900347 self.PathJoin(self.src_dir, 'tools', 'luci-go', self.isolate_exe),
Oleh Prypinb708e932018-03-18 17:34:20 +0100348 'archive',
Ye Kuange55f0c32020-03-19 16:44:08 +0900349 '-i',
350 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
Mirko Bonadei989e6e72021-01-29 14:34:52 +0100351 '-cas-instance',
352 cas_instance,
353 '-dump-json',
354 archive_json_path,
355 ]
356
357 # Talking to the isolateserver may fail because we're not logged in.
358 # We trap the command explicitly and rewrite the error output so that
359 # the error message is actually correct for a Chromium check out.
360 self.PrintCmd(cmd, env=None)
361 ret, out, err = self.Run(cmd, force_verbose=False)
Oleh Prypinb708e932018-03-18 17:34:20 +0100362 if ret:
Mirko Bonadei989e6e72021-01-29 14:34:52 +0100363 self.Print(' -> returned %d' % ret)
364 if out:
365 self.Print(out, end='')
366 if err:
367 # The swarming client will return an exit code of 2 (via
368 # argparse.ArgumentParser.error()) and print a message to indicate
369 # that auth failed, so we have to parse the message to check.
370 if (ret == 2 and 'Please login to' in err):
371 err = err.replace(' auth.py', ' tools/swarming_client/auth.py')
372 self.Print(err, end='', file=sys.stderr)
373
Oleh Prypinb708e932018-03-18 17:34:20 +0100374 return ret
375
Ye Kuange55f0c32020-03-19 16:44:08 +0900376 try:
377 archive_hashes = json.loads(self.ReadFile(archive_json_path))
378 except Exception:
379 self.Print(
380 'Failed to read JSON file "%s"' % archive_json_path, file=sys.stderr)
381 return 1
382 try:
Mirko Bonadei989e6e72021-01-29 14:34:52 +0100383 cas_digest = archive_hashes[target]
Ye Kuange55f0c32020-03-19 16:44:08 +0900384 except Exception:
385 self.Print(
386 'Cannot find hash for "%s" in "%s", file content: %s' %
387 (target, archive_json_path, archive_hashes),
388 file=sys.stderr)
389 return 1
390
Mirko Bonadei989e6e72021-01-29 14:34:52 +0100391 try:
392 json_dir = self.TempDir()
393 json_file = self.PathJoin(json_dir, 'task.json')
394
395 cmd = [
396 self.PathJoin('tools', 'luci-go', 'swarming'),
397 'trigger',
398 '-digest',
399 cas_digest,
400 '-server',
401 swarming_server,
402 '-tag=purpose:user-debug-mb',
403 '-relative-cwd',
404 self.ToSrcRelPath(build_dir),
405 '-dump-json',
406 json_file,
407 ] + dimensions + ['--'] + list(isolate_cmd)
408
409 if self.args.extra_args:
410 cmd += ['--'] + self.args.extra_args
411 self.Print('')
412 ret, _, _ = self.Run(cmd, force_verbose=True, buffer_output=False)
413 if ret:
414 return ret
415 task_json = self.ReadFile(json_file)
416 task_id = json.loads(task_json)["tasks"][0]['task_id']
417 finally:
418 if json_dir:
419 self.RemoveDirectory(json_dir)
420
Oleh Prypinb708e932018-03-18 17:34:20 +0100421 cmd = [
Mirko Bonadei989e6e72021-01-29 14:34:52 +0100422 self.PathJoin('tools', 'luci-go', 'swarming'),
423 'collect',
424 '-server',
425 swarming_server,
426 '-task-output-stdout=console',
427 task_id,
428 ]
Oleh Prypinb708e932018-03-18 17:34:20 +0100429 ret, _, _ = self.Run(cmd, force_verbose=True, buffer_output=False)
430 return ret
431
432 def _RunLocallyIsolated(self, build_dir, target):
kjellandera013a022016-11-14 05:54:22 -0800433 cmd = [
Ye Kuangb28f0202020-03-16 10:56:20 +0900434 self.PathJoin(self.src_dir, 'tools', 'luci-go', self.isolate_exe),
kjellandera013a022016-11-14 05:54:22 -0800435 'run',
Ye Kuangb28f0202020-03-16 10:56:20 +0900436 '-i',
437 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
Oleh Prypinb708e932018-03-18 17:34:20 +0100438 ]
kjellandera013a022016-11-14 05:54:22 -0800439 if self.args.extra_args:
Oleh Prypinb708e932018-03-18 17:34:20 +0100440 cmd += ['--'] + self.args.extra_args
441 ret, _, _ = self.Run(cmd, force_verbose=True, buffer_output=False)
kjellandera013a022016-11-14 05:54:22 -0800442 return ret
443
444 def CmdValidate(self, print_ok=True):
445 errs = []
446
447 # Read the file to make sure it parses.
448 self.ReadConfigFile()
449
450 # Build a list of all of the configs referenced by builders.
451 all_configs = {}
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100452 for builder_group in self.builder_groups:
453 for config in self.builder_groups[builder_group].values():
kjellandera013a022016-11-14 05:54:22 -0800454 if isinstance(config, dict):
455 for c in config.values():
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100456 all_configs[c] = builder_group
kjellandera013a022016-11-14 05:54:22 -0800457 else:
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100458 all_configs[config] = builder_group
kjellandera013a022016-11-14 05:54:22 -0800459
460 # Check that every referenced args file or config actually exists.
461 for config, loc in all_configs.items():
462 if config.startswith('//'):
463 if not self.Exists(self.ToAbsPath(config)):
464 errs.append('Unknown args file "%s" referenced from "%s".' %
465 (config, loc))
466 elif not config in self.configs:
467 errs.append('Unknown config "%s" referenced from "%s".' %
468 (config, loc))
469
470 # Check that every actual config is actually referenced.
471 for config in self.configs:
472 if not config in all_configs:
473 errs.append('Unused config "%s".' % config)
474
475 # Figure out the whole list of mixins, and check that every mixin
476 # listed by a config or another mixin actually exists.
477 referenced_mixins = set()
478 for config, mixins in self.configs.items():
479 for mixin in mixins:
480 if not mixin in self.mixins:
481 errs.append('Unknown mixin "%s" referenced by config "%s".' %
482 (mixin, config))
483 referenced_mixins.add(mixin)
484
485 for mixin in self.mixins:
486 for sub_mixin in self.mixins[mixin].get('mixins', []):
487 if not sub_mixin in self.mixins:
488 errs.append('Unknown mixin "%s" referenced by mixin "%s".' %
489 (sub_mixin, mixin))
490 referenced_mixins.add(sub_mixin)
491
492 # Check that every mixin defined is actually referenced somewhere.
493 for mixin in self.mixins:
494 if not mixin in referenced_mixins:
495 errs.append('Unreferenced mixin "%s".' % mixin)
496
kjellandera013a022016-11-14 05:54:22 -0800497 if errs:
498 raise MBErr(('mb config file %s has problems:' % self.args.config_file) +
499 '\n ' + '\n '.join(errs))
500
501 if print_ok:
502 self.Print('mb config file %s looks ok.' % self.args.config_file)
503 return 0
504
kjellandera013a022016-11-14 05:54:22 -0800505 def GetConfig(self):
506 build_dir = self.args.path[0]
507
508 vals = self.DefaultVals()
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100509 if self.args.builder or self.args.builder_group or self.args.config:
kjellandera013a022016-11-14 05:54:22 -0800510 vals = self.Lookup()
Oleh Prypinb708e932018-03-18 17:34:20 +0100511 # Re-run gn gen in order to ensure the config is consistent with the
512 # build dir.
513 self.RunGNGen(vals)
kjellandera013a022016-11-14 05:54:22 -0800514 return vals
515
Oleh Prypinb708e932018-03-18 17:34:20 +0100516 toolchain_path = self.PathJoin(self.ToAbsPath(build_dir),
517 'toolchain.ninja')
518 if not self.Exists(toolchain_path):
519 self.Print('Must either specify a path to an existing GN build dir '
520 'or pass in a -m/-b pair or a -c flag to specify the '
521 'configuration')
522 return {}
kjellandera013a022016-11-14 05:54:22 -0800523
Oleh Prypinb708e932018-03-18 17:34:20 +0100524 vals['gn_args'] = self.GNArgsFromDir(build_dir)
kjellandera013a022016-11-14 05:54:22 -0800525 return vals
526
527 def GNArgsFromDir(self, build_dir):
528 args_contents = ""
529 gn_args_path = self.PathJoin(self.ToAbsPath(build_dir), 'args.gn')
530 if self.Exists(gn_args_path):
531 args_contents = self.ReadFile(gn_args_path)
532 gn_args = []
533 for l in args_contents.splitlines():
534 fields = l.split(' ')
535 name = fields[0]
536 val = ' '.join(fields[2:])
537 gn_args.append('%s=%s' % (name, val))
538
539 return ' '.join(gn_args)
540
541 def Lookup(self):
Oleh Prypin82ac2402019-01-29 16:18:30 +0100542 self.ReadConfigFile()
543 config = self.ConfigFromArgs()
544 if config.startswith('//'):
545 if not self.Exists(self.ToAbsPath(config)):
546 raise MBErr('args file "%s" not found' % config)
547 vals = self.DefaultVals()
548 vals['args_file'] = config
549 else:
550 if not config in self.configs:
551 raise MBErr('Config "%s" not found in %s' %
552 (config, self.args.config_file))
553 vals = self.FlattenConfig(config)
kjellandera013a022016-11-14 05:54:22 -0800554 return vals
555
556 def ReadConfigFile(self):
557 if not self.Exists(self.args.config_file):
558 raise MBErr('config file not found at %s' % self.args.config_file)
559
560 try:
561 contents = ast.literal_eval(self.ReadFile(self.args.config_file))
562 except SyntaxError as e:
563 raise MBErr('Failed to parse config file "%s": %s' %
564 (self.args.config_file, e))
565
566 self.configs = contents['configs']
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100567 self.builder_groups = contents['builder_groups']
kjellandera013a022016-11-14 05:54:22 -0800568 self.mixins = contents['mixins']
569
570 def ReadIsolateMap(self):
Oleh Prypinb708e932018-03-18 17:34:20 +0100571 isolate_map = self.args.isolate_map_file
572 if not self.Exists(isolate_map):
573 raise MBErr('isolate map file not found at %s' % isolate_map)
kjellandera013a022016-11-14 05:54:22 -0800574 try:
Oleh Prypinb708e932018-03-18 17:34:20 +0100575 return ast.literal_eval(self.ReadFile(isolate_map))
kjellandera013a022016-11-14 05:54:22 -0800576 except SyntaxError as e:
Oleh Prypinb708e932018-03-18 17:34:20 +0100577 raise MBErr(
578 'Failed to parse isolate map file "%s": %s' % (isolate_map, e))
kjellandera013a022016-11-14 05:54:22 -0800579
580 def ConfigFromArgs(self):
581 if self.args.config:
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100582 if self.args.builder_group or self.args.builder:
583 raise MBErr('Can not specific both -c/--config and -m/--builder-group '
584 'or -b/--builder')
kjellandera013a022016-11-14 05:54:22 -0800585
586 return self.args.config
587
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100588 if not self.args.builder_group or not self.args.builder:
kjellandera013a022016-11-14 05:54:22 -0800589 raise MBErr('Must specify either -c/--config or '
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100590 '(-m/--builder-group and -b/--builder)')
kjellandera013a022016-11-14 05:54:22 -0800591
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100592 if not self.args.builder_group in self.builder_groups:
kjellandera013a022016-11-14 05:54:22 -0800593 raise MBErr('Master name "%s" not found in "%s"' %
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100594 (self.args.builder_group, self.args.config_file))
kjellandera013a022016-11-14 05:54:22 -0800595
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100596 if not self.args.builder in self.builder_groups[self.args.builder_group]:
597 raise MBErr(
598 'Builder name "%s" not found under builder_groups[%s] in "%s"' %
599 (self.args.builder, self.args.builder_group, self.args.config_file))
kjellandera013a022016-11-14 05:54:22 -0800600
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100601 config = self.builder_groups[self.args.builder_group][self.args.builder]
kjellandera013a022016-11-14 05:54:22 -0800602 if isinstance(config, dict):
603 if self.args.phase is None:
604 raise MBErr('Must specify a build --phase for %s on %s' %
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100605 (self.args.builder, self.args.builder_group))
kjellandera013a022016-11-14 05:54:22 -0800606 phase = str(self.args.phase)
607 if phase not in config:
608 raise MBErr('Phase %s doesn\'t exist for %s on %s' %
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100609 (phase, self.args.builder, self.args.builder_group))
kjellandera013a022016-11-14 05:54:22 -0800610 return config[phase]
611
612 if self.args.phase is not None:
613 raise MBErr('Must not specify a build --phase for %s on %s' %
Mirko Bonadei8606b9c2021-01-12 14:29:40 +0100614 (self.args.builder, self.args.builder_group))
kjellandera013a022016-11-14 05:54:22 -0800615 return config
616
617 def FlattenConfig(self, config):
618 mixins = self.configs[config]
619 vals = self.DefaultVals()
620
621 visited = []
622 self.FlattenMixins(mixins, vals, visited)
623 return vals
624
625 def DefaultVals(self):
626 return {
627 'args_file': '',
628 'cros_passthrough': False,
629 'gn_args': '',
kjellandera013a022016-11-14 05:54:22 -0800630 }
631
632 def FlattenMixins(self, mixins, vals, visited):
633 for m in mixins:
634 if m not in self.mixins:
635 raise MBErr('Unknown mixin "%s"' % m)
636
637 visited.append(m)
638
639 mixin_vals = self.mixins[m]
640
641 if 'cros_passthrough' in mixin_vals:
642 vals['cros_passthrough'] = mixin_vals['cros_passthrough']
643 if 'gn_args' in mixin_vals:
644 if vals['gn_args']:
645 vals['gn_args'] += ' ' + mixin_vals['gn_args']
646 else:
647 vals['gn_args'] = mixin_vals['gn_args']
kjellandera013a022016-11-14 05:54:22 -0800648
649 if 'mixins' in mixin_vals:
650 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
651 return vals
652
kjellandera013a022016-11-14 05:54:22 -0800653 def RunGNGen(self, vals):
654 build_dir = self.args.path[0]
655
656 cmd = self.GNCmd('gen', build_dir, '--check')
657 gn_args = self.GNArgs(vals)
658
659 # Since GN hasn't run yet, the build directory may not even exist.
660 self.MaybeMakeDirectory(self.ToAbsPath(build_dir))
661
662 gn_args_path = self.ToAbsPath(build_dir, 'args.gn')
663 self.WriteFile(gn_args_path, gn_args, force_verbose=True)
664
665 swarming_targets = []
666 if getattr(self.args, 'swarming_targets_file', None):
667 # We need GN to generate the list of runtime dependencies for
668 # the compile targets listed (one per line) in the file so
669 # we can run them via swarming. We use gn_isolate_map.pyl to convert
670 # the compile targets to the matching GN labels.
671 path = self.args.swarming_targets_file
672 if not self.Exists(path):
673 self.WriteFailureAndRaise('"%s" does not exist' % path,
674 output_path=None)
675 contents = self.ReadFile(path)
676 swarming_targets = set(contents.splitlines())
677
678 isolate_map = self.ReadIsolateMap()
679 err, labels = self.MapTargetsToLabels(isolate_map, swarming_targets)
680 if err:
681 raise MBErr(err)
682
683 gn_runtime_deps_path = self.ToAbsPath(build_dir, 'runtime_deps')
684 self.WriteFile(gn_runtime_deps_path, '\n'.join(labels) + '\n')
685 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path)
686
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700687 ret, output, _ = self.Run(cmd)
kjellandera013a022016-11-14 05:54:22 -0800688 if ret:
Debrian Figueroa3f53edb2019-07-19 15:15:01 -0700689 if self.args.json_output:
690 # write errors to json.output
691 self.WriteJSON({'output': output}, self.args.json_output)
kjellandera013a022016-11-14 05:54:22 -0800692 # If `gn gen` failed, we should exit early rather than trying to
693 # generate isolates. Run() will have already logged any error output.
694 self.Print('GN gen failed: %d' % ret)
695 return ret
696
697 android = 'target_os="android"' in vals['gn_args']
698 for target in swarming_targets:
699 if android:
700 # Android targets may be either android_apk or executable. The former
701 # will result in runtime_deps associated with the stamp file, while the
702 # latter will result in runtime_deps associated with the executable.
703 label = isolate_map[target]['label']
704 runtime_deps_targets = [
705 target + '.runtime_deps',
706 'obj/%s.stamp.runtime_deps' % label.replace(':', '/')]
707 elif isolate_map[target]['type'] == 'gpu_browser_test':
708 if self.platform == 'win32':
709 runtime_deps_targets = ['browser_tests.exe.runtime_deps']
710 else:
711 runtime_deps_targets = ['browser_tests.runtime_deps']
Edward Lemur20110752017-09-28 16:14:37 +0200712 elif isolate_map[target]['type'] == 'script':
713 label = isolate_map[target]['label'].split(':')[1]
kjellandera013a022016-11-14 05:54:22 -0800714 runtime_deps_targets = [
Edward Lemur20110752017-09-28 16:14:37 +0200715 '%s.runtime_deps' % label]
kjellandera013a022016-11-14 05:54:22 -0800716 if self.platform == 'win32':
Edward Lemur20110752017-09-28 16:14:37 +0200717 runtime_deps_targets += [ label + '.exe.runtime_deps' ]
kjellandera013a022016-11-14 05:54:22 -0800718 else:
Edward Lemur20110752017-09-28 16:14:37 +0200719 runtime_deps_targets += [ label + '.runtime_deps' ]
kjellandera013a022016-11-14 05:54:22 -0800720 elif self.platform == 'win32':
721 runtime_deps_targets = [target + '.exe.runtime_deps']
722 else:
723 runtime_deps_targets = [target + '.runtime_deps']
724
725 for r in runtime_deps_targets:
726 runtime_deps_path = self.ToAbsPath(build_dir, r)
727 if self.Exists(runtime_deps_path):
728 break
729 else:
730 raise MBErr('did not generate any of %s' %
731 ', '.join(runtime_deps_targets))
732
Mirko Bonadei989e6e72021-01-29 14:34:52 +0100733 command, extra_files = self.GetSwarmingCommand(target, vals)
kjellandera013a022016-11-14 05:54:22 -0800734
735 runtime_deps = self.ReadFile(runtime_deps_path).splitlines()
736
737 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
738 extra_files)
739
740 return 0
741
742 def RunGNIsolate(self, vals):
743 target = self.args.target[0]
744 isolate_map = self.ReadIsolateMap()
745 err, labels = self.MapTargetsToLabels(isolate_map, [target])
746 if err:
747 raise MBErr(err)
748 label = labels[0]
749
750 build_dir = self.args.path[0]
Mirko Bonadei989e6e72021-01-29 14:34:52 +0100751 command, extra_files = self.GetSwarmingCommand(target, vals)
kjellandera013a022016-11-14 05:54:22 -0800752
753 cmd = self.GNCmd('desc', build_dir, label, 'runtime_deps')
754 ret, out, _ = self.Call(cmd)
755 if ret:
756 if out:
757 self.Print(out)
758 return ret
759
760 runtime_deps = out.splitlines()
761
762 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
763 extra_files)
764
765 ret, _, _ = self.Run([
Ye Kuangb28f0202020-03-16 10:56:20 +0900766 self.PathJoin(self.src_dir, 'tools', 'luci-go', self.isolate_exe),
kjellandera013a022016-11-14 05:54:22 -0800767 'check',
768 '-i',
Ye Kuangb28f0202020-03-16 10:56:20 +0900769 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target))],
kjellandera013a022016-11-14 05:54:22 -0800770 buffer_output=False)
771
772 return ret
773
774 def WriteIsolateFiles(self, build_dir, command, target, runtime_deps,
775 extra_files):
776 isolate_path = self.ToAbsPath(build_dir, target + '.isolate')
777 self.WriteFile(isolate_path,
778 pprint.pformat({
779 'variables': {
780 'command': command,
781 'files': sorted(runtime_deps + extra_files),
782 }
783 }) + '\n')
784
785 self.WriteJSON(
786 {
787 'args': [
788 '--isolated',
789 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
790 '--isolate',
791 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
792 ],
kjellander1c3548c2017-02-15 22:38:22 -0800793 'dir': self.src_dir,
kjellandera013a022016-11-14 05:54:22 -0800794 'version': 1,
795 },
796 isolate_path + 'd.gen.json',
797 )
798
799 def MapTargetsToLabels(self, isolate_map, targets):
800 labels = []
801 err = ''
802
803 def StripTestSuffixes(target):
804 for suffix in ('_apk_run', '_apk', '_run'):
805 if target.endswith(suffix):
806 return target[:-len(suffix)], suffix
807 return None, None
808
809 for target in targets:
810 if target == 'all':
811 labels.append(target)
812 elif target.startswith('//'):
813 labels.append(target)
814 else:
815 if target in isolate_map:
816 stripped_target, suffix = target, ''
817 else:
818 stripped_target, suffix = StripTestSuffixes(target)
819 if stripped_target in isolate_map:
820 if isolate_map[stripped_target]['type'] == 'unknown':
821 err += ('test target "%s" type is unknown\n' % target)
822 else:
823 labels.append(isolate_map[stripped_target]['label'] + suffix)
824 else:
825 err += ('target "%s" not found in '
826 '//testing/buildbot/gn_isolate_map.pyl\n' % target)
827
828 return err, labels
829
830 def GNCmd(self, subcommand, path, *args):
Oleh Prypinb708e932018-03-18 17:34:20 +0100831 if self.platform.startswith('linux'):
kjellandera013a022016-11-14 05:54:22 -0800832 subdir, exe = 'linux64', 'gn'
833 elif self.platform == 'darwin':
834 subdir, exe = 'mac', 'gn'
835 else:
836 subdir, exe = 'win', 'gn.exe'
837
kjellander1c3548c2017-02-15 22:38:22 -0800838 gn_path = self.PathJoin(self.src_dir, 'buildtools', subdir, exe)
kjellandera013a022016-11-14 05:54:22 -0800839 return [gn_path, subcommand, path] + list(args)
840
841
842 def GNArgs(self, vals):
843 if vals['cros_passthrough']:
844 if not 'GN_ARGS' in os.environ:
845 raise MBErr('MB is expecting GN_ARGS to be in the environment')
846 gn_args = os.environ['GN_ARGS']
847 if not re.search('target_os.*=.*"chromeos"', gn_args):
848 raise MBErr('GN_ARGS is missing target_os = "chromeos": (GN_ARGS=%s)' %
849 gn_args)
850 else:
851 gn_args = vals['gn_args']
852
853 if self.args.goma_dir:
854 gn_args += ' goma_dir="%s"' % self.args.goma_dir
855
856 android_version_code = self.args.android_version_code
857 if android_version_code:
858 gn_args += ' android_default_version_code="%s"' % android_version_code
859
860 android_version_name = self.args.android_version_name
861 if android_version_name:
862 gn_args += ' android_default_version_name="%s"' % android_version_name
863
864 # Canonicalize the arg string into a sorted, newline-separated list
865 # of key-value pairs, and de-dup the keys if need be so that only
866 # the last instance of each arg is listed.
867 gn_args = gn_helpers.ToGNString(gn_helpers.FromGNArgs(gn_args))
868
869 args_file = vals.get('args_file', None)
870 if args_file:
871 gn_args = ('import("%s")\n' % vals['args_file']) + gn_args
872 return gn_args
873
Mirko Bonadei989e6e72021-01-29 14:34:52 +0100874 def GetSwarmingCommand(self, target, vals):
kjellandera013a022016-11-14 05:54:22 -0800875 isolate_map = self.ReadIsolateMap()
876 test_type = isolate_map[target]['type']
877
Oleh Prypinb708e932018-03-18 17:34:20 +0100878 is_android = 'target_os="android"' in vals['gn_args']
879 is_linux = self.platform.startswith('linux') and not is_android
kjellandera013a022016-11-14 05:54:22 -0800880
881 if test_type == 'nontest':
882 self.WriteFailureAndRaise('We should not be isolating %s.' % target,
883 output_path=None)
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800884 if test_type not in ('console_test_launcher', 'windowed_test_launcher',
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100885 'non_parallel_console_test_launcher', 'raw',
Edward Lemur20110752017-09-28 16:14:37 +0200886 'additional_compile_target', 'junit_test', 'script'):
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800887 self.WriteFailureAndRaise('No command line for %s found (test type %s).'
888 % (target, test_type), output_path=None)
kjellandera013a022016-11-14 05:54:22 -0800889
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800890 cmdline = []
Oleh Prypinb708e932018-03-18 17:34:20 +0100891 extra_files = [
892 '../../.vpython',
893 '../../testing/test_env.py',
894 ]
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800895
Yves Gerey2e0c6552018-10-08 21:59:25 +0200896 must_retry = False
Edward Lemur98d40362018-01-15 17:37:04 +0100897 if test_type == 'script':
Oleh Prypin3a51b0e2019-07-17 15:17:53 +0200898 cmdline += ['../../' + self.ToSrcRelPath(isolate_map[target]['script'])]
Oleh Prypinb708e932018-03-18 17:34:20 +0100899 elif is_android:
Oleh Prypin3a51b0e2019-07-17 15:17:53 +0200900 cmdline += ['../../build/android/test_wrapper/logdog_wrapper.py',
901 '--target', target,
902 '--logdog-bin-cmd', '../../bin/logdog_butler',
903 '--logcat-output-file', '${ISOLATED_OUTDIR}/logcats',
904 '--store-tombstones']
kjellandera013a022016-11-14 05:54:22 -0800905 else:
Patrik Höglund28b8a0b2020-03-26 20:30:50 +0100906 if test_type == 'raw':
907 cmdline.append('../../tools_webrtc/flags_compatibility.py')
908 extra_files.append('../../tools_webrtc/flags_compatibility.py')
909
Oleh Prypin739b8162018-05-17 13:28:29 +0200910 if isolate_map[target].get('use_webcam', False):
911 cmdline.append('../../tools_webrtc/ensure_webcam_is_running.py')
912 extra_files.append('../../tools_webrtc/ensure_webcam_is_running.py')
kjellandera013a022016-11-14 05:54:22 -0800913
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800914 # This needs to mirror the settings in //build/config/ui.gni:
915 # use_x11 = is_linux && !use_ozone.
916 use_x11 = is_linux and not 'use_ozone=true' in vals['gn_args']
917
918 xvfb = use_x11 and test_type == 'windowed_test_launcher'
919 if xvfb:
Oleh Prypin739b8162018-05-17 13:28:29 +0200920 cmdline.append('../../testing/xvfb.py')
921 extra_files.append('../../testing/xvfb.py')
922 else:
923 cmdline.append('../../testing/test_env.py')
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800924
Mirko Bonadei264bee82018-08-07 08:53:41 +0200925 if test_type != 'raw':
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800926 extra_files += [
927 '../../third_party/gtest-parallel/gtest-parallel',
ehmaldonadoa7507eb2017-05-10 13:40:29 -0700928 '../../third_party/gtest-parallel/gtest_parallel.py',
Henrik Kjellander90fd7d82017-05-09 08:30:10 +0200929 '../../tools_webrtc/gtest-parallel-wrapper.py',
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800930 ]
ehmaldonado55833842017-02-13 03:58:13 -0800931 sep = '\\' if self.platform == 'win32' else '/'
932 output_dir = '${ISOLATED_OUTDIR}' + sep + 'test_logs'
Edward Lemurbeffdd42017-09-27 13:07:47 +0200933 timeout = isolate_map[target].get('timeout', 900)
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100934 cmdline += [
Henrik Kjellander90fd7d82017-05-09 08:30:10 +0200935 '../../tools_webrtc/gtest-parallel-wrapper.py',
ehmaldonado55833842017-02-13 03:58:13 -0800936 '--output_dir=%s' % output_dir,
ehmaldonado76e60e92017-05-04 06:18:26 -0700937 '--gtest_color=no',
938 # We tell gtest-parallel to interrupt the test after 900 seconds,
939 # so it can exit cleanly and report results, instead of being
940 # interrupted by swarming and not reporting anything.
Edward Lemurbeffdd42017-09-27 13:07:47 +0200941 '--timeout=%s' % timeout,
ehmaldonado55833842017-02-13 03:58:13 -0800942 ]
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100943 if test_type == 'non_parallel_console_test_launcher':
944 # Still use the gtest-parallel-wrapper.py script since we need it to
945 # run tests on swarming, but don't execute tests in parallel.
946 cmdline.append('--workers=1')
Yves Gerey2e0c6552018-10-08 21:59:25 +0200947 must_retry = True
948
949 asan = 'is_asan=true' in vals['gn_args']
950 lsan = 'is_lsan=true' in vals['gn_args']
951 msan = 'is_msan=true' in vals['gn_args']
952 tsan = 'is_tsan=true' in vals['gn_args']
953 sanitizer = asan or lsan or msan or tsan
954 if must_retry and not sanitizer:
955 # Retry would hide most sanitizers detections.
956 cmdline.append('--retry_failed=3')
Edward Lemur2b67f5c2018-02-07 18:09:44 +0100957
958 executable_prefix = '.\\' if self.platform == 'win32' else './'
959 executable_suffix = '.exe' if self.platform == 'win32' else ''
960 executable = executable_prefix + target + executable_suffix
961
962 cmdline.append(executable)
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800963
kjellander382f2b22017-04-11 04:07:01 -0700964 cmdline.extend([
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800965 '--asan=%d' % asan,
kjellander461a5602017-05-05 06:39:16 -0700966 '--lsan=%d' % lsan,
ehmaldonadoed8c8ed2016-11-23 12:58:35 -0800967 '--msan=%d' % msan,
968 '--tsan=%d' % tsan,
kjellander382f2b22017-04-11 04:07:01 -0700969 ])
kjellandera013a022016-11-14 05:54:22 -0800970
kjellander74e81262017-03-23 00:51:11 -0700971 cmdline += isolate_map[target].get('args', [])
972
kjellandera013a022016-11-14 05:54:22 -0800973 return cmdline, extra_files
974
975 def ToAbsPath(self, build_path, *comps):
kjellander1c3548c2017-02-15 22:38:22 -0800976 return self.PathJoin(self.src_dir,
kjellandera013a022016-11-14 05:54:22 -0800977 self.ToSrcRelPath(build_path),
978 *comps)
979
980 def ToSrcRelPath(self, path):
981 """Returns a relative path from the top of the repo."""
982 if path.startswith('//'):
983 return path[2:].replace('/', self.sep)
kjellander1c3548c2017-02-15 22:38:22 -0800984 return self.RelPath(path, self.src_dir)
kjellandera013a022016-11-14 05:54:22 -0800985
kjellandera013a022016-11-14 05:54:22 -0800986 def RunGNAnalyze(self, vals):
987 # Analyze runs before 'gn gen' now, so we need to run gn gen
988 # in order to ensure that we have a build directory.
989 ret = self.RunGNGen(vals)
990 if ret:
991 return ret
992
993 build_path = self.args.path[0]
994 input_path = self.args.input_path[0]
995 gn_input_path = input_path + '.gn'
996 output_path = self.args.output_path[0]
997 gn_output_path = output_path + '.gn'
998
999 inp = self.ReadInputJSON(['files', 'test_targets',
1000 'additional_compile_targets'])
1001 if self.args.verbose:
1002 self.Print()
1003 self.Print('analyze input:')
1004 self.PrintJSON(inp)
1005 self.Print()
1006
1007
1008 # This shouldn't normally happen, but could due to unusual race conditions,
1009 # like a try job that gets scheduled before a patch lands but runs after
1010 # the patch has landed.
1011 if not inp['files']:
1012 self.Print('Warning: No files modified in patch, bailing out early.')
1013 self.WriteJSON({
1014 'status': 'No dependency',
1015 'compile_targets': [],
1016 'test_targets': [],
1017 }, output_path)
1018 return 0
1019
1020 gn_inp = {}
1021 gn_inp['files'] = ['//' + f for f in inp['files'] if not f.startswith('//')]
1022
1023 isolate_map = self.ReadIsolateMap()
1024 err, gn_inp['additional_compile_targets'] = self.MapTargetsToLabels(
1025 isolate_map, inp['additional_compile_targets'])
1026 if err:
1027 raise MBErr(err)
1028
1029 err, gn_inp['test_targets'] = self.MapTargetsToLabels(
1030 isolate_map, inp['test_targets'])
1031 if err:
1032 raise MBErr(err)
1033 labels_to_targets = {}
1034 for i, label in enumerate(gn_inp['test_targets']):
1035 labels_to_targets[label] = inp['test_targets'][i]
1036
1037 try:
1038 self.WriteJSON(gn_inp, gn_input_path)
1039 cmd = self.GNCmd('analyze', build_path, gn_input_path, gn_output_path)
Debrian Figueroa3f53edb2019-07-19 15:15:01 -07001040 ret, output, _ = self.Run(cmd, force_verbose=True)
kjellandera013a022016-11-14 05:54:22 -08001041 if ret:
Debrian Figueroa3f53edb2019-07-19 15:15:01 -07001042 if self.args.json_output:
1043 # write errors to json.output
1044 self.WriteJSON({'output': output}, self.args.json_output)
kjellandera013a022016-11-14 05:54:22 -08001045 return ret
1046
1047 gn_outp_str = self.ReadFile(gn_output_path)
1048 try:
1049 gn_outp = json.loads(gn_outp_str)
1050 except Exception as e:
1051 self.Print("Failed to parse the JSON string GN returned: %s\n%s"
1052 % (repr(gn_outp_str), str(e)))
1053 raise
1054
1055 outp = {}
1056 if 'status' in gn_outp:
1057 outp['status'] = gn_outp['status']
1058 if 'error' in gn_outp:
1059 outp['error'] = gn_outp['error']
1060 if 'invalid_targets' in gn_outp:
1061 outp['invalid_targets'] = gn_outp['invalid_targets']
1062 if 'compile_targets' in gn_outp:
1063 if 'all' in gn_outp['compile_targets']:
1064 outp['compile_targets'] = ['all']
1065 else:
1066 outp['compile_targets'] = [
1067 label.replace('//', '') for label in gn_outp['compile_targets']]
1068 if 'test_targets' in gn_outp:
1069 outp['test_targets'] = [
1070 labels_to_targets[label] for label in gn_outp['test_targets']]
1071
1072 if self.args.verbose:
1073 self.Print()
1074 self.Print('analyze output:')
1075 self.PrintJSON(outp)
1076 self.Print()
1077
1078 self.WriteJSON(outp, output_path)
1079
1080 finally:
1081 if self.Exists(gn_input_path):
1082 self.RemoveFile(gn_input_path)
1083 if self.Exists(gn_output_path):
1084 self.RemoveFile(gn_output_path)
1085
1086 return 0
1087
1088 def ReadInputJSON(self, required_keys):
1089 path = self.args.input_path[0]
1090 output_path = self.args.output_path[0]
1091 if not self.Exists(path):
1092 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
1093
1094 try:
1095 inp = json.loads(self.ReadFile(path))
1096 except Exception as e:
1097 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
1098 (path, e), output_path)
1099
1100 for k in required_keys:
1101 if not k in inp:
1102 self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
1103 output_path)
1104
1105 return inp
1106
1107 def WriteFailureAndRaise(self, msg, output_path):
1108 if output_path:
1109 self.WriteJSON({'error': msg}, output_path, force_verbose=True)
1110 raise MBErr(msg)
1111
1112 def WriteJSON(self, obj, path, force_verbose=False):
1113 try:
1114 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n',
1115 force_verbose=force_verbose)
1116 except Exception as e:
1117 raise MBErr('Error %s writing to the output path "%s"' %
1118 (e, path))
1119
kjellandera013a022016-11-14 05:54:22 -08001120 def PrintCmd(self, cmd, env):
1121 if self.platform == 'win32':
1122 env_prefix = 'set '
1123 env_quoter = QuoteForSet
1124 shell_quoter = QuoteForCmd
1125 else:
1126 env_prefix = ''
1127 env_quoter = pipes.quote
1128 shell_quoter = pipes.quote
1129
1130 def print_env(var):
1131 if env and var in env:
1132 self.Print('%s%s=%s' % (env_prefix, var, env_quoter(env[var])))
1133
kjellandera013a022016-11-14 05:54:22 -08001134 print_env('LLVM_FORCE_HEAD_REVISION')
1135
1136 if cmd[0] == self.executable:
1137 cmd = ['python'] + cmd[1:]
1138 self.Print(*[shell_quoter(arg) for arg in cmd])
1139
1140 def PrintJSON(self, obj):
1141 self.Print(json.dumps(obj, indent=2, sort_keys=True))
1142
1143 def Build(self, target):
1144 build_dir = self.ToSrcRelPath(self.args.path[0])
Oleh Prypinb708e932018-03-18 17:34:20 +01001145 ninja_cmd = ['ninja', '-C', build_dir]
kjellandera013a022016-11-14 05:54:22 -08001146 if self.args.jobs:
1147 ninja_cmd.extend(['-j', '%d' % self.args.jobs])
1148 ninja_cmd.append(target)
1149 ret, _, _ = self.Run(ninja_cmd, force_verbose=False, buffer_output=False)
1150 return ret
1151
1152 def Run(self, cmd, env=None, force_verbose=True, buffer_output=True):
1153 # This function largely exists so it can be overridden for testing.
1154 if self.args.dryrun or self.args.verbose or force_verbose:
1155 self.PrintCmd(cmd, env)
1156 if self.args.dryrun:
1157 return 0, '', ''
1158
1159 ret, out, err = self.Call(cmd, env=env, buffer_output=buffer_output)
1160 if self.args.verbose or force_verbose:
1161 if ret:
1162 self.Print(' -> returned %d' % ret)
1163 if out:
1164 self.Print(out, end='')
1165 if err:
1166 self.Print(err, end='', file=sys.stderr)
1167 return ret, out, err
1168
1169 def Call(self, cmd, env=None, buffer_output=True):
1170 if buffer_output:
kjellander1c3548c2017-02-15 22:38:22 -08001171 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001172 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1173 env=env)
1174 out, err = p.communicate()
1175 else:
kjellander1c3548c2017-02-15 22:38:22 -08001176 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001177 env=env)
1178 p.wait()
1179 out = err = ''
1180 return p.returncode, out, err
1181
1182 def ExpandUser(self, path):
1183 # This function largely exists so it can be overridden for testing.
1184 return os.path.expanduser(path)
1185
1186 def Exists(self, path):
1187 # This function largely exists so it can be overridden for testing.
1188 return os.path.exists(path)
1189
1190 def Fetch(self, url):
1191 # This function largely exists so it can be overridden for testing.
1192 f = urllib2.urlopen(url)
1193 contents = f.read()
1194 f.close()
1195 return contents
1196
1197 def MaybeMakeDirectory(self, path):
1198 try:
1199 os.makedirs(path)
1200 except OSError, e:
1201 if e.errno != errno.EEXIST:
1202 raise
1203
1204 def PathJoin(self, *comps):
1205 # This function largely exists so it can be overriden for testing.
1206 return os.path.join(*comps)
1207
1208 def Print(self, *args, **kwargs):
1209 # This function largely exists so it can be overridden for testing.
1210 print(*args, **kwargs)
1211 if kwargs.get('stream', sys.stdout) == sys.stdout:
1212 sys.stdout.flush()
1213
1214 def ReadFile(self, path):
1215 # This function largely exists so it can be overriden for testing.
1216 with open(path) as fp:
1217 return fp.read()
1218
1219 def RelPath(self, path, start='.'):
1220 # This function largely exists so it can be overriden for testing.
1221 return os.path.relpath(path, start)
1222
1223 def RemoveFile(self, path):
1224 # This function largely exists so it can be overriden for testing.
1225 os.remove(path)
1226
1227 def RemoveDirectory(self, abs_path):
1228 if self.platform == 'win32':
1229 # In other places in chromium, we often have to retry this command
1230 # because we're worried about other processes still holding on to
1231 # file handles, but when MB is invoked, it will be early enough in the
1232 # build that their should be no other processes to interfere. We
1233 # can change this if need be.
1234 self.Run(['cmd.exe', '/c', 'rmdir', '/q', '/s', abs_path])
1235 else:
1236 shutil.rmtree(abs_path, ignore_errors=True)
1237
Mirko Bonadei989e6e72021-01-29 14:34:52 +01001238 def TempDir(self):
1239 # This function largely exists so it can be overriden for testing.
1240 return tempfile.mkdtemp(prefix='mb_')
1241
kjellandera013a022016-11-14 05:54:22 -08001242 def TempFile(self, mode='w'):
1243 # This function largely exists so it can be overriden for testing.
1244 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
1245
1246 def WriteFile(self, path, contents, force_verbose=False):
1247 # This function largely exists so it can be overriden for testing.
1248 if self.args.dryrun or self.args.verbose or force_verbose:
1249 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
1250 with open(path, 'w') as fp:
1251 return fp.write(contents)
1252
1253
1254class MBErr(Exception):
1255 pass
1256
1257
1258# See http://goo.gl/l5NPDW and http://goo.gl/4Diozm for the painful
1259# details of this next section, which handles escaping command lines
1260# so that they can be copied and pasted into a cmd window.
1261UNSAFE_FOR_SET = set('^<>&|')
1262UNSAFE_FOR_CMD = UNSAFE_FOR_SET.union(set('()%'))
1263ALL_META_CHARS = UNSAFE_FOR_CMD.union(set('"'))
1264
1265
1266def QuoteForSet(arg):
1267 if any(a in UNSAFE_FOR_SET for a in arg):
1268 arg = ''.join('^' + a if a in UNSAFE_FOR_SET else a for a in arg)
1269 return arg
1270
1271
1272def QuoteForCmd(arg):
1273 # First, escape the arg so that CommandLineToArgvW will parse it properly.
kjellandera013a022016-11-14 05:54:22 -08001274 if arg == '' or ' ' in arg or '"' in arg:
1275 quote_re = re.compile(r'(\\*)"')
1276 arg = '"%s"' % (quote_re.sub(lambda mo: 2 * mo.group(1) + '\\"', arg))
1277
1278 # Then check to see if the arg contains any metacharacters other than
1279 # double quotes; if it does, quote everything (including the double
1280 # quotes) for safety.
1281 if any(a in UNSAFE_FOR_CMD for a in arg):
1282 arg = ''.join('^' + a if a in ALL_META_CHARS else a for a in arg)
1283 return arg
1284
1285
1286if __name__ == '__main__':
1287 sys.exit(main(sys.argv[1:]))