#!/usr/bin/env python3 # Copyright 2020 The Chromium Authors. All rights reserved. # 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 import sys from util import build_utils _CHROMIUM_SRC = os.path.normpath(os.path.join(__file__, '..', '..', '..', '..')) _NINJA_PATH = os.path.join(_CHROMIUM_SRC, 'third_party', 'depot_tools', 'ninja') # Relative to _CHROMIUM_SRC _GN_SRC_REL_PATH = os.path.join('third_party', 'depot_tools', '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, 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) pout, _ = p.communicate() if p.returncode == 0: return None # For Python3 only: if isinstance(pout, bytes) and sys.version_info >= (3, ): pout = pout.decode('utf-8') return '' if pout is None else pout 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) as f_in, open(dest_args_path, 'w') 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 the regex does not contain '\n': the first 5 lines after the 'FAILED' message (including the text on the line after the 'FAILED' message) is searched. Otherwise: the entire test output after the 'FAILED' message is searched. """ 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')[:5], 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) 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', # GOMA does not work with non-standard output directories. 'use_goma = false', ] _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()