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

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

Issue 1062613004: Implement mb - a meta-build wrapper for bots to use in the GYP->GN migration. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: bug fixes and tests, reorganize mb_config.pyl Created 5 years, 8 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/mb.bat ('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
(Empty)
1 #!/usr/bin/env python
2 # Copyright 2015 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 """MB - the Meta-Build wrapper around GYP and GN
7
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.
10 """
11
12 from __future__ import print_function
13
14 import argparse
15 import ast
16 import json
17 import os
18 import pipes
19 import shlex
20 import shutil
21 import sys
22 import subprocess
23
24
25 def main(args):
26 mb = MetaBuildWrapper()
27 mb.ParseArgs(args)
28 return mb.args.func()
29
30
31 class MetaBuildWrapper(object):
32 def __init__(self):
33 p = os.path
34 d = os.path.dirname
35 self.chromium_src_dir = p.normpath(d(d(d(p.abspath(__file__)))))
36 self.default_config = p.join(self.chromium_src_dir, 'tools', 'mb',
37 'mb_config.pyl')
38 self.args = argparse.Namespace()
39 self.configs = {}
40 self.masters = {}
41 self.mixins = {}
42 self.private_configs = []
43 self.common_dev_configs = []
44 self.unsupported_configs = []
45
46 def ParseArgs(self, argv):
47 def AddCommonOptions(subp):
48 subp.add_argument('-b', '--builder',
49 help='builder name to look up config from')
50 subp.add_argument('-m', '--master',
51 help='master name to look up config from')
52 subp.add_argument('-c', '--config',
53 help='configuration to analyze')
54 subp.add_argument('-f', '--config-file', metavar='PATH',
55 default=self.default_config,
56 help='path to config file '
57 '(default is //tools/mb/mb_config.pyl)')
58 subp.add_argument('-g', '--goma-dir', default=self.ExpandUser('~/goma'),
59 help='path to goma directory (default is %(default)s).')
60 subp.add_argument('-n', '--dryrun', action='store_true',
61 help='Do a dry run (i.e., do nothing, just print '
62 'the commands that will run)')
63 subp.add_argument('-q', '--quiet', action='store_true',
64 help='Do not print anything, just return an exit '
65 'code.')
66 subp.add_argument('-v', '--verbose', action='count',
67 help='verbose logging (may specify multiple times).')
68
69 parser = argparse.ArgumentParser(prog='mb')
70 subps = parser.add_subparsers()
71
72 subp = subps.add_parser('analyze',
73 help='analyze whether changes to a set of files '
74 'will cause a set of binaries to be rebuilt.')
75 AddCommonOptions(subp)
76 subp.add_argument('path', type=str, nargs=1,
77 help='path build was generated into.')
78 subp.add_argument('input_path', nargs=1,
79 help='path to a file containing the input arguments '
80 'as a JSON object.')
81 subp.add_argument('output_path', nargs=1,
82 help='path to a file containing the output arguments '
83 'as a JSON object.')
84 subp.set_defaults(func=self.CmdAnalyze)
85
86 subp = subps.add_parser('gen',
87 help='generate a new set of build files')
88 AddCommonOptions(subp)
89 subp.add_argument('path', type=str, nargs=1,
90 help='path to generate build into')
91 subp.set_defaults(func=self.CmdGen)
92
93 subp = subps.add_parser('lookup',
94 help='look up the command for a given config or '
95 'builder')
96 AddCommonOptions(subp)
97 subp.set_defaults(func=self.CmdLookup)
98
99 subp = subps.add_parser('validate',
100 help='validate the config file')
101 AddCommonOptions(subp)
102 subp.set_defaults(func=self.CmdValidate)
103
104 subp = subps.add_parser('help',
105 help='Get help on a subcommand.')
106 subp.add_argument(nargs='?', action='store', dest='subcommand',
107 help='The command to get help for.')
108 subp.set_defaults(func=self.CmdHelp)
109
110 self.args = parser.parse_args(argv)
111
112 def CmdAnalyze(self):
113 vals = self.GetConfig()
114 if vals['type'] == 'gn':
115 return self.RunGNAnalyze(vals)
116 elif vals['type'] == 'gyp':
117 return self.RunGYPAnalyze(vals)
118 else:
119 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
120
121 def CmdGen(self):
122 vals = self.GetConfig()
123 if vals['type'] == 'gn':
124 self.RunGNGen(self.args.path[0], vals)
125 elif vals['type'] == 'gyp':
126 self.RunGYPGen(self.args.path[0], vals)
127 else:
128 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
129 return 0
130
131 def CmdLookup(self):
132 vals = self.GetConfig()
133 if vals['type'] == 'gn':
134 cmd = self.GNCmd('<path>', vals['gn_args'])
135 elif vals['type'] == 'gyp':
136 cmd = self.GYPCmd('<path>', vals['gyp_defines'], vals['gyp_config'])
137 else:
138 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
139
140 self.PrintCmd(cmd)
141 return 0
142
143 def CmdHelp(self):
144 if self.args.subcommand:
145 self.ParseArgs([self.args.subcommand, '--help'])
146 else:
147 self.ParseArgs(['--help'])
148
149 def CmdValidate(self):
150 errs = []
151
152 # Read the file to make sure it parses.
153 self.ReadConfigFile()
154
155 # Figure out the whole list of configs and ensure that no config is
156 # listed in more than one category.
157 all_configs = {}
158 for config in self.common_dev_configs:
159 all_configs[config] = 'common_dev_configs'
160 for config in self.private_configs:
161 if config in all_configs:
162 errs.append('config "%s" listed in "private_configs" also '
163 'listed in "%s"' % (config, all_configs['config']))
164 else:
165 all_configs[config] = 'private_configs'
166 for config in self.unsupported_configs:
167 if config in all_configs:
168 errs.append('config "%s" listed in "unsupported_configs" also '
169 'listed in "%s"' % (config, all_configs['config']))
170 else:
171 all_configs[config] = 'unsupported_configs'
172
173 for master in self.masters:
174 for builder in self.masters[master]:
175 config = self.masters[master][builder]
176 if config in all_configs and all_configs[config] not in self.masters:
177 errs.append('Config "%s" used by a bot is also listed in "%s".' %
178 (config, all_configs[config]))
179 else:
180 all_configs[config] = master
181
182 # Check that every referenced config actually exists.
183 for config, loc in all_configs.items():
184 if not config in self.configs:
185 errs.append('Unknown config "%s" referenced from "%s".' %
186 (config, loc))
187
188 # Check that every actual config is actually referenced.
189 for config in self.configs:
190 if not config in all_configs:
191 errs.append('Unused config "%s".' % config)
192
193 # Figure out the whole list of mixins, and check that every mixin
194 # listed by a config or another mixin actually exists.
195 referenced_mixins = set()
196 for config, mixins in self.configs.items():
197 for mixin in mixins:
198 if not mixin in self.mixins:
199 errs.append('Unknown mixin "%s" referenced by config "%s".' %
200 (mixin, config))
201 referenced_mixins.add(mixin)
202
203 for mixin in self.mixins:
204 for sub_mixin in self.mixins[mixin].get('mixins', []):
205 if not sub_mixin in self.mixins:
206 errs.append('Unknown mixin "%s" referenced by mixin "%s".' %
207 (sub_mixin, mixin))
208 referenced_mixins.add(sub_mixin)
209
210 # Check that every mixin defined is actually referenced somewhere.
211 for mixin in self.mixins:
212 if not mixin in referenced_mixins:
213 errs.append('Unreferenced mixin "%s".' % mixin)
214
215 if errs:
216 raise MBErr('mb config file %s has problems:\n ' + '\n '.join(errs))
217
218 if not self.args.quiet:
219 self.Print('mb config file %s looks ok.' % self.args.config_file)
220 return 0
221
222 def GetConfig(self):
223 self.ReadConfigFile()
224 config = self.ConfigFromArgs()
225 if not config in self.configs:
226 raise MBErr('Config "%s" not found in %s' %
227 (config, self.args.config_file))
228
229 return self.FlattenConfig(config)
230
231 def ReadConfigFile(self):
232 if not self.Exists(self.args.config_file):
233 raise MBErr('config file not found at %s' % self.args.config_file)
234
235 try:
236 contents = ast.literal_eval(self.ReadFile(self.args.config_file))
237 except SyntaxError as e:
238 raise MBErr('Failed to parse config file "%s": %s' %
239 (self.args.config_file, e))
240
241 self.common_dev_configs = contents['common_dev_configs']
242 self.configs = contents['configs']
243 self.masters = contents['masters']
244 self.mixins = contents['mixins']
245 self.private_configs = contents['private_configs']
246 self.unsupported_configs = contents['unsupported_configs']
247
248 def ConfigFromArgs(self):
249 if self.args.config:
250 if self.args.master or self.args.builder:
251 raise MBErr('Can not specific both -c/--config and -m/--master or '
252 '-b/--builder')
253
254 return self.args.config
255
256 if not self.args.master or not self.args.builder:
257 raise MBErr('Must specify either -c/--config or '
258 '(-m/--master and -b/--builder)')
259
260 if not self.args.master in self.masters:
261 raise MBErr('Master name "%s" not found in "%s"' %
262 (self.args.master, self.args.config_file))
263
264 if not self.args.builder in self.masters[self.args.master]:
265 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' %
266 (self.args.builder, self.args.master, self.args.config_file))
267
268 return self.masters[self.args.master][self.args.builder]
269
270 def FlattenConfig(self, config):
271 mixins = self.configs[config]
272 vals = {
273 'type': None,
274 'gn_args': [],
275 'gyp_config': [],
276 'gyp_defines': [],
277 }
278
279 visited = []
280 self.FlattenMixins(mixins, vals, visited)
281 return vals
282
283 def FlattenMixins(self, mixins, vals, visited):
284 for m in mixins:
285 if m not in self.mixins:
286 raise MBErr('Unknown mixin "%s"' % m)
287 if m in visited:
288 raise MBErr('Cycle in mixins for "%s": %s' % (m, visited))
289
290 visited.append(m)
291
292 mixin_vals = self.mixins[m]
293 if 'type' in mixin_vals:
294 vals['type'] = mixin_vals['type']
295 if 'gn_args' in mixin_vals:
296 if vals['gn_args']:
297 vals['gn_args'] += ' ' + mixin_vals['gn_args']
298 else:
299 vals['gn_args'] = mixin_vals['gn_args']
300 if 'gyp_config' in mixin_vals:
301 vals['gyp_config'] = mixin_vals['gyp_config']
302 if 'gyp_defines' in mixin_vals:
303 if vals['gyp_defines']:
304 vals['gyp_defines'] += ' ' + mixin_vals['gyp_defines']
305 else:
306 vals['gyp_defines'] = mixin_vals['gyp_defines']
307 if 'mixins' in mixin_vals:
308 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
309 return vals
310
311 def RunGNGen(self, path, vals):
312 cmd = self.GNCmd(path, vals['gn_args'])
313 ret, _, _ = self.Run(cmd)
314 return ret
315
316 def GNCmd(self, path, gn_args):
317 # TODO(dpranke): Find gn explicitly in the path ...
318 cmd = ['gn', 'gen', path]
319 gn_args = gn_args.replace("$(goma_dir)", gn_args)
320 if gn_args:
321 cmd.append('--args=%s' % gn_args)
322 return cmd
323
324 def RunGYPGen(self, path, vals):
325 output_dir, gyp_config = self.ParseGYPConfigPath(path)
326 if gyp_config != vals['gyp_config']:
327 raise MBErr('The last component of the path (%s) must match the '
328 'GYP configuration specified in the config (%s), and '
329 'it does not.' % (gyp_config, vals['gyp_config']))
330 cmd = self.GYPCmd(output_dir, vals['gyp_defines'], config=gyp_config)
331 ret, _, _ = self.Run(cmd)
332 return ret
333
334 def RunGYPAnalyze(self, vals):
335 output_dir, gyp_config = self.ParseGYPConfigPath(self.args.path[0])
336 if gyp_config != vals['gyp_config']:
337 raise MBErr('The last component of the path (%s) must match the '
338 'GYP configuration specified in the config (%s), and '
339 'it does not.' % (gyp_config, vals['gyp_config']))
340 cmd = self.GYPCmd(output_dir, vals['gyp_defines'], config=gyp_config)
341 cmd.extend(['-G', 'config_path=%s' % self.args.input_path[0],
342 '-G', 'analyzer_output_path=%s' % self.args.output_path[0]])
343 ret, _, _ = self.Run(cmd)
344 return ret
345
346 def ParseGYPOutputPath(self, path):
347 assert(path.startswith('//'))
348 return path[2:]
349
350 def ParseGYPConfigPath(self, path):
351 assert(path.startswith('//'))
352 output_dir, _, config = path[2:].rpartition('/')
353 self.CheckGYPConfigIsSupported(config, path)
354 return output_dir, config
355
356 def CheckGYPConfigIsSupported(self, config, path):
357 if config not in ('Debug', 'Release'):
358 if (sys.platform in ('win32', 'cygwin') and
359 config not in ('Debug_x64', 'Release_x64')):
360 raise MBErr('Unknown or unsupported config type "%s" in "%s"' %
361 config, path)
362
363 def GYPCmd(self, output_dir, gyp_defines, config):
364 gyp_defines = gyp_defines.replace("$(goma_dir)", self.args.goma_dir)
365 cmd = [
366 sys.executable,
367 os.path.join('build', 'gyp_chromium'),
368 '-G',
369 'output_dir=' + output_dir,
370 '-G',
371 'config=' + config,
372 ]
373 for d in shlex.split(gyp_defines):
374 cmd += ['-D', d]
375 return cmd
376
377 def RunGNAnalyze(self, _vals):
378 inp = self.GetAnalyzeInput()
379
380 # Bail out early if a GN file was modified, since 'gn refs' won't know
381 # what to do about it.
382 if any(f.endswith('.gn') or f.endswith('.gni') for f in inp['files']):
383 self.WriteJSONOutput({'status': 'Found dependency (all)'})
384 return 0
385
386 # TODO: Break long lists of files that might exceed the max command line
387 # up into chunks so that we can return more accurate info.
388 if len(' '.join(inp['files'])) > 1024:
389 self.WriteJSONOutput({'status': 'Found dependency (all)'})
390 return 0
391
392 cmd = (['gn', 'refs', self.args.path[0]] + inp['files'] +
393 ['--type=executable', '--all', '--as=output'])
394 needed_targets = []
395 ret, out, _ = self.Run(cmd)
396
397 if ret:
398 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out))
399
400 rpath = os.path.relpath(self.args.path[0], self.chromium_src_dir) + os.sep
401 needed_targets = [t.replace(rpath, '') for t in out.splitlines()]
402 needed_targets = [nt for nt in needed_targets if nt in inp['targets']]
403
404 for nt in needed_targets:
405 self.Print(nt)
406
407 if needed_targets:
408 # TODO: it could be that a target X might depend on a target Y
409 # and both would be listed in the input, but we would only need
410 # to specify target X as a build_target (whereas both X and Y are
411 # targets). I'm not sure if that optimization is generally worth it.
412 self.WriteJSON({'targets': needed_targets,
413 'build_targets': needed_targets,
414 'status': 'Found dependency'})
415 else:
416 self.WriteJSON({'targets': [],
417 'build_targets': [],
418 'status': 'No dependency'})
419
420 return 0
421
422 def GetAnalyzeInput(self):
423 path = self.args.input_path[0]
424 if not self.Exists(path):
425 self.WriteFailureAndRaise('"%s" does not exist' % path)
426
427 try:
428 inp = json.loads(self.ReadFile(path))
429 except Exception as e:
430 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
431 (path, e))
432 if not 'files' in inp:
433 self.WriteFailureAndRaise('input file is missing a "files" key')
434 if not 'targets' in inp:
435 self.WriteFailureAndRaise('input file is missing a "targets" key')
436
437 return inp
438
439 def WriteFailureAndRaise(self, msg):
440 self.WriteJSON({'error': msg})
441 raise MBErr(msg)
442
443 def WriteJSON(self, obj):
444 output_path = self.args.output_path[0]
445 if output_path:
446 try:
447 self.WriteFile(output_path, json.dumps(obj))
448 except Exception as e:
449 raise MBErr('Error %s writing to the output path "%s"' %
450 (e, output_path))
451
452 def PrintCmd(self, cmd):
453 if cmd[0] == sys.executable:
454 cmd = ['python'] + cmd[1:]
455 self.Print(*[pipes.quote(c) for c in cmd])
456
457 def Print(self, *args, **kwargs):
458 # This function largely exists so it can be overridden for testing.
459 print(*args, **kwargs)
460
461 def Run(self, cmd):
462 # This function largely exists so it can be overridden for testing.
463 if self.args.dryrun or self.args.verbose:
464 self.PrintCmd(cmd)
465 if self.args.dryrun:
466 return 0, '', ''
467 ret, out, err = self.Call(cmd)
468 if self.args.verbose:
469 if out:
470 self.Print(out)
471 if err:
472 self.Print(err, file=sys.stderr)
473 return ret, out, err
474
475 def Call(self, cmd):
476 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir,
477 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
478 out, err = p.communicate()
479 return p.returncode, out, err
480
481 def ExpandUser(self, path):
482 # This function largely exists so it can be overridden for testing.
483 return os.path.expanduser(path)
484
485 def Exists(self, path):
486 # This function largely exists so it can be overridden for testing.
487 return os.path.exists(path)
488
489 def ReadFile(self, path):
490 # This function largely exists so it can be overriden for testing.
491 with open(path) as fp:
492 return fp.read()
493
494 def WriteFile(self, path, contents):
495 # This function largely exists so it can be overriden for testing.
496 with open(path, 'w') as fp:
497 return fp.write(contents)
498
499 class MBErr(Exception):
500 pass
501
502
503 if __name__ == '__main__':
504 try:
505 sys.exit(main(sys.argv[1:]))
506 except MBErr as e:
507 print(e)
508 sys.exit(1)
509 except KeyboardInterrupt:
510 print("interrupted, exiting", stream=sys.stderr)
511 sys.exit(130)
OLDNEW
« no previous file with comments | « tools/mb/mb.bat ('k') | tools/mb/mb_config.pyl » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698