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

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: 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
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 import tempfile
24
25
26 def main(args):
27 mb = MetaBuildWrapper()
28 mb.ParseArgs(args)
29 return mb.args.func()
30
31
32 class MetaBuildWrapper(object):
33 def __init__(self):
34 p = os.path
35 d = os.path.dirname
36 self.chromium_src_dir = p.normpath(d(d(d(p.abspath(__file__)))))
37 self.default_config = p.join(self.chromium_src_dir, 'tools', 'mb',
38 'mb_config.pyl')
39 self.configs = {}
40 self.masters = {}
41 self.mixins = {}
42
43 def ParseArgs(self, argv):
44 def AddCommonOptions(subp):
45 subp.add_argument('-b', '--builder',
46 help='builder name to look up config from')
47 subp.add_argument('-m', '--master',
48 help='master name to look up config from'),
49 subp.add_argument('-c', '--config',
50 help='configuration to analyze')
51 subp.add_argument('-f', '--config-file', metavar='PATH',
52 default=self.default_config,
53 help='path to config file '
54 '(default is //tools/mb/mb_config.pyl)')
55 subp.add_argument('-g', '--goma-dir', default=self.ExpandUser('~/goma'),
56 help='path to goma directory (default is %(default)s).')
57 subp.add_argument('-n', '--dryrun', action='store_true',
58 help='Do a dry run (i.e., do nothing, just print '
59 'the commands that will run)')
60 subp.add_argument('-q', '--quiet', action='store_true',
61 help='Do not print anything, just return an exit '
62 'code.')
63 subp.add_argument('-v', '--verbose', action='count',
64 help='verbose logging (may specify multiple times).')
65
66 parser = argparse.ArgumentParser(prog='mb')
67 subps = parser.add_subparsers()
68
69 subp = subps.add_parser('analyze',
70 help='analyze whether changes to a set of files '
71 'will cause a set of binaries to be rebuilt.')
72 AddCommonOptions(subp)
73 subp.add_argument('input-path', nargs=1,
74 help='path to a file containing the input arguments '
75 'as a JSON object.')
76 subp.add_argument('output-path', nargs=1,
77 help='path to a file containing the output arguments '
78 'as a JSON object.')
79 subp.set_defaults(func=self.CmdAnalyze)
80
81 subp = subps.add_parser('gen',
82 help='generate a new set of build files')
83 AddCommonOptions(subp)
84 subp.add_argument('path', type=str, nargs=1,
85 help='path to generate build into')
86 subp.set_defaults(func=self.CmdGen)
87
88 subp = subps.add_parser('lookup',
89 help='look up the command for a given config or '
90 'builder')
91 AddCommonOptions(subp)
92 subp.set_defaults(func=self.CmdLookup)
93
94 subp = subps.add_parser('validate',
95 help='validate the config file')
96 AddCommonOptions(subp)
97 subp.set_defaults(func=self.CmdValidate)
98
99 subp = subps.add_parser('help',
100 help='Get help on a subcommand.')
101 subp.add_argument(nargs='?', action='store', dest='subcommand',
102 help='The command to get help for.')
103 subp.set_defaults(func=self.CmdHelp)
104
105 self.args = parser.parse_args(argv)
106
107 def CmdAnalyze(self):
108 vals = self.GetConfig()
109 if vals['type'] == 'gn':
110 self.RunGNAnalyze(vals)
111 elif vals['type'] in ('gyp', 'gyp_one_config'):
112 self.RunGypAnalyze(vals)
113 else:
114 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
115
116 def CmdGen(self):
117 vals = self.GetConfig()
118 if vals['type'] == 'gn':
119 self.RunGNGen(self.args.path[0], vals['gn_args'])
120 elif vals['type'] == 'gyp':
121 self.RunGYPGen(self.args.path[0], vals['gyp_defines'])
122 else:
123 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
124 return 0
125
126 def CmdLookup(self):
127 vals = self.GetConfig()
128 if vals['type'] == 'gn':
129 cmd = self.GNCmd('<path>', vals['gn_args'])
130 elif vals['type'] == 'gyp':
131 cmd = self.GYPCmd('<path>', vals['gyp_defines'], vals['gyp_config'])
132 else:
133 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
134
135 self.PrintCmd(cmd)
136 return 0
137
138 def CmdHelp(self):
139 if self.args.subcommand:
140 self.ParseArgs([self.args.subcommand, '--help'])
141 else:
142 self.ParseArgs(['--help'])
143
144 def CmdValidate(self):
145 errs = []
146
147 # Read the file to make sure it parses.
148 self.ReadConfigFile()
149
150 # Figure out the whole list of configs and ensure that no config is
151 # listed in more than one category.
152 all_configs = {}
153 for config in self.common_dev_configs:
154 all_configs[config] = 'common_dev_configs'
155 for config in self.private_configs:
156 if config in all_configs:
157 errs.append('config "%s" listed in "private_configs" also '
158 'listed in "%s"' % (config, all_configs['config']))
159 else:
160 all_configs[config] = 'private_configs'
161 for config in self.unsupported_configs:
162 if config in all_configs:
163 errs.append('config "%s" listed in "unsupported_configs" also '
164 'listed in "%s"' % (config, all_configs['config']))
165 else:
166 all_configs[config] = 'unsupported_configs'
167
168 for master in self.masters:
169 for builder in self.masters[master]:
170 config = self.masters[master][builder]
171 if config in all_configs and all_configs[config] not in self.masters:
172 errs.append('Config "%s" used by a bot is also listed in "%s".' %
173 (config, all_configs[config]))
174 else:
175 all_configs[config] = master
176
177 # Check that every referenced config actually exists.
178 for config, loc in all_configs.items():
179 if not config in self.configs:
180 errs.append('Unknown config "%s" referenced from "%s".' %
181 (config, loc))
182
183 # Check that every actual config is actually referenced.
184 for config in self.configs:
185 if not config in all_configs:
186 errs.append('Unused config "%s".' % config)
187
188 # Figure out the whole list of mixins, and check that every mixin
189 # listed by a config or another mixin actually exists.
190 referenced_mixins = set()
191 for config, mixins in self.configs.items():
192 for mixin in mixins:
193 if not mixin in self.mixins:
194 errs.append('Unknown mixin "%s" referenced by config "%s".' %
195 (mixin, config))
196 referenced_mixins.add(mixin)
197
198 for mixin in self.mixins:
199 for sub_mixin in self.mixins[mixin].get('mixins', []):
200 if not sub_mixin in self.mixins:
201 errs.append('Unknown mixin "%s" referenced by mixin "%s".' %
202 (sub_mixin, mixin))
203 referenced_mixins.add(sub_mixin)
204
205 # Check that every mixin defined is actually referenced somewhere.
206 for mixin in self.mixins:
207 if not mixin in referenced_mixins:
208 errs.append('Unreferenced mixin "%s".' % mixin)
209
210 if errs:
211 raise MBErr('mb config file %s has problems:\n ' + '\n '.join(errs))
212
213 if not self.args.quiet:
214 self.Print('mb config file %s looks ok.' % self.args.config_file)
215 return 0
216
217 def GetConfig(self):
218 self.ReadConfigFile()
219 config = self.ConfigFromArgs()
220 if not config in self.configs:
221 raise MBErr('Config "%s" not found in %s' %
222 (config, self.args.config_file))
223
224 return self.FlattenConfig(config)
225
226 def ReadConfigFile(self):
227 if not self.Exists(self.args.config_file):
228 raise MBErr('config file not found at %s' % self.args.config_file)
229
230 try:
231 contents = ast.literal_eval(self.ReadFile(self.args.config_file))
232 except SyntaxError as e:
233 raise MBErr('Failed to parse config file "%s": %s' %
234 (self.args.config_file, e))
235
236 self.common_dev_configs = contents['common_dev_configs']
237 self.configs = contents['configs']
238 self.masters = contents['masters']
239 self.mixins = contents['mixins']
240 self.private_configs = contents['private_configs']
241 self.unsupported_configs = contents['unsupported_configs']
242
243 def ConfigFromArgs(self):
244 if self.args.config:
245 if self.args.master or self.args.builder:
246 raise MBErr('Can not specific both -c/--config and -m/--master or '
247 '-b/--builder')
248
249 return self.args.config
250
251 if not self.args.master or not self.args.builder:
252 raise MBErr('Must specify either -c/--config or '
253 '(-m/--master and -b/--builder)')
254
255 if not self.args.master in self.masters:
256 raise MBErr('Master name "%s" not found in "%s"' %
257 (self.args.master, self.args.config_file))
258
259 if not self.args.builder in self.masters[self.args.master]:
260 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' %
261 (self.args.builder, self.args.master, self.args.config_file))
262
263 return self.masters[self.args.master][self.args.builder]
264
265 def FlattenConfig(self, config):
266 mixins = self.configs[config]
267 vals = {
268 'type': None,
269 'gn_args': [],
270 'gyp_config': [],
271 'gyp_defines': [],
272 }
273
274 visited = []
275 self.FlattenMixins(mixins, vals, visited)
276 return vals
277
278 def FlattenMixins(self, mixins, vals, visited):
279 for m in mixins:
280 if m not in self.mixins:
281 raise MBErr('Unknown mixin "%s"' % m)
282 if m in visited:
283 raise MBErr('Cycle in build configs for "%s": %s' % (config, visited))
284
285 visited.append(m)
286
287 mixin_vals = self.mixins[m]
288 if 'type' in mixin_vals:
289 vals['type'] = mixin_vals['type']
290 if 'gn_args' in mixin_vals:
291 if vals['gn_args']:
292 vals['gn_args'] += ' ' + mixin_vals['gn_args']
293 else:
294 vals['gn_args'] = mixin_vals['gn_args']
295 if 'gyp_config' in mixin_vals:
296 vals['gyp_config'] = mixin_vals['gyp_config']
297 if 'gyp_defines' in mixin_vals:
298 if vals['gyp_defines']:
299 vals['gyp_defines'] += ' ' + mixin_vals['gyp_defines']
300 else:
301 vals['gyp_defines'] = mixin_vals['gyp_defines']
302 if 'mixins' in mixin_vals:
303 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
304 return vals
305
306 def RunGNGen(self, path, gn_args):
307 cmd = self.GNCmd(path, gn_args)
308 ret, _, _ = self.Run(cmd)
309 return ret
310
311 def GNCmd(self, path, gn_args):
312 # TODO(dpranke): Find gn explicitly in the path ...
313 cmd = ['gn', 'gen', path]
314 gn_args = gn_args.replace("$(goma_dir)", gn_args)
315 if gn_args:
316 cmd.append('--args=%s' % ' '.join(gn_args))
317 return cmd
318
319 def RunGYPGen(self, path, gyp_defines):
320 output_dir, gyp_config = self.ParseGYPConfigPath(path)
321 if gyp_config != vals['gyp_config']:
322 raise MBErr('The last component of the path (%s) must match the '
323 'GYP configuration specified in the config (%s), and '
324 'it does not.' % (gyp_config, vals['gyp_config']))
325 cmd = self.GYPCmd(output_dir, gyp_defines, config=gyp_config)
326 ret, _, _ = self.Run(cmd)
327 return ret
328
329 def ParseGYPOutputPath(self, path):
330 assert(path.startswith('//'))
331 return path[2:]
332
333 def ParseGYPConfigPath(self, path):
334 assert(path.startswith('//'))
335 output_dir, _, config = path[2:].rpartition('/')
336 self.CheckGYPConfigIsSupported(config, path)
337 return output_dir, config
338
339 def CheckGYPConfigIsSupported(self, config, path):
340 if config not in ('Debug', 'Release'):
341 if (sys.platform in ('win32', 'cygwin') and
342 config not in ('Debug_x64', 'Release_x64')):
343 raise MBErr('Unknown or unsupported config type "%s" in "%s"' %
344 config, path)
345
346 def GYPCmd(self, output_dir, gyp_defines, config):
347 gyp_defines = gyp_defines.replace("$(goma_dir)", self.args.goma_dir)
348 cmd = [
349 sys.executable,
350 os.path.join('build', 'gyp_chromium'),
351 '-G',
352 'output_dir=' + output_dir,
353 '-G',
354 'config=' + config,
355 ]
356 for d in shlex.split(gyp_defines):
357 cmd += ['-D', d]
358 return cmd
359
360 def RunGNAnalyze(self, vals):
361 # TODO: Skip this if a valid gn build already exists, which it should on
362 # the bots.
363 if self.args.dryrun:
364 tmpdir = '//out/$tmpdir'
365 else:
366 tmpdir = tempfile.mkdtemp(prefix='analyze', suffix='tmp',
367 dir=os.path.join(self.chromium_src_dir, 'out'))
368
369 files = self.args.paths
370 try:
371 idx = files.index('-')
372 targets = files[idx + 1:]
373 files = files[:idx]
374 except ValueError:
375 targets = []
376
377 self.RunGN(tmpdir, vals['gn_args'])
378
379 cmd = ['gn', 'refs', tmpdir] + files + ['--type=executable',
380 '--all', '--as=output']
381 needed_targets = []
382 ret, out, _ = self.Run(cmd)
383
384 # TODO: handle failures from 'gn refs'
385
386 if self.args.dryrun or self.args.verbose:
387 self.Print('rm -fr', tmpdir)
388 if not self.args.dryrun:
389 shutil.rmtree(tmpdir, ignore_errors=True)
390
391 rpath = os.path.relpath(tmpdir, self.chromium_src_dir) + os.sep
392 needed_targets = [t.replace(rpath, '') for t in out.splitlines()]
393 if targets:
394 needed_targets = [nt for nt in needed_targets if nt in targets]
395
396 for nt in needed_targets:
397 self.Print(nt)
398
399 def PrintCmd(self, cmd):
400 if cmd[0] == sys.executable:
401 cmd = ['python'] + cmd[1:]
402 self.Print(*[pipes.quote(c) for c in cmd])
403
404 def Print(self, *args, **kwargs):
405 # This function largely exists so it can be overridden for testing.
406 print(*args, **kwargs)
407
408 def Run(self, cmd):
409 # This function largely exists so it can be overridden for testing.
410 if self.args.dryrun or self.args.verbose:
411 self.PrintCmd(cmd)
412 if self.args.dryrun:
413 return 0, '', ''
414 if self.args.verbose:
415 if out:
416 self.Print(out)
417 if err:
418 self.Print(err, file=sys.stderr)
419 return self.Call(cmd)
420
421 def Call(self, cmd):
422 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir,
423 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
424 out, err = p.communicate()
425 return p.returncode, out, err
426
427 def ExpandUser(self, path):
428 # This function largely exists so it can be overridden for testing.
429 return os.path.expanduser(path)
430
431 def Exists(self, path):
432 # This function largely exists so it can be overridden for testing.
433 return os.path.exists(path)
434
435 def ReadFile(self, path):
436 # This function largely exists so it can be overriden for testing.
437 with open(path) as fp:
438 return fp.read()
439
440
441 class MBErr(Exception):
442 pass
443
444
445 if __name__ == '__main__':
446 try:
447 sys.exit(main(sys.argv[1:]))
448 except Err as e:
449 print(e)
450 sys.exit(1)
451 except KeyboardInterrupt:
452 print("interrupted, exiting", stream=sys.stderr)
453 sys.exit(130)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698