Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(877)

Side by Side Diff: tools/mb/mb.py

Issue 1370593003: Merge MB from trunk back to M45. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@branch_2454
Patch Set: Created 5 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « tools/mb/docs/user_guide.md ('k') | tools/mb/mb_config.pyl » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright 2015 The Chromium Authors. All rights reserved. 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 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """MB - the Meta-Build wrapper around GYP and GN 6 """MB - the Meta-Build wrapper around GYP and GN
7 7
8 MB is a wrapper script for GYP and GN that can be used to generate build files 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. 9 for sets of canned configurations and analyze them.
10 """ 10 """
11 11
12 from __future__ import print_function 12 from __future__ import print_function
13 13
14 import argparse 14 import argparse
15 import ast 15 import ast
16 import errno 16 import errno
17 import json 17 import json
18 import os 18 import os
19 import pipes 19 import pipes
20 import pprint 20 import pprint
21 import shlex 21 import re
22 import shutil 22 import shutil
23 import sys 23 import sys
24 import subprocess 24 import subprocess
25 import tempfile 25 import tempfile
26 26
27
28 def main(args): 27 def main(args):
29 mbw = MetaBuildWrapper() 28 mbw = MetaBuildWrapper()
30 mbw.ParseArgs(args) 29 mbw.ParseArgs(args)
31 return mbw.args.func() 30 return mbw.args.func()
32 31
33 32
34 class MetaBuildWrapper(object): 33 class MetaBuildWrapper(object):
35 def __init__(self): 34 def __init__(self):
36 p = os.path 35 p = os.path
37 d = os.path.dirname 36 d = os.path.dirname
38 self.chromium_src_dir = p.normpath(d(d(d(p.abspath(__file__))))) 37 self.chromium_src_dir = p.normpath(d(d(d(p.abspath(__file__)))))
39 self.default_config = p.join(self.chromium_src_dir, 'tools', 'mb', 38 self.default_config = p.join(self.chromium_src_dir, 'tools', 'mb',
40 'mb_config.pyl') 39 'mb_config.pyl')
40 self.executable = sys.executable
41 self.platform = sys.platform 41 self.platform = sys.platform
42 self.sep = os.sep
42 self.args = argparse.Namespace() 43 self.args = argparse.Namespace()
43 self.configs = {} 44 self.configs = {}
44 self.masters = {} 45 self.masters = {}
45 self.mixins = {} 46 self.mixins = {}
46 self.private_configs = [] 47 self.private_configs = []
47 self.common_dev_configs = [] 48 self.common_dev_configs = []
48 self.unsupported_configs = [] 49 self.unsupported_configs = []
49 50
50 def ParseArgs(self, argv): 51 def ParseArgs(self, argv):
51 def AddCommonOptions(subp): 52 def AddCommonOptions(subp):
52 subp.add_argument('-b', '--builder', 53 subp.add_argument('-b', '--builder',
53 help='builder name to look up config from') 54 help='builder name to look up config from')
54 subp.add_argument('-m', '--master', 55 subp.add_argument('-m', '--master',
55 help='master name to look up config from') 56 help='master name to look up config from')
56 subp.add_argument('-c', '--config', 57 subp.add_argument('-c', '--config',
57 help='configuration to analyze') 58 help='configuration to analyze')
58 subp.add_argument('-f', '--config-file', metavar='PATH', 59 subp.add_argument('-f', '--config-file', metavar='PATH',
59 default=self.default_config, 60 default=self.default_config,
60 help='path to config file ' 61 help='path to config file '
61 '(default is //tools/mb/mb_config.pyl)') 62 '(default is //tools/mb/mb_config.pyl)')
62 subp.add_argument('-g', '--goma-dir', default=self.ExpandUser('~/goma'), 63 subp.add_argument('-g', '--goma-dir', default=self.ExpandUser('~/goma'),
63 help='path to goma directory (default is %(default)s).') 64 help='path to goma directory (default is %(default)s).')
64 subp.add_argument('-n', '--dryrun', action='store_true', 65 subp.add_argument('-n', '--dryrun', action='store_true',
65 help='Do a dry run (i.e., do nothing, just print ' 66 help='Do a dry run (i.e., do nothing, just print '
66 'the commands that will run)') 67 'the commands that will run)')
67 subp.add_argument('-q', '--quiet', action='store_true', 68 subp.add_argument('-v', '--verbose', action='store_true',
68 help='Do not print anything, just return an exit ' 69 help='verbose logging')
69 'code.')
70 subp.add_argument('-v', '--verbose', action='count',
71 help='verbose logging (may specify multiple times).')
72 70
73 parser = argparse.ArgumentParser(prog='mb') 71 parser = argparse.ArgumentParser(prog='mb')
74 subps = parser.add_subparsers() 72 subps = parser.add_subparsers()
75 73
76 subp = subps.add_parser('analyze', 74 subp = subps.add_parser('analyze',
77 help='analyze whether changes to a set of files ' 75 help='analyze whether changes to a set of files '
78 'will cause a set of binaries to be rebuilt.') 76 'will cause a set of binaries to be rebuilt.')
79 AddCommonOptions(subp) 77 AddCommonOptions(subp)
80 subp.add_argument('--swarming-targets-file', 78 subp.add_argument('--swarming-targets-file',
81 help='save runtime dependencies for targets listed ' 79 help='save runtime dependencies for targets listed '
(...skipping 19 matching lines...) Expand all
101 subp.set_defaults(func=self.CmdGen) 99 subp.set_defaults(func=self.CmdGen)
102 100
103 subp = subps.add_parser('lookup', 101 subp = subps.add_parser('lookup',
104 help='look up the command for a given config or ' 102 help='look up the command for a given config or '
105 'builder') 103 'builder')
106 AddCommonOptions(subp) 104 AddCommonOptions(subp)
107 subp.set_defaults(func=self.CmdLookup) 105 subp.set_defaults(func=self.CmdLookup)
108 106
109 subp = subps.add_parser('validate', 107 subp = subps.add_parser('validate',
110 help='validate the config file') 108 help='validate the config file')
111 AddCommonOptions(subp) 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)')
112 subp.set_defaults(func=self.CmdValidate) 113 subp.set_defaults(func=self.CmdValidate)
113 114
114 subp = subps.add_parser('help', 115 subp = subps.add_parser('help',
115 help='Get help on a subcommand.') 116 help='Get help on a subcommand.')
116 subp.add_argument(nargs='?', action='store', dest='subcommand', 117 subp.add_argument(nargs='?', action='store', dest='subcommand',
117 help='The command to get help for.') 118 help='The command to get help for.')
118 subp.set_defaults(func=self.CmdHelp) 119 subp.set_defaults(func=self.CmdHelp)
119 120
120 self.args = parser.parse_args(argv) 121 self.args = parser.parse_args(argv)
121 122
122 def CmdAnalyze(self): 123 def CmdAnalyze(self):
123 vals = self.GetConfig() 124 vals = self.GetConfig()
124 if vals['type'] == 'gn': 125 if vals['type'] == 'gn':
125 return self.RunGNAnalyze(vals) 126 return self.RunGNAnalyze(vals)
126 elif vals['type'] == 'gyp': 127 elif vals['type'] == 'gyp':
127 return self.RunGYPAnalyze(vals) 128 return self.RunGYPAnalyze(vals)
128 else: 129 else:
129 raise MBErr('Unknown meta-build type "%s"' % vals['type']) 130 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
130 131
131 def CmdGen(self): 132 def CmdGen(self):
132 vals = self.GetConfig() 133 vals = self.GetConfig()
134
135 self.ClobberIfNeeded(vals)
136
133 if vals['type'] == 'gn': 137 if vals['type'] == 'gn':
134 return self.RunGNGen(vals) 138 return self.RunGNGen(vals)
135 if vals['type'] == 'gyp': 139 if vals['type'] == 'gyp':
136 return self.RunGYPGen(vals) 140 return self.RunGYPGen(vals)
137 141
138 raise MBErr('Unknown meta-build type "%s"' % vals['type']) 142 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
139 143
140 def CmdLookup(self): 144 def CmdLookup(self):
141 vals = self.GetConfig() 145 vals = self.GetConfig()
142 if vals['type'] == 'gn': 146 if vals['type'] == 'gn':
143 cmd = self.GNCmd('gen', '<path>', vals['gn_args']) 147 cmd = self.GNCmd('gen', '_path_', vals['gn_args'])
148 env = None
144 elif vals['type'] == 'gyp': 149 elif vals['type'] == 'gyp':
145 cmd = self.GYPCmd('<path>', vals['gyp_defines'], vals['gyp_config']) 150 if vals['gyp_crosscompile']:
151 self.Print('GYP_CROSSCOMPILE=1')
152 cmd, env = self.GYPCmd('_path_', vals['gyp_defines'])
146 else: 153 else:
147 raise MBErr('Unknown meta-build type "%s"' % vals['type']) 154 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
148 155
149 self.PrintCmd(cmd) 156 self.PrintCmd(cmd, env)
150 return 0 157 return 0
151 158
152 def CmdHelp(self): 159 def CmdHelp(self):
153 if self.args.subcommand: 160 if self.args.subcommand:
154 self.ParseArgs([self.args.subcommand, '--help']) 161 self.ParseArgs([self.args.subcommand, '--help'])
155 else: 162 else:
156 self.ParseArgs(['--help']) 163 self.ParseArgs(['--help'])
157 164
158 def CmdValidate(self): 165 def CmdValidate(self):
159 errs = [] 166 errs = []
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after
215 errs.append('Unknown mixin "%s" referenced by mixin "%s".' % 222 errs.append('Unknown mixin "%s" referenced by mixin "%s".' %
216 (sub_mixin, mixin)) 223 (sub_mixin, mixin))
217 referenced_mixins.add(sub_mixin) 224 referenced_mixins.add(sub_mixin)
218 225
219 # Check that every mixin defined is actually referenced somewhere. 226 # Check that every mixin defined is actually referenced somewhere.
220 for mixin in self.mixins: 227 for mixin in self.mixins:
221 if not mixin in referenced_mixins: 228 if not mixin in referenced_mixins:
222 errs.append('Unreferenced mixin "%s".' % mixin) 229 errs.append('Unreferenced mixin "%s".' % mixin)
223 230
224 if errs: 231 if errs:
225 raise MBErr('mb config file %s has problems:\n ' + '\n '.join(errs)) 232 raise MBErr(('mb config file %s has problems:' % self.args.config_file) +
233 '\n ' + '\n '.join(errs))
226 234
227 if not self.args.quiet: 235 self.Print('mb config file %s looks ok.' % self.args.config_file)
228 self.Print('mb config file %s looks ok.' % self.args.config_file)
229 return 0 236 return 0
230 237
231 def GetConfig(self): 238 def GetConfig(self):
232 self.ReadConfigFile() 239 self.ReadConfigFile()
233 config = self.ConfigFromArgs() 240 config = self.ConfigFromArgs()
234 if not config in self.configs: 241 if not config in self.configs:
235 raise MBErr('Config "%s" not found in %s' % 242 raise MBErr('Config "%s" not found in %s' %
236 (config, self.args.config_file)) 243 (config, self.args.config_file))
237 244
238 return self.FlattenConfig(config) 245 return self.FlattenConfig(config)
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
274 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' % 281 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' %
275 (self.args.builder, self.args.master, self.args.config_file)) 282 (self.args.builder, self.args.master, self.args.config_file))
276 283
277 return self.masters[self.args.master][self.args.builder] 284 return self.masters[self.args.master][self.args.builder]
278 285
279 def FlattenConfig(self, config): 286 def FlattenConfig(self, config):
280 mixins = self.configs[config] 287 mixins = self.configs[config]
281 vals = { 288 vals = {
282 'type': None, 289 'type': None,
283 'gn_args': [], 290 'gn_args': [],
284 'gyp_config': [], 291 'gyp_defines': '',
285 'gyp_defines': [], 292 'gyp_crosscompile': False,
286 } 293 }
287 294
288 visited = [] 295 visited = []
289 self.FlattenMixins(mixins, vals, visited) 296 self.FlattenMixins(mixins, vals, visited)
290 return vals 297 return vals
291 298
292 def FlattenMixins(self, mixins, vals, visited): 299 def FlattenMixins(self, mixins, vals, visited):
293 for m in mixins: 300 for m in mixins:
294 if m not in self.mixins: 301 if m not in self.mixins:
295 raise MBErr('Unknown mixin "%s"' % m) 302 raise MBErr('Unknown mixin "%s"' % m)
296 303
297 # TODO: check for cycles in mixins. 304 # TODO: check for cycles in mixins.
298 305
299 visited.append(m) 306 visited.append(m)
300 307
301 mixin_vals = self.mixins[m] 308 mixin_vals = self.mixins[m]
302 if 'type' in mixin_vals: 309 if 'type' in mixin_vals:
303 vals['type'] = mixin_vals['type'] 310 vals['type'] = mixin_vals['type']
304 if 'gn_args' in mixin_vals: 311 if 'gn_args' in mixin_vals:
305 if vals['gn_args']: 312 if vals['gn_args']:
306 vals['gn_args'] += ' ' + mixin_vals['gn_args'] 313 vals['gn_args'] += ' ' + mixin_vals['gn_args']
307 else: 314 else:
308 vals['gn_args'] = mixin_vals['gn_args'] 315 vals['gn_args'] = mixin_vals['gn_args']
309 if 'gyp_config' in mixin_vals: 316 if 'gyp_crosscompile' in mixin_vals:
310 vals['gyp_config'] = mixin_vals['gyp_config'] 317 vals['gyp_crosscompile'] = mixin_vals['gyp_crosscompile']
311 if 'gyp_defines' in mixin_vals: 318 if 'gyp_defines' in mixin_vals:
312 if vals['gyp_defines']: 319 if vals['gyp_defines']:
313 vals['gyp_defines'] += ' ' + mixin_vals['gyp_defines'] 320 vals['gyp_defines'] += ' ' + mixin_vals['gyp_defines']
314 else: 321 else:
315 vals['gyp_defines'] = mixin_vals['gyp_defines'] 322 vals['gyp_defines'] = mixin_vals['gyp_defines']
316 if 'mixins' in mixin_vals: 323 if 'mixins' in mixin_vals:
317 self.FlattenMixins(mixin_vals['mixins'], vals, visited) 324 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
318 return vals 325 return vals
319 326
327 def ClobberIfNeeded(self, vals):
328 path = self.args.path[0]
329 build_dir = self.ToAbsPath(path)
330 mb_type_path = self.PathJoin(build_dir, 'mb_type')
331 needs_clobber = False
332 new_mb_type = vals['type']
333 if self.Exists(build_dir):
334 if self.Exists(mb_type_path):
335 old_mb_type = self.ReadFile(mb_type_path)
336 if old_mb_type != new_mb_type:
337 self.Print("Build type mismatch: was %s, will be %s, clobbering %s" %
338 (old_mb_type, new_mb_type, path))
339 needs_clobber = True
340 else:
341 # There is no 'mb_type' file in the build directory, so this probably
342 # means that the prior build(s) were not done through mb, and we
343 # have no idea if this was a GYP build or a GN build. Clobber it
344 # to be safe.
345 self.Print("%s/mb_type missing, clobbering to be safe" % path)
346 needs_clobber = True
347
348 if self.args.dryrun:
349 return
350
351 if needs_clobber:
352 self.RemoveDirectory(build_dir)
353
354 self.MaybeMakeDirectory(build_dir)
355 self.WriteFile(mb_type_path, new_mb_type)
356
320 def RunGNGen(self, vals): 357 def RunGNGen(self, vals):
321 path = self.args.path[0] 358 path = self.args.path[0]
322 359
323 cmd = self.GNCmd('gen', path, vals['gn_args']) 360 cmd = self.GNCmd('gen', path, vals['gn_args'], extra_args=['--check'])
324 361
325 swarming_targets = [] 362 swarming_targets = []
326 if self.args.swarming_targets_file: 363 if self.args.swarming_targets_file:
327 # We need GN to generate the list of runtime dependencies for 364 # We need GN to generate the list of runtime dependencies for
328 # the compile targets listed (one per line) in the file so 365 # the compile targets listed (one per line) in the file so
329 # we can run them via swarming. We use ninja_to_gn.pyl to convert 366 # we can run them via swarming. We use ninja_to_gn.pyl to convert
330 # the compile targets to the matching GN labels. 367 # the compile targets to the matching GN labels.
331 contents = self.ReadFile(self.args.swarming_targets_file) 368 contents = self.ReadFile(self.args.swarming_targets_file)
332 swarming_targets = contents.splitlines() 369 swarming_targets = contents.splitlines()
333 ninja_targets_to_labels = ast.literal_eval(self.ReadFile(os.path.join( 370 gn_isolate_map = ast.literal_eval(self.ReadFile(self.PathJoin(
334 self.chromium_src_dir, 'testing', 'buildbot', 'ninja_to_gn.pyl'))) 371 self.chromium_src_dir, 'testing', 'buildbot', 'gn_isolate_map.pyl')))
335 gn_labels = [] 372 gn_labels = []
336 for target in swarming_targets: 373 for target in swarming_targets:
337 if not target in ninja_targets_to_labels: 374 if not target in gn_isolate_map:
338 raise MBErr('test target "%s" not found in %s' % 375 raise MBErr('test target "%s" not found in %s' %
339 (target, '//testing/buildbot/ninja_to_gn.pyl')) 376 (target, '//testing/buildbot/gn_isolate_map.pyl'))
340 gn_labels.append(ninja_targets_to_labels[target]) 377 gn_labels.append(gn_isolate_map[target]['label'])
341 378
342 gn_runtime_deps_path = self.ToAbsPath(path, 'runtime_deps') 379 gn_runtime_deps_path = self.ToAbsPath(path, 'runtime_deps')
343 380
344 # Since GN hasn't run yet, the build directory may not even exist. 381 # Since GN hasn't run yet, the build directory may not even exist.
345 self.MaybeMakeDirectory(self.ToAbsPath(path)) 382 self.MaybeMakeDirectory(self.ToAbsPath(path))
346 383
347 self.WriteFile(gn_runtime_deps_path, '\n'.join(gn_labels) + '\n') 384 self.WriteFile(gn_runtime_deps_path, '\n'.join(gn_labels) + '\n')
348 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path) 385 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path)
349 386
350 ret, _, _ = self.Run(cmd) 387 ret, _, _ = self.Run(cmd)
388 if ret:
389 # If `gn gen` failed, we should exit early rather than trying to
390 # generate isolates. Run() will have already logged any error output.
391 self.Print('GN gen failed: %d' % ret)
392 return ret
351 393
352 for target in swarming_targets: 394 for target in swarming_targets:
353 if sys.platform == 'win32': 395 if gn_isolate_map[target]['type'] == 'gpu_browser_test':
354 deps_path = self.ToAbsPath(path, target + '.exe.runtime_deps') 396 runtime_deps_target = 'browser_tests'
397 elif gn_isolate_map[target]['type'] == 'script':
398 # For script targets, the build target is usually a group,
399 # for which gn generates the runtime_deps next to the stamp file
400 # for the label, which lives under the obj/ directory.
401 label = gn_isolate_map[target]['label']
402 runtime_deps_target = 'obj/%s.stamp' % label.replace(':', '/')
355 else: 403 else:
356 deps_path = self.ToAbsPath(path, target + '.runtime_deps') 404 runtime_deps_target = target
405 if self.platform == 'win32':
406 deps_path = self.ToAbsPath(path,
407 runtime_deps_target + '.exe.runtime_deps')
408 else:
409 deps_path = self.ToAbsPath(path,
410 runtime_deps_target + '.runtime_deps')
357 if not self.Exists(deps_path): 411 if not self.Exists(deps_path):
358 raise MBErr('did not generate %s' % deps_path) 412 raise MBErr('did not generate %s' % deps_path)
359 413
360 command, extra_files = self.GetIsolateCommand(target, vals) 414 command, extra_files = self.GetIsolateCommand(target, vals,
415 gn_isolate_map)
361 416
362 runtime_deps = self.ReadFile(deps_path).splitlines() 417 runtime_deps = self.ReadFile(deps_path).splitlines()
363 418
364 isolate_path = self.ToAbsPath(path, target + '.isolate') 419 isolate_path = self.ToAbsPath(path, target + '.isolate')
365 self.WriteFile(isolate_path, 420 self.WriteFile(isolate_path,
366 pprint.pformat({ 421 pprint.pformat({
367 'variables': { 422 'variables': {
368 'command': command, 423 'command': command,
369 'files': sorted(runtime_deps + extra_files), 424 'files': sorted(runtime_deps + extra_files),
370 'read_only': 1,
371 } 425 }
372 }) + '\n') 426 }) + '\n')
373 427
374 self.WriteJSON( 428 self.WriteJSON(
375 { 429 {
376 'args': [ 430 'args': [
377 '--isolated', 431 '--isolated',
378 self.ToSrcRelPath('%s%s%s.isolated' % (path, os.sep, target)), 432 self.ToSrcRelPath('%s%s%s.isolated' % (path, self.sep, target)),
379 '--isolate', 433 '--isolate',
380 self.ToSrcRelPath('%s%s%s.isolate' % (path, os.sep, target)), 434 self.ToSrcRelPath('%s%s%s.isolate' % (path, self.sep, target)),
381 ], 435 ],
382 'dir': self.chromium_src_dir, 436 'dir': self.chromium_src_dir,
383 'version': 1, 437 'version': 1,
384 }, 438 },
385 isolate_path + 'd.gen.json', 439 isolate_path + 'd.gen.json',
386 ) 440 )
387 441
388
389 return ret 442 return ret
390 443
391 def GNCmd(self, subcommand, path, gn_args=''): 444 def GNCmd(self, subcommand, path, gn_args='', extra_args=None):
392 if self.platform == 'linux2': 445 if self.platform == 'linux2':
393 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'linux64', 446 subdir = 'linux64'
394 'gn')
395 elif self.platform == 'darwin': 447 elif self.platform == 'darwin':
396 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'mac', 448 subdir = 'mac'
397 'gn')
398 else: 449 else:
399 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'win', 450 subdir = 'win'
400 'gn.exe') 451 gn_path = self.PathJoin(self.chromium_src_dir, 'buildtools', subdir, 'gn')
401 452
402 cmd = [gn_path, subcommand, path] 453 cmd = [gn_path, subcommand, path]
403 gn_args = gn_args.replace("$(goma_dir)", self.args.goma_dir) 454 gn_args = gn_args.replace("$(goma_dir)", self.args.goma_dir)
404 if gn_args: 455 if gn_args:
405 cmd.append('--args=%s' % gn_args) 456 cmd.append('--args=%s' % gn_args)
457 if extra_args:
458 cmd.extend(extra_args)
406 return cmd 459 return cmd
407 460
408 def RunGYPGen(self, vals): 461 def RunGYPGen(self, vals):
409 path = self.args.path[0] 462 path = self.args.path[0]
410 463
411 output_dir, gyp_config = self.ParseGYPConfigPath(path) 464 output_dir = self.ParseGYPConfigPath(path)
412 if gyp_config != vals['gyp_config']: 465 cmd, env = self.GYPCmd(output_dir, vals['gyp_defines'])
413 raise MBErr('The last component of the path (%s) must match the ' 466 if vals['gyp_crosscompile']:
414 'GYP configuration specified in the config (%s), and ' 467 env['GYP_CROSSCOMPILE'] = '1'
415 'it does not.' % (gyp_config, vals['gyp_config'])) 468 ret, _, _ = self.Run(cmd, env=env)
416 cmd = self.GYPCmd(output_dir, vals['gyp_defines'], config=gyp_config)
417 ret, _, _ = self.Run(cmd)
418 return ret 469 return ret
419 470
420 def RunGYPAnalyze(self, vals): 471 def RunGYPAnalyze(self, vals):
421 output_dir, gyp_config = self.ParseGYPConfigPath(self.args.path[0]) 472 output_dir = self.ParseGYPConfigPath(self.args.path[0])
422 if gyp_config != vals['gyp_config']:
423 raise MBErr('The last component of the path (%s) must match the '
424 'GYP configuration specified in the config (%s), and '
425 'it does not.' % (gyp_config, vals['gyp_config']))
426 if self.args.verbose: 473 if self.args.verbose:
427 inp = self.GetAnalyzeInput() 474 inp = self.ReadInputJSON(['files', 'targets'])
428 self.Print() 475 self.Print()
429 self.Print('analyze input:') 476 self.Print('analyze input:')
430 self.PrintJSON(inp) 477 self.PrintJSON(inp)
431 self.Print() 478 self.Print()
432 479
433 cmd = self.GYPCmd(output_dir, vals['gyp_defines'], config=gyp_config) 480 cmd, env = self.GYPCmd(output_dir, vals['gyp_defines'])
434 cmd.extend(['-G', 'config_path=%s' % self.args.input_path[0], 481 cmd.extend(['-f', 'analyzer',
482 '-G', 'config_path=%s' % self.args.input_path[0],
435 '-G', 'analyzer_output_path=%s' % self.args.output_path[0]]) 483 '-G', 'analyzer_output_path=%s' % self.args.output_path[0]])
436 ret, _, _ = self.Run(cmd) 484 ret, _, _ = self.Run(cmd, env=env)
437 if not ret and self.args.verbose: 485 if not ret and self.args.verbose:
438 outp = json.loads(self.ReadFile(self.args.output_path[0])) 486 outp = json.loads(self.ReadFile(self.args.output_path[0]))
439 self.Print() 487 self.Print()
440 self.Print('analyze output:') 488 self.Print('analyze output:')
441 self.PrintJSON(outp) 489 self.PrintJSON(outp)
442 self.Print() 490 self.Print()
443 491
444 return ret 492 return ret
445 493
446 def GetIsolateCommand(self, target, vals): 494 def GetIsolateCommand(self, target, vals, gn_isolate_map):
447 extra_files = []
448
449 # TODO(dpranke): We should probably pull this from
450 # the test list info in //testing/buildbot/*.json,
451 # and assert that the test has can_use_on_swarming_builders: True,
452 # but we hardcode it here for now.
453 test_type = {}.get(target, 'gtest_test')
454
455 # This needs to mirror the settings in //build/config/ui.gni: 495 # This needs to mirror the settings in //build/config/ui.gni:
456 # use_x11 = is_linux && !use_ozone. 496 # use_x11 = is_linux && !use_ozone.
457 # TODO(dpranke): Figure out how to keep this in sync better. 497 # TODO(dpranke): Figure out how to keep this in sync better.
458 use_x11 = (sys.platform == 'linux2' and 498 use_x11 = (self.platform == 'linux2' and
459 not 'target_os="android"' in vals['gn_args'] and 499 not 'target_os="android"' in vals['gn_args'] and
460 not 'use_ozone=true' in vals['gn_args']) 500 not 'use_ozone=true' in vals['gn_args'])
461 501
462 asan = 'is_asan=true' in vals['gn_args'] 502 asan = 'is_asan=true' in vals['gn_args']
463 msan = 'is_msan=true' in vals['gn_args'] 503 msan = 'is_msan=true' in vals['gn_args']
464 tsan = 'is_tsan=true' in vals['gn_args'] 504 tsan = 'is_tsan=true' in vals['gn_args']
465 505
466 executable_suffix = '.exe' if sys.platform == 'win32' else '' 506 executable_suffix = '.exe' if self.platform == 'win32' else ''
467 507
468 if test_type == 'gtest_test': 508 test_type = gn_isolate_map[target]['type']
469 extra_files.append('../../testing/test_env.py') 509 cmdline = []
510 extra_files = []
470 511
471 if use_x11: 512 if use_x11 and test_type == 'windowed_test_launcher':
472 # TODO(dpranke): Figure out some way to figure out which 513 extra_files = [
473 # test steps really need xvfb. 514 'xdisplaycheck',
474 extra_files.append('xdisplaycheck') 515 '../../testing/test_env.py',
475 extra_files.append('../../testing/xvfb.py')
476
477 cmdline = [
478 '../../testing/xvfb.py', 516 '../../testing/xvfb.py',
479 '.', 517 ]
480 './' + str(target), 518 cmdline = [
481 '--brave-new-test-launcher', 519 '../../testing/xvfb.py',
482 '--test-launcher-bot-mode', 520 '.',
483 '--asan=%d' % asan, 521 './' + str(target),
484 '--msan=%d' % msan, 522 '--brave-new-test-launcher',
485 '--tsan=%d' % tsan, 523 '--test-launcher-bot-mode',
486 ] 524 '--asan=%d' % asan,
487 else: 525 '--msan=%d' % msan,
488 cmdline = [ 526 '--tsan=%d' % tsan,
527 ]
528 elif test_type in ('windowed_test_launcher', 'console_test_launcher'):
529 extra_files = [
530 '../../testing/test_env.py'
531 ]
532 cmdline = [
489 '../../testing/test_env.py', 533 '../../testing/test_env.py',
490 '.',
491 './' + str(target) + executable_suffix, 534 './' + str(target) + executable_suffix,
492 '--brave-new-test-launcher', 535 '--brave-new-test-launcher',
493 '--test-launcher-bot-mode', 536 '--test-launcher-bot-mode',
494 '--asan=%d' % asan, 537 '--asan=%d' % asan,
495 '--msan=%d' % msan, 538 '--msan=%d' % msan,
496 '--tsan=%d' % tsan, 539 '--tsan=%d' % tsan,
497 ] 540 ]
541 elif test_type == 'gpu_browser_test':
542 extra_files = [
543 '../../testing/test_env.py'
544 ]
545 gtest_filter = gn_isolate_map[target]['gtest_filter']
546 cmdline = [
547 '../../testing/test_env.py',
548 './browser_tests' + executable_suffix,
549 '--test-launcher-bot-mode',
550 '--enable-gpu',
551 '--test-launcher-jobs=1',
552 '--gtest_filter=%s' % gtest_filter,
553 ]
554 elif test_type == 'script':
555 extra_files = [
556 '../../testing/test_env.py'
557 ]
558 cmdline = [
559 '../../testing/test_env.py',
560 ] + ['../../' + self.ToSrcRelPath(gn_isolate_map[target]['script'])]
561 elif test_type in ('raw'):
562 extra_files = []
563 cmdline = [
564 './' + str(target) + executable_suffix,
565 ] + gn_isolate_map[target].get('args')
566
498 else: 567 else:
499 # TODO(dpranke): Handle script_tests and other types of swarmed tests. 568 self.WriteFailureAndRaise('No command line for %s found (test type %s).'
500 self.WriteFailureAndRaise('unknown test type "%s" for %s' % 569 % (target, test_type), output_path=None)
501 (test_type, target), output_path=None)
502
503 570
504 return cmdline, extra_files 571 return cmdline, extra_files
505 572
506 def ToAbsPath(self, build_path, *comps): 573 def ToAbsPath(self, build_path, *comps):
507 return os.path.join(self.chromium_src_dir, 574 return self.PathJoin(self.chromium_src_dir,
508 self.ToSrcRelPath(build_path), 575 self.ToSrcRelPath(build_path),
509 *comps) 576 *comps)
510 577
511 def ToSrcRelPath(self, path): 578 def ToSrcRelPath(self, path):
512 """Returns a relative path from the top of the repo.""" 579 """Returns a relative path from the top of the repo."""
513 # TODO: Support normal paths in addition to source-absolute paths. 580 # TODO: Support normal paths in addition to source-absolute paths.
514 assert(path.startswith('//')) 581 assert(path.startswith('//'))
515 return path[2:].replace('/', os.sep) 582 return path[2:].replace('/', self.sep)
516 583
517 def ParseGYPConfigPath(self, path): 584 def ParseGYPConfigPath(self, path):
518 rpath = self.ToSrcRelPath(path) 585 rpath = self.ToSrcRelPath(path)
519 output_dir, _, config = rpath.rpartition('/') 586 output_dir, _, _ = rpath.rpartition(self.sep)
520 self.CheckGYPConfigIsSupported(config, path) 587 return output_dir
521 return output_dir, config
522 588
523 def CheckGYPConfigIsSupported(self, config, path): 589 def GYPCmd(self, output_dir, gyp_defines):
524 if config not in ('Debug', 'Release'): 590 goma_dir = self.args.goma_dir
525 if (sys.platform in ('win32', 'cygwin') and
526 config not in ('Debug_x64', 'Release_x64')):
527 raise MBErr('Unknown or unsupported config type "%s" in "%s"' %
528 config, path)
529 591
530 def GYPCmd(self, output_dir, gyp_defines, config): 592 # GYP uses shlex.split() to split the gyp defines into separate arguments,
531 gyp_defines = gyp_defines.replace("$(goma_dir)", self.args.goma_dir) 593 # so we can support backslashes and and spaces in arguments by quoting
594 # them, even on Windows, where this normally wouldn't work.
595 if '\\' in goma_dir or ' ' in goma_dir:
596 goma_dir = "'%s'" % goma_dir
597 gyp_defines = gyp_defines.replace("$(goma_dir)", goma_dir)
598
532 cmd = [ 599 cmd = [
533 sys.executable, 600 self.executable,
534 os.path.join('build', 'gyp_chromium'), 601 self.PathJoin('build', 'gyp_chromium'),
535 '-G', 602 '-G',
536 'output_dir=' + output_dir, 603 'output_dir=' + output_dir,
537 '-G',
538 'config=' + config,
539 ] 604 ]
540 for d in shlex.split(gyp_defines): 605 env = os.environ.copy()
541 cmd += ['-D', d] 606 env['GYP_DEFINES'] = gyp_defines
542 return cmd 607 return cmd, env
543 608
544 def RunGNAnalyze(self, vals): 609 def RunGNAnalyze(self, vals):
545 # analyze runs before 'gn gen' now, so we need to run gn gen 610 # analyze runs before 'gn gen' now, so we need to run gn gen
546 # in order to ensure that we have a build directory. 611 # in order to ensure that we have a build directory.
547 ret = self.RunGNGen(vals) 612 ret = self.RunGNGen(vals)
548 if ret: 613 if ret:
549 return ret 614 return ret
550 615
551 inp = self.ReadInputJSON(['files', 'targets']) 616 inp = self.ReadInputJSON(['files', 'targets'])
552 if self.args.verbose: 617 if self.args.verbose:
553 self.Print() 618 self.Print()
554 self.Print('analyze input:') 619 self.Print('analyze input:')
555 self.PrintJSON(inp) 620 self.PrintJSON(inp)
556 self.Print() 621 self.Print()
557 622
558 output_path = self.args.output_path[0] 623 output_path = self.args.output_path[0]
559 624
560 # Bail out early if a GN file was modified, since 'gn refs' won't know 625 # Bail out early if a GN file was modified, since 'gn refs' won't know
561 # what to do about it. 626 # what to do about it.
562 if any(f.endswith('.gn') or f.endswith('.gni') for f in inp['files']): 627 if any(f.endswith('.gn') or f.endswith('.gni') for f in inp['files']):
563 self.WriteJSON({'status': 'Found dependency (all)'}, output_path) 628 self.WriteJSON({'status': 'Found dependency (all)'}, output_path)
564 return 0 629 return 0
565 630
566 # Bail out early if 'all' was asked for, since 'gn refs' won't recognize it. 631 # Bail out early if 'all' was asked for, since 'gn refs' won't recognize it.
567 if 'all' in inp['targets']: 632 if 'all' in inp['targets']:
568 self.WriteJSON({'status': 'Found dependency (all)'}, output_path) 633 self.WriteJSON({'status': 'Found dependency (all)'}, output_path)
569 return 0 634 return 0
570 635
636 # This shouldn't normally happen, but could due to unusual race conditions,
637 # like a try job that gets scheduled before a patch lands but runs after
638 # the patch has landed.
639 if not inp['files']:
640 self.Print('Warning: No files modified in patch, bailing out early.')
641 self.WriteJSON({'targets': [],
642 'build_targets': [],
643 'status': 'No dependency'}, output_path)
644 return 0
645
571 ret = 0 646 ret = 0
572 response_file = self.TempFile() 647 response_file = self.TempFile()
573 response_file.write('\n'.join(inp['files']) + '\n') 648 response_file.write('\n'.join(inp['files']) + '\n')
574 response_file.close() 649 response_file.close()
575 650
576 matching_targets = [] 651 matching_targets = []
577 try: 652 try:
578 cmd = self.GNCmd('refs', self.args.path[0]) + [ 653 cmd = self.GNCmd('refs', self.args.path[0]) + [
579 '@%s' % response_file.name, '--all', '--as=output'] 654 '@%s' % response_file.name, '--all', '--as=output']
580 ret, out, _ = self.Run(cmd) 655 ret, out, _ = self.Run(cmd, force_verbose=False)
581 if ret and not 'The input matches no targets' in out: 656 if ret and not 'The input matches no targets' in out:
582 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out), 657 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out),
583 output_path) 658 output_path)
584 build_dir = self.ToSrcRelPath(self.args.path[0]) + os.sep 659 build_dir = self.ToSrcRelPath(self.args.path[0]) + self.sep
585 for output in out.splitlines(): 660 for output in out.splitlines():
586 build_output = output.replace(build_dir, '') 661 build_output = output.replace(build_dir, '')
587 if build_output in inp['targets']: 662 if build_output in inp['targets']:
588 matching_targets.append(build_output) 663 matching_targets.append(build_output)
589 664
590 cmd = self.GNCmd('refs', self.args.path[0]) + [ 665 cmd = self.GNCmd('refs', self.args.path[0]) + [
591 '@%s' % response_file.name, '--all'] 666 '@%s' % response_file.name, '--all']
592 ret, out, _ = self.Run(cmd) 667 ret, out, _ = self.Run(cmd, force_verbose=False)
593 if ret and not 'The input matches no targets' in out: 668 if ret and not 'The input matches no targets' in out:
594 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out), 669 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out),
595 output_path) 670 output_path)
596 for label in out.splitlines(): 671 for label in out.splitlines():
597 build_target = label[2:] 672 build_target = label[2:]
598 # We want to accept 'chrome/android:chrome_shell_apk' and 673 # We want to accept 'chrome/android:chrome_public_apk' and
599 # just 'chrome_shell_apk'. This may result in too many targets 674 # just 'chrome_public_apk'. This may result in too many targets
600 # getting built, but we can adjust that later if need be. 675 # getting built, but we can adjust that later if need be.
601 for input_target in inp['targets']: 676 for input_target in inp['targets']:
602 if (input_target == build_target or 677 if (input_target == build_target or
603 build_target.endswith(':' + input_target)): 678 build_target.endswith(':' + input_target)):
604 matching_targets.append(input_target) 679 matching_targets.append(input_target)
605 finally: 680 finally:
606 self.RemoveFile(response_file.name) 681 self.RemoveFile(response_file.name)
607 682
608 if matching_targets: 683 if matching_targets:
609 # TODO: it could be that a target X might depend on a target Y 684 # TODO: it could be that a target X might depend on a target Y
610 # and both would be listed in the input, but we would only need 685 # and both would be listed in the input, but we would only need
611 # to specify target X as a build_target (whereas both X and Y are 686 # to specify target X as a build_target (whereas both X and Y are
612 # targets). I'm not sure if that optimization is generally worth it. 687 # targets). I'm not sure if that optimization is generally worth it.
613 self.WriteJSON({'targets': sorted(matching_targets), 688 self.WriteJSON({'targets': sorted(set(matching_targets)),
614 'build_targets': sorted(matching_targets), 689 'build_targets': sorted(set(matching_targets)),
615 'status': 'Found dependency'}, output_path) 690 'status': 'Found dependency'}, output_path)
616 else: 691 else:
617 self.WriteJSON({'targets': [], 692 self.WriteJSON({'targets': [],
618 'build_targets': [], 693 'build_targets': [],
619 'status': 'No dependency'}, output_path) 694 'status': 'No dependency'}, output_path)
620 695
621 if not ret and self.args.verbose: 696 if self.args.verbose:
622 outp = json.loads(self.ReadFile(output_path)) 697 outp = json.loads(self.ReadFile(output_path))
623 self.Print() 698 self.Print()
624 self.Print('analyze output:') 699 self.Print('analyze output:')
625 self.PrintJSON(outp) 700 self.PrintJSON(outp)
626 self.Print() 701 self.Print()
627 702
628 return 0 703 return 0
629 704
630 def ReadInputJSON(self, required_keys): 705 def ReadInputJSON(self, required_keys):
631 path = self.args.input_path[0] 706 path = self.args.input_path[0]
632 output_path = self.args.output_path[0] 707 output_path = self.args.output_path[0]
633 if not self.Exists(path): 708 if not self.Exists(path):
634 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path) 709 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
635 710
636 try: 711 try:
637 inp = json.loads(self.ReadFile(path)) 712 inp = json.loads(self.ReadFile(path))
638 except Exception as e: 713 except Exception as e:
639 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' % 714 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
640 (path, e), output_path) 715 (path, e), output_path)
641 716
642 for k in required_keys: 717 for k in required_keys:
643 if not k in inp: 718 if not k in inp:
644 self.WriteFailureAndRaise('input file is missing a "%s" key' % k, 719 self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
645 output_path) 720 output_path)
646 721
647 return inp 722 return inp
648 723
649 def WriteFailureAndRaise(self, msg, output_path): 724 def WriteFailureAndRaise(self, msg, output_path):
650 if output_path: 725 if output_path:
651 self.WriteJSON({'error': msg}, output_path) 726 self.WriteJSON({'error': msg}, output_path, force_verbose=True)
652 raise MBErr(msg) 727 raise MBErr(msg)
653 728
654 def WriteJSON(self, obj, path): 729 def WriteJSON(self, obj, path, force_verbose=False):
655 try: 730 try:
656 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n') 731 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n',
732 force_verbose=force_verbose)
657 except Exception as e: 733 except Exception as e:
658 raise MBErr('Error %s writing to the output path "%s"' % 734 raise MBErr('Error %s writing to the output path "%s"' %
659 (e, path)) 735 (e, path))
660 736
661 def PrintCmd(self, cmd): 737 def PrintCmd(self, cmd, env):
662 if cmd[0] == sys.executable: 738 if self.platform == 'win32':
739 env_prefix = 'set '
740 env_quoter = QuoteForSet
741 shell_quoter = QuoteForCmd
742 else:
743 env_prefix = ''
744 env_quoter = pipes.quote
745 shell_quoter = pipes.quote
746
747 def print_env(var):
748 if env and var in env:
749 self.Print('%s%s=%s' % (env_prefix, var, env_quoter(env[var])))
750
751 print_env('GYP_CROSSCOMPILE')
752 print_env('GYP_DEFINES')
753
754 if cmd[0] == self.executable:
663 cmd = ['python'] + cmd[1:] 755 cmd = ['python'] + cmd[1:]
664 self.Print(*[pipes.quote(c) for c in cmd]) 756 self.Print(*[shell_quoter(arg) for arg in cmd])
665 757
666 def PrintJSON(self, obj): 758 def PrintJSON(self, obj):
667 self.Print(json.dumps(obj, indent=2, sort_keys=True)) 759 self.Print(json.dumps(obj, indent=2, sort_keys=True))
668 760
669 def Print(self, *args, **kwargs): 761 def Print(self, *args, **kwargs):
670 # This function largely exists so it can be overridden for testing. 762 # This function largely exists so it can be overridden for testing.
671 print(*args, **kwargs) 763 print(*args, **kwargs)
672 764
673 def Run(self, cmd): 765 def Run(self, cmd, env=None, force_verbose=True):
674 # This function largely exists so it can be overridden for testing. 766 # This function largely exists so it can be overridden for testing.
675 if self.args.dryrun or self.args.verbose: 767 if self.args.dryrun or self.args.verbose or force_verbose:
676 self.PrintCmd(cmd) 768 self.PrintCmd(cmd, env)
677 if self.args.dryrun: 769 if self.args.dryrun:
678 return 0, '', '' 770 return 0, '', ''
679 ret, out, err = self.Call(cmd) 771
680 if self.args.verbose: 772 ret, out, err = self.Call(cmd, env=env)
773 if self.args.verbose or force_verbose:
681 if out: 774 if out:
682 self.Print(out, end='') 775 self.Print(out, end='')
683 if err: 776 if err:
684 self.Print(err, end='', file=sys.stderr) 777 self.Print(err, end='', file=sys.stderr)
685 return ret, out, err 778 return ret, out, err
686 779
687 def Call(self, cmd): 780 def Call(self, cmd, env=None):
688 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir, 781 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir,
689 stdout=subprocess.PIPE, stderr=subprocess.PIPE) 782 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
783 env=env)
690 out, err = p.communicate() 784 out, err = p.communicate()
691 return p.returncode, out, err 785 return p.returncode, out, err
692 786
693 def ExpandUser(self, path): 787 def ExpandUser(self, path):
694 # This function largely exists so it can be overridden for testing. 788 # This function largely exists so it can be overridden for testing.
695 return os.path.expanduser(path) 789 return os.path.expanduser(path)
696 790
697 def Exists(self, path): 791 def Exists(self, path):
698 # This function largely exists so it can be overridden for testing. 792 # This function largely exists so it can be overridden for testing.
699 return os.path.exists(path) 793 return os.path.exists(path)
700 794
701 def MaybeMakeDirectory(self, path): 795 def MaybeMakeDirectory(self, path):
702 try: 796 try:
703 os.makedirs(path) 797 os.makedirs(path)
704 except OSError, e: 798 except OSError, e:
705 if e.errno != errno.EEXIST: 799 if e.errno != errno.EEXIST:
706 raise 800 raise
707 801
802 def PathJoin(self, *comps):
803 # This function largely exists so it can be overriden for testing.
804 return os.path.join(*comps)
805
708 def ReadFile(self, path): 806 def ReadFile(self, path):
709 # This function largely exists so it can be overriden for testing. 807 # This function largely exists so it can be overriden for testing.
710 with open(path) as fp: 808 with open(path) as fp:
711 return fp.read() 809 return fp.read()
712 810
713 def RemoveFile(self, path): 811 def RemoveFile(self, path):
714 # This function largely exists so it can be overriden for testing. 812 # This function largely exists so it can be overriden for testing.
715 os.remove(path) 813 os.remove(path)
716 814
815 def RemoveDirectory(self, abs_path):
816 if self.platform == 'win32':
817 # In other places in chromium, we often have to retry this command
818 # because we're worried about other processes still holding on to
819 # file handles, but when MB is invoked, it will be early enough in the
820 # build that their should be no other processes to interfere. We
821 # can change this if need be.
822 self.Run(['cmd.exe', '/c', 'rmdir', '/q', '/s', abs_path])
823 else:
824 shutil.rmtree(abs_path, ignore_errors=True)
825
717 def TempFile(self, mode='w'): 826 def TempFile(self, mode='w'):
718 # This function largely exists so it can be overriden for testing. 827 # This function largely exists so it can be overriden for testing.
719 return tempfile.NamedTemporaryFile(mode=mode, delete=False) 828 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
720 829
721 def WriteFile(self, path, contents): 830 def WriteFile(self, path, contents, force_verbose=False):
722 # This function largely exists so it can be overriden for testing. 831 # This function largely exists so it can be overriden for testing.
723 if self.args.dryrun or self.args.verbose: 832 if self.args.dryrun or self.args.verbose or force_verbose:
724 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path)) 833 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
725 with open(path, 'w') as fp: 834 with open(path, 'w') as fp:
726 return fp.write(contents) 835 return fp.write(contents)
727 836
728 837
729 class MBErr(Exception): 838 class MBErr(Exception):
730 pass 839 pass
731 840
732 841
842 # See http://goo.gl/l5NPDW and http://goo.gl/4Diozm for the painful
843 # details of this next section, which handles escaping command lines
844 # so that they can be copied and pasted into a cmd window.
845 UNSAFE_FOR_SET = set('^<>&|')
846 UNSAFE_FOR_CMD = UNSAFE_FOR_SET.union(set('()%'))
847 ALL_META_CHARS = UNSAFE_FOR_CMD.union(set('"'))
848
849
850 def QuoteForSet(arg):
851 if any(a in UNSAFE_FOR_SET for a in arg):
852 arg = ''.join('^' + a if a in UNSAFE_FOR_SET else a for a in arg)
853 return arg
854
855
856 def QuoteForCmd(arg):
857 # First, escape the arg so that CommandLineToArgvW will parse it properly.
858 # From //tools/gyp/pylib/gyp/msvs_emulation.py:23.
859 if arg == '' or ' ' in arg or '"' in arg:
860 quote_re = re.compile(r'(\\*)"')
861 arg = '"%s"' % (quote_re.sub(lambda mo: 2 * mo.group(1) + '\\"', arg))
862
863 # Then check to see if the arg contains any metacharacters other than
864 # double quotes; if it does, quote everything (including the double
865 # quotes) for safety.
866 if any(a in UNSAFE_FOR_CMD for a in arg):
867 arg = ''.join('^' + a if a in ALL_META_CHARS else a for a in arg)
868 return arg
869
870
733 if __name__ == '__main__': 871 if __name__ == '__main__':
734 try: 872 try:
735 sys.exit(main(sys.argv[1:])) 873 sys.exit(main(sys.argv[1:]))
736 except MBErr as e: 874 except MBErr as e:
737 print(e) 875 print(e)
738 sys.exit(1) 876 sys.exit(1)
739 except KeyboardInterrupt: 877 except KeyboardInterrupt:
740 print("interrupted, exiting", stream=sys.stderr) 878 print("interrupted, exiting", stream=sys.stderr)
741 sys.exit(130) 879 sys.exit(130)
OLDNEW
« no previous file with comments | « tools/mb/docs/user_guide.md ('k') | tools/mb/mb_config.pyl » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698