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