| OLD | NEW |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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) |
| OLD | NEW |