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

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

Issue 1005723003: First pass at a meta-build wrapper for the gyp->gn migration (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: rename to mb, add docs 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
« tools/mb/README.md ('K') | « tools/mb/mb.bat ('k') | no next file » | 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 shutil
20 import sys
21 import subprocess
22 import tempfile
23
24
25 DEFAULT_BUILDS = {
26 '//out': 'gyp'
27 }
28
29
30 def main(argv):
31 mb = MetaBuildWrapper()
32 mb.ParseArgs(argv)
33 return mb.args.func()
34
35
36 class MetaBuildWrapper(object):
37 def __init__(self):
38 p = os.path
39 d = os.path.dirname
40 self.chromium_src_dir = p.normpath(p.abspath(d(d(d(__file__)))))
41 self.mb_conf_pyl = p.join(self.chromium_src_dir, 'build', 'mb_conf.pyl')
42 self.builds_pyl = p.normpath(p.join(self.chromium_src_dir, '..',
43 'builds.pyl'))
44 self.build_configs = {}
45 self.mixins = {}
46
47 def ParseArgs(self, argv):
48 parser = argparse.ArgumentParser(prog='mb')
49 subps = parser.add_subparsers()
50
51 subp = subps.add_parser('analyze',
52 help='analyze whether changes to a set of files '
53 'will cause a set of binaries to be rebuilt.')
54 subp.add_argument('-i', '--json-input', action='store',
55 help='path to a file containing the input arguments '
56 'as a JSON object.')
57 subp.add_argument('-o', '--json-output', action='store',
58 help='path to a file containing the output arguments '
59 'as a JSON object.')
60 subp.add_argument('-n', '--dryrun', action='store_true',
61 help='Do a dry run (i.e., do nothing, just print '
62 'the commands')
63 subp.add_argument('-v', '--verbose', action='count',
64 help='verbose logging (may specify multiple times.')
65
66 subp.add_argument('build_config', type=str, nargs='?',
67 help='configuration to analyze')
68 subp.add_argument('paths', type=str, nargs='*',
69 help='list of files or targets to analyze')
70 subp.set_defaults(func=self.CmdAnalyze)
71
72 subp = subps.add_parser('gen',
73 help='generate a new set of build files')
74 subp.add_argument('-n', '--dryrun', action='store_true',
75 help='Do a dry run (i.e., do nothing, just print '
76 'the commands')
77 subp.add_argument('-v', '--verbose', action='count',
78 help='verbose logging (may specify multiple times.')
79 subp.add_argument('path', type=str, nargs='?',
80 help='path to generate build into')
81 subp.add_argument('build_config', type=str, nargs='?',
82 help='build configuration to generate')
83 subp.set_defaults(func=self.CmdGen)
84
85 subp = subps.add_parser('help',
86 help='Get help on a subcommand.')
87 subp.add_argument(nargs='?', action='store', dest='subcommand',
88 help='The command to get help for.')
89 subp.set_defaults(func=self.CmdHelp)
90
91 self.args = parser.parse_args(argv)
92
93 def ReadConf(self):
94 if not self.PathExists(self.mwb_conf_pyl):
95 raise _Err('mwb conf file not found at %s' % self.mwb_conf_pyl)
96 contents = self.ReadPyl(self.mwb_conf_pyl, 'mwb conf file')
97 self.build_configs = contents['build_configs']
98 self.mixins = contents['mixins']
99
100 def PathExists(self, path):
101 return os.path.exists(path)
102
103 def ReadPyl(self, path, desc):
104 try:
105 return ast.literal_eval(open(path).read())
106 except SyntaxError as e:
107 raise _Err('Failed to parse %s at "%s": %s' % (desc, path, e))
108
109 def Call(self, cmd):
110 if self.args.dryrun or self.args.verbose:
111 if cmd[0] == sys.executable:
112 cmd = ['python'] + cmd[1:]
113 print(*[pipes.quote(c) for c in cmd])
114
115 if self.args.dryrun:
116 return 0, '', ''
117 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir,
118 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
119 out, err = p.communicate()
120 if self.args.verbose:
121 if out:
122 print(out)
123 if err:
124 print(err, stream=sys.stderr)
125 return p.returncode, out, err
126
127 def CmdHelp(self):
128 if self.args.subcommand:
129 self.ParseArgs([self.args.subcommand, '--help'])
130 else:
131 self.ParseArgs(['--help'])
132
133 def CmdGen(self):
134 self.ReadConf()
135
136 if self.args.build_config and self.args.path:
137 builds = self.BuildsFromArgs()
138 elif os.path.exists(self.builds_pyl):
139 builds = self.BuildsFromPyl()
140 else:
141 build = DEFAULT_BUILDS
142
143 for path, build_config in builds.items():
144 vals = self.FlattenBuildConfig(build_config)
145 if vals['type'] == 'gn':
146 self.RunGN(path, vals['gn_args'])
147 elif vals['type'] == 'gyp':
148 self.RunGYP(path, vals['gyp_defines'])
149 elif vals['type'] == 'gyp_one_config':
150 self.RunGYPOneConfig(path, vals['gyp_defines'])
151 else:
152 raise _Err('Unknown meta-build type "%s"' % vals['type'])
153 return 0
154
155 def BuildsFromArgs(self):
156 builds = {}
157 builds[self.args.path] = self.args.build_config
158 return builds
159
160 def BuildsFromPyl(self):
161 return self.ReadPyl(self.builds_pyl, "build file")
162
163 def RunGN(self, path, gn_args=''):
164 cmd = self.GNCmd(path, gn_args)
165 ret, _, _ = self.Call(cmd)
166 return ret
167
168 def FlattenBuildConfig(self, build_config):
169 if build_config not in self.build_configs:
170 raise _Err('Unknown build config "%s"' % build_config)
171 if self.build_configs[build_config] is None:
172 mixins = build_config.split('_')
173 else:
174 mixins = self.build_configs[build_config]
175
176 vals = {
177 'type': None,
178 'gn_args': [],
179 'gyp_config': [],
180 'gyp_defines': [],
181 }
182
183 visited = []
184 self.FlattenMixins(mixins, vals, visited)
185 return vals
186
187 def FlattenMixins(self, mixins, vals, visited):
188 for m in mixins:
189 if m not in self.mixins:
190 raise _Err('Unknown mixin "%s"' % m)
191 if m in visited:
192 raise _Err('Cycle in build configs for "%s": %s' % (config, visited))
193
194 visited.append(m)
195
196 mixin_vals = self.mixins[m]
197 if 'type' in mixin_vals:
198 vals['type'] = mixin_vals['type']
199 if 'gn_args' in mixin_vals:
200 vals['gn_args'].extend(mixin_vals['gn_args'])
201 if 'gyp_config' in mixin_vals:
202 vals['gyp_config'] = mixin_vals['gyp_config']
203 if 'gyp_defines' in mixin_vals:
204 vals['gyp_defines'].extend(mixin_vals['gyp_defines'])
205 if 'mixins' in mixin_vals:
206 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
207 return vals
208
209 def GNCmd(self, path, gn_args):
210 # TODO(dpranke): Find gn explicitly in the path ...
211 cmd = ['gn', 'gen', path]
212 if gn_args:
213 cmd.append('--args=%s' % ' '.join(gn_args))
214 return cmd
215
216 def RunGYP(self, path, gyp_defines):
217 output_dir = self.ParseGYPOutputPath(path)
218 cmd = self.GYPCmd(output_dir, gyp_defines, config=None)
219 ret, _, _ = self.Call(cmd)
220 return ret
221
222 def RunGYPConfig(self, path, gyp_defines):
223 output_dir, gyp_config = self.ParseGYPConfigPath(path)
224 cmd = self.GYPCmd(output_dir, gyp_defines, config=gyp_config)
225 ret, _, _ = self.Call(cmd)
226 return ret
227
228 def ParseGYPOutputPath(self, path):
229 # Do we need to handle absolute paths or relative paths?
230 assert(path.startswith('//'))
231 return path[2:]
232
233 def ParseGYPConfigPath(self, path):
234 # Do we need to handle absolute paths or relative paths?
235 assert(path.startswith('//'))
236 output_dir, _, config = path[2:].rpartition('/')
237 self.CheckGYPConfigIsSupported(config, path)
238 return output_dir, config
239
240 def CheckGYPConfigIsSupported(self, config, path):
241 if config not in ('Debug', 'Release'):
242 if (sys.platform in ('win32', 'cygwin') and
243 config not in ('Debug_x64', 'Release_x64')):
244 raise _Err('Unknown or unsupported config type "%s" in "%s"' %
245 config, path)
246
247 def GYPCmd(self, output_dir, gyp_defines, config):
248 cmd = [
249 sys.executable,
250 os.path.join('build', 'gyp_chromium'),
251 '-G',
252 'output_dir=' + output_dir
253 ]
254 if config:
255 cmd += ['-G', 'config=' + config]
256 for d in gyp_defines:
257 cmd += ['-D', d]
258 return cmd
259
260 def CmdAnalyze(self):
261 self.ReadConf()
262 vals = self.FlattenBuildConfig(self.args.build_config)
263 if vals['type'] == 'gn':
264 self.RunGNAnalyze(vals)
265 elif vals['type'] in ('gyp', 'gyp_one_config'):
266 self.RunGypAnalyze(vals)
267 else:
268 raise _Err('Unknown meta-build type "%s"' % vals['type'])
269
270 def RunGNAnalyze(self, vals):
271 if self.args.dryrun:
272 tmpdir = '//out/$tmpdir'
273 else:
274 tmpdir = tempfile.mkdtemp(prefix='analyze', suffix='tmp',
275 dir=os.path.join(self.chromium_src_dir, 'out'))
276
277 files = self.args.paths
278 try:
279 idx = files.index('-')
280 targets = files[idx + 1:]
281 files = files[:idx]
282 except ValueError:
283 targets = []
284
285 self.RunGN(tmpdir, vals['gn_args'])
286
287 cmd = ['gn', 'refs', tmpdir] + files + ['--type=executable',
288 '--all', '--as=output']
289 needed_targets = []
290 ret, out, _ = self.Call(cmd)
291
292 # TODO: handle failures from 'gn refs'
293
294 if self.args.dryrun or self.args.verbose:
295 print('rm -fr', tmpdir)
296 if not self.args.dryrun:
297 shutil.rmtree(tmpdir, ignore_errors=True)
298
299 rpath = os.path.relpath(tmpdir, self.chromium_src_dir) + os.sep
300 needed_targets = [t.replace(rpath, '') for t in out.splitlines()]
301 if targets:
302 needed_targets = [nt for nt in needed_targets if nt in targets]
303
304 for nt in needed_targets:
305 print(nt)
306
307
308 class _Err(Exception):
309 pass
310
311
312 if __name__ == '__main__':
313 try:
314 sys.exit(main(sys.argv[1:]))
315 except _Err as e:
316 print(e)
317 sys.exit(1)
318 except KeyboardInterrupt:
319 print("interrupted, exiting", stream=sys.stderr)
320 sys.exit(130)
OLDNEW
« tools/mb/README.md ('K') | « tools/mb/mb.bat ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698