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 test_output is None: 96 return False 97 98 failed_index = test_output.find('FAILED') 99 if failed_index < 0: 100 return False 101 102 failure_message = test_output[failed_index:] 103 if regex.find('\n') >= 0: 104 return re.search(regex, failure_message) 105 return _search_regex_in_list(failure_message.split('\n'), regex) 106 107 108def _search_regex_in_list(value, regex): 109 for line in value: 110 if re.search(regex, line): 111 return True 112 return False 113 114 115def _do_build_get_failure_output(gn_path, gn_cmd, options): 116 # Extract directory from test target. As all of the test targets are declared 117 # in the same BUILD.gn file, it does not matter which test target is used. 118 target_dir = gn_path.rsplit(':', 1)[0] 119 120 if gn_cmd is not None: 121 gn_args = [ 122 _GN_SRC_REL_PATH, '--root-target=' + target_dir, gn_cmd, 123 os.path.relpath(options.out_dir, _CHROMIUM_SRC) 124 ] 125 _run_command(gn_args, cwd=_CHROMIUM_SRC) 126 127 ninja_args = [_NINJA_PATH, '-C', options.out_dir, gn_path] 128 return _run_command_get_failure_output(ninja_args) 129 130 131def main(): 132 parser = argparse.ArgumentParser() 133 parser.add_argument('--gn-args-path', 134 required=True, 135 help='Path to args.gn file.') 136 parser.add_argument('--test-configs-path', 137 required=True, 138 help='Path to file with test configurations') 139 parser.add_argument('--out-dir', 140 required=True, 141 help='Path to output directory to use for compilation.') 142 parser.add_argument('--stamp', help='Path to touch.') 143 options = parser.parse_args() 144 145 with open(options.test_configs_path) as f: 146 # Escape '\' in '\.' now. This avoids having to do the escaping in the test 147 # specification. 148 config_text = f.read().replace(r'\.', r'\\.') 149 test_configs = json.loads(config_text) 150 151 if not os.path.exists(options.out_dir): 152 os.makedirs(options.out_dir) 153 154 out_gn_args_path = os.path.join(options.out_dir, 'args.gn') 155 extra_gn_args = [ 156 'enable_android_nocompile_tests = true', 157 'treat_warnings_as_errors = true', 158 # RBE does not work with non-standard output directories. 159 'use_remoteexec = false', 160 ] 161 _copy_and_append_gn_args(options.gn_args_path, out_gn_args_path, 162 extra_gn_args) 163 164 ran_gn_gen = False 165 did_clean_build = False 166 error_messages = [] 167 for config in test_configs: 168 # Strip leading '//' 169 gn_path = config['target'][2:] 170 expect_regex = config['expect_regex'] 171 172 test_output = _do_build_get_failure_output(gn_path, None, options) 173 174 # 'gn gen' takes > 1s to run. Only run 'gn gen' if it is needed for compile. 175 if (test_output 176 and _search_regex_in_list(test_output.split('\n'), _GN_GEN_REGEX)): 177 assert not ran_gn_gen 178 ran_gn_gen = True 179 test_output = _do_build_get_failure_output(gn_path, 'gen', options) 180 181 if (not _find_regex_in_test_failure_output(test_output, expect_regex) 182 and not did_clean_build): 183 # Ensure the failure is not due to incremental build. 184 did_clean_build = True 185 test_output = _do_build_get_failure_output(gn_path, 'clean', options) 186 187 if not _find_regex_in_test_failure_output(test_output, expect_regex): 188 if test_output is None: 189 # Purpose of quotes at beginning of message is to make it clear that 190 # "Compile successful." is not a compiler log message. 191 test_output = '""\nCompile successful.' 192 error_message = '//{} failed.\nExpected compile output pattern:\n'\ 193 '{}\nActual compile output:\n{}'.format( 194 gn_path, expect_regex, test_output) 195 error_messages.append(error_message) 196 197 if error_messages: 198 raise Exception('\n'.join(error_messages)) 199 200 if options.stamp: 201 build_utils.Touch(options.stamp) 202 203 204if __name__ == '__main__': 205 main() 206