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