Yuke Liao | bb571bd6 | 2018-10-31 21:51:52 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # Copyright 2018 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. |
Sajjad Mirza | bf9e66a | 2019-03-07 21:49:06 | [diff] [blame] | 5 | """Removes code coverage flags from invocations of the Clang C/C++ compiler. |
Yuke Liao | bb571bd6 | 2018-10-31 21:51:52 | [diff] [blame] | 6 | |
Sajjad Mirza | bf9e66a | 2019-03-07 21:49:06 | [diff] [blame] | 7 | If the GN arg `use_clang_coverage=true`, this script will be invoked by default. |
| 8 | GN will add coverage instrumentation flags to almost all source files. |
| 9 | |
| 10 | This script is used to remove instrumentation flags from a subset of the source |
| 11 | files. 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 |
| 13 | except the ones listed in --files-to-instrument. |
| 14 | |
| 15 | This script also contains hard-coded exclusion lists of files to never |
| 16 | instrument, indexed by target operating system. Files in these lists have their |
| 17 | flags removed in both modes. The OS can be selected with --target-os. |
Yuke Liao | bb571bd6 | 2018-10-31 21:51:52 | [diff] [blame] | 18 | |
| 19 | The path to the coverage instrumentation input file should be relative to the |
| 20 | root build directory, and the file consists of multiple lines where each line |
| 21 | represents a path to a source file, and the specified paths must be relative to |
| 22 | the root build directory. e.g. ../../base/task/post_task.cc for build |
| 23 | directory 'out/Release'. |
| 24 | |
| 25 | One caveat with this compiler wrapper is that it may introduce unexpected |
| 26 | behaviors in incremental builds when the file path to the coverage |
| 27 | instrumentation input file changes between consecutive runs, so callers of this |
| 28 | script are strongly advised to always use the same path such as |
| 29 | "${root_build_dir}/coverage_instrumentation_input.txt". |
| 30 | |
| 31 | It's worth noting on try job builders, if the contents of the instrumentation |
| 32 | file changes so that a file doesn't need to be instrumented any longer, it will |
| 33 | be recompiled automatically because if try job B runs after try job A, the files |
| 34 | that were instrumented in A will be updated (i.e., reverted to the checked in |
| 35 | version) in B, and so they'll be considered out of date by ninja and recompiled. |
| 36 | |
| 37 | Example usage: |
| 38 | clang_code_coverage_wrapper.py \\ |
| 39 | --files-to-instrument=coverage_instrumentation_input.txt |
| 40 | """ |
| 41 | |
Raul Tambre | 9e24293b | 2019-05-12 06:11:07 | [diff] [blame] | 42 | from __future__ import print_function |
| 43 | |
Yuke Liao | bb571bd6 | 2018-10-31 21:51:52 | [diff] [blame] | 44 | import argparse |
| 45 | import os |
| 46 | import subprocess |
| 47 | import sys |
| 48 | |
| 49 | # Flags used to enable coverage instrumentation. |
Sajjad Mirza | bf9e66a | 2019-03-07 21:49:06 | [diff] [blame] | 50 | # Flags should be listed in the same order that they are added in |
| 51 | # build/config/coverage/BUILD.gn |
Yuke Liao | 5eff782 | 2019-02-28 03:56:24 | [diff] [blame] | 52 | _COVERAGE_FLAGS = [ |
Sajjad Mirza | 49c00e3 | 2019-03-01 22:46:52 | [diff] [blame] | 53 | '-fprofile-instr-generate', '-fcoverage-mapping', |
| 54 | # Following experimental flags remove unused header functions from the |
| 55 | # coverage mapping data embedded in the test binaries, and the reduction |
| 56 | # of binary size enables building Chrome's large unit test targets on |
| 57 | # MacOS. Please refer to crbug.com/796290 for more details. |
| 58 | '-mllvm', '-limited-coverage-experimental=true' |
Yuke Liao | 5eff782 | 2019-02-28 03:56:24 | [diff] [blame] | 59 | ] |
Yuke Liao | bb571bd6 | 2018-10-31 21:51:52 | [diff] [blame] | 60 | |
Sajjad Mirza | bf9e66a | 2019-03-07 21:49:06 | [diff] [blame] | 61 | # Map of exclusion lists indexed by target OS. |
| 62 | # If no target OS is defined, or one is defined that doesn't have a specific |
| 63 | # entry, use the 'default' exclusion_list. Anything added to 'default' will |
| 64 | # apply to all platforms that don't have their own specific list. |
| 65 | _COVERAGE_EXCLUSION_LIST_MAP = { |
| 66 | 'default': [], |
Yuke Liao | 8098d70 | 2019-08-05 23:57:41 | [diff] [blame^] | 67 | 'linux': [ |
| 68 | # These files caused a static initializer to be generated, which |
| 69 | # shouldn't. |
| 70 | # TODO(crbug.com/990948): Remove when the bug is fixed. |
| 71 | '../../chrome/browser/media/router/providers/cast/cast_internal_message_util.cc', #pylint: disable=line-too-long |
| 72 | '../../chrome/common/media_router/providers/cast/cast_media_source.cc', |
| 73 | '../../components/cast_channel/cast_channel_enum.cc', |
| 74 | '../../components/cast_channel/cast_message_util.cc' |
| 75 | |
| 76 | ], |
Sajjad Mirza | bf9e66a | 2019-03-07 21:49:06 | [diff] [blame] | 77 | 'chromeos': [ |
| 78 | # These files caused clang to crash while compiling them. They are |
| 79 | # excluded pending an investigation into the underlying compiler bug. |
| 80 | '../../third_party/webrtc/p2p/base/p2p_transport_channel.cc', |
| 81 | '../../third_party/icu/source/common/uts46.cpp', |
| 82 | '../../third_party/icu/source/common/ucnvmbcs.cpp', |
| 83 | '../../base/android/android_image_reader_compat.cc', |
| 84 | ] |
| 85 | } |
| 86 | |
| 87 | |
| 88 | def _remove_flags_from_command(command): |
| 89 | # We need to remove the coverage flags for this file, but we only want to |
| 90 | # remove them if we see the exact sequence defined in _COVERAGE_FLAGS. |
| 91 | # That ensures that we only remove the flags added by GN when |
| 92 | # "use_clang_coverage" is true. Otherwise, we would remove flags set by |
| 93 | # other parts of the build system. |
| 94 | start_flag = _COVERAGE_FLAGS[0] |
| 95 | num_flags = len(_COVERAGE_FLAGS) |
| 96 | start_idx = 0 |
| 97 | try: |
| 98 | while True: |
| 99 | idx = command.index(start_flag, start_idx) |
| 100 | start_idx = idx + 1 |
| 101 | if command[idx:idx+num_flags] == _COVERAGE_FLAGS: |
| 102 | del command[idx:idx+num_flags] |
| 103 | break |
| 104 | except ValueError: |
| 105 | pass |
Yuke Liao | bb571bd6 | 2018-10-31 21:51:52 | [diff] [blame] | 106 | |
| 107 | def main(): |
| 108 | # TODO(crbug.com/898695): Make this wrapper work on Windows platform. |
| 109 | arg_parser = argparse.ArgumentParser() |
| 110 | arg_parser.usage = __doc__ |
| 111 | arg_parser.add_argument( |
| 112 | '--files-to-instrument', |
| 113 | type=str, |
Yuke Liao | bb571bd6 | 2018-10-31 21:51:52 | [diff] [blame] | 114 | help='Path to a file that contains a list of file names to instrument.') |
Sajjad Mirza | bf9e66a | 2019-03-07 21:49:06 | [diff] [blame] | 115 | arg_parser.add_argument( |
| 116 | '--target-os', |
| 117 | required=False, |
| 118 | help='The OS to compile for.') |
Yuke Liao | bb571bd6 | 2018-10-31 21:51:52 | [diff] [blame] | 119 | arg_parser.add_argument('args', nargs=argparse.REMAINDER) |
| 120 | parsed_args = arg_parser.parse_args() |
| 121 | |
Sajjad Mirza | bf9e66a | 2019-03-07 21:49:06 | [diff] [blame] | 122 | if (parsed_args.files_to_instrument and |
| 123 | not os.path.isfile(parsed_args.files_to_instrument)): |
Yuke Liao | bb571bd6 | 2018-10-31 21:51:52 | [diff] [blame] | 124 | raise Exception('Path to the coverage instrumentation file: "%s" doesn\'t ' |
| 125 | 'exist.' % parsed_args.files_to_instrument) |
| 126 | |
| 127 | compile_command = parsed_args.args |
Sajjad Mirza | bf9e66a | 2019-03-07 21:49:06 | [diff] [blame] | 128 | if not any('clang' in s for s in compile_command): |
| 129 | return subprocess.call(compile_command) |
| 130 | |
Yuke Liao | bb571bd6 | 2018-10-31 21:51:52 | [diff] [blame] | 131 | try: |
| 132 | # The command is assumed to use Clang as the compiler, and the path to the |
| 133 | # source file is behind the -c argument, and the path to the source path is |
| 134 | # relative to the root build directory. For example: |
| 135 | # clang++ -fvisibility=hidden -c ../../base/files/file_path.cc -o \ |
| 136 | # obj/base/base/file_path.o |
| 137 | index_dash_c = compile_command.index('-c') |
| 138 | except ValueError: |
Raul Tambre | 9e24293b | 2019-05-12 06:11:07 | [diff] [blame] | 139 | print('-c argument is not found in the compile command.') |
Yuke Liao | bb571bd6 | 2018-10-31 21:51:52 | [diff] [blame] | 140 | raise |
| 141 | |
| 142 | if index_dash_c + 1 >= len(compile_command): |
| 143 | raise Exception('Source file to be compiled is missing from the command.') |
| 144 | |
| 145 | compile_source_file = compile_command[index_dash_c + 1] |
Sajjad Mirza | bf9e66a | 2019-03-07 21:49:06 | [diff] [blame] | 146 | target_os = parsed_args.target_os |
| 147 | if target_os not in _COVERAGE_EXCLUSION_LIST_MAP: |
| 148 | target_os = 'default' |
| 149 | exclusion_list = _COVERAGE_EXCLUSION_LIST_MAP[target_os] |
| 150 | |
| 151 | if compile_source_file in exclusion_list: |
| 152 | _remove_flags_from_command(compile_command) |
| 153 | elif parsed_args.files_to_instrument: |
| 154 | with open(parsed_args.files_to_instrument) as f: |
| 155 | if compile_source_file not in f.read(): |
| 156 | _remove_flags_from_command(compile_command) |
Yuke Liao | bb571bd6 | 2018-10-31 21:51:52 | [diff] [blame] | 157 | |
| 158 | return subprocess.call(compile_command) |
| 159 | |
Yuke Liao | bb571bd6 | 2018-10-31 21:51:52 | [diff] [blame] | 160 | if __name__ == '__main__': |
| 161 | sys.exit(main()) |