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

Side by Side Diff: tools/gyp_flag_compare.py

Issue 2246203004: Add a copy of gyp_flag_compare from Chromium to WebRTC's webrtc/tools. (Closed) Base URL: https://chromium.googlesource.com/external/webrtc.git@master
Patch Set: Update comments. Created 4 years, 4 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 | « no previous file | 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
3 # Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
4 #
5 # Use of this source code is governed by a BSD-style license
6 # that can be found in the LICENSE file in the root of the source
7 # tree. An additional intellectual property rights grant can be found
8 # in the file PATENTS. All contributing project authors may
9 # be found in the AUTHORS file in the root of the source tree.
10
11 """Given the output of -t commands from a ninja build for a gyp and GN generated
12 build, report on differences between the command lines.
13
14
15 When invoked from the command line, this script assumes that the GN and GYP
16 targets have been generated in the specified folders. It is meant to be used as
17 follows:
18 $ python tools/gyp_flag_compare.py gyp_dir gn_dir target
19
20 When the GN and GYP targets differ, it should be called invoked as follows:
kjellander_webrtc 2016/08/22 06:34:30 Rephrase to: When the GN and GYP target names diff
21 $ python tools/gyp_flag_compare.py gyp_dir gn_dir gyp_target gn_target
22
23
24 This script can be used interactively. Here's a recommended setup:
kjellander_webrtc 2016/08/22 06:34:30 I suggest: This script can also be used interactiv
25 $ PYTHONPATH=tools python
kjellander_webrtc 2016/08/22 06:34:30 This example assumes the current working directory
26 >>> import sys
27 >>> import pprint
28 >>> sys.displayhook = pprint.pprint
29 >>> import gyp_flag_compare as fc
30 >>> fc.ConfigureBuild(['gyp_define=1', 'define=2'], ['gn_arg=1', 'arg=2'])
31 >>> modules_unittests = fc.Comparison('modules_unittests')
32
33 The above starts interactive Python, sets up the output to be pretty-printed
34 (useful for making lists, dicts, and sets readable), configures the build with
35 GN arguments and GYP defines, and then generates a comparison for that build
36 configuration for the "modules_unittests" target.
37
38 After that, the |modules_unittests| object can be used to investigate
39 differences in the build.
40
41 To configure an official build, use this configuration. Disabling NaCl produces
42 a more meaningful comparison, as certain files need to get compiled twice
43 for the IRT build, which uses different flags:
44 >>> fc.ConfigureBuild(
45 ['disable_nacl=1', 'buildtype=Official', 'branding=Chrome'],
46 ['enable_nacl=false', 'is_official_build=true',
47 'is_chrome_branded=true'])
48 """
49
50
51 import os
52 import shlex
53 import subprocess
54 import sys
55
56 # Must be in src/.
57 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
58 os.chdir(BASE_DIR)
59
60 _DEFAULT_GN_DIR = 'out/gn'
61 _DEFAULT_GYP_DIR = 'out/Release'
62
63
64 def FilterChromium(filename):
65 """Replaces 'chromium/src/' by '' in the filename."""
66 return filename.replace('chromium/src/', '')
67
68
69 def ConfigureBuild(gyp_args=None, gn_args=None, gn_dir=_DEFAULT_GN_DIR):
70 """Generates gn and gyp targets with the given arguments."""
71 gyp_args = gyp_args or []
72 gn_args = gn_args or []
73
74 print >> sys.stderr, 'Regenerating GN in %s...' % gn_dir
75 # Currently only Release, non-component.
76 Run('gn gen %s --args="is_debug=false is_component_build=false %s"' % \
77 (gn_dir, ' '.join(gn_args)))
78
79 os.environ.pop('GYP_DEFINES', None)
80 # Remove environment variables required by gn but conflicting with GYP.
81 # Relevant if Windows toolchain isn't provided by depot_tools.
82 os.environ.pop('GYP_MSVS_OVERRIDE_PATH', None)
83 os.environ.pop('WINDOWSSDKDIR', None)
84
85 gyp_defines = ''
86 if len(gyp_args) > 0:
87 gyp_defines = '-D' + ' -D'.join(gyp_args)
88
89 print >> sys.stderr, 'Regenerating GYP in %s...' % _DEFAULT_GYP_DIR
90 Run('python webrtc/build/gyp_webrtc -Gconfig=Release %s' % gyp_defines)
kjellander_webrtc 2016/08/22 06:34:30 gyp_webrtc -> gyp_webrtc.py since the former is ju
91
92
93 def Counts(dict_of_list):
94 """Given a dictionary whose value are lists, returns a dictionary whose values
95 are the length of the list. This can be used to summarize a dictionary.
96 """
97 return {k: len(v) for k, v in dict_of_list.iteritems()}
98
99
100 def CountsByDirname(dict_of_list):
101 """Given a list of files, returns a dict of dirname to counts in that dir."""
102 r = {}
103 for path in dict_of_list:
104 dirname = os.path.dirname(path)
105 r.setdefault(dirname, 0)
106 r[dirname] += 1
107 return r
108
109
110 class Comparison(object):
111 """A comparison of the currently-configured build for a target."""
112
113 def __init__(self, gyp_target, gn_target=None, gyp_dir=_DEFAULT_GYP_DIR,
114 gn_dir=_DEFAULT_GN_DIR):
115 """Creates a comparison of a GN and GYP target. If the target names differ
116 between the two build systems, then two names may be passed.
117 """
118 if gn_target is None:
119 gn_target = gyp_target
120 self._gyp_target = gyp_target
121 self._gn_target = gn_target
122
123 self._gyp_dir = gyp_dir
124 self._gn_dir = gn_dir
125
126 self._skipped = []
127
128 self._total_diffs = 0
129
130 self._missing_gyp_flags = {}
131 self._missing_gn_flags = {}
132
133 self._missing_gyp_files = {}
134 self._missing_gn_files = {}
135
136 self._CompareFiles()
137
138 @property
139 def gyp_files(self):
140 """Returns the set of files that are in the GYP target."""
141 return set(self._gyp_flags.keys())
142
143 @property
144 def gn_files(self):
145 """Returns the set of files that are in the GN target."""
146 return set(self._gn_flags.keys())
147
148 @property
149 def skipped(self):
150 """Returns the list of compiler commands that were not processed during the
151 comparison.
152 """
153 return self._skipped
154
155 @property
156 def total_differences(self):
157 """Returns the total number of differences detected."""
158 return self._total_diffs
159
160 @property
161 def missing_in_gyp(self):
162 """Differences that are only in GYP build but not in GN, indexed by the
163 difference."""
164 return self._missing_gyp_flags
165
166 @property
167 def missing_in_gn(self):
168 """Differences that are only in the GN build but not in GYP, indexed by
169 the difference."""
170 return self._missing_gn_flags
171
172 @property
173 def missing_in_gyp_by_file(self):
174 """Differences that are only in the GYP build but not in GN, indexed by
175 file.
176 """
177 return self._missing_gyp_files
178
179 @property
180 def missing_in_gn_by_file(self):
181 """Differences that are only in the GYP build but not in GN, indexed by
182 file.
183 """
184 return self._missing_gn_files
185
186 def _CompareFiles(self):
187 """Performs the actual target comparison."""
188 if sys.platform == 'win32':
189 # On Windows flags are stored in .rsp files which are created by building.
190 print >> sys.stderr, 'Building in %s...' % self._gn_dir
191 Run('ninja -C %s -d keeprsp %s' % (self._gn_dir, self._gn_target))
192 print >> sys.stderr, 'Building in %s...' % self._gyp_dir
193 Run('ninja -C %s -d keeprsp %s' % (self._gyp_dir, self._gn_target))
194
195 gn = Run('ninja -C %s -t commands %s' % (self._gn_dir, self._gn_target))
196 gyp = Run('ninja -C %s -t commands %s' % (self._gyp_dir, self._gyp_target))
197
198 self._gn_flags = self._GetFlags(gn.splitlines(),
199 os.path.join(os.getcwd(), self._gn_dir))
200 self._gyp_flags = self._GetFlags(gyp.splitlines(),
201 os.path.join(os.getcwd(), self._gyp_dir))
202
203 self._gn_flags = dict((FilterChromium(filename), value)
204 for filename, value in self._gn_flags.iteritems())
205 self._gyp_flags = dict((FilterChromium(filename), value)
206 for filename, value in self._gyp_flags.iteritems())
207
208 all_files = sorted(self.gn_files & self.gyp_files)
209 for filename in all_files:
210 gyp_flags = self._gyp_flags[filename]
211 gn_flags = self._gn_flags[filename]
212 self._CompareLists(filename, gyp_flags, gn_flags, 'dash_f')
213 self._CompareLists(filename, gyp_flags, gn_flags, 'defines')
214 self._CompareLists(filename, gyp_flags, gn_flags, 'include_dirs')
215 self._CompareLists(filename, gyp_flags, gn_flags, 'warnings',
216 # More conservative warnings in GN we consider to be OK.
217 dont_care_gyp=[
218 '/wd4091', # 'keyword' : ignored on left of 'type' when no variable
219 # is declared.
220 '/wd4456', # Declaration hides previous local declaration.
221 '/wd4457', # Declaration hides function parameter.
222 '/wd4458', # Declaration hides class member.
223 '/wd4459', # Declaration hides global declaration.
224 '/wd4702', # Unreachable code.
225 '/wd4800', # Forcing value to bool 'true' or 'false'.
226 '/wd4838', # Conversion from 'type' to 'type' requires a narrowing
227 # conversion.
228 ] if sys.platform == 'win32' else None,
229 dont_care_gn=[
230 '-Wendif-labels',
231 '-Wextra',
232 '-Wsign-compare',
233 ] if not sys.platform == 'win32' else None)
234 self._CompareLists(filename, gyp_flags, gn_flags, 'other')
235
236 def _CompareLists(self, filename, gyp, gn, name,
237 dont_care_gyp=None, dont_care_gn=None):
238 """Return a report of any differences between gyp and gn lists, ignoring
239 anything in |dont_care_{gyp|gn}| respectively."""
240 if gyp[name] == gn[name]:
241 return
242 if not dont_care_gyp:
243 dont_care_gyp = []
244 if not dont_care_gn:
245 dont_care_gn = []
246 gyp_set = set(gyp[name])
247 gn_set = set(gn[name])
248 missing_in_gyp = gyp_set - gn_set
249 missing_in_gn = gn_set - gyp_set
250 missing_in_gyp -= set(dont_care_gyp)
251 missing_in_gn -= set(dont_care_gn)
252
253 for m in missing_in_gyp:
254 self._missing_gyp_flags.setdefault(name, {}) \
255 .setdefault(m, []).append(filename)
256 self._total_diffs += 1
257 self._missing_gyp_files.setdefault(filename, {}) \
258 .setdefault(name, set()).update(missing_in_gyp)
259
260 for m in missing_in_gn:
261 self._missing_gn_flags.setdefault(name, {}) \
262 .setdefault(m, []).append(filename)
263 self._total_diffs += 1
264 self._missing_gn_files.setdefault(filename, {}) \
265 .setdefault(name, set()).update(missing_in_gn)
266
267 def _GetFlags(self, lines, build_dir):
268 """Turn a list of command lines into a semi-structured dict."""
269 is_win = sys.platform == 'win32'
270 flags_by_output = {}
271 for line in lines:
272 command_line = shlex.split(line.strip(), posix=not is_win)[1:]
273
274 output_name = _FindAndRemoveArgWithValue(command_line, '-o')
275 dep_name = _FindAndRemoveArgWithValue(command_line, '-MF')
276
277 command_line = _MergeSpacedArgs(command_line, '-Xclang')
278
279 cc_file = [x for x in command_line if x.endswith('.cc') or
280 x.endswith('.c') or
281 x.endswith('.cpp') or
282 x.endswith('.mm') or
283 x.endswith('.m')]
284 if len(cc_file) != 1:
285 self._skipped.append(command_line)
286 continue
287 assert len(cc_file) == 1
288
289 if is_win:
290 rsp_file = [x for x in command_line if x.endswith('.rsp')]
291 assert len(rsp_file) <= 1
292 if rsp_file:
293 rsp_file = os.path.join(build_dir, rsp_file[0][1:])
294 with open(rsp_file, "r") as open_rsp_file:
295 command_line = shlex.split(open_rsp_file, posix=False)
296
297 defines = [x for x in command_line if x.startswith('-D')]
298 include_dirs = [x for x in command_line if x.startswith('-I')]
299 dash_f = [x for x in command_line if x.startswith('-f')]
300 warnings = \
301 [x for x in command_line if x.startswith('/wd' if is_win else '-W')]
302 others = [x for x in command_line if x not in defines and \
303 x not in include_dirs and \
304 x not in dash_f and \
305 x not in warnings and \
306 x not in cc_file]
307
308 for index, value in enumerate(include_dirs):
309 if value == '-Igen':
310 continue
311 path = value[2:]
312 if not os.path.isabs(path):
313 path = os.path.join(build_dir, path)
314 include_dirs[index] = '-I' + os.path.normpath(path)
315
316 # GYP supports paths above the source root like <(DEPTH)/../foo while such
317 # paths are unsupported by gn. But gn allows to use system-absolute paths
318 # instead (paths that start with single '/'). Normalize all paths.
319 cc_file = [os.path.normpath(os.path.join(build_dir, cc_file[0]))]
320
321 # Filter for libFindBadConstructs.so having a relative path in one and
322 # absolute path in the other.
323 others_filtered = []
324 for x in others:
325 if x.startswith('-Xclang ') and \
326 (x.endswith('libFindBadConstructs.so') or \
327 x.endswith('libFindBadConstructs.dylib')):
328 others_filtered.append(
329 '-Xclang ' +
330 os.path.join(os.getcwd(), os.path.normpath(
331 os.path.join('out/gn_flags', x.split(' ', 1)[1]))))
332 elif x.startswith('-B'):
333 others_filtered.append(
334 '-B' +
335 os.path.join(os.getcwd(), os.path.normpath(
336 os.path.join('out/gn_flags', x[2:]))))
337 else:
338 others_filtered.append(x)
339 others = others_filtered
340
341 flags_by_output[cc_file[0]] = {
342 'output': output_name,
343 'depname': dep_name,
344 'defines': sorted(defines),
345 'include_dirs': sorted(include_dirs), # TODO(scottmg): This is wrong.
346 'dash_f': sorted(dash_f),
347 'warnings': sorted(warnings),
348 'other': sorted(others),
349 }
350 return flags_by_output
351
352
353 def _FindAndRemoveArgWithValue(command_line, argname):
354 """Given a command line as a list, remove and return the value of an option
355 that takes a value as a separate entry.
356
357 Modifies |command_line| in place.
358 """
359 if argname not in command_line:
360 return ''
361 location = command_line.index(argname)
362 value = command_line[location + 1]
363 command_line[location:location + 2] = []
364 return value
365
366
367 def _MergeSpacedArgs(command_line, argname):
368 """Combine all arguments |argname| with their values, separated by a space."""
369 i = 0
370 result = []
371 while i < len(command_line):
372 arg = command_line[i]
373 if arg == argname:
374 result.append(arg + ' ' + command_line[i + 1])
375 i += 1
376 else:
377 result.append(arg)
378 i += 1
379 return result
380
381
382 def Run(command_line):
383 """Run |command_line| as a subprocess and return stdout. Raises on error."""
384 print >> sys.stderr, command_line
385 return subprocess.check_output(command_line, shell=True)
386
387
388 def main():
389 if len(sys.argv) < 4:
390 print 'usage: %s gyp_dir gn_dir target' % __file__
391 print ' or: %s gyp_dir gn_dir gyp_target gn_target' % __file__
392 return 1
393
394 gyp_dir = sys.argv[1]
395 gn_dir = sys.argv[2]
396
397 gyp_target = sys.argv[3]
398 if len(sys.argv) == 4:
399 gn_target = gyp_target
400 else:
401 gn_target = sys.argv[4]
402
403 print 'GYP output directory is %s' % gyp_dir
404 print 'GN output directory is %s' % gn_dir
405
406 comparison = Comparison(gyp_target, gn_target, gyp_dir, gn_dir)
407
408 gyp_files = comparison.gyp_files
409 gn_files = comparison.gn_files
410 different_source_list = comparison.gyp_files != comparison.gn_files
411 if different_source_list:
412 print 'Different set of sources files:'
413 print ' In gyp, not in GN:\n %s' % '\n '.join(
414 sorted(gyp_files - gn_files))
415 print ' In GN, not in gyp:\n %s' % '\n '.join(
416 sorted(gn_files - gyp_files))
417 print '\nNote that flags will only be compared for files in both sets.\n'
418
419 differing_files = set(comparison.missing_in_gn_by_file.keys()) & \
420 set(comparison.missing_in_gyp_by_file.keys())
421 files_with_given_differences = {}
422 for filename in differing_files:
423 output = ''
424 missing_in_gyp = comparison.missing_in_gyp_by_file.get(filename, {})
425 missing_in_gn = comparison.missing_in_gn_by_file.get(filename, {})
426 difference_types = sorted(set(missing_in_gyp.keys() + missing_in_gn.keys()))
427 for difference_type in difference_types:
428 output += ' %s differ:\n' % difference_type
429 if difference_type in missing_in_gyp:
430 output += ' In gyp, but not in GN:\n %s' % '\n '.join(
431 sorted(missing_in_gyp[difference_type])) + '\n'
432 if difference_type in missing_in_gn:
433 output += ' In GN, but not in gyp:\n %s' % '\n '.join(
434 sorted(missing_in_gn[difference_type])) + '\n'
435 if output:
436 files_with_given_differences.setdefault(output, []).append(filename)
437
438 for diff, files in files_with_given_differences.iteritems():
439 print '\n'.join(sorted(files))
440 print diff
441
442 print 'Total differences:', comparison.total_differences
443 # TODO(scottmg): Return failure on difference once we're closer to identical.
444 return 0
445
446
447 if __name__ == '__main__':
448 sys.exit(main())
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698