blob: 3dab323adf157028b4374e109fb098ec5097952d [file] [log] [blame] [edit]
#!/usr/bin/env fuchsia-vendored-python
# Copyright 2023 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.
#
# IMPORTANT: This script should not depend on any Bazel workspace setup
# or specific environment. Only depend on standard Python3 modules
# and assume a minimum of Python3.8 is being used.
"""A tool to perform useful Bazel operations easily."""
import argparse
import re
import subprocess
import sys
from pathlib import Path
def error_message(msg: str) -> int:
"""Print error message to stderr, then return 1."""
print(f"ERROR: {msg}", file=sys.stderr)
return 1
def make_bazel_quiet_command(bazel: str, command: str) -> list[str]:
"""Create command argument list for a Bazel command that does not print too much.
Args:
bazel: Path to Bazel program.
command: Bazel command (e.g. 'query', 'build', etc..)
Returns:
A sequence of strings that can be used as a command line prefix.
"""
result = [
bazel,
command,
"--noshow_loading_progress",
"--noshow_progress",
"--ui_event_filters=-info",
]
if command != "query":
result += ["--show_result=0"]
return result
def cmd_target_dump(args: argparse.Namespace) -> int:
"""Implement the target_dump command."""
bazel_cmd = (
make_bazel_quiet_command(args.bazel, "query")
+ ["--output=build"]
+ args.target_set
)
buildifier_cmd = [
args.buildifier,
"--type=build",
"--mode=fix",
"--lint=off",
]
proc1 = subprocess.Popen(
bazel_cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=None
)
proc2 = subprocess.Popen(
buildifier_cmd, stdin=proc1.stdout, stdout=None, stderr=None
)
proc1.wait()
proc2.wait()
if proc1.returncode != 0:
return proc1.returncode
return proc2.returncode
def cmd_actions(args: argparse.Namespace) -> int:
"""Implement the 'actions' command."""
bazel_cmd = (
make_bazel_quiet_command(args.bazel, "aquery")
+ args.extra_args[1:]
+ args.target_set
)
print("CMD %s" % bazel_cmd)
ret = subprocess.run(
bazel_cmd, stdin=subprocess.DEVNULL, capture_output=True, text=True
)
if ret.returncode != 0:
print("%s\n" % ret.stdout, file=sys.stdout)
print("%s\n" % ret.stderr, file=sys.stderr)
return ret.returncode
def ignored_path(path: str) -> bool:
if path.startswith(
(
"external/prebuilt_clang/",
"external/fuchsia_clang/",
"prebuilt/third_party/sysroot/",
)
):
return True
if path.find("external/fuchsia_prebuilt_rust/") >= 0:
return True
return False
inputs_re = re.compile(r"^ Inputs: \[(.*)\]$")
for line in ret.stdout.splitlines():
m = inputs_re.match(line)
if m:
input_paths = []
for path in m.group(1).split(","):
path = path.strip()
if not ignored_path(path):
input_paths.append(path)
line = " Inputs: [%s]" % (", ".join(input_paths))
print(line)
return 0
def cmd_set_gn_targets(args: argparse.Namespace) -> int:
"""Implement the set_gn_targets command."""
# Lazy import of workspace_utils.py and gn_targets_utils.py
sys.path.insert(0, str(Path(__file__).parent))
import build_utils
import gn_targets_utils
verbosity = args.verbose - args.quiet
def log(level: int, msg: str) -> None:
if verbosity >= level:
print(msg)
# Determine Ninja build directory
if args.build_dir:
build_dir = Path(args.build_dir).resolve()
else:
build_dir = build_utils.find_fx_build_dir(args.fuchsia_dir)
if not build_dir:
return error_message(
"Could not find Fuchsia build directory, use --build-dir=BUILD_DIR."
)
log(2, f"Found build directory: {build_dir}")
bazel_target = args.bazel_target
if not bazel_target.startswith(("//", "@")):
return error_message(f"Invalid Bazel target label: {bazel_target}")
if "(" in bazel_target:
return error_message(
f"Target label cannot use a GN toolchain suffix: {bazel_target}"
)
if bazel_target[0] == "@//":
bazel_target = bazel_target[1:]
def format_targets_list(targets: list[str]) -> str:
"""Helper to pretty-print a list of GN or Bazel targets."""
return "\n".join(f" {target}" for target in targets)
# Load actions map then use it.
actions_map = gn_targets_utils.BazelBuildActionsMap.FromBuildDir(build_dir)
if args.bazel:
bazel_args = [args.bazel]
else:
bazel_paths = build_utils.BazelPaths(args.fuchsia_dir, build_dir)
bazel_launcher = bazel_paths.launcher
if not bazel_launcher.exists():
return error_message("Cannot find Bazel launcher, use --bazel=PATH")
bazel_args = [str(bazel_launcher)]
errors: list[str] = []
gn_actions = gn_targets_utils.find_gn_bazel_action_infos_for(
args.bazel_target,
actions_map,
bazel_args,
log_step=lambda msg: log(2, msg),
log_err=lambda msg: errors.append(msg),
)
if errors:
print(f"ERROR: {errors[0]}", file=sys.stderr)
for error in errors[1:]:
print(error, file=sys.stderr)
return 1
if not gn_actions:
return error_message(
f"This Bazel target is not a dependency of any known GN bazel_action() target: {bazel_target}\n"
+ "\nIt should be a dependency of one of the following Bazel targets:\n"
+ format_targets_list(actions_map.bazel_targets)
+ "\n\nWhich are built by one of these GN targets:\n"
+ format_targets_list(actions_map.gn_targets)
+ "\n"
)
if len(gn_actions) > 1:
top_level_bazel_targets = []
gn_targets = []
for gn_action in gn_actions:
top_level_bazel_targets += gn_action.bazel_targets
gn_targets.append(gn_action.gn_target)
return error_message(
f"Several GN targets depend on {bazel_target}\n"
+ "\nChoose a higher Bazel target, one of:\n"
+ format_targets_list(sorted(top_level_bazel_targets))
+ "\n\nWhich are built by one of these GN targets:\n"
+ format_targets_list(sorted(gn_targets))
+ "\n"
)
gn_action = gn_actions[0]
if verbosity >= 1: # Do not generate large strings if they are not needed.
log(
1,
f"Bazel target {bazel_target} is a dependency of GN target {gn_action.gn_target}\nWhich builds the following bazel targets:\n%s\n"
% format_targets_list(gn_action.bazel_targets),
)
gn_targets_dest = build_dir / gn_action.gn_targets_dir
build_utils.force_symlink(
Path(args.workspace) / workspace_utils.GN_TARGETS_DIR_SYMLINK,
gn_targets_dest,
)
log(0, f"@gn_targets now points to {gn_targets_dest}")
return 0
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--bazel", default="bazel", help="Specify bazel executable"
)
parser.add_argument(
"--buildifier",
default="buildifier",
help="Specify buildifier executable",
)
parser.add_argument(
"--workspace", type=Path, help="Specify workspace directory"
)
parser.add_argument(
"--fuchsia-dir", type=Path, help="Specify path to Fuchsia directory"
)
parser.add_argument(
"--build-dir", type=Path, help="Specify path to Ninja build directory"
)
subparsers = parser.add_subparsers(help="available commands")
# The target_dump command.
parser_target_dump = subparsers.add_parser(
"target_dump",
help="Dump definitions of Bazel targets.",
formatter_class=argparse.RawDescriptionHelpFormatter,
description=r"""
Print the real definition of a target, or set of targets, in a
Bazel graph, after all macros have been expanded. Note that:
- Each TARGET_SET argument is a standard Bazel target set expression
(e.g. `//src/lib:foo` or `//:*`).
- The output is pretty printed for readability.
- For each target that is the result of a macro expansion, comments
are added to describe the call chain that led to it.
- For each target generated by native.filegroup() in macros, new
`generator_{function,location,name}` fields are added to indicate
how it was generated.
""",
)
parser_target_dump.add_argument(
"target_set",
metavar="TARGET_SET",
nargs="+",
help="Set of targets to dump.",
)
parser_target_dump.set_defaults(func=cmd_target_dump)
# The target_commands command.
parser_actions = subparsers.add_parser(
"actions",
help="Dump action commands of Bazel targets.",
formatter_class=argparse.RawDescriptionHelpFormatter,
description=r"""
Print the action commands of a target, or set of targets, omitting
from the list of inputs sysroot and toolchain related headers, which
can be _very_ long when using the Fuchsia C++ toolchain.
""",
)
parser_actions.add_argument(
"target_set",
metavar="TARGET_SET",
nargs="+",
help="Set to targets to print actions for.",
)
parser_actions.add_argument(
"--",
dest="extra_args",
default=[],
nargs=argparse.REMAINDER,
help="extra Bazel aquery-compatible arguments.",
)
parser_actions.set_defaults(func=cmd_actions)
parser_set_gn_targets = subparsers.add_parser(
"set_gn_targets",
help="Update the @gn_targets symlink for a given Bazel target.",
formatter_class=argparse.RawDescriptionHelpFormatter,
description=r"""
Ensure the @gn_targets repository symlink points to the content required to
build a given Bazel target.
fx bazel-tool set_gn_targets //vendor/acme/products/coyote:product_bundle
This is required before calling `fx bazel build <bazel_target>` directly
to ensure any filegroup in the @gn_targets repository points to the correct
up-to-date artifacts in the Ninja build directory.
""",
)
parser_set_gn_targets.add_argument(
"-v",
"--verbose",
action="count",
default=0,
help="Increase verbosity for debugging.",
)
parser_set_gn_targets.add_argument(
"--quiet", action="count", default=0, help="Decrease verbosity."
)
parser_set_gn_targets.add_argument(
"bazel_target",
metavar="BAZEL_TARGET",
help="A Bazel target label, must begin with // or @",
)
parser_set_gn_targets.set_defaults(func=cmd_set_gn_targets)
args = parser.parse_args()
if not args.fuchsia_dir:
# Assume this script is under //scripts/
args.fuchsia_dir = Path(__file__).parent.parent.resolve()
try:
return args.func(args)
except AttributeError:
# See https://bugs.python.org/issue16308
parser.error("Too few arguments.")
if __name__ == "__main__":
sys.exit(main())