• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# Copyright 2018 The Chromium Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Removes code coverage flags from invocations of the Clang C/C++ compiler.
6
7If the GN arg `use_clang_coverage=true`, this script will be invoked by default.
8GN will add coverage instrumentation flags to almost all source files.
9
10This script is used to remove instrumentation flags from a subset of the source
11files. By default, it will not remove flags from any files. If the option
12--files-to-instrument is passed, this script will remove flags from all files
13except the ones listed in --files-to-instrument.
14
15This script also contains hard-coded exclusion lists of files to never
16instrument, indexed by target operating system. Files in these lists have their
17flags removed in both modes. The OS can be selected with --target-os.
18
19This script also contains hard-coded force lists of files to always instrument,
20indexed by target operating system. Files in these lists never have their flags
21removed in either mode. The OS can be selected with --target-os.
22
23The order of precedence is: force list, exclusion list, --files-to-instrument.
24
25The path to the coverage instrumentation input file should be relative to the
26root build directory, and the file consists of multiple lines where each line
27represents a path to a source file, and the specified paths must be relative to
28the root build directory. e.g. ../../base/task/post_task.cc for build
29directory 'out/Release'. The paths should be written using OS-native path
30separators for the current platform.
31
32One caveat with this compiler wrapper is that it may introduce unexpected
33behaviors in incremental builds when the file path to the coverage
34instrumentation input file changes between consecutive runs, so callers of this
35script are strongly advised to always use the same path such as
36"${root_build_dir}/coverage_instrumentation_input.txt".
37
38It's worth noting on try job builders, if the contents of the instrumentation
39file changes so that a file doesn't need to be instrumented any longer, it will
40be recompiled automatically because if try job B runs after try job A, the files
41that were instrumented in A will be updated (i.e., reverted to the checked in
42version) in B, and so they'll be considered out of date by ninja and recompiled.
43
44Example usage:
45  clang_code_coverage_wrapper.py \\
46      --files-to-instrument=coverage_instrumentation_input.txt
47"""
48
49
50import argparse
51import os
52import subprocess
53import sys
54
55# Flags used to enable coverage instrumentation.
56# Flags should be listed in the same order that they are added in
57# build/config/coverage/BUILD.gn
58_COVERAGE_FLAGS = [
59    '-fprofile-instr-generate',
60    '-fcoverage-mapping',
61    # Following experimental flags remove unused header functions from the
62    # coverage mapping data embedded in the test binaries, and the reduction
63    # of binary size enables building Chrome's large unit test targets on
64    # MacOS. Please refer to crbug.com/796290 for more details.
65    '-mllvm',
66    '-limited-coverage-experimental=true',
67]
68
69# Files that should not be built with coverage flags by default.
70_DEFAULT_COVERAGE_EXCLUSION_LIST = [
71    # TODO(crbug.com/1051561): angle_unittests affected by coverage.
72    '../../base/message_loop/message_pump_default.cc',
73    '../../base/message_loop/message_pump_libevent.cc',
74    '../../base/message_loop/message_pump_win.cc',
75    '../../base/task/sequence_manager/thread_controller_with_message_pump_impl.cc',  #pylint: disable=line-too-long
76]
77
78# Map of exclusion lists indexed by target OS.
79# If no target OS is defined, or one is defined that doesn't have a specific
80# entry, use _DEFAULT_COVERAGE_EXCLUSION_LIST.
81_COVERAGE_EXCLUSION_LIST_MAP = {
82    'android': [
83        # This file caused webview native library failed on arm64.
84        '../../device/gamepad/dualshock4_controller.cc',
85    ],
86    'fuchsia': [
87        # TODO(crbug.com/1174725): These files caused clang to crash while
88        # compiling them.
89        '../../base/allocator/partition_allocator/pcscan.cc',
90        '../../third_party/skia/src/core/SkOpts.cpp',
91        '../../third_party/skia/src/opts/SkOpts_hsw.cpp',
92        '../../third_party/skia/third_party/skcms/skcms.cc',
93    ],
94    'linux': [
95        # These files caused a static initializer to be generated, which
96        # shouldn't.
97        # TODO(crbug.com/990948): Remove when the bug is fixed.
98        '../../chrome/browser/media/router/providers/cast/cast_internal_message_util.cc',  #pylint: disable=line-too-long
99        '../../components/media_router/common/providers/cast/channel/cast_channel_enum.cc',  #pylint: disable=line-too-long
100        '../../components/media_router/common/providers/cast/channel/cast_message_util.cc',  #pylint: disable=line-too-long
101        '../../components/media_router/common/providers/cast/cast_media_source.cc',  #pylint: disable=line-too-long
102        '../../ui/events/keycodes/dom/keycode_converter.cc',
103        # TODO(crbug.com/1051561): angle_unittests affected by coverage.
104        '../../base/message_loop/message_pump_default.cc',
105        '../../base/message_loop/message_pump_libevent.cc',
106        '../../base/message_loop/message_pump_win.cc',
107        '../../base/task/sequence_manager/thread_controller_with_message_pump_impl.cc',  #pylint: disable=line-too-long
108    ],
109    'chromeos': [
110        # These files caused clang to crash while compiling them. They are
111        # excluded pending an investigation into the underlying compiler bug.
112        '../../third_party/webrtc/p2p/base/p2p_transport_channel.cc',
113        '../../third_party/icu/source/common/uts46.cpp',
114        '../../third_party/icu/source/common/ucnvmbcs.cpp',
115        '../../base/android/android_image_reader_compat.cc',
116        # TODO(crbug.com/1051561): angle_unittests affected by coverage.
117        '../../base/message_loop/message_pump_default.cc',
118        '../../base/message_loop/message_pump_libevent.cc',
119        '../../base/message_loop/message_pump_win.cc',
120        '../../base/task/sequence_manager/thread_controller_with_message_pump_impl.cc',  #pylint: disable=line-too-long
121    ],
122    'win': [
123        # TODO(crbug.com/1051561): angle_unittests affected by coverage.
124        '../../base/message_loop/message_pump_default.cc',
125        '../../base/message_loop/message_pump_libevent.cc',
126        '../../base/message_loop/message_pump_win.cc',
127        '../../base/task/sequence_manager/thread_controller_with_message_pump_impl.cc',  #pylint: disable=line-too-long
128    ],
129}
130
131# Map of force lists indexed by target OS.
132_COVERAGE_FORCE_LIST_MAP = {
133    # clang_profiling.cc refers to the symbol `__llvm_profile_dump` from the
134    # profiling runtime. In a partial coverage build, it is possible for a
135    # binary to include clang_profiling.cc but have no instrumented files, thus
136    # causing an unresolved symbol error because the profiling runtime will not
137    # be linked in. Therefore we force coverage for this file to ensure that
138    # any target that includes it will also get the profiling runtime.
139    'win': [r'..\..\base\test\clang_profiling.cc'],
140    # TODO(crbug.com/1141727) We're seeing runtime LLVM errors in mac-rel when
141    # no files are changed, so we suspect that this is similar to the other
142    # problem with clang_profiling.cc on Windows. The TODO here is to force
143    # coverage for this specific file on ALL platforms, if it turns out to fix
144    # this issue on Mac as well. It's the only file that directly calls
145    # `__llvm_profile_dump` so it warrants some special treatment.
146    'mac': ['../../base/test/clang_profiling.cc'],
147}
148
149
150def _remove_flags_from_command(command):
151  # We need to remove the coverage flags for this file, but we only want to
152  # remove them if we see the exact sequence defined in _COVERAGE_FLAGS.
153  # That ensures that we only remove the flags added by GN when
154  # "use_clang_coverage" is true. Otherwise, we would remove flags set by
155  # other parts of the build system.
156  start_flag = _COVERAGE_FLAGS[0]
157  num_flags = len(_COVERAGE_FLAGS)
158  start_idx = 0
159  try:
160    while True:
161      idx = command.index(start_flag, start_idx)
162      if command[idx:idx + num_flags] == _COVERAGE_FLAGS:
163        del command[idx:idx + num_flags]
164        # There can be multiple sets of _COVERAGE_FLAGS. All of these need to be
165        # removed.
166        start_idx = idx
167      else:
168        start_idx = idx + 1
169  except ValueError:
170    pass
171
172
173def main():
174  arg_parser = argparse.ArgumentParser()
175  arg_parser.usage = __doc__
176  arg_parser.add_argument(
177      '--files-to-instrument',
178      type=str,
179      help='Path to a file that contains a list of file names to instrument.')
180  arg_parser.add_argument(
181      '--target-os', required=False, help='The OS to compile for.')
182  arg_parser.add_argument('args', nargs=argparse.REMAINDER)
183  parsed_args = arg_parser.parse_args()
184
185  if (parsed_args.files_to_instrument and
186      not os.path.isfile(parsed_args.files_to_instrument)):
187    raise Exception('Path to the coverage instrumentation file: "%s" doesn\'t '
188                    'exist.' % parsed_args.files_to_instrument)
189
190  compile_command = parsed_args.args
191  if not any('clang' in s for s in compile_command):
192    return subprocess.call(compile_command)
193
194  target_os = parsed_args.target_os
195
196  try:
197    # The command is assumed to use Clang as the compiler, and the path to the
198    # source file is behind the -c argument, and the path to the source path is
199    # relative to the root build directory. For example:
200    # clang++ -fvisibility=hidden -c ../../base/files/file_path.cc -o \
201    #   obj/base/base/file_path.o
202    # On Windows, clang-cl.exe uses /c instead of -c.
203    source_flag = '/c' if target_os == 'win' else '-c'
204    source_flag_index = compile_command.index(source_flag)
205  except ValueError:
206    print('%s argument is not found in the compile command.' % source_flag)
207    raise
208
209  if source_flag_index + 1 >= len(compile_command):
210    raise Exception('Source file to be compiled is missing from the command.')
211
212  # On Windows, filesystem paths should use '\', but GN creates build commands
213  # that use '/'. We invoke os.path.normpath to ensure that the path uses the
214  # correct separator for the current platform (i.e. '\' on Windows and '/'
215  # otherwise).
216  compile_source_file = os.path.normpath(compile_command[source_flag_index + 1])
217  extension = os.path.splitext(compile_source_file)[1]
218  if not extension in ['.c', '.cc', '.cpp', '.cxx', '.m', '.mm', '.S']:
219    raise Exception('Invalid source file %s found' % compile_source_file)
220  exclusion_list = _COVERAGE_EXCLUSION_LIST_MAP.get(
221      target_os, _DEFAULT_COVERAGE_EXCLUSION_LIST)
222  force_list = _COVERAGE_FORCE_LIST_MAP.get(target_os, [])
223
224  should_remove_flags = False
225  if compile_source_file not in force_list:
226    if compile_source_file in exclusion_list:
227      should_remove_flags = True
228    elif parsed_args.files_to_instrument:
229      with open(parsed_args.files_to_instrument) as f:
230        if compile_source_file not in f.read():
231          should_remove_flags = True
232
233  if should_remove_flags:
234    _remove_flags_from_command(compile_command)
235
236  return subprocess.call(compile_command)
237
238
239if __name__ == '__main__':
240  sys.exit(main())
241