• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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