1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
|
#!/usr/bin/env python3
# Copyright 2020 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Checks that compiling targets in BUILD.gn file fails."""
import argparse
import json
import os
import subprocess
import re
from util import build_utils
_CHROMIUM_SRC = os.path.normpath(os.path.join(__file__, '..', '..', '..', '..'))
_NINJA_PATH = os.path.join(_CHROMIUM_SRC, 'third_party', 'ninja', 'ninja')
# Relative to _CHROMIUM_SRC
_GN_SRC_REL_PATH = os.path.join('buildtools', 'linux64', 'gn')
# Regex for determining whether compile failed because 'gn gen' needs to be run.
_GN_GEN_REGEX = re.compile(r'ninja: (error|fatal):')
def _raise_command_exception(args, returncode, output):
"""Raises an exception whose message describes a command failure.
Args:
args: shell command-line (as passed to subprocess.Popen())
returncode: status code.
output: command output.
Raises:
a new Exception.
"""
message = 'Command failed with status {}: {}\n' \
'Output:-----------------------------------------\n{}\n' \
'------------------------------------------------\n'.format(
returncode, args, output)
raise Exception(message)
def _run_command(args, cwd=None):
"""Runs shell command. Raises exception if command fails."""
p = subprocess.Popen(args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
encoding='utf-8',
cwd=cwd)
pout, _ = p.communicate()
if p.returncode != 0:
_raise_command_exception(args, p.returncode, pout)
def _run_command_get_failure_output(args):
"""Runs shell command.
Returns:
Command output if command fails, None if command succeeds.
"""
p = subprocess.Popen(args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
encoding='utf-8')
pout, _ = p.communicate()
if p.returncode == 0:
return None
return pout or ''
def _copy_and_append_gn_args(src_args_path, dest_args_path, extra_args):
"""Copies args.gn.
Args:
src_args_path: args.gn file to copy.
dest_args_path: Copy file destination.
extra_args: Text to append to args.gn after copy.
"""
with open(src_args_path, encoding='utf-8') as f_in:
with open(dest_args_path, 'w', encoding='utf-8') as f_out:
f_out.write(f_in.read())
f_out.write('\n')
f_out.write('\n'.join(extra_args))
def _find_regex_in_test_failure_output(test_output, regex):
"""Searches for regex in test output.
Args:
test_output: test output.
regex: regular expression to search for.
Returns:
Whether the regular expression was found in the part of the test output
after the 'FAILED' message.
"""
if test_output is None:
return False
failed_index = test_output.find('FAILED')
if failed_index < 0:
return False
failure_message = test_output[failed_index:]
if regex.find('\n') >= 0:
return re.search(regex, failure_message)
return _search_regex_in_list(failure_message.split('\n'), regex)
def _search_regex_in_list(value, regex):
for line in value:
if re.search(regex, line):
return True
return False
def _do_build_get_failure_output(gn_path, gn_cmd, options):
# Extract directory from test target. As all of the test targets are declared
# in the same BUILD.gn file, it does not matter which test target is used.
target_dir = gn_path.rsplit(':', 1)[0]
if gn_cmd is not None:
gn_args = [
_GN_SRC_REL_PATH, '--root-target=' + target_dir, gn_cmd,
os.path.relpath(options.out_dir, _CHROMIUM_SRC)
]
_run_command(gn_args, cwd=_CHROMIUM_SRC)
ninja_args = [_NINJA_PATH, '-C', options.out_dir, gn_path]
return _run_command_get_failure_output(ninja_args)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--gn-args-path',
required=True,
help='Path to args.gn file.')
parser.add_argument('--test-configs-path',
required=True,
help='Path to file with test configurations')
parser.add_argument('--out-dir',
required=True,
help='Path to output directory to use for compilation.')
parser.add_argument('--stamp', help='Path to touch.')
options = parser.parse_args()
with open(options.test_configs_path, encoding='utf-8') as f:
# Escape '\' in '\.' now. This avoids having to do the escaping in the test
# specification.
config_text = f.read().replace(r'\.', r'\\.')
test_configs = json.loads(config_text)
if not os.path.exists(options.out_dir):
os.makedirs(options.out_dir)
out_gn_args_path = os.path.join(options.out_dir, 'args.gn')
extra_gn_args = [
'enable_android_nocompile_tests = true',
'treat_warnings_as_errors = true',
# RBE does not work with non-standard output directories.
'use_remoteexec = false',
'use_reclient = false',
# Do not use fast_local_dev_server.py.
'android_static_analysis = "on"',
]
_copy_and_append_gn_args(options.gn_args_path, out_gn_args_path,
extra_gn_args)
ran_gn_gen = False
did_clean_build = False
error_messages = []
for config in test_configs:
# Strip leading '//'
gn_path = config['target'][2:]
expect_regex = config['expect_regex']
test_output = _do_build_get_failure_output(gn_path, None, options)
# 'gn gen' takes > 1s to run. Only run 'gn gen' if it is needed for compile.
if (test_output
and _search_regex_in_list(test_output.split('\n'), _GN_GEN_REGEX)):
assert not ran_gn_gen
ran_gn_gen = True
test_output = _do_build_get_failure_output(gn_path, 'gen', options)
if (not _find_regex_in_test_failure_output(test_output, expect_regex)
and not did_clean_build):
# Ensure the failure is not due to incremental build.
did_clean_build = True
test_output = _do_build_get_failure_output(gn_path, 'clean', options)
if not _find_regex_in_test_failure_output(test_output, expect_regex):
if test_output is None:
# Purpose of quotes at beginning of message is to make it clear that
# "Compile successful." is not a compiler log message.
test_output = '""\nCompile successful.'
error_message = '//{} failed.\nExpected compile output pattern:\n'\
'{}\nActual compile output:\n{}'.format(
gn_path, expect_regex, test_output)
error_messages.append(error_message)
if error_messages:
raise Exception('\n'.join(error_messages))
if options.stamp:
build_utils.Touch(options.stamp)
if __name__ == '__main__':
main()
|