blob: 375ffb5dc3446cd830668eea1e37a3ae813fa6a1 [file]
#!/usr/bin/env fuchsia-vendored-python
# Copyright 2026 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.
"""Check for unknown Ninja implicit inputs after an `fx build` invocation.
For context, see //build/ninja_implicit_inputs/README.md.
This script is used to detect which GN target declarations must be fixed,
to declare properly their implicit source inputs through GN metadata
collection.
After the build, invoke the script passing as arguments a list of root
GN labels or Ninja paths. All implicit inputs stored in the Ninja deps log
will be compared with the following:
- The list of explicit inputs declared in the Ninja build plan.
- The list of implicit input files collected through GN metadata
collection, and listed in the //build/ninja_implicit_inputs:manifest
output file.
- The list of implicit input directories collected through GN metadata
collection, and listed in the //build/ninja_implicit_inputs:manifest
output file.
Any implicit input that doesn't belong to one of the list above is an error.
This script will print the GN labels of targets which depend on these files,
as well as their list, plus some human-readable instructions on how to fix
most of the problems.
Most of the time, this corresponds to missing headers declarations in
C++ target definitions.
Example usage, this is the simplest, which omits missing input files
```
fx build //zircon/public/sysroot_sdk
python3 build/ninja_implicit_inputs/check_tool.py //zircon/public/sysroot_sdk
```
Use --check-missing-inputs to also check for inputs that are missing from
the Fuchsia source tree. These are generally C++ headers that no longer
exist, or due to typographical errors.
This is not enabled by default because this over-reports errors from
unrelated //third_party targets, unless your `args.gn` is modified like
in the following example:
```
ROOT_LABEL=//zircon/public/sysroot_sdk
echo "ninja_implicit_inputs_root_labels = [ \"$ROOT_LABEL\" ]" >> out/default/args.gn
fx build $ROOT_LABEL
python3 build/ninja_implicit_inputs/check_tool.py \
--check-missing-inputs $ROOT_LABEL
```
"""
import argparse
import json
import os
import subprocess
import sys
import typing as T
from pathlib import Path
_SCRIPT_DIR = os.path.dirname(__file__)
sys.path.insert(0, _SCRIPT_DIR)
import ninja_implicit_inputs as nii
sys.path.insert(0, os.path.join(_SCRIPT_DIR, "../bazel/scripts"))
import build_utils
_DEFAULT_OUTPUT = "/tmp/implicit_inputs.txt"
class DebugDir:
"""Models a directory where to store large files computed by this script.
Useful to inspect the results when debugging what this tool does.
Usage is:
1) Create instance
2) Call log_lines() or log_json() as many times as needed.
Each file is written with an sequence index prefix, e.g.
1-first
2-second
3-third
...
"""
def __init__(self, path: None | Path) -> None:
self._dir = path
self._index = 1
@property
def enabled(self) -> bool:
"""Return True is logging is enabled."""
return bool(self._dir)
def _get_log_path(self, filename: str) -> None | Path:
if not self._dir:
return None
result = self._dir / f"{self._index}-{filename}"
self._index += 1
return result
def log_lines(self, filename: str, lines: T.Iterable[str]) -> None:
"""Write a list of strings into a debug text file.
Args:
filename: A filename. A unique '<index>-' prefix will
be prepended to it automatically.
lines: A sequence of strings. Each one will be written
as a new text line in the output file.
"""
log_path = self._get_log_path(filename)
if log_path:
log_path.write_text("\n".join(sorted(lines)))
def log_json(self, filename: str, content: T.Any) -> None:
"""Write a JSON value into a debug output file.
Args:
filename: A filename. A unique '<index>-' prefix will
be prepended to it automatically.
content: A JSON-serializable value. Written as indented
JSON string into the output file, for readability.
"""
log_path = self._get_log_path(filename)
if log_path:
with log_path.open("w") as f:
json.dump(content, f, indent=2)
def main() -> int:
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument(
"--fuchsia-dir",
type=Path,
help="Path to Fuchsia source directory (auto-detected)",
)
parser.add_argument(
"--build-dir",
type=Path,
help="Path to Ninja build directory (auto-detected)",
)
parser.add_argument(
"--debug-dir",
type=Path,
help="Path to a directory that will contain debug files for this tool.",
)
parser.add_argument(
"--output",
type=Path,
default=_DEFAULT_OUTPUT,
help=f"Save output to file, default to {_DEFAULT_OUTPUT}",
)
parser.add_argument(
"--build",
action="store_true",
help="Perform build before running the tool. Implies --check-missing-inputs.",
)
parser.add_argument(
"--check-missing-inputs",
action="store_true",
help="Also check for missing explicit inputs. The result is only correct "
+ "if a build was performed.",
)
parser.add_argument(
"targets",
metavar="TARGET",
type=str,
nargs="+",
help="Either a Ninja target path, or a GN label.",
)
args = parser.parse_args()
def log(msg: str) -> None:
print(msg, file=sys.stderr)
bazel_paths = build_utils.BazelPaths.new(args.fuchsia_dir, args.build_dir)
host_tag = build_utils.get_host_tag()
fuchsia_dir = bazel_paths.fuchsia_dir
build_dir = bazel_paths.ninja_build_dir
if args.build:
args.check_missing_inputs = True
build_cmd = [f"{fuchsia_dir}/scripts/fx", "build"] + args.targets
ret = subprocess.run(build_cmd)
if ret.returncode != 0:
return ret.returncode
debug_dir = DebugDir(args.debug_dir)
ninja_tool = fuchsia_dir / f"prebuilt/third_party/ninja/{host_tag}/ninja"
ninja_runner, ninja_outputs = nii.create_ninja_runner_and_outputs(
ninja_tool, build_dir
)
# First qualify all root GN targets passed as argument
gn_qualifier = nii.create_gn_qualifier(build_dir)
root_targets = [
gn_qualifier.qualify_label(target)
for target in args.targets
if target.startswith("//")
] + [target for target in args.targets if not target.startswith("//")]
if not args.check_missing_inputs:
# If the value of ninja_implicit_inputs_root_label matches
# or root_targets set, we can enable --check-missing-inputs
# automatically since the results will be correct.
with (build_dir / "args.json").open() as f:
args_json = json.load(f)
args_root_targets = [
gn_qualifier.qualify_label(target)
for target in args_json.get("ninja_implicit_inputs_root_labels", [])
]
if set(root_targets) == set(args_root_targets):
log("Enabling --check-missing-inputs due to build configuration.")
args.check_missing_inputs = True
# In order to properly map toolchain-less GN labels to their
# full form (e.g. //src/foo -> //src/foo:foo(//build/toolchain/fuchsia:arm64)),
# a GnLabelQualifier instance which knows about the current build
# configuration is required.
ninja_targets = []
for target in root_targets:
if target.startswith("//"):
qualified_label = gn_qualifier.qualify_label(target)
ninja_paths = ninja_outputs.gn_label_to_paths(qualified_label)
if not ninja_paths:
parser.error(f"Unknown GN label: {target}")
# Only the first output is necessary for the queries below.
ninja_targets.append(ninja_paths[0])
else:
ninja_targets.append(target)
log(f"Number of root Ninja paths: {len(ninja_targets)}")
debug_dir.log_lines("ninja_targets.txt", ninja_targets)
# Retrieve known implicit input files and directories.
log(f"Loading implicit inputs manifest.")
implicit_inputs = nii.ImplicitInputs.create_from_build_dir(
bazel_paths.ninja_build_dir
)
log(
f"Found {len(implicit_inputs.all_known_files)} implicit files, and {len(implicit_inputs.all_known_dirs)} implicit directories"
)
debug_dir.log_lines("known_files.txt", implicit_inputs.all_known_files)
debug_dir.log_lines("known_directories.txt", implicit_inputs.all_known_dirs)
log(f"Finding unknown implicit source inputs from Ninja deps log.")
unknown_source_inputs = nii.find_unknown_implicit_source_inputs(
ninja_targets, implicit_inputs, ninja_runner
)
if debug_dir.enabled:
# set() is not JSON-serializable, so convert it to a sorted list for output.
unknown_map_json = {
build_target: sorted(implicit_inputs)
for build_target, implicit_inputs in unknown_source_inputs.items()
}
debug_dir.log_json("unknown_map.json", unknown_map_json)
unknown_inputs: set[str] = set()
for unknown_sources in unknown_source_inputs.values():
unknown_inputs.update(unknown_sources)
log(f"Found {len(unknown_inputs)} unknown input paths.")
debug_dir.log_lines("unknown_inputs.txt", unknown_inputs)
output_file: None | T.TextIO = None
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
output_file = args.output.open("w")
has_error = False
# First, check for missing files
if args.check_missing_inputs:
log(f"Checking for missing input files.")
gn_labels_to_missing = implicit_inputs.check_for_missing_files(
fuchsia_dir, build_dir
)
if debug_dir.enabled:
gn_labels_to_missing_json = {
gn_label: sorted(missing_files)
for gn_label, missing_files in gn_labels_to_missing.items()
}
debug_dir.log_json(
"gn_labels_to_missing.json", gn_labels_to_missing_json
)
if gn_labels_to_missing:
has_error = True
nii.print_missing_source_inputs_error(
gn_labels_to_missing, fuchsia_dir, build_dir, sys.stdout
)
if output_file:
nii.print_missing_source_inputs_error(
gn_labels_to_missing, fuchsia_dir, build_dir, output_file
)
# Second, check for unlisted depfile inputs.
log(f"Checking for unknown depfile inputs.")
gn_labels_to_sources = nii.map_implicit_source_inputs(
unknown_inputs, bazel_paths.fuchsia_dir, ninja_runner, ninja_outputs
)
if debug_dir.enabled:
# set() is not serializable.
gn_labels_json = {
gn_label: sorted(implicit_inputs)
for gn_label, implicit_inputs in gn_labels_to_sources.items()
}
debug_dir.log_json("gn_labels_to_sources.json", gn_labels_json)
if gn_labels_to_sources:
has_error = True
nii.print_implicit_source_inputs_error(gn_labels_to_sources, sys.stdout)
if output_file:
nii.print_implicit_source_inputs_error(
gn_labels_to_sources, output_file
)
if has_error:
if output_file:
print(
f"This report has been saved to {args.output} for easier reading."
)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())