2 # Copyright 2015 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """MB - the Meta-Build wrapper around GYP and GN
8 MB is a wrapper script for GYP and GN that can be used to generate build files
9 for sets of canned configurations and analyze them.
12 from __future__
import print_function
28 mbw
= MetaBuildWrapper()
30 return mbw
.args
.func()
33 class MetaBuildWrapper(object):
37 self
.chromium_src_dir
= p
.normpath(d(d(d(p
.abspath(__file__
)))))
38 self
.default_config
= p
.join(self
.chromium_src_dir
, 'tools', 'mb',
40 self
.executable
= sys
.executable
41 self
.platform
= sys
.platform
43 self
.args
= argparse
.Namespace()
47 self
.private_configs
= []
48 self
.common_dev_configs
= []
49 self
.unsupported_configs
= []
51 def ParseArgs(self
, argv
):
52 def AddCommonOptions(subp
):
53 subp
.add_argument('-b', '--builder',
54 help='builder name to look up config from')
55 subp
.add_argument('-m', '--master',
56 help='master name to look up config from')
57 subp
.add_argument('-c', '--config',
58 help='configuration to analyze')
59 subp
.add_argument('-f', '--config-file', metavar
='PATH',
60 default
=self
.default_config
,
61 help='path to config file '
62 '(default is //tools/mb/mb_config.pyl)')
63 subp
.add_argument('-g', '--goma-dir', default
=self
.ExpandUser('~/goma'),
64 help='path to goma directory (default is %(default)s).')
65 subp
.add_argument('-n', '--dryrun', action
='store_true',
66 help='Do a dry run (i.e., do nothing, just print '
67 'the commands that will run)')
68 subp
.add_argument('-v', '--verbose', action
='store_true',
69 help='verbose logging')
71 parser
= argparse
.ArgumentParser(prog
='mb')
72 subps
= parser
.add_subparsers()
74 subp
= subps
.add_parser('analyze',
75 help='analyze whether changes to a set of files '
76 'will cause a set of binaries to be rebuilt.')
77 AddCommonOptions(subp
)
78 subp
.add_argument('--swarming-targets-file',
79 help='save runtime dependencies for targets listed '
81 subp
.add_argument('path', nargs
=1,
82 help='path build was generated into.')
83 subp
.add_argument('input_path', nargs
=1,
84 help='path to a file containing the input arguments '
86 subp
.add_argument('output_path', nargs
=1,
87 help='path to a file containing the output arguments '
89 subp
.set_defaults(func
=self
.CmdAnalyze
)
91 subp
= subps
.add_parser('gen',
92 help='generate a new set of build files')
93 AddCommonOptions(subp
)
94 subp
.add_argument('--swarming-targets-file',
95 help='save runtime dependencies for targets listed '
97 subp
.add_argument('path', nargs
=1,
98 help='path to generate build into')
99 subp
.set_defaults(func
=self
.CmdGen
)
101 subp
= subps
.add_parser('lookup',
102 help='look up the command for a given config or '
104 AddCommonOptions(subp
)
105 subp
.set_defaults(func
=self
.CmdLookup
)
107 subp
= subps
.add_parser('validate',
108 help='validate the config file')
109 subp
.add_argument('-f', '--config-file', metavar
='PATH',
110 default
=self
.default_config
,
111 help='path to config file '
112 '(default is //tools/mb/mb_config.pyl)')
113 subp
.set_defaults(func
=self
.CmdValidate
)
115 subp
= subps
.add_parser('help',
116 help='Get help on a subcommand.')
117 subp
.add_argument(nargs
='?', action
='store', dest
='subcommand',
118 help='The command to get help for.')
119 subp
.set_defaults(func
=self
.CmdHelp
)
121 self
.args
= parser
.parse_args(argv
)
123 def CmdAnalyze(self
):
124 vals
= self
.GetConfig()
125 if vals
['type'] == 'gn':
126 return self
.RunGNAnalyze(vals
)
127 elif vals
['type'] == 'gyp':
128 return self
.RunGYPAnalyze(vals
)
130 raise MBErr('Unknown meta-build type "%s"' % vals
['type'])
133 vals
= self
.GetConfig()
135 self
.ClobberIfNeeded(vals
)
137 if vals
['type'] == 'gn':
138 return self
.RunGNGen(vals
)
139 if vals
['type'] == 'gyp':
140 return self
.RunGYPGen(vals
)
142 raise MBErr('Unknown meta-build type "%s"' % vals
['type'])
145 vals
= self
.GetConfig()
146 if vals
['type'] == 'gn':
147 cmd
= self
.GNCmd('gen', '<path>', vals
['gn_args'])
148 elif vals
['type'] == 'gyp':
149 if vals
['gyp_crosscompile']:
150 self
.Print('GYP_CROSSCOMPILE=1')
151 cmd
= self
.GYPCmd('<path>', vals
['gyp_defines'])
153 raise MBErr('Unknown meta-build type "%s"' % vals
['type'])
159 if self
.args
.subcommand
:
160 self
.ParseArgs([self
.args
.subcommand
, '--help'])
162 self
.ParseArgs(['--help'])
164 def CmdValidate(self
):
167 # Read the file to make sure it parses.
168 self
.ReadConfigFile()
170 # Figure out the whole list of configs and ensure that no config is
171 # listed in more than one category.
173 for config
in self
.common_dev_configs
:
174 all_configs
[config
] = 'common_dev_configs'
175 for config
in self
.private_configs
:
176 if config
in all_configs
:
177 errs
.append('config "%s" listed in "private_configs" also '
178 'listed in "%s"' % (config
, all_configs
['config']))
180 all_configs
[config
] = 'private_configs'
181 for config
in self
.unsupported_configs
:
182 if config
in all_configs
:
183 errs
.append('config "%s" listed in "unsupported_configs" also '
184 'listed in "%s"' % (config
, all_configs
['config']))
186 all_configs
[config
] = 'unsupported_configs'
188 for master
in self
.masters
:
189 for builder
in self
.masters
[master
]:
190 config
= self
.masters
[master
][builder
]
191 if config
in all_configs
and all_configs
[config
] not in self
.masters
:
192 errs
.append('Config "%s" used by a bot is also listed in "%s".' %
193 (config
, all_configs
[config
]))
195 all_configs
[config
] = master
197 # Check that every referenced config actually exists.
198 for config
, loc
in all_configs
.items():
199 if not config
in self
.configs
:
200 errs
.append('Unknown config "%s" referenced from "%s".' %
203 # Check that every actual config is actually referenced.
204 for config
in self
.configs
:
205 if not config
in all_configs
:
206 errs
.append('Unused config "%s".' % config
)
208 # Figure out the whole list of mixins, and check that every mixin
209 # listed by a config or another mixin actually exists.
210 referenced_mixins
= set()
211 for config
, mixins
in self
.configs
.items():
213 if not mixin
in self
.mixins
:
214 errs
.append('Unknown mixin "%s" referenced by config "%s".' %
216 referenced_mixins
.add(mixin
)
218 for mixin
in self
.mixins
:
219 for sub_mixin
in self
.mixins
[mixin
].get('mixins', []):
220 if not sub_mixin
in self
.mixins
:
221 errs
.append('Unknown mixin "%s" referenced by mixin "%s".' %
223 referenced_mixins
.add(sub_mixin
)
225 # Check that every mixin defined is actually referenced somewhere.
226 for mixin
in self
.mixins
:
227 if not mixin
in referenced_mixins
:
228 errs
.append('Unreferenced mixin "%s".' % mixin
)
231 raise MBErr(('mb config file %s has problems:' % self
.args
.config_file
) +
232 '\n ' + '\n '.join(errs
))
234 self
.Print('mb config file %s looks ok.' % self
.args
.config_file
)
238 self
.ReadConfigFile()
239 config
= self
.ConfigFromArgs()
240 if not config
in self
.configs
:
241 raise MBErr('Config "%s" not found in %s' %
242 (config
, self
.args
.config_file
))
244 return self
.FlattenConfig(config
)
246 def ReadConfigFile(self
):
247 if not self
.Exists(self
.args
.config_file
):
248 raise MBErr('config file not found at %s' % self
.args
.config_file
)
251 contents
= ast
.literal_eval(self
.ReadFile(self
.args
.config_file
))
252 except SyntaxError as e
:
253 raise MBErr('Failed to parse config file "%s": %s' %
254 (self
.args
.config_file
, e
))
256 self
.common_dev_configs
= contents
['common_dev_configs']
257 self
.configs
= contents
['configs']
258 self
.masters
= contents
['masters']
259 self
.mixins
= contents
['mixins']
260 self
.private_configs
= contents
['private_configs']
261 self
.unsupported_configs
= contents
['unsupported_configs']
263 def ConfigFromArgs(self
):
265 if self
.args
.master
or self
.args
.builder
:
266 raise MBErr('Can not specific both -c/--config and -m/--master or '
269 return self
.args
.config
271 if not self
.args
.master
or not self
.args
.builder
:
272 raise MBErr('Must specify either -c/--config or '
273 '(-m/--master and -b/--builder)')
275 if not self
.args
.master
in self
.masters
:
276 raise MBErr('Master name "%s" not found in "%s"' %
277 (self
.args
.master
, self
.args
.config_file
))
279 if not self
.args
.builder
in self
.masters
[self
.args
.master
]:
280 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' %
281 (self
.args
.builder
, self
.args
.master
, self
.args
.config_file
))
283 return self
.masters
[self
.args
.master
][self
.args
.builder
]
285 def FlattenConfig(self
, config
):
286 mixins
= self
.configs
[config
]
291 'gyp_crosscompile': False,
295 self
.FlattenMixins(mixins
, vals
, visited
)
298 def FlattenMixins(self
, mixins
, vals
, visited
):
300 if m
not in self
.mixins
:
301 raise MBErr('Unknown mixin "%s"' % m
)
303 # TODO: check for cycles in mixins.
307 mixin_vals
= self
.mixins
[m
]
308 if 'type' in mixin_vals
:
309 vals
['type'] = mixin_vals
['type']
310 if 'gn_args' in mixin_vals
:
312 vals
['gn_args'] += ' ' + mixin_vals
['gn_args']
314 vals
['gn_args'] = mixin_vals
['gn_args']
315 if 'gyp_crosscompile' in mixin_vals
:
316 vals
['gyp_crosscompile'] = mixin_vals
['gyp_crosscompile']
317 if 'gyp_defines' in mixin_vals
:
318 if vals
['gyp_defines']:
319 vals
['gyp_defines'] += ' ' + mixin_vals
['gyp_defines']
321 vals
['gyp_defines'] = mixin_vals
['gyp_defines']
322 if 'mixins' in mixin_vals
:
323 self
.FlattenMixins(mixin_vals
['mixins'], vals
, visited
)
326 def ClobberIfNeeded(self
, vals
):
327 path
= self
.args
.path
[0]
328 build_dir
= self
.ToAbsPath(path
)
329 mb_type_path
= self
.PathJoin(build_dir
, 'mb_type')
330 needs_clobber
= False
331 new_mb_type
= vals
['type']
332 if self
.Exists(build_dir
):
333 if self
.Exists(mb_type_path
):
334 old_mb_type
= self
.ReadFile(mb_type_path
)
335 if old_mb_type
!= new_mb_type
:
336 self
.Print("Build type mismatch: was %s, will be %s, clobbering %s" %
337 (old_mb_type
, new_mb_type
, path
))
340 # There is no 'mb_type' file in the build directory, so this probably
341 # means that the prior build(s) were not done through mb, and we
342 # have no idea if this was a GYP build or a GN build. Clobber it
344 self
.Print("%s/mb_type missing, clobbering to be safe" % path
)
348 self
.RemoveDirectory(build_dir
)
350 self
.MaybeMakeDirectory(build_dir
)
351 self
.WriteFile(mb_type_path
, new_mb_type
)
353 def RunGNGen(self
, vals
):
354 path
= self
.args
.path
[0]
356 cmd
= self
.GNCmd('gen', path
, vals
['gn_args'], extra_args
=['--check'])
358 swarming_targets
= []
359 if self
.args
.swarming_targets_file
:
360 # We need GN to generate the list of runtime dependencies for
361 # the compile targets listed (one per line) in the file so
362 # we can run them via swarming. We use ninja_to_gn.pyl to convert
363 # the compile targets to the matching GN labels.
364 contents
= self
.ReadFile(self
.args
.swarming_targets_file
)
365 swarming_targets
= contents
.splitlines()
366 gn_isolate_map
= ast
.literal_eval(self
.ReadFile(self
.PathJoin(
367 self
.chromium_src_dir
, 'testing', 'buildbot', 'gn_isolate_map.pyl')))
369 for target
in swarming_targets
:
370 if not target
in gn_isolate_map
:
371 raise MBErr('test target "%s" not found in %s' %
372 (target
, '//testing/buildbot/gn_isolate_map.pyl'))
373 gn_labels
.append(gn_isolate_map
[target
]['label'])
375 gn_runtime_deps_path
= self
.ToAbsPath(path
, 'runtime_deps')
377 # Since GN hasn't run yet, the build directory may not even exist.
378 self
.MaybeMakeDirectory(self
.ToAbsPath(path
))
380 self
.WriteFile(gn_runtime_deps_path
, '\n'.join(gn_labels
) + '\n')
381 cmd
.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path
)
383 ret
, _
, _
= self
.Run(cmd
)
385 # If `gn gen` failed, we should exit early rather than trying to
386 # generate isolates. Run() will have already logged any error output.
387 self
.Print('GN gen failed: %d' % ret
)
390 for target
in swarming_targets
:
391 if gn_isolate_map
[target
]['type'] == 'gpu_browser_test':
392 runtime_deps_target
= 'browser_tests'
393 elif gn_isolate_map
[target
]['type'] == 'script':
394 # For script targets, the build target is usually a group,
395 # for which gn generates the runtime_deps next to the stamp file
396 # for the label, which lives under the obj/ directory.
397 label
= gn_isolate_map
[target
]['label']
398 runtime_deps_target
= 'obj/%s.stamp' % label
.replace(':', '/')
400 runtime_deps_target
= target
401 if self
.platform
== 'win32':
402 deps_path
= self
.ToAbsPath(path
,
403 runtime_deps_target
+ '.exe.runtime_deps')
405 deps_path
= self
.ToAbsPath(path
,
406 runtime_deps_target
+ '.runtime_deps')
407 if not self
.Exists(deps_path
):
408 raise MBErr('did not generate %s' % deps_path
)
410 command
, extra_files
= self
.GetIsolateCommand(target
, vals
,
413 runtime_deps
= self
.ReadFile(deps_path
).splitlines()
415 isolate_path
= self
.ToAbsPath(path
, target
+ '.isolate')
416 self
.WriteFile(isolate_path
,
420 'files': sorted(runtime_deps
+ extra_files
),
428 self
.ToSrcRelPath('%s%s%s.isolated' % (path
, self
.sep
, target
)),
430 self
.ToSrcRelPath('%s%s%s.isolate' % (path
, self
.sep
, target
)),
432 'dir': self
.chromium_src_dir
,
435 isolate_path
+ 'd.gen.json',
440 def GNCmd(self
, subcommand
, path
, gn_args
='', extra_args
=None):
441 if self
.platform
== 'linux2':
443 elif self
.platform
== 'darwin':
447 gn_path
= self
.PathJoin(self
.chromium_src_dir
, 'buildtools', subdir
, 'gn')
449 cmd
= [gn_path
, subcommand
, path
]
450 gn_args
= gn_args
.replace("$(goma_dir)", self
.args
.goma_dir
)
452 cmd
.append('--args=%s' % gn_args
)
454 cmd
.extend(extra_args
)
457 def RunGYPGen(self
, vals
):
458 path
= self
.args
.path
[0]
460 output_dir
= self
.ParseGYPConfigPath(path
)
461 cmd
= self
.GYPCmd(output_dir
, vals
['gyp_defines'])
463 if vals
['gyp_crosscompile']:
464 if self
.args
.verbose
:
465 self
.Print('Setting GYP_CROSSCOMPILE=1 in the environment')
466 env
= os
.environ
.copy()
467 env
['GYP_CROSSCOMPILE'] = '1'
468 ret
, _
, _
= self
.Run(cmd
, env
=env
)
471 def RunGYPAnalyze(self
, vals
):
472 output_dir
= self
.ParseGYPConfigPath(self
.args
.path
[0])
473 if self
.args
.verbose
:
474 inp
= self
.ReadInputJSON(['files', 'targets'])
476 self
.Print('analyze input:')
480 cmd
= self
.GYPCmd(output_dir
, vals
['gyp_defines'])
481 cmd
.extend(['-f', 'analyzer',
482 '-G', 'config_path=%s' % self
.args
.input_path
[0],
483 '-G', 'analyzer_output_path=%s' % self
.args
.output_path
[0]])
484 ret
, _
, _
= self
.Run(cmd
)
485 if not ret
and self
.args
.verbose
:
486 outp
= json
.loads(self
.ReadFile(self
.args
.output_path
[0]))
488 self
.Print('analyze output:')
494 def GetIsolateCommand(self
, target
, vals
, gn_isolate_map
):
495 # This needs to mirror the settings in //build/config/ui.gni:
496 # use_x11 = is_linux && !use_ozone.
497 # TODO(dpranke): Figure out how to keep this in sync better.
498 use_x11
= (self
.platform
== 'linux2' and
499 not 'target_os="android"' in vals
['gn_args'] and
500 not 'use_ozone=true' in vals
['gn_args'])
502 asan
= 'is_asan=true' in vals
['gn_args']
503 msan
= 'is_msan=true' in vals
['gn_args']
504 tsan
= 'is_tsan=true' in vals
['gn_args']
506 executable_suffix
= '.exe' if self
.platform
== 'win32' else ''
508 test_type
= gn_isolate_map
[target
]['type']
512 if use_x11
and test_type
== 'windowed_test_launcher':
515 '../../testing/test_env.py',
516 '../../testing/xvfb.py',
519 '../../testing/xvfb.py',
522 '--brave-new-test-launcher',
523 '--test-launcher-bot-mode',
528 elif test_type
in ('windowed_test_launcher', 'console_test_launcher'):
530 '../../testing/test_env.py'
533 '../../testing/test_env.py',
534 './' + str(target
) + executable_suffix
,
535 '--brave-new-test-launcher',
536 '--test-launcher-bot-mode',
541 elif test_type
== 'gpu_browser_test':
543 '../../testing/test_env.py'
545 gtest_filter
= gn_isolate_map
[target
]['gtest_filter']
547 '../../testing/test_env.py',
548 './browser_tests' + executable_suffix
,
549 '--test-launcher-bot-mode',
551 '--test-launcher-jobs=1',
552 '--gtest_filter=%s' % gtest_filter
,
554 elif test_type
== 'script':
556 '../../testing/test_env.py'
559 '../../testing/test_env.py',
560 ] + ['../../' + self
.ToSrcRelPath(gn_isolate_map
[target
]['script'])]
561 elif test_type
in ('raw'):
564 './' + str(target
) + executable_suffix
,
565 ] + gn_isolate_map
[target
].get('args')
568 self
.WriteFailureAndRaise('No command line for %s found (test type %s).'
569 % (target
, test_type
), output_path
=None)
571 return cmdline
, extra_files
573 def ToAbsPath(self
, build_path
, *comps
):
574 return self
.PathJoin(self
.chromium_src_dir
,
575 self
.ToSrcRelPath(build_path
),
578 def ToSrcRelPath(self
, path
):
579 """Returns a relative path from the top of the repo."""
580 # TODO: Support normal paths in addition to source-absolute paths.
581 assert(path
.startswith('//'))
582 return path
[2:].replace('/', self
.sep
)
584 def ParseGYPConfigPath(self
, path
):
585 rpath
= self
.ToSrcRelPath(path
)
586 output_dir
, _
, _
= rpath
.rpartition(self
.sep
)
589 def GYPCmd(self
, output_dir
, gyp_defines
):
590 gyp_defines
= gyp_defines
.replace("$(goma_dir)", self
.args
.goma_dir
)
593 self
.PathJoin('build', 'gyp_chromium'),
595 'output_dir=' + output_dir
,
597 for d
in shlex
.split(gyp_defines
):
601 def RunGNAnalyze(self
, vals
):
602 # analyze runs before 'gn gen' now, so we need to run gn gen
603 # in order to ensure that we have a build directory.
604 ret
= self
.RunGNGen(vals
)
608 inp
= self
.ReadInputJSON(['files', 'targets'])
609 if self
.args
.verbose
:
611 self
.Print('analyze input:')
615 output_path
= self
.args
.output_path
[0]
617 # Bail out early if a GN file was modified, since 'gn refs' won't know
618 # what to do about it.
619 if any(f
.endswith('.gn') or f
.endswith('.gni') for f
in inp
['files']):
620 self
.WriteJSON({'status': 'Found dependency (all)'}, output_path
)
623 # Bail out early if 'all' was asked for, since 'gn refs' won't recognize it.
624 if 'all' in inp
['targets']:
625 self
.WriteJSON({'status': 'Found dependency (all)'}, output_path
)
628 # This shouldn't normally happen, but could due to unusual race conditions,
629 # like a try job that gets scheduled before a patch lands but runs after
630 # the patch has landed.
632 self
.Print('Warning: No files modified in patch, bailing out early.')
633 self
.WriteJSON({'targets': [],
635 'status': 'No dependency'}, output_path
)
639 response_file
= self
.TempFile()
640 response_file
.write('\n'.join(inp
['files']) + '\n')
641 response_file
.close()
643 matching_targets
= []
645 cmd
= self
.GNCmd('refs', self
.args
.path
[0]) + [
646 '@%s' % response_file
.name
, '--all', '--as=output']
647 ret
, out
, _
= self
.Run(cmd
, force_verbose
=False)
648 if ret
and not 'The input matches no targets' in out
:
649 self
.WriteFailureAndRaise('gn refs returned %d: %s' % (ret
, out
),
651 build_dir
= self
.ToSrcRelPath(self
.args
.path
[0]) + self
.sep
652 for output
in out
.splitlines():
653 build_output
= output
.replace(build_dir
, '')
654 if build_output
in inp
['targets']:
655 matching_targets
.append(build_output
)
657 cmd
= self
.GNCmd('refs', self
.args
.path
[0]) + [
658 '@%s' % response_file
.name
, '--all']
659 ret
, out
, _
= self
.Run(cmd
, force_verbose
=False)
660 if ret
and not 'The input matches no targets' in out
:
661 self
.WriteFailureAndRaise('gn refs returned %d: %s' % (ret
, out
),
663 for label
in out
.splitlines():
664 build_target
= label
[2:]
665 # We want to accept 'chrome/android:chrome_public_apk' and
666 # just 'chrome_public_apk'. This may result in too many targets
667 # getting built, but we can adjust that later if need be.
668 for input_target
in inp
['targets']:
669 if (input_target
== build_target
or
670 build_target
.endswith(':' + input_target
)):
671 matching_targets
.append(input_target
)
673 self
.RemoveFile(response_file
.name
)
676 # TODO: it could be that a target X might depend on a target Y
677 # and both would be listed in the input, but we would only need
678 # to specify target X as a build_target (whereas both X and Y are
679 # targets). I'm not sure if that optimization is generally worth it.
680 self
.WriteJSON({'targets': sorted(set(matching_targets
)),
681 'build_targets': sorted(set(matching_targets
)),
682 'status': 'Found dependency'}, output_path
)
684 self
.WriteJSON({'targets': [],
686 'status': 'No dependency'}, output_path
)
688 if self
.args
.verbose
:
689 outp
= json
.loads(self
.ReadFile(output_path
))
691 self
.Print('analyze output:')
697 def ReadInputJSON(self
, required_keys
):
698 path
= self
.args
.input_path
[0]
699 output_path
= self
.args
.output_path
[0]
700 if not self
.Exists(path
):
701 self
.WriteFailureAndRaise('"%s" does not exist' % path
, output_path
)
704 inp
= json
.loads(self
.ReadFile(path
))
705 except Exception as e
:
706 self
.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
707 (path
, e
), output_path
)
709 for k
in required_keys
:
711 self
.WriteFailureAndRaise('input file is missing a "%s" key' % k
,
716 def WriteFailureAndRaise(self
, msg
, output_path
):
718 self
.WriteJSON({'error': msg
}, output_path
, force_verbose
=True)
721 def WriteJSON(self
, obj
, path
, force_verbose
=False):
723 self
.WriteFile(path
, json
.dumps(obj
, indent
=2, sort_keys
=True) + '\n',
724 force_verbose
=force_verbose
)
725 except Exception as e
:
726 raise MBErr('Error %s writing to the output path "%s"' %
729 def PrintCmd(self
, cmd
):
730 if cmd
[0] == self
.executable
:
731 cmd
= ['python'] + cmd
[1:]
732 self
.Print(*[pipes
.quote(c
) for c
in cmd
])
734 def PrintJSON(self
, obj
):
735 self
.Print(json
.dumps(obj
, indent
=2, sort_keys
=True))
737 def Print(self
, *args
, **kwargs
):
738 # This function largely exists so it can be overridden for testing.
739 print(*args
, **kwargs
)
741 def Run(self
, cmd
, env
=None, force_verbose
=True):
742 # This function largely exists so it can be overridden for testing.
743 if self
.args
.dryrun
or self
.args
.verbose
or force_verbose
:
748 ret
, out
, err
= self
.Call(cmd
, env
=env
)
749 if self
.args
.verbose
or force_verbose
:
751 self
.Print(out
, end
='')
753 self
.Print(err
, end
='', file=sys
.stderr
)
756 def Call(self
, cmd
, env
=None):
757 p
= subprocess
.Popen(cmd
, shell
=False, cwd
=self
.chromium_src_dir
,
758 stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
,
760 out
, err
= p
.communicate()
761 return p
.returncode
, out
, err
763 def ExpandUser(self
, path
):
764 # This function largely exists so it can be overridden for testing.
765 return os
.path
.expanduser(path
)
767 def Exists(self
, path
):
768 # This function largely exists so it can be overridden for testing.
769 return os
.path
.exists(path
)
771 def MaybeMakeDirectory(self
, path
):
775 if e
.errno
!= errno
.EEXIST
:
778 def PathJoin(self
, *comps
):
779 # This function largely exists so it can be overriden for testing.
780 return os
.path
.join(*comps
)
782 def ReadFile(self
, path
):
783 # This function largely exists so it can be overriden for testing.
784 with
open(path
) as fp
:
787 def RemoveFile(self
, path
):
788 # This function largely exists so it can be overriden for testing.
791 def RemoveDirectory(self
, abs_path
):
792 if self
.platform
== 'win32':
793 # In other places in chromium, we often have to retry this command
794 # because we're worried about other processes still holding on to
795 # file handles, but when MB is invoked, it will be early enough in the
796 # build that their should be no other processes to interfere. We
797 # can change this if need be.
798 self
.Run(['cmd.exe', '/c', 'rmdir', '/q', '/s', abs_path
])
800 shutil
.rmtree(abs_path
, ignore_errors
=True)
802 def TempFile(self
, mode
='w'):
803 # This function largely exists so it can be overriden for testing.
804 return tempfile
.NamedTemporaryFile(mode
=mode
, delete
=False)
806 def WriteFile(self
, path
, contents
, force_verbose
=False):
807 # This function largely exists so it can be overriden for testing.
808 if self
.args
.dryrun
or self
.args
.verbose
or force_verbose
:
809 self
.Print('\nWriting """\\\n%s""" to %s.\n' % (contents
, path
))
810 with
open(path
, 'w') as fp
:
811 return fp
.write(contents
)
814 class MBErr(Exception):
818 if __name__
== '__main__':
820 sys
.exit(main(sys
.argv
[1:]))
824 except KeyboardInterrupt:
825 print("interrupted, exiting", stream
=sys
.stderr
)