blob: 3367aac2f4506bf211064c84f5c39fbc944b2190 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2019 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Example usage:
$ fx set ...
$ scripts/gn/suppress_errors.py\
--error=-Wconversion
--config="//build/config:Wno-conversion"
"""
import argparse
import fileinput
import multiprocessing
import os
import re
import subprocess
import sys
def run_command(command):
return subprocess.check_output(
command, stderr=subprocess.STDOUT, encoding="utf8")
def get_common_gn_args(is_zircon):
"""Retrieve common GN args shared by several commands in this script.
Args:
is_zircon: True iff dealing with the Zircon build output directory.
Returns:
A list of GN command-line arguments (to be used after "gn <command>").
"""
if is_zircon:
return [
"out/default.zircon",
"--root=zircon",
"--all-toolchains",
"//:instrumented-ulib-redirect.asan(//public/gn/toolchain:user-arm64-clang)",
"//:instrumented-ulib-redirect.asan(//public/gn/toolchain:user-x64-clang)",
]
else:
return ["out/default"]
def can_have_config(params):
"""Returns whether the given target can have a config.
If not sure, returns True.
This function is module-level for reasons to do with how Python
multiprocessing works.
Args:
params: tuple of target to examine, config to add, is target in Zircon.
Packed in a single tuple for reasons to do with how Python
multiprocessing works.
"""
target, config, is_zircon = params
desc_command = ["fx", "gn", "desc"]
desc_command += get_common_gn_args(is_zircon)
try:
desc_out = run_command(desc_command + [target, "configs"])
# Target has configs and they include the given config.
# Can't add a duplicate config!
return config not in desc_out
except subprocess.CalledProcessError as e:
if 'Don\'t know how to display "configs" for ' in e.output:
# Target type cannot have configs,
# or the actual target is in another toolchain.
# Assume the latter.
return True
elif " matches no targets, configs or files" in e.output:
# The target probably exists in a non-default toolchain.
# Assume that it can have the config.
return True
else:
raise e
def main():
parser = argparse.ArgumentParser(
description="Adds a given config to all compile targets with "
"a given compiler error")
parser.add_argument("--error", required=True, help="Compiler error marker")
parser.add_argument("--config", required=True, help="Config to add")
parser.add_argument("--comment", help="Comment to add before config")
parser.add_argument(
"--zircon", help="//zircon build (ZN)", action="store_true")
args = parser.parse_args()
error = args.error
config = args.config
comment = args.comment
zircon = args.zircon
# Harvest all compilation errors
print("Building...")
try:
# On error continue building to discover all failures
run_command(["fx", "build", "-k0"])
print("Build successful!")
return 0
except subprocess.CalledProcessError as e:
build_out = e.output
error_regex = re.compile(
"(?:../)*([^:]*):\d*:\d*: error: .*" + re.escape(error))
error_files = set()
for line in build_out.splitlines():
match = error_regex.match(line)
if match:
path = os.path.normpath(match.group(1))
if zircon:
path = "//" + os.path.relpath(path, "zircon")
error_files.add(path)
print("Sources with compilation errors:")
print("\n".join(sorted(error_files)))
print()
if not error_files:
return 0
# Collect all BUILD.gn files with targets referencing failing sources
print("Resolving references...")
outdir = "out/default.zircon" if zircon else "out/default"
refs_command = ["fx", "gn", "refs"]
refs_command += get_common_gn_args(zircon)
refs_command += sorted(error_files)
try:
refs_out = run_command(refs_command)
except subprocess.CalledProcessError as e:
print("Failed to resolve references!")
print(e.output)
return 1
target_no_toolchain = re.compile("(\/\/[^:]*:[^(]*)(?:\(.*\))?")
error_targets = {
target_no_toolchain.match(ref).group(1)
for ref in refs_out.splitlines()
}
# Remove targets that already have the given config
# or can't have a config in the first place
print("Removing irrelevant targets...")
# `gn desc` is slow so parallelize
with multiprocessing.Pool(multiprocessing.cpu_count()) as p:
target_can_have = zip(
error_targets,
p.map(
can_have_config,
((target, config, zircon) for target in error_targets)),
)
error_targets = {
target for target, can_have in target_can_have if can_have
}
print("Failing targets:")
print("\n".join(sorted(error_targets)))
print()
# Fix failing targets
ref_regex = re.compile("//([^:]*):([^.(]*).*")
for target in sorted(error_targets):
match = ref_regex.match(target)
build_dir, target_name = match.groups()
target_regex = re.compile(
'^\s*\w*\("' + re.escape(target_name) + '"\) {')
build_file = os.path.join(
build_dir, "BUILD.gn" if not zircon else "BUILD.zircon.gn")
# Format file before processing
run_command(["fx", "format-code", "--files=" + build_file])
print("Fixing", target)
in_target = False
config_printed = False
in_configs = False
curly_brace_depth = 0
if zircon:
build_file = os.path.join("zircon", build_file)
secondary_build_file = os.path.join("build", "secondary", build_file)
if os.path.exists(secondary_build_file):
# Sometimes we put third_party BUILD.gn files in a shadow directory
# to avoid conflicting with original BUILD.gn files.
build_file = secondary_build_file
start_configs_inline = re.compile('configs \+?= \[ "')
start_configs = re.compile("configs \+?= \[")
for line in fileinput.FileInput(build_file, inplace=True):
curly_brace_depth += line.count("{") - line.count("}")
assert curly_brace_depth >= 0
if config_printed:
# We already printed the config, keep running until we exit the target
if curly_brace_depth == target_end_depth:
in_target = False
elif not in_target:
# Try to enter target
in_target = bool(target_regex.match(line))
if in_target:
target_end_depth = curly_brace_depth - 1
elif curly_brace_depth > target_end_depth + 1:
# Ignore inner blocks such as inner definitions and conditionals
pass
elif curly_brace_depth == target_end_depth:
# Last chance to print config before exiting
if comment:
print("#", comment)
print('configs += [ "' + config + '"]')
config_printed = True
in_target = False
elif start_configs_inline.match(line) and config in line:
config_printed = True
elif start_configs_inline.match(line):
line = line[:-3] + ', "' + config + '" ]\n'
config_printed = True
elif start_configs.match(line):
in_configs = True
elif in_configs and line.strip() == '"' + config + '",':
in_configs = False
config_printed = True
elif in_configs and line.strip() == "]":
if comment:
print("#", comment)
print('"' + config + '",')
in_configs = False
config_printed = True
print(line, end="")
if config_printed and not in_target:
# Reset for a possible redefinition of the same target
# (e.g. within another conditional block)
config_printed = False
run_command(["fx", "format-code", "--files=" + build_file])
print("Fixed all of:")
for error_target in sorted(error_targets):
print(' "' + error_target + '",')
return 0
if __name__ == "__main__":
sys.exit(main())