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

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