blob: 8a44a9bcc078921500e0068d696a89a77d40fe67 [file] [log] [blame]
Christoffer Jansson4e8a7732022-02-08 09:01:12 +01001#!/usr/bin/env vpython3
2
kjellandera013a022016-11-14 05:54:22 -08003# Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
4#
5# Use of this source code is governed by a BSD-style license
6# that can be found in the LICENSE file in the root of the source
7# tree. An additional intellectual property rights grant can be found
8# in the file PATENTS. All contributing project authors may
9# be found in the AUTHORS file in the root of the source tree.
10
Oleh Prypinb708e932018-03-18 17:34:20 +010011"""MB - the Meta-Build wrapper around GN.
kjellandera013a022016-11-14 05:54:22 -080012
Oleh Prypinb708e932018-03-18 17:34:20 +010013MB is a wrapper script for GN that can be used to generate build files
kjellandera013a022016-11-14 05:54:22 -080014for sets of canned configurations and analyze them.
15"""
16
Jeremy Lecontead3f4902022-03-28 07:21:46 +000017import argparse
18import ast
19import errno
20import json
kjellandera013a022016-11-14 05:54:22 -080021import os
Jeremy Lecontead3f4902022-03-28 07:21:46 +000022import pipes
23import pprint
24import re
25import shutil
kjellandera013a022016-11-14 05:54:22 -080026import sys
Jeremy Lecontead3f4902022-03-28 07:21:46 +000027import subprocess
28import tempfile
29import traceback
30from urllib.request import urlopen
kjellandera013a022016-11-14 05:54:22 -080031
Jeremy Lecontead3f4902022-03-28 07:21:46 +000032SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
33SRC_DIR = os.path.dirname(os.path.dirname(SCRIPT_DIR))
34sys.path = [os.path.join(SRC_DIR, 'build')] + sys.path
kjellandera013a022016-11-14 05:54:22 -080035
Jeremy Lecontead3f4902022-03-28 07:21:46 +000036import gn_helpers
kjellandera013a022016-11-14 05:54:22 -080037
38
39def main(args):
Jeremy Lecontead3f4902022-03-28 07:21:46 +000040 mbw = MetaBuildWrapper()
Jeremy Lecontef22c78b2021-12-07 19:49:48 +010041 return mbw.Main(args)
kjellandera013a022016-11-14 05:54:22 -080042
43
Jeremy Lecontead3f4902022-03-28 07:21:46 +000044class MetaBuildWrapper:
Jeremy Lecontef22c78b2021-12-07 19:49:48 +010045 def __init__(self):
Jeremy Lecontead3f4902022-03-28 07:21:46 +000046 self.src_dir = SRC_DIR
47 self.default_config = os.path.join(SCRIPT_DIR, 'mb_config.pyl')
48 self.default_isolate_map = os.path.join(SCRIPT_DIR, 'gn_isolate_map.pyl')
49 self.executable = sys.executable
50 self.platform = sys.platform
51 self.sep = os.sep
52 self.args = argparse.Namespace()
53 self.configs = {}
54 self.builder_groups = {}
55 self.mixins = {}
56 self.isolate_exe = 'isolate.exe' if self.platform.startswith(
57 'win') else 'isolate'
58
59 def Main(self, args):
60 self.ParseArgs(args)
61 try:
62 ret = self.args.func()
63 if ret:
64 self.DumpInputFiles()
65 return ret
66 except KeyboardInterrupt:
67 self.Print('interrupted, exiting')
68 return 130
69 except Exception:
70 self.DumpInputFiles()
71 s = traceback.format_exc()
72 for l in s.splitlines():
73 self.Print(l)
74 return 1
75
76 def ParseArgs(self, argv):
77 def AddCommonOptions(subp):
78 subp.add_argument('-b',
79 '--builder',
80 help='builder name to look up config from')
81 subp.add_argument('-m',
82 '--builder-group',
83 help='builder group name to look up config from')
84 subp.add_argument('-c', '--config', help='configuration to analyze')
85 subp.add_argument('--phase',
86 help='optional phase name (used when builders '
87 'do multiple compiles with different '
88 'arguments in a single build)')
89 subp.add_argument('-f',
90 '--config-file',
91 metavar='PATH',
92 default=self.default_config,
93 help='path to config file '
94 '(default is %(default)s)')
95 subp.add_argument('-i',
96 '--isolate-map-file',
97 metavar='PATH',
98 default=self.default_isolate_map,
99 help='path to isolate map file '
100 '(default is %(default)s)')
101 subp.add_argument('-r',
102 '--realm',
103 default='webrtc:try',
104 help='optional LUCI realm to use (for example '
105 'when triggering tasks on Swarming)')
106 subp.add_argument('-g', '--goma-dir', help='path to goma directory')
107 subp.add_argument('--android-version-code',
108 help='Sets GN arg android_default_version_code')
109 subp.add_argument('--android-version-name',
110 help='Sets GN arg android_default_version_name')
111 subp.add_argument('-n',
112 '--dryrun',
113 action='store_true',
114 help='Do a dry run (i.e., do nothing, just '
115 'print the commands that will run)')
116 subp.add_argument('-v',
117 '--verbose',
118 action='store_true',
119 help='verbose logging')
120
121 parser = argparse.ArgumentParser(prog='mb')
122 subps = parser.add_subparsers()
123
124 subp = subps.add_parser('analyze',
125 help='analyze whether changes to a set of '
126 'files will cause a set of binaries '
127 'to be rebuilt.')
128 AddCommonOptions(subp)
129 subp.add_argument('path', nargs=1, help='path build was generated into.')
130 subp.add_argument('input_path',
131 nargs=1,
132 help='path to a file containing the input '
133 'arguments as a JSON object.')
134 subp.add_argument('output_path',
135 nargs=1,
136 help='path to a file containing the output '
137 'arguments as a JSON object.')
138 subp.add_argument('--json-output', help='Write errors to json.output')
139 subp.set_defaults(func=self.CmdAnalyze)
140
141 subp = subps.add_parser('export',
142 help='print out the expanded configuration for'
143 'each builder as a JSON object')
144 subp.add_argument('-f',
145 '--config-file',
146 metavar='PATH',
147 default=self.default_config,
148 help='path to config file (default is %(default)s)')
149 subp.add_argument('-g', '--goma-dir', help='path to goma directory')
150 subp.set_defaults(func=self.CmdExport)
151
152 subp = subps.add_parser('gen', help='generate a new set of build files')
153 AddCommonOptions(subp)
154 subp.add_argument('--swarming-targets-file',
155 help='save runtime dependencies for targets listed '
156 'in file.')
157 subp.add_argument('--json-output', help='Write errors to json.output')
158 subp.add_argument('path', nargs=1, help='path to generate build into')
159 subp.set_defaults(func=self.CmdGen)
160
161 subp = subps.add_parser('isolate',
162 help='generate the .isolate files for a given'
163 'binary')
164 AddCommonOptions(subp)
165 subp.add_argument('path', nargs=1, help='path build was generated into')
166 subp.add_argument('target',
167 nargs=1,
168 help='ninja target to generate the isolate for')
169 subp.set_defaults(func=self.CmdIsolate)
170
171 subp = subps.add_parser('lookup',
172 help='look up the command for a given config '
173 'or builder')
174 AddCommonOptions(subp)
175 subp.add_argument('--quiet',
176 default=False,
177 action='store_true',
178 help='Print out just the arguments, do '
179 'not emulate the output of the gen subcommand.')
180 subp.set_defaults(func=self.CmdLookup)
181
182 subp = subps.add_parser(
183 'run',
184 help='build and run the isolated version of a '
185 'binary',
186 formatter_class=argparse.RawDescriptionHelpFormatter)
187 subp.description = (
188 'Build, isolate, and run the given binary with the command line\n'
189 'listed in the isolate. You may pass extra arguments after the\n'
190 'target; use "--" if the extra arguments need to include switches.'
191 '\n\n'
192 'Examples:\n'
193 '\n'
194 ' % tools/mb/mb.py run -m chromium.linux -b "Linux Builder" \\\n'
195 ' //out/Default content_browsertests\n'
196 '\n'
197 ' % tools/mb/mb.py run out/Default content_browsertests\n'
198 '\n'
199 ' % tools/mb/mb.py run out/Default content_browsertests -- \\\n'
200 ' --test-launcher-retry-limit=0'
201 '\n')
202 AddCommonOptions(subp)
203 subp.add_argument('-j',
204 '--jobs',
205 dest='jobs',
206 type=int,
207 help='Number of jobs to pass to ninja')
208 subp.add_argument('--no-build',
209 dest='build',
210 default=True,
211 action='store_false',
212 help='Do not build, just isolate and run')
213 subp.add_argument('path',
214 nargs=1,
215 help=('path to generate build into (or use).'
216 ' This can be either a regular path or a '
217 'GN-style source-relative path like '
218 '//out/Default.'))
219 subp.add_argument('-s',
220 '--swarmed',
221 action='store_true',
222 help='Run under swarming')
223 subp.add_argument('-d',
224 '--dimension',
225 default=[],
226 action='append',
227 nargs=2,
228 dest='dimensions',
229 metavar='FOO bar',
230 help='dimension to filter on')
231 subp.add_argument('target', nargs=1, help='ninja target to build and run')
232 subp.add_argument('extra_args',
233 nargs='*',
234 help=('extra args to pass to the isolate to run. '
235 'Use "--" as the first arg if you need to '
236 'pass switches'))
237 subp.set_defaults(func=self.CmdRun)
238
239 subp = subps.add_parser('validate', help='validate the config file')
240 subp.add_argument('-f',
241 '--config-file',
242 metavar='PATH',
243 default=self.default_config,
244 help='path to config file (default is %(default)s)')
245 subp.set_defaults(func=self.CmdValidate)
246
247 subp = subps.add_parser('help', help='Get help on a subcommand.')
248 subp.add_argument(nargs='?',
249 action='store',
250 dest='subcommand',
251 help='The command to get help for.')
252 subp.set_defaults(func=self.CmdHelp)
253
254 self.args = parser.parse_args(argv)
255
256 def DumpInputFiles(self):
257 def DumpContentsOfFilePassedTo(arg_name, path):
258 if path and self.Exists(path):
259 self.Print("\n# To recreate the file passed to %s:" % arg_name)
260 self.Print("%% cat > %s <<EOF" % path)
261 contents = self.ReadFile(path)
262 self.Print(contents)
263 self.Print("EOF\n%\n")
264
265 if getattr(self.args, 'input_path', None):
266 DumpContentsOfFilePassedTo('argv[0] (input_path)',
267 self.args.input_path[0])
268 if getattr(self.args, 'swarming_targets_file', None):
269 DumpContentsOfFilePassedTo('--swarming-targets-file',
270 self.args.swarming_targets_file)
271
272 def CmdAnalyze(self):
273 vals = self.Lookup()
274 return self.RunGNAnalyze(vals)
275
276 def CmdExport(self):
277 self.ReadConfigFile()
278 obj = {}
279 for builder_group, builders in list(self.builder_groups.items()):
280 obj[builder_group] = {}
281 for builder in builders:
282 config = self.builder_groups[builder_group][builder]
283 if not config:
284 continue
285
286 if isinstance(config, dict):
287 args = {
288 k: self.FlattenConfig(v)['gn_args']
289 for k, v in list(config.items())
290 }
291 elif config.startswith('//'):
292 args = config
293 else:
294 args = self.FlattenConfig(config)['gn_args']
295 if 'error' in args:
296 continue
297
298 obj[builder_group][builder] = args
299
300 # Dump object and trim trailing whitespace.
301 s = '\n'.join(
302 l.rstrip()
303 for l in json.dumps(obj, sort_keys=True, indent=2).splitlines())
304 self.Print(s)
305 return 0
306
307 def CmdGen(self):
308 vals = self.Lookup()
309 return self.RunGNGen(vals)
310
311 def CmdHelp(self):
312 if self.args.subcommand:
313 self.ParseArgs([self.args.subcommand, '--help'])
314 else:
315 self.ParseArgs(['--help'])
316
317 def CmdIsolate(self):
318 vals = self.GetConfig()
319 if not vals:
320 return 1
321 return self.RunGNIsolate(vals)
322
323 def CmdLookup(self):
324 vals = self.Lookup()
325 gn_args = self.GNArgs(vals)
326 if self.args.quiet:
327 self.Print(gn_args, end='')
328 else:
329 cmd = self.GNCmd('gen', '_path_')
330 self.Print('\nWriting """\\\n%s""" to _path_/args.gn.\n' % gn_args)
331 env = None
332
333 self.PrintCmd(cmd, env)
334 return 0
335
336 def CmdRun(self):
337 vals = self.GetConfig()
338 if not vals:
339 return 1
340
341 build_dir = self.args.path[0]
342 target = self.args.target[0]
343
344 if self.args.build:
345 ret = self.Build(target)
346 if ret:
347 return ret
348 ret = self.RunGNIsolate(vals)
349 if ret:
350 return ret
351
352 if self.args.swarmed:
353 cmd, _ = self.GetSwarmingCommand(self.args.target[0], vals)
354 return self._RunUnderSwarming(build_dir, target, cmd)
355 return self._RunLocallyIsolated(build_dir, target)
356
357 def _RunUnderSwarming(self, build_dir, target, isolate_cmd):
358 cas_instance = 'chromium-swarm'
359 swarming_server = 'chromium-swarm.appspot.com'
360 # TODO(dpranke): Look up the information for the target in
361 # the //testing/buildbot.json file, if possible, so that we
362 # can determine the isolate target, command line, and additional
363 # swarming parameters, if possible.
364 #
365 # TODO(dpranke): Also, add support for sharding and merging results.
366 dimensions = []
367 for k, v in self.args.dimensions:
368 dimensions += ['-d', '%s=%s' % (k, v)]
369
370 archive_json_path = self.ToSrcRelPath('%s/%s.archive.json' %
371 (build_dir, target))
372 cmd = [
373 self.PathJoin(self.src_dir, 'tools', 'luci-go', self.isolate_exe),
374 'archive',
375 '-i',
376 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
377 '-cas-instance',
378 cas_instance,
379 '-dump-json',
380 archive_json_path,
381 ]
382
383 # Talking to the isolateserver may fail because we're not logged in.
384 # We trap the command explicitly and rewrite the error output so that
385 # the error message is actually correct for a Chromium check out.
386 self.PrintCmd(cmd, env=None)
387 ret, out, err = self.Run(cmd, force_verbose=False)
388 if ret:
389 self.Print(' -> returned %d' % ret)
390 if out:
391 self.Print(out, end='')
392 if err:
393 self.Print(err, end='', file=sys.stderr)
394
395 return ret
396
397 try:
398 archive_hashes = json.loads(self.ReadFile(archive_json_path))
399 except Exception:
400 self.Print('Failed to read JSON file "%s"' % archive_json_path,
401 file=sys.stderr)
402 return 1
403 try:
404 cas_digest = archive_hashes[target]
405 except Exception:
406 self.Print('Cannot find hash for "%s" in "%s", file content: %s' %
407 (target, archive_json_path, archive_hashes),
408 file=sys.stderr)
409 return 1
410
411 try:
412 json_dir = self.TempDir()
413 json_file = self.PathJoin(json_dir, 'task.json')
414
415 cmd = [
416 self.PathJoin('tools', 'luci-go', 'swarming'),
417 'trigger',
418 '-realm',
419 self.args.realm,
420 '-digest',
421 cas_digest,
422 '-server',
423 swarming_server,
424 '-tag=purpose:user-debug-mb',
425 '-relative-cwd',
426 self.ToSrcRelPath(build_dir),
427 '-dump-json',
428 json_file,
429 ] + dimensions + ['--'] + list(isolate_cmd)
430
431 if self.args.extra_args:
432 cmd += ['--'] + self.args.extra_args
433 self.Print('')
434 ret, _, _ = self.Run(cmd, force_verbose=True, buffer_output=False)
435 if ret:
436 return ret
437 task_json = self.ReadFile(json_file)
438 task_id = json.loads(task_json)["tasks"][0]['task_id']
439 finally:
440 if json_dir:
441 self.RemoveDirectory(json_dir)
442
443 cmd = [
444 self.PathJoin('tools', 'luci-go', 'swarming'),
445 'collect',
446 '-server',
447 swarming_server,
448 '-task-output-stdout=console',
449 task_id,
450 ]
451 ret, _, _ = self.Run(cmd, force_verbose=True, buffer_output=False)
452 return ret
453
454 def _RunLocallyIsolated(self, build_dir, target):
455 cmd = [
456 self.PathJoin(self.src_dir, 'tools', 'luci-go', self.isolate_exe),
457 'run',
458 '-i',
459 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
460 ]
461 if self.args.extra_args:
462 cmd += ['--'] + self.args.extra_args
463 ret, _, _ = self.Run(cmd, force_verbose=True, buffer_output=False)
464 return ret
465
466 def CmdValidate(self, print_ok=True):
467 errs = []
468
469 # Read the file to make sure it parses.
470 self.ReadConfigFile()
471
472 # Build a list of all of the configs referenced by builders.
473 all_configs = {}
474 for builder_group in self.builder_groups:
475 for config in list(self.builder_groups[builder_group].values()):
476 if isinstance(config, dict):
477 for c in list(config.values()):
478 all_configs[c] = builder_group
479 else:
480 all_configs[config] = builder_group
481
482 # Check that every referenced args file or config actually exists.
483 for config, loc in list(all_configs.items()):
484 if config.startswith('//'):
485 if not self.Exists(self.ToAbsPath(config)):
486 errs.append('Unknown args file "%s" referenced from "%s".' %
487 (config, loc))
488 elif not config in self.configs:
489 errs.append('Unknown config "%s" referenced from "%s".' % (config, loc))
490
491 # Check that every actual config is actually referenced.
492 for config in self.configs:
493 if not config in all_configs:
494 errs.append('Unused config "%s".' % config)
495
496 # Figure out the whole list of mixins, and check that every mixin
497 # listed by a config or another mixin actually exists.
498 referenced_mixins = set()
499 for config, mixins in list(self.configs.items()):
500 for mixin in mixins:
501 if not mixin in self.mixins:
502 errs.append('Unknown mixin "%s" referenced by config "%s".' %
503 (mixin, config))
504 referenced_mixins.add(mixin)
505
506 for mixin in self.mixins:
507 for sub_mixin in self.mixins[mixin].get('mixins', []):
508 if not sub_mixin in self.mixins:
509 errs.append('Unknown mixin "%s" referenced by mixin "%s".' %
510 (sub_mixin, mixin))
511 referenced_mixins.add(sub_mixin)
512
513 # Check that every mixin defined is actually referenced somewhere.
514 for mixin in self.mixins:
515 if not mixin in referenced_mixins:
516 errs.append('Unreferenced mixin "%s".' % mixin)
517
518 if errs:
519 raise MBErr(('mb config file %s has problems:' % self.args.config_file) +
520 '\n ' + '\n '.join(errs))
521
522 if print_ok:
523 self.Print('mb config file %s looks ok.' % self.args.config_file)
524 return 0
525
526 def GetConfig(self):
527 build_dir = self.args.path[0]
528
529 vals = self.DefaultVals()
530 if self.args.builder or self.args.builder_group or self.args.config:
531 vals = self.Lookup()
532 # Re-run gn gen in order to ensure the config is consistent with
533 # the build dir.
534 self.RunGNGen(vals)
535 return vals
536
537 toolchain_path = self.PathJoin(self.ToAbsPath(build_dir), 'toolchain.ninja')
538 if not self.Exists(toolchain_path):
539 self.Print('Must either specify a path to an existing GN build '
540 'dir or pass in a -m/-b pair or a -c flag to specify '
541 'the configuration')
542 return {}
543
544 vals['gn_args'] = self.GNArgsFromDir(build_dir)
545 return vals
546
547 def GNArgsFromDir(self, build_dir):
548 args_contents = ""
549 gn_args_path = self.PathJoin(self.ToAbsPath(build_dir), 'args.gn')
550 if self.Exists(gn_args_path):
551 args_contents = self.ReadFile(gn_args_path)
552 gn_args = []
553 for l in args_contents.splitlines():
554 fields = l.split(' ')
555 name = fields[0]
556 val = ' '.join(fields[2:])
557 gn_args.append('%s=%s' % (name, val))
558
559 return ' '.join(gn_args)
560
561 def Lookup(self):
562 self.ReadConfigFile()
563 config = self.ConfigFromArgs()
564 if config.startswith('//'):
565 if not self.Exists(self.ToAbsPath(config)):
566 raise MBErr('args file "%s" not found' % config)
567 vals = self.DefaultVals()
568 vals['args_file'] = config
569 else:
570 if not config in self.configs:
571 raise MBErr('Config "%s" not found in %s' %
572 (config, self.args.config_file))
573 vals = self.FlattenConfig(config)
574 return vals
575
576 def ReadConfigFile(self):
577 if not self.Exists(self.args.config_file):
578 raise MBErr('config file not found at %s' % self.args.config_file)
579
580 try:
581 contents = ast.literal_eval(self.ReadFile(self.args.config_file))
582 except SyntaxError as e:
583 raise MBErr('Failed to parse config file "%s"' %
584 self.args.config_file) from e
585
586 self.configs = contents['configs']
587 self.builder_groups = contents['builder_groups']
588 self.mixins = contents['mixins']
589
590 def ReadIsolateMap(self):
591 isolate_map = self.args.isolate_map_file
592 if not self.Exists(isolate_map):
593 raise MBErr('isolate map file not found at %s' % isolate_map)
594 try:
595 return ast.literal_eval(self.ReadFile(isolate_map))
596 except SyntaxError as e:
597 raise MBErr('Failed to parse isolate map file "%s"' % isolate_map) from e
598
599 def ConfigFromArgs(self):
600 if self.args.config:
601 if self.args.builder_group or self.args.builder:
602 raise MBErr('Can not specific both -c/--config and '
603 '-m/--builder-group or -b/--builder')
604
605 return self.args.config
606
607 if not self.args.builder_group or not self.args.builder:
608 raise MBErr('Must specify either -c/--config or '
609 '(-m/--builder-group and -b/--builder)')
610
611 if not self.args.builder_group in self.builder_groups:
612 raise MBErr('Master name "%s" not found in "%s"' %
613 (self.args.builder_group, self.args.config_file))
614
615 if not self.args.builder in self.builder_groups[self.args.builder_group]:
616 raise MBErr(
617 'Builder name "%s" not found under builder_groups[%s] in "%s"' %
618 (self.args.builder, self.args.builder_group, self.args.config_file))
619
620 config = (self.builder_groups[self.args.builder_group][self.args.builder])
621 if isinstance(config, dict):
622 if self.args.phase is None:
623 raise MBErr('Must specify a build --phase for %s on %s' %
624 (self.args.builder, self.args.builder_group))
625 phase = str(self.args.phase)
626 if phase not in config:
627 raise MBErr('Phase %s doesn\'t exist for %s on %s' %
628 (phase, self.args.builder, self.args.builder_group))
629 return config[phase]
630
631 if self.args.phase is not None:
632 raise MBErr('Must not specify a build --phase for %s on %s' %
633 (self.args.builder, self.args.builder_group))
634 return config
635
636 def FlattenConfig(self, config):
637 mixins = self.configs[config]
638 vals = self.DefaultVals()
639
640 visited = []
641 self.FlattenMixins(mixins, vals, visited)
642 return vals
643
644 @staticmethod
645 def DefaultVals():
646 return {
647 'args_file': '',
648 'cros_passthrough': False,
649 'gn_args': '',
650 }
651
652 def FlattenMixins(self, mixins, vals, visited):
653 for m in mixins:
654 if m not in self.mixins:
655 raise MBErr('Unknown mixin "%s"' % m)
656
657 visited.append(m)
658
659 mixin_vals = self.mixins[m]
660
661 if 'cros_passthrough' in mixin_vals:
662 vals['cros_passthrough'] = mixin_vals['cros_passthrough']
663 if 'gn_args' in mixin_vals:
664 if vals['gn_args']:
665 vals['gn_args'] += ' ' + mixin_vals['gn_args']
666 else:
667 vals['gn_args'] = mixin_vals['gn_args']
668
669 if 'mixins' in mixin_vals:
670 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
671 return vals
672
673 def RunGNGen(self, vals):
674 build_dir = self.args.path[0]
675
676 cmd = self.GNCmd('gen', build_dir, '--check')
677 gn_args = self.GNArgs(vals)
678
679 # Since GN hasn't run yet, the build directory may not even exist.
680 self.MaybeMakeDirectory(self.ToAbsPath(build_dir))
681
682 gn_args_path = self.ToAbsPath(build_dir, 'args.gn')
683 self.WriteFile(gn_args_path, gn_args, force_verbose=True)
684
685 swarming_targets = set()
686 if getattr(self.args, 'swarming_targets_file', None):
687 # We need GN to generate the list of runtime dependencies for
688 # the compile targets listed (one per line) in the file so
689 # we can run them via swarming. We use gn_isolate_map.pyl to
690 # convert the compile targets to the matching GN labels.
691 path = self.args.swarming_targets_file
692 if not self.Exists(path):
693 self.WriteFailureAndRaise('"%s" does not exist' % path,
694 output_path=None)
695 contents = self.ReadFile(path)
696 swarming_targets = set(contents.splitlines())
697
698 isolate_map = self.ReadIsolateMap()
699 err, labels = self.MapTargetsToLabels(isolate_map, swarming_targets)
700 if err:
701 raise MBErr(err)
702
703 gn_runtime_deps_path = self.ToAbsPath(build_dir, 'runtime_deps')
704 self.WriteFile(gn_runtime_deps_path, '\n'.join(labels) + '\n')
705 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path)
706
707 ret, output, _ = self.Run(cmd)
708 if ret:
709 if self.args.json_output:
710 # write errors to json.output
711 self.WriteJSON({'output': output}, self.args.json_output)
712 # If `gn gen` failed, we should exit early rather than trying to
713 # generate isolates. Run() will have already logged any error
714 # output.
715 self.Print('GN gen failed: %d' % ret)
716 return ret
717
718 android = 'target_os="android"' in vals['gn_args']
719 for target in swarming_targets:
720 if android:
721 # Android targets may be either android_apk or executable. The
722 # former will result in runtime_deps associated with the stamp
723 # file, while the latter will result in runtime_deps associated
724 # with the executable.
725 label = isolate_map[target]['label']
726 runtime_deps_targets = [
727 target + '.runtime_deps',
728 'obj/%s.stamp.runtime_deps' % label.replace(':', '/')
729 ]
730 elif isolate_map[target]['type'] == 'gpu_browser_test':
731 if self.platform == 'win32':
732 runtime_deps_targets = ['browser_tests.exe.runtime_deps']
733 else:
734 runtime_deps_targets = ['browser_tests.runtime_deps']
735 elif isolate_map[target]['type'] == 'script':
736 label = isolate_map[target]['label'].split(':')[1]
737 runtime_deps_targets = ['%s.runtime_deps' % label]
738 if self.platform == 'win32':
739 runtime_deps_targets += [label + '.exe.runtime_deps']
740 else:
741 runtime_deps_targets += [label + '.runtime_deps']
742 elif self.platform == 'win32':
743 runtime_deps_targets = [target + '.exe.runtime_deps']
744 else:
745 runtime_deps_targets = [target + '.runtime_deps']
746
747 for r in runtime_deps_targets:
748 runtime_deps_path = self.ToAbsPath(build_dir, r)
749 if self.Exists(runtime_deps_path):
750 break
751 else:
752 raise MBErr('did not generate any of %s' %
753 ', '.join(runtime_deps_targets))
754
755 command, extra_files = self.GetSwarmingCommand(target, vals)
756
757 runtime_deps = self.ReadFile(runtime_deps_path).splitlines()
758
759 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
760 extra_files)
761
762 return 0
763
764 def RunGNIsolate(self, vals):
765 target = self.args.target[0]
766 isolate_map = self.ReadIsolateMap()
767 err, labels = self.MapTargetsToLabels(isolate_map, [target])
768 if err:
769 raise MBErr(err)
770 label = labels[0]
771
772 build_dir = self.args.path[0]
773 command, extra_files = self.GetSwarmingCommand(target, vals)
774
775 cmd = self.GNCmd('desc', build_dir, label, 'runtime_deps')
776 ret, out, _ = self.Call(cmd)
777 if ret:
778 if out:
779 self.Print(out)
780 return ret
781
782 runtime_deps = out.splitlines()
783
784 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
785 extra_files)
786
787 ret, _, _ = self.Run([
788 self.PathJoin(self.src_dir, 'tools', 'luci-go', self.isolate_exe),
789 'check', '-i',
790 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target))
791 ],
792 buffer_output=False)
793
794 return ret
795
796 def WriteIsolateFiles(self, build_dir, command, target, runtime_deps,
797 extra_files):
798 isolate_path = self.ToAbsPath(build_dir, target + '.isolate')
799 self.WriteFile(
800 isolate_path,
801 pprint.pformat({
802 'variables': {
803 'command': command,
804 'files': sorted(runtime_deps + extra_files),
805 }
806 }) + '\n')
807
808 self.WriteJSON(
809 {
810 'args': [
811 '--isolate',
812 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
813 ],
814 'dir':
815 self.src_dir,
816 'version':
817 1,
818 },
819 isolate_path + 'd.gen.json',
820 )
821
822 @staticmethod
823 def MapTargetsToLabels(isolate_map, targets):
824 labels = []
825 err = ''
826
827 def StripTestSuffixes(target):
828 for suffix in ('_apk_run', '_apk', '_run'):
829 if target.endswith(suffix):
830 return target[:-len(suffix)], suffix
831 return None, None
832
833 for target in targets:
834 if target == 'all':
835 labels.append(target)
836 elif target.startswith('//'):
837 labels.append(target)
838 else:
839 if target in isolate_map:
840 stripped_target, suffix = target, ''
841 else:
842 stripped_target, suffix = StripTestSuffixes(target)
843 if stripped_target in isolate_map:
844 if isolate_map[stripped_target]['type'] == 'unknown':
845 err += ('test target "%s" type is unknown\n' % target)
846 else:
847 labels.append(isolate_map[stripped_target]['label'] + suffix)
848 else:
849 err += ('target "%s" not found in '
850 '//testing/buildbot/gn_isolate_map.pyl\n' % target)
851
852 return err, labels
853
854 def GNCmd(self, subcommand, path, *args):
855 if self.platform.startswith('linux'):
856 subdir, exe = 'linux64', 'gn'
857 elif self.platform == 'darwin':
858 subdir, exe = 'mac', 'gn'
859 else:
860 subdir, exe = 'win', 'gn.exe'
861
862 gn_path = self.PathJoin(self.src_dir, 'buildtools', subdir, exe)
863 return [gn_path, subcommand, path] + list(args)
864
865 def GNArgs(self, vals):
866 if vals['cros_passthrough']:
867 if not 'GN_ARGS' in os.environ:
868 raise MBErr('MB is expecting GN_ARGS to be in the environment')
869 gn_args = os.environ['GN_ARGS']
870 if not re.search('target_os.*=.*"chromeos"', gn_args):
871 raise MBErr('GN_ARGS is missing target_os = "chromeos": '
872 '(GN_ARGS=%s)' % gn_args)
873 else:
874 gn_args = vals['gn_args']
875
876 if self.args.goma_dir:
877 gn_args += ' goma_dir="%s"' % self.args.goma_dir
878
879 android_version_code = self.args.android_version_code
880 if android_version_code:
881 gn_args += (' android_default_version_code="%s"' % android_version_code)
882
883 android_version_name = self.args.android_version_name
884 if android_version_name:
885 gn_args += (' android_default_version_name="%s"' % android_version_name)
886
887 # Canonicalize the arg string into a sorted, newline-separated list
888 # of key-value pairs, and de-dup the keys if need be so that only
889 # the last instance of each arg is listed.
890 gn_args = gn_helpers.ToGNString(gn_helpers.FromGNArgs(gn_args))
891
892 args_file = vals.get('args_file', None)
893 if args_file:
894 gn_args = ('import("%s")\n' % vals['args_file']) + gn_args
895 return gn_args
Jeremy Lecontef22c78b2021-12-07 19:49:48 +0100896
897 def GetSwarmingCommand(self, target, vals):
898 isolate_map = self.ReadIsolateMap()
899 test_type = isolate_map[target]['type']
900
901 is_android = 'target_os="android"' in vals['gn_args']
902 is_linux = self.platform.startswith('linux') and not is_android
Jeremy Leconted15f3e12022-02-18 10:16:32 +0100903 is_ios = 'target_os="ios"' in vals['gn_args']
Jeremy Lecontef22c78b2021-12-07 19:49:48 +0100904
905 if test_type == 'nontest':
906 self.WriteFailureAndRaise('We should not be isolating %s.' % target,
907 output_path=None)
908 if test_type not in ('console_test_launcher', 'windowed_test_launcher',
909 'non_parallel_console_test_launcher', 'raw',
910 'additional_compile_target', 'junit_test', 'script'):
911 self.WriteFailureAndRaise('No command line for '
912 '%s found (test type %s).' %
913 (target, test_type),
914 output_path=None)
915
916 cmdline = []
917 extra_files = [
Mirko Bonadei5d9ae862022-01-27 20:18:16 +0100918 '../../.vpython3',
Jeremy Lecontef22c78b2021-12-07 19:49:48 +0100919 '../../testing/test_env.py',
920 ]
Mirko Bonadei5d9ae862022-01-27 20:18:16 +0100921 vpython_exe = 'vpython3'
Jeremy Lecontef22c78b2021-12-07 19:49:48 +0100922
923 must_retry = False
924 if test_type == 'script':
925 cmdline += [
926 vpython_exe,
927 '../../' + self.ToSrcRelPath(isolate_map[target]['script'])
928 ]
929 elif is_android:
930 cmdline += [
931 vpython_exe, '../../build/android/test_wrapper/logdog_wrapper.py',
932 '--target', target, '--logdog-bin-cmd', '../../bin/logdog_butler',
933 '--logcat-output-file', '${ISOLATED_OUTDIR}/logcats',
934 '--store-tombstones'
935 ]
Jeremy Leconted15f3e12022-02-18 10:16:32 +0100936 elif is_ios:
937 cmdline += [
938 vpython_exe, '../../tools_webrtc/flags_compatibility.py',
939 'bin/run_%s' % target, '--out-dir', '${ISOLATED_OUTDIR}'
940 ]
941 extra_files.append('../../tools_webrtc/flags_compatibility.py')
Jeremy Lecontef22c78b2021-12-07 19:49:48 +0100942 else:
943 if test_type == 'raw':
944 cmdline += [vpython_exe, '../../tools_webrtc/flags_compatibility.py']
945 extra_files.append('../../tools_webrtc/flags_compatibility.py')
946
947 if isolate_map[target].get('use_webcam', False):
948 cmdline += [
949 vpython_exe, '../../tools_webrtc/ensure_webcam_is_running.py'
950 ]
951 extra_files.append('../../tools_webrtc/ensure_webcam_is_running.py')
952
953 # is_linux uses use_ozone and x11 by default.
954 use_x11 = is_linux
955
956 xvfb = use_x11 and test_type == 'windowed_test_launcher'
957 if xvfb:
958 cmdline += [vpython_exe, '../../testing/xvfb.py']
959 extra_files.append('../../testing/xvfb.py')
960 else:
961 cmdline += [vpython_exe, '../../testing/test_env.py']
962
963 if test_type != 'raw':
964 extra_files += [
965 '../../third_party/gtest-parallel/gtest-parallel',
966 '../../third_party/gtest-parallel/gtest_parallel.py',
967 '../../tools_webrtc/gtest-parallel-wrapper.py',
968 ]
969 sep = '\\' if self.platform == 'win32' else '/'
970 output_dir = '${ISOLATED_OUTDIR}' + sep + 'test_logs'
Jeremy Lecontef22c78b2021-12-07 19:49:48 +0100971 timeout = isolate_map[target].get('timeout', 900)
972 cmdline += [
973 '../../tools_webrtc/gtest-parallel-wrapper.py',
974 '--output_dir=%s' % output_dir,
Jeremy Lecontef22c78b2021-12-07 19:49:48 +0100975 '--gtest_color=no',
976 # We tell gtest-parallel to interrupt the test after 900
977 # seconds, so it can exit cleanly and report results,
978 # instead of being interrupted by swarming and not
979 # reporting anything.
980 '--timeout=%s' % timeout,
981 ]
982 if test_type == 'non_parallel_console_test_launcher':
983 # Still use the gtest-parallel-wrapper.py script since we
984 # need it to run tests on swarming, but don't execute tests
985 # in parallel.
986 cmdline.append('--workers=1')
987 must_retry = True
988
989 asan = 'is_asan=true' in vals['gn_args']
990 lsan = 'is_lsan=true' in vals['gn_args']
991 msan = 'is_msan=true' in vals['gn_args']
992 tsan = 'is_tsan=true' in vals['gn_args']
993 sanitizer = asan or lsan or msan or tsan
994 if must_retry and not sanitizer:
995 # Retry would hide most sanitizers detections.
996 cmdline.append('--retry_failed=3')
997
998 executable_prefix = '.\\' if self.platform == 'win32' else './'
999 executable_suffix = '.exe' if self.platform == 'win32' else ''
1000 executable = executable_prefix + target + executable_suffix
1001
1002 cmdline.append(executable)
1003
1004 cmdline.extend([
1005 '--asan=%d' % asan,
1006 '--lsan=%d' % lsan,
1007 '--msan=%d' % msan,
1008 '--tsan=%d' % tsan,
1009 ])
1010
1011 cmdline += isolate_map[target].get('args', [])
1012
1013 return cmdline, extra_files
1014
Jeremy Lecontead3f4902022-03-28 07:21:46 +00001015 def ToAbsPath(self, build_path, *comps):
1016 return self.PathJoin(self.src_dir, self.ToSrcRelPath(build_path), *comps)
1017
1018 def ToSrcRelPath(self, path):
1019 """Returns a relative path from the top of the repo."""
1020 if path.startswith('//'):
1021 return path[2:].replace('/', self.sep)
1022 return self.RelPath(path, self.src_dir)
1023
1024 def RunGNAnalyze(self, vals):
1025 # Analyze runs before 'gn gen' now, so we need to run gn gen
1026 # in order to ensure that we have a build directory.
1027 ret = self.RunGNGen(vals)
1028 if ret:
1029 return ret
1030
1031 build_path = self.args.path[0]
1032 input_path = self.args.input_path[0]
1033 gn_input_path = input_path + '.gn'
1034 output_path = self.args.output_path[0]
1035 gn_output_path = output_path + '.gn'
1036
1037 inp = self.ReadInputJSON(
1038 ['files', 'test_targets', 'additional_compile_targets'])
1039 if self.args.verbose:
1040 self.Print()
1041 self.Print('analyze input:')
1042 self.PrintJSON(inp)
1043 self.Print()
1044
1045 # This shouldn't normally happen, but could due to unusual race
1046 # conditions, like a try job that gets scheduled before a patch
1047 # lands but runs after the patch has landed.
1048 if not inp['files']:
1049 self.Print('Warning: No files modified in patch, bailing out early.')
1050 self.WriteJSON(
1051 {
1052 'status': 'No dependency',
1053 'compile_targets': [],
1054 'test_targets': [],
1055 }, output_path)
1056 return 0
1057
1058 gn_inp = {}
1059 gn_inp['files'] = ['//' + f for f in inp['files'] if not f.startswith('//')]
1060
1061 isolate_map = self.ReadIsolateMap()
1062 err, gn_inp['additional_compile_targets'] = self.MapTargetsToLabels(
1063 isolate_map, inp['additional_compile_targets'])
1064 if err:
1065 raise MBErr(err)
1066
1067 err, gn_inp['test_targets'] = self.MapTargetsToLabels(
1068 isolate_map, inp['test_targets'])
1069 if err:
1070 raise MBErr(err)
1071 labels_to_targets = {}
1072 for i, label in enumerate(gn_inp['test_targets']):
1073 labels_to_targets[label] = inp['test_targets'][i]
1074
1075 try:
1076 self.WriteJSON(gn_inp, gn_input_path)
1077 cmd = self.GNCmd('analyze', build_path, gn_input_path, gn_output_path)
1078 ret, output, _ = self.Run(cmd, force_verbose=True)
1079 if ret:
1080 if self.args.json_output:
1081 # write errors to json.output
1082 self.WriteJSON({'output': output}, self.args.json_output)
1083 return ret
1084
1085 gn_outp_str = self.ReadFile(gn_output_path)
1086 try:
1087 gn_outp = json.loads(gn_outp_str)
1088 except Exception as e:
1089 self.Print("Failed to parse the JSON string GN "
1090 "returned: %s\n%s" % (repr(gn_outp_str), str(e)))
1091 raise
1092
1093 outp = {}
1094 if 'status' in gn_outp:
1095 outp['status'] = gn_outp['status']
1096 if 'error' in gn_outp:
1097 outp['error'] = gn_outp['error']
1098 if 'invalid_targets' in gn_outp:
1099 outp['invalid_targets'] = gn_outp['invalid_targets']
1100 if 'compile_targets' in gn_outp:
1101 if 'all' in gn_outp['compile_targets']:
1102 outp['compile_targets'] = ['all']
1103 else:
1104 outp['compile_targets'] = [
1105 label.replace('//', '') for label in gn_outp['compile_targets']
1106 ]
1107 if 'test_targets' in gn_outp:
1108 outp['test_targets'] = [
1109 labels_to_targets[label] for label in gn_outp['test_targets']
1110 ]
1111
1112 if self.args.verbose:
1113 self.Print()
1114 self.Print('analyze output:')
1115 self.PrintJSON(outp)
1116 self.Print()
1117
1118 self.WriteJSON(outp, output_path)
1119
1120 finally:
1121 if self.Exists(gn_input_path):
1122 self.RemoveFile(gn_input_path)
1123 if self.Exists(gn_output_path):
1124 self.RemoveFile(gn_output_path)
1125
1126 return 0
1127
1128 def ReadInputJSON(self, required_keys):
1129 path = self.args.input_path[0]
1130 output_path = self.args.output_path[0]
1131 if not self.Exists(path):
1132 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
1133
1134 try:
1135 inp = json.loads(self.ReadFile(path))
1136 except Exception as e:
1137 self.WriteFailureAndRaise(
1138 'Failed to read JSON input from "%s": %s' % (path, e), output_path)
1139
1140 for k in required_keys:
1141 if not k in inp:
1142 self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
1143 output_path)
1144
1145 return inp
1146
1147 def WriteFailureAndRaise(self, msg, output_path):
1148 if output_path:
1149 self.WriteJSON({'error': msg}, output_path, force_verbose=True)
1150 raise MBErr(msg)
1151
1152 def WriteJSON(self, obj, path, force_verbose=False):
1153 try:
1154 self.WriteFile(path,
1155 json.dumps(obj, indent=2, sort_keys=True) + '\n',
1156 force_verbose=force_verbose)
1157 except Exception as e:
1158 raise MBErr('Error writing to the output path "%s"' % path) from e
1159
1160 def PrintCmd(self, cmd, env):
1161 if self.platform == 'win32':
1162 env_prefix = 'set '
1163 env_quoter = QuoteForSet
1164 shell_quoter = QuoteForCmd
1165 else:
1166 env_prefix = ''
1167 env_quoter = pipes.quote
1168 shell_quoter = pipes.quote
1169
1170 var = 'LLVM_FORCE_HEAD_REVISION'
1171 if env and var in env:
1172 self.Print('%s%s=%s' % (env_prefix, var, env_quoter(env[var])))
1173
1174 if cmd[0] == self.executable:
1175 cmd = ['vpython3'] + cmd[1:]
1176 self.Print(*[shell_quoter(arg) for arg in cmd])
1177
1178 def PrintJSON(self, obj):
1179 self.Print(json.dumps(obj, indent=2, sort_keys=True))
1180
1181 def Build(self, target):
1182 build_dir = self.ToSrcRelPath(self.args.path[0])
1183 ninja_cmd = ['ninja', '-C', build_dir]
1184 if self.args.jobs:
1185 ninja_cmd.extend(['-j', '%d' % self.args.jobs])
1186 ninja_cmd.append(target)
1187 ret, _, _ = self.Run(ninja_cmd, force_verbose=False, buffer_output=False)
1188 return ret
1189
1190 def Run(self, cmd, env=None, force_verbose=True, buffer_output=True):
1191 # This function largely exists so it can be overridden for testing.
1192 if self.args.dryrun or self.args.verbose or force_verbose:
1193 self.PrintCmd(cmd, env)
1194 if self.args.dryrun:
1195 return 0, '', ''
1196
1197 ret, out, err = self.Call(cmd, env=env, buffer_output=buffer_output)
1198 if self.args.verbose or force_verbose:
1199 if ret:
1200 self.Print(' -> returned %d' % ret)
1201 if out:
1202 self.Print(out, end='')
1203 if err:
1204 self.Print(err, end='', file=sys.stderr)
1205 return ret, out, err
1206
1207 def Call(self, cmd, env=None, buffer_output=True):
1208 if buffer_output:
1209 p = subprocess.Popen(cmd,
1210 shell=False,
1211 cwd=self.src_dir,
1212 stdout=subprocess.PIPE,
1213 stderr=subprocess.PIPE,
1214 env=env)
1215 out, err = p.communicate()
1216 out = out.decode('utf-8')
1217 err = err.decode('utf-8')
1218 else:
1219 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir, env=env)
1220 p.wait()
1221 out = err = ''
1222 return p.returncode, out, err
1223
1224 @staticmethod
1225 def ExpandUser(path):
1226 # This function largely exists so it can be overridden for testing.
1227 return os.path.expanduser(path)
1228
1229 @staticmethod
1230 def Exists(path):
1231 # This function largely exists so it can be overridden for testing.
1232 return os.path.exists(path)
1233
1234 @staticmethod
1235 def Fetch(url):
1236 # This function largely exists so it can be overridden for testing.
1237 f = urlopen(url)
1238 contents = f.read()
1239 f.close()
1240 return contents
1241
1242 @staticmethod
1243 def MaybeMakeDirectory(path):
1244 try:
1245 os.makedirs(path)
1246 except OSError as e:
1247 if e.errno != errno.EEXIST:
1248 raise
1249
1250 @staticmethod
1251 def PathJoin(*comps):
1252 # This function largely exists so it can be overriden for testing.
1253 return os.path.join(*comps)
1254
1255 @staticmethod
1256 def Print(*args, **kwargs):
1257 # This function largely exists so it can be overridden for testing.
1258 print(*args, **kwargs)
1259 if kwargs.get('stream', sys.stdout) == sys.stdout:
1260 sys.stdout.flush()
1261
1262 @staticmethod
1263 def ReadFile(path):
1264 # This function largely exists so it can be overriden for testing.
1265 with open(path) as fp:
1266 return fp.read()
1267
1268 @staticmethod
1269 def RelPath(path, start='.'):
1270 # This function largely exists so it can be overriden for testing.
1271 return os.path.relpath(path, start)
1272
1273 @staticmethod
1274 def RemoveFile(path):
1275 # This function largely exists so it can be overriden for testing.
1276 os.remove(path)
1277
1278 def RemoveDirectory(self, abs_path):
1279 if self.platform == 'win32':
1280 # In other places in chromium, we often have to retry this command
1281 # because we're worried about other processes still holding on to
1282 # file handles, but when MB is invoked, it will be early enough in
1283 # the build that their should be no other processes to interfere.
1284 # We can change this if need be.
1285 self.Run(['cmd.exe', '/c', 'rmdir', '/q', '/s', abs_path])
1286 else:
1287 shutil.rmtree(abs_path, ignore_errors=True)
1288
1289 @staticmethod
1290 def TempDir():
1291 # This function largely exists so it can be overriden for testing.
1292 return tempfile.mkdtemp(prefix='mb_')
1293
1294 @staticmethod
1295 def TempFile(mode='w'):
1296 # This function largely exists so it can be overriden for testing.
1297 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
1298
1299 def WriteFile(self, path, contents, force_verbose=False):
1300 # This function largely exists so it can be overriden for testing.
1301 if self.args.dryrun or self.args.verbose or force_verbose:
1302 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
1303 with open(path, 'w') as fp:
1304 return fp.write(contents)
1305
1306
1307class MBErr(Exception):
1308 pass
1309
1310
1311# See http://goo.gl/l5NPDW and http://goo.gl/4Diozm for the painful
1312# details of this next section, which handles escaping command lines
1313# so that they can be copied and pasted into a cmd window.
1314UNSAFE_FOR_SET = set('^<>&|')
1315UNSAFE_FOR_CMD = UNSAFE_FOR_SET.union(set('()%'))
1316ALL_META_CHARS = UNSAFE_FOR_CMD.union(set('"'))
1317
1318
1319def QuoteForSet(arg):
1320 if any(a in UNSAFE_FOR_SET for a in arg):
1321 arg = ''.join('^' + a if a in UNSAFE_FOR_SET else a for a in arg)
1322 return arg
1323
1324
1325def QuoteForCmd(arg):
1326 # First, escape the arg so that CommandLineToArgvW will parse it properly.
1327 if arg == '' or ' ' in arg or '"' in arg:
1328 quote_re = re.compile(r'(\\*)"')
1329 arg = '"%s"' % (quote_re.sub(lambda mo: 2 * mo.group(1) + '\\"', arg))
1330
1331 # Then check to see if the arg contains any metacharacters other than
1332 # double quotes; if it does, quote everything (including the double
1333 # quotes) for safety.
1334 if any(a in UNSAFE_FOR_CMD for a in arg):
1335 arg = ''.join('^' + a if a in ALL_META_CHARS else a for a in arg)
1336 return arg
1337
1338
kjellandera013a022016-11-14 05:54:22 -08001339if __name__ == '__main__':
Jeremy Lecontef22c78b2021-12-07 19:49:48 +01001340 sys.exit(main(sys.argv[1:]))