blob: a4744d493ca4fbf62a28884feb768e70a4f5d2a5 [file] [log] [blame]
#!/usr/bin/env python3
# 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.
"""Run the Fuchsia Bazel SDK test suite in-tree.
You must have built the 'generate_fuchsia_sdk_repository'
target with 'fx build' before calling this script, as in:
fx build generate_fuchsia_sdk_repository
You can use -- to separate the arguments that will be sent
to the underlying `bazel test` command. Useful for debugging
and experimentation.
"""
import argparse
import errno
import json
import os
import platform
import shlex
import sys
import subprocess
from pathlib import Path
from typing import (
Any,
Callable,
Dict,
Iterable,
Optional,
Sequence,
Set,
Tuple,
Union,
)
_HAS_FX = None
_VERBOSE = False
# Type alias for string or Path type.
# NOTE: With python 3.10+, it is possible to use 'str | Path' directly.
StrOrPath = Union[str, Path]
def _force_symlink(target_path: Path, link_path: Path) -> None:
link_path.parent.mkdir(parents=True, exist_ok=True)
target_path = Path(os.path.relpath(target_path, link_path.parent))
try:
link_path.symlink_to(target_path)
except OSError as e:
if e.errno == errno.EEXIST:
link_path.unlink()
link_path.symlink_to(target_path)
else:
raise
def _generate_command_string(args: Sequence[StrOrPath], **kwargs: Any) -> str:
"""Generate a string that prints a command to be run.
Args:
args: a list of string or Path items corresponding to the command.
**kwargs: extra subprocess.run() extra arguments.
Returns:
A string that can be printed to a terminal showing the command to
run.
"""
output = ""
margin = ""
wrap_command = False
cwd = kwargs.get("cwd")
if cwd:
margin = " "
output = f"(\n{margin}cd {cwd} &&\n"
wrap_command = True
env = kwargs.get("env")
if env:
for key, value in sorted(env.items()):
if os.environ.get(key, None) != value:
output += "%s%s=%s \\\n" % (margin, key, shlex.quote(value))
for a in args:
output += "%s%s \\\n" % (margin, shlex.quote(str(a)))
if wrap_command:
output += ")\n"
return output
def _run_command(
cmd_args: Sequence[StrOrPath],
check_failure: bool = True,
**kwargs: Any,
) -> "subprocess.CompletedProcess[str]":
"""Run a given command.
Args:
cmd_args: a list of string or Path items corresponding to the command.
check_failure: set to False to ignore command failures. Otherwise the default
if to print the command's stderr, then raising an exception.
**kwargs: extra subprocess.run() named arguments.
Returns:
a subprocess.CompletedProcess value.
"""
args = [str(a) for a in cmd_args]
if _VERBOSE:
print("RUN_COMMAND: %s" % _generate_command_string(args, **kwargs))
ret = subprocess.run(args, **kwargs)
if ret.returncode != 0 and check_failure:
print(
"FAILED COMMAND: %s" % _generate_command_string(args, **kwargs),
file=sys.stderr,
)
if ret.stderr:
print("ERROR: %s" % ret.stderr, file=sys.stderr)
ret.check_returncode()
return ret
def _get_command_output_lines(
args: Sequence[StrOrPath],
extra_env: Optional[Dict[str, str]] = None,
**kwargs: Any,
) -> Sequence[str]:
"""Run a given command, then return its standard output as text lines.
Args:
args: a list of string or Path items corresponding to the command.
extra_env: a dictionary of optional extra environment variable definitions.
**kwargs: extra subprocess.run() named arguments.
Returns:
A sequence of strings, each one corresponding to one line of the output
(line terminators are not included).
"""
if extra_env:
env = kwargs.get("env")
if env is None:
env = os.environ.copy()
kwargs["env"] = env | extra_env
ret = _run_command(
args, capture_output=True, text=True, check_failure=True, **kwargs
)
return ret.stdout.splitlines()
def _print_error(msg: str) -> int:
"""Print error message to stderr then return 1."""
print("ERROR: " + msg, file=sys.stderr)
return 1
def _find_fuchsia_source_dir_from(path: Path) -> Optional[Path]:
"""Try to find the Fuchsia source directory from a starting location.
Args:
path: Path to a file or directory in the Fuchsia source tree.
Returns:
Path value for the Fuchsia source directory, or None if not found.
"""
if path.is_file():
path = path.parent
path = path.resolve()
while True:
if str(path) == "/":
return None
if (path / ".jiri_manifest").exists():
return path
path = path.parent
def _find_fuchsia_build_dir(fuchsia_source_dir: Path) -> Optional[Path]:
"""Find the current Fuchsia build directory.
Args:
fuchsia_source_dir: Path value for the Fuchsia source directory.
Returns:
Path value for the current build directory selected with `fx` or None.
"""
fx_build_dir = fuchsia_source_dir / ".fx-build-dir"
if not fx_build_dir.exists():
return None
with open(fx_build_dir) as f:
return fuchsia_source_dir / f.read().strip()
def _relative_path(path: Path) -> Path:
return Path(os.path.relpath(path))
def _depfile_quote(path: str) -> str:
"""Quote a path properly for depfiles, if necessary.
shlex.quote() does not work because paths with spaces
are simply encased in single-quotes, while the Ninja
depfile parser only supports escaping single chars
(e.g. ' ' -> '\ ').
Args:
path: input file path.
Returns:
The input file path with proper quoting to be included
directly in a depfile.
"""
return path.replace("\\", "\\\\").replace(" ", "\\ ")
def _flatten_comma_list(items: Iterable[str]) -> Iterable[str]:
"""Flatten ["a,b", "c,d"] -> ["a", "b", "c", "d"].
This is useful for merging repeatable flags, which also
have comma-separated values.
Yields:
Elements that were separated by commas, flattened over
the original sequence..
"""
for item in items:
yield from item.split(",")
def build_metadata_flags() -> Sequence[str]:
"""Convert environment variables into build metadata flags."""
result_flags = []
# Propagate some build metadata from the environment.
# Some of these values are set by infra.
def forward_build_metadata_from_env(var: str) -> Optional[str]:
env_value = os.environ.get(var) # set by infra
if env_value is None:
return None
result_flags.append(f"--build_metadata={var}={env_value}")
return env_value
bb_id = forward_build_metadata_from_env("BUILDBUCKET_ID")
# Provide click-able/paste-able link for convenience.
if bb_id:
result_flags.append(
f"--build_metadata=SIBLING_BUILDS_LINK=http://sponge/invocations/?q=BUILDBUCKET_ID:{bb_id}"
)
if "/led/" in bb_id:
result_flags.append(
f"--build_metadata=PARENT_BUILD_LINK=go/lucibuild/{bb_id}/+/build.proto"
)
else:
result_flags.append(
f"--build_metadata=PARENT_BUILD_LINK=go/bbid/{bb_id}"
)
forward_build_metadata_from_env("BUILDBUCKET_BUILDER")
# Developers' builds will have one uuid per `fx build` invocation
# that can be used to correlate multiple bazel sub-builds.
fx_build_id = forward_build_metadata_from_env("FX_BUILD_UUID")
if fx_build_id:
result_flags.append(
f"--build_metadata=SIBLING_BUILDS_LINK=http://sponge/invocations/?q=FX_BUILD_UUID:{fx_build_id}"
)
return result_flags
class BazelRepositoryMap(object):
IGNORED_REPO = Path("IGNORED")
def __init__(
self,
fuchsia_source_dir: Path,
fuchsia_sdk_dir: Optional[Path],
workspace_dir: Path,
output_base: Path,
):
self._fuchsia_source_dir = fuchsia_source_dir
self._workspace_dir = workspace_dir
self._output_base = output_base
# These repository overrides are passed to the Bazel invocation.
self._overrides: Dict[str, Path] = {
"bazel_skylib": fuchsia_source_dir / "third_party/bazel_skylib",
"com_google_googletest": fuchsia_source_dir
/ "third_party/googletest/src",
"rules_cc": fuchsia_source_dir / "third_party/bazel_rules_cc",
"rules_python": fuchsia_source_dir
/ "third_party/bazel_rules_python",
"rules_license": fuchsia_source_dir
/ "third_party/bazel_rules_license",
"platforms": fuchsia_source_dir / "third_party/bazel_platforms",
"rules_java": fuchsia_source_dir
/ "build/bazel/local_repositories/rules_java",
"remote_coverage_tools": fuchsia_source_dir
/ "build/bazel/local_repositories/remote_coverage_tools",
}
if fuchsia_sdk_dir:
_fuchsia_sdk_dir = fuchsia_sdk_dir.resolve()
self._overrides["fuchsia_sdk"] = _fuchsia_sdk_dir
self._overrides["fuchsia_sdk_common"] = _fuchsia_sdk_dir / "common"
# These repository overrides are used when converting Bazel labels to actual paths.
# NOTE: Mapping labels to repository inputs is considerably simpler than
# //build/bazel/scripts/bazel_action.py because there are way less edge
# cases to consider and because ignoring @prebuilt_python and
# @fuchsia_clang entirely is safe, since these are already populated
# by the GN build_fuchsia_sdk_repository target which is an input
# dependency for running this script.
self._internal_overrides = self._overrides | {
"prebuilt_python": self.IGNORED_REPO,
"fuchsia_clang": self.IGNORED_REPO,
"bazel_tools": self.IGNORED_REPO,
"local_config_cc": self.IGNORED_REPO,
}
if not fuchsia_sdk_dir:
fuchsia_sdk_dir = (
self._fuchsia_source_dir / "build/bazel_sdk/bazel_rules_fuchsia"
)
self._internal_overrides["fuchsia_sdk"] = fuchsia_sdk_dir
self._internal_overrides["fuchsia_sdk_common"] = (
fuchsia_sdk_dir / "common"
)
def get_repository_overrides_flags(self) -> Sequence[str]:
"""Return a sequence command-line flags for overriding Bazel external repositories."""
return [
f"--override_repository={name}={path}"
for name, path in self._overrides.items()
]
def resolve_bazel_path(self, bazel_path: str) -> Optional[Path]:
"""Convert a Bazel path label to a real Path or None if it should be ignored."""
if bazel_path.startswith("//"):
target_path = bazel_path[2:]
repo_dir = self._workspace_dir
repo_name = ""
elif bazel_path.startswith("@"):
repo_name, sep, target_path = bazel_path.partition("//")
assert (
sep
), f"Build file path has invalid repository root: {bazel_path}"
repo_name = repo_name[1:]
_repo_dir = self._internal_overrides.get(repo_name, None)
assert _repo_dir, (
f"Unknown repository name in build file path: {bazel_path}\n"
+ f"Please modify {__file__} to handle it!"
)
repo_dir = _repo_dir
if repo_dir == self.IGNORED_REPO:
return None
else:
assert False, f"Invalid build file path: {bazel_path}"
package_dir, colon, target_name = target_path.partition(":")
if colon == ":":
if package_dir:
target_path = f"{package_dir}/{target_name}"
else:
target_path = target_name
else:
target_path = package_dir + "/" + os.path.basename(package_dir)
final_path = repo_dir / target_path
if final_path.exists():
return final_path
# Sometimes the path will point to a non-existent file, for example
#
# @fuchsia_sdk//:api_version.bzl
# corresponds to a file generated by the repository rule that
# generates the @fuchsia_sdk repository.
#
# //build/bazel_sdk/bazel_rules_fuchsia/api_version.bzl does
# not exist.
#
# $OUTPUT_BASE/external/fuchsia/api_version.bzl is the actual
# location of that file.
#
if repo_name:
external_repo_dir = self._output_base / "external" / repo_name
final_path = external_repo_dir / target_path
if final_path.exists():
return final_path.resolve()
# This should not happen, but print an error message pointing to this
# script in case it really does!
assert (
False
), f"Unknown input label, please update {__file__} to handle it: {bazel_path}"
def main() -> int:
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--bazel", help="Specify bazel binary.")
mutex_group = parser.add_mutually_exclusive_group()
mutex_group.add_argument(
"--fuchsia_build_dir",
help="Specify Fuchsia build directory (default is auto-detected).",
)
mutex_group.add_argument(
"--fuchsia_idk_directory", help="Specify Fuchsia IDK directory."
)
parser.add_argument(
"--fuchsia_source_dir",
help="Specify Fuchsia source directory (default is auto-detected).",
)
parser.add_argument(
"--fuchsia_sdk_dir",
help="Specify alternative @fuchsia_sdk source repository (default is $fuchsia_source_dir/build/bazel_sdk/bazel_rules_fuchsia)",
)
parser.add_argument(
"--output_base", help="Use specific Bazel output base directory."
)
parser.add_argument(
"--output_user_root",
help="Use specific Bazel output user root directory.",
)
parser.add_argument(
"--prebuilt-python-version-file",
type=Path,
help="Optional path to version file for prebuilt python toolchain.",
)
parser.add_argument(
"--prebuilt-clang-version-file",
type=Path,
help="Optional path to version file for prebuilt Clang toolchain.",
)
parser.add_argument(
"--stamp-file", help="Output stamp file, written on success only."
)
parser.add_argument("--depfile", help="Output Ninja depfile file.")
parser.add_argument(
"--target_cpu",
help="Target cpu name, using Fuchsia conventions (default is auto-detected).",
)
parser.add_argument(
"--verbose", action="store_true", help="Enable verbose mode."
)
parser.add_argument(
"--quiet",
action="store_true",
help="Do not print anything unless there is an error.",
)
parser.add_argument(
"--clean", action="store_true", help="Force clean build."
)
parser.add_argument(
"--command",
default="test",
help="Override bazel command, default is 'test'. Use -- to pass extra arguments.",
)
parser.add_argument(
"--test_target",
default="//:tests",
help="Which target to invoke with `bazel test` (default is '//:tests')",
)
parser.add_argument(
"--test_output",
default="",
help="See `bazel test --help` for the `test_output` argument.",
)
parser.add_argument(
"--bazelrc",
help="Additional Bazel configuration file to load",
type=Path,
default=[],
metavar="FILE",
action="append",
)
parser.add_argument(
"--bazel-config",
help="Additional Bazel --config options, comma-separated, repeatable",
default=[],
metavar="CFG",
action="append",
)
parser.add_argument(
"--bazel-build-events-log-json",
type=Path,
help="Output path to JSON-formatted Build Event Protocol log file",
metavar="LOG",
)
parser.add_argument(
"--bazel-exec-log-compact",
type=Path,
help="Output path to zstd-compressed action execution log file (protobuf: spawn.proto)",
metavar="LOG",
)
parser.add_argument("extra_args", nargs=argparse.REMAINDER)
args = parser.parse_args()
if args.quiet:
args.verbose = None
if args.verbose:
global _VERBOSE
_VERBOSE = True
if args.depfile and not args.stamp_file:
parser.error(
"The --depfile option requires a --stamp-file output path!"
)
extra_args = []
if args.extra_args:
if args.extra_args[0] != "--":
parser.error(
'Use "--" to separate extra arguments passed to the bazel test command.'
)
extra_args = args.extra_args[1:]
# Get Fuchsia source directory.
if args.fuchsia_source_dir:
fuchsia_source_dir = Path(args.fuchsia_source_dir)
else:
_fuchsia_source_dir = _find_fuchsia_source_dir_from(Path(__file__))
if not _fuchsia_source_dir:
return _print_error(
"Cannot find Fuchsia source directory, please use --fuchsia_source_dir=DIR"
)
fuchsia_source_dir = _fuchsia_source_dir
if not fuchsia_source_dir.exists():
return _print_error(
f"Fuchsia source directory does not exist: {fuchsia_source_dir}"
)
# Get Fuchsia build directory.
if args.fuchsia_build_dir:
fuchsia_build_dir = Path(args.fuchsia_build_dir)
has_fuchsia_build_dir = True
elif args.fuchsia_idk_directory:
has_fuchsia_build_dir = False
fuchsia_idk_directory = Path(args.fuchsia_idk_directory)
if not fuchsia_idk_directory.exists():
return _print_error(
f"Fuchsia IDK directory does not exist: {fuchsia_idk_directory}"
)
else:
_fuchsia_build_dir = _find_fuchsia_build_dir(fuchsia_source_dir)
if not _fuchsia_build_dir:
return _print_error(
"Cannot auto-detect Fuchsia build directory, use --fuchsia_build_dir=DIR"
)
fuchsia_build_dir = _fuchsia_build_dir
has_fuchsia_build_dir = True
if has_fuchsia_build_dir:
if not fuchsia_build_dir.exists():
return _print_error(
f"Fuchsia build directory does not exist: {fuchsia_build_dir}"
)
fuchsia_build_dir = fuchsia_build_dir.resolve()
# fuchsia_source_dir must be an absollute path or Bazel will complain
# when it is used for --override_repository options below.
fuchsia_source_dir = fuchsia_source_dir.resolve()
# Compute Fuchsia host tag
u = platform.uname()
host_os = {
"Linux": "linux",
"Darwin": "mac",
"Windows": "win",
}.get(u.system, u.system)
host_cpu = {
"x86_64": "x64",
"AMD64": "x64",
"aarch64": "arm64",
}.get(u.machine, u.machine)
host_tag = f"{host_os}-{host_cpu}"
# Find Bazel binary
if args.bazel:
bazel = Path(args.bazel)
else:
bazel = (
fuchsia_source_dir
/ "prebuilt"
/ "third_party"
/ "bazel"
/ host_tag
/ "bazel"
)
if not bazel.exists():
return _print_error(f"Bazel binary does not exist: {bazel}")
bazel = bazel.resolve()
# The Bazel workspace assumes that the Fuchsia cpu is the host
# CPU unless --cpu or --platforms is used. Extract the target_cpu
# from ${fuchsia_build_dir}/args.json and construct the corresponding
# bazel test argument.
#
# Note that there is a subtle issue here: for historical reasons, the default
# --cpu value is `k8`, an old moniker for the x86_64 cpu architecture. This
# impacts the location of build artifacts in the default/target build
# configuration, which would go under bazel-out/k8-fastbuild/bin/...
# then.
#
# However, our --config=fuchsia_x64 argument below changes --cpu to `x86_64`
# which is also recognized properly, but changes the location of build
# artifacts to bazel-out/x86_64-fastbuild/ instead, which is important when
# these paths go into build artifacts (e.g. product bundle manifests) and
# need to be compared to golden files by the test suite.
#
# In short, this test suite does not support invoking `bazel test` without
# an appropriate `--config=fuchsia_<cpu>` argument.
#
if args.target_cpu:
target_cpu = args.target_cpu
else:
if not has_fuchsia_build_dir:
parser.error(
"Cannot auto-detect --target_cpu with --fuchsia_idk_directory"
)
args_json = fuchsia_build_dir / "args.json"
if not args_json.exists():
return _print_error(
"Cannot auto-detect target cpu, please use --target_cpu=CPU"
)
with open(args_json) as f:
target_cpu = json.load(f)["target_cpu"]
# Assume this script is under '$WORKSPACE/scripts'
script_dir = Path(__file__).parent.resolve()
workspace_dir = script_dir.parent
downloader_config_file = script_dir / "downloader_config"
# To ensure that the repository rules for @fuchsia_clang and
# @prebuilt_python are re-run properly when the content of the prebuilt
# toolchain directory changes, use a version file that is symlinked into
# the workspace, and whose path is passed through environment variables
# LOCAL_FUCHSIA_CLANG_VERSION_FILE and LOCAL_PREBUILT_PYTHON_VERSION_FILE
# respectively. The workspace symlinks are necessary to ensure that
# Bazel will track changes to these files properly, as repository rules
# cannot track changes to files outside the workspace :-(
def setup_version_file(name: str, source_path: Path) -> Optional[str]:
if not source_path.exists():
return None
dst_path = f".versions/{name}"
_force_symlink(source_path, workspace_dir / dst_path)
return dst_path
python_prebuilt_dir = (
fuchsia_source_dir / f"prebuilt/third_party/python3/{host_tag}"
)
python_version_file = args.prebuilt_python_version_file
if not python_version_file:
python_version_file = (
python_prebuilt_dir / ".versions/cpython3.cipd_version"
)
workspace_python_version_file = setup_version_file(
"prebuilt_python", python_version_file
)
clang_version_file = args.prebuilt_clang_version_file
if not clang_version_file:
clang_version_file = (
fuchsia_source_dir
/ f"prebuilt/third_party/clang/{host_tag}/.versions/clang.cipd_version"
)
workspace_clang_version_file = setup_version_file(
"prebuilt_clang", clang_version_file
)
# These argument remove verbose output from Bazel, used in queries.
bazel_quiet_args = [
"--noshow_loading_progress",
"--noshow_progress",
"--ui_event_filters=-info",
]
# The list of command-line options that appear between the 'bazel' binary path
# and the bazel command (such as "test", "query", or "build").
bazel_startup_args = [str(bazel)]
# The list of command-line options that appear after the bazel command, and
# are shared by all commands.
bazel_common_args = []
# The list of command-line options that are only used for commands that perform
# analysis (e.g. "cquery", "aquery", "build", "run" and "test", but not "query")
bazel_config_args = []
# Command line arguments for the 'test' command only.
bazel_test_args = []
# These options must appear before the Bazel command
bazel_startup_args += [
# Disable parsing of $HOME/.bazelrc to avoid unwanted side-effects.
"--nohome_rc",
]
# bazelrc files are passed relative to the current working directory,
# but need to be adjusted relative to the workspace dir.
rc_relpath = os.path.relpath(os.curdir, start=workspace_dir)
for rc in args.bazelrc:
bazel_startup_args += [
f"--bazelrc={rc_relpath}/{rc}" for rc in args.bazelrc
]
if args.output_user_root:
output_user_root = Path(args.output_user_root).resolve()
output_user_root.mkdir(parents=True, exist_ok=True)
bazel_startup_args += [f"--output_user_root={output_user_root}"]
if args.output_base:
output_base = Path(args.output_base).resolve()
output_base.mkdir(parents=True, exist_ok=True)
bazel_startup_args += [f"--output_base={output_base}"]
else:
# Get output base from Bazel directly.
output_base = Path(
_get_command_output_lines(
bazel_startup_args + ["info", "output_base"], cwd=workspace_dir
)[0]
)
fuchsia_sdk_dir: Optional[Path] = None
if args.fuchsia_sdk_dir:
fuchsia_sdk_dir = Path(args.fuchsia_sdk_dir)
bazel_repo_map = BazelRepositoryMap(
fuchsia_source_dir=fuchsia_source_dir,
fuchsia_sdk_dir=fuchsia_sdk_dir,
workspace_dir=workspace_dir,
output_base=output_base,
)
bazel_common_args += [
# Prevent all downloads through a downloader configuration file.
# Note that --experimental_repository_disable_download does not
# seem to work at all.
#
# Fun fact: the path must be relative to the workspace, or an absolute
# path, and if the file does not exist, the Bazel server will crash
# *silently* with a Java exception, leaving no traces on the client
# terminal :-(
f"--experimental_downloader_config={downloader_config_file}",
# TODO: b/318334703 - Enable bzlmod when the Fuchsia build is ready.
#
# NOTE: Bazel 7 turns on bzlmod by default, so it need to be explicitly
# turned off here.
"--enable_bzlmod=false",
]
# Override repositories since all downloads are forbidden.
# This allows the original WORKSPACE.bazel to work out-of-tree by
# download repositories as usual, while running the test suite in-tree
# will use the Fuchsia checkout's versions of these external dependencies.
bazel_common_args += bazel_repo_map.get_repository_overrides_flags()
# These options must appear for commands that act on the configure graph (i.e. all except `bazel query`
bazel_config_args += [
# TODO: b/321637402 - Enable platform-based toolchain resolution in
# these tests.
"--incompatible_enable_cc_toolchain_resolution=false",
# Ensure binaries are generated for the right Fuchsia CPU architecture.
# Without this, @fuchsia_sdk rules assume that target_cpu == host_cpu,
# and will use an incorrect output path prefix (i.e.
# bazel-out/k8-fastbuild/ instead of bazel-out/x86_64-fastbuild/ on
# x64 hosts, leading to golden file comparison failures later.
f"--config=fuchsia_{target_cpu}",
# Ensure the embedded JDK that comes with Bazel is always used
# This prevents Bazel from downloading extra host JDKs from the
# network, even when a project never uses Java-related rules
# (for some still-very-mysterious reasons!)
"--java_runtime_version=embedded_jdk",
"--tool_java_runtime_version=embedded_jdk",
# Ensure outputs are writable (umask 0755) instead of readonly (0555),
# which prevent removing output directories with `rm -rf`.
# See https://fxbug.dev/42072059
"--experimental_writable_outputs",
# TODO(http://b/319377689#comment5): Remove this flag when the linked issue is
# fixed upstream.
"--sandbox_add_mount_pair=/tmp",
]
# Forward additional --config's, intended for `bazel test`.
# These configs should not affect the build graph.
bazel_config_args += [
f"--config={cfg}" for cfg in _flatten_comma_list(args.bazel_config)
]
bazel_config_args += [
# In case of build or test errors, provide more details about the failed
# command. See https://fxbug.dev/325346878
"--verbose_failures",
]
if args.quiet:
bazel_test_args += [
"--show_result=0",
"--test_output=errors",
"--test_summary=none",
]
elif args.test_output:
bazel_test_args += [f"--test_output={args.test_output}"]
# Detect when to use remote service endpoint overrides from infra.
for config_arg, env_var, bazel_flag in (
("sponge", "BAZEL_sponge_socket_path", "--bes_proxy"),
("sponge_infra", "BAZEL_sponge_socket_path", "--bes_proxy"),
("resultstore", "BAZEL_resultstore_socket_path", "--bes_proxy"),
("resultstore_infra", "BAZEL_resultstore_socket_path", "--bes_proxy"),
("remote", "BAZEL_rbe_socket_path", "--remote_proxy"),
):
if f"--config={config_arg}" in bazel_config_args:
env_value = os.environ.get(env_var)
if env_value:
bazel_config_args += [f"{bazel_flag}=unix://{env_value}"]
bazel_config_args += build_metadata_flags()
if args.bazel_build_events_log_json:
args.bazel_build_events_log_json.parent.mkdir(
parents=True, exist_ok=True
)
bazel_config_args += [
"--build_event_json_file=%s"
% args.bazel_build_events_log_json.resolve()
]
# Bazel action execution log.
# This contains records of local and remote executions.
# Same as --config=exec_log from template.bazelrc
if args.bazel_exec_log_compact:
args.bazel_exec_log_compact.parent.mkdir(parents=True, exist_ok=True)
bazel_config_args += [
"--experimental_execution_log_compact_file=%s"
% args.bazel_exec_log_compact.resolve(),
"--remote_build_event_upload=all",
]
if args.clean:
# Perform clean build
ret = _run_command(
bazel_startup_args + ["clean", "--expunge"],
check_failure=False,
cwd=workspace_dir,
)
if ret.returncode != 0:
return _print_error(
"Could not clean bazel output base?\n%s\n" % ret.stderr
)
PATH = os.environ["PATH"]
bazel_env = {
# An undocumented, but widely used, environment variable that tells Bazel to
# not auto-detect the host C++ installation. This makes workspace setup faster
# and ensures this can be used on containers where GCC or Clang are not
# installed (Bazel would complain otherwise with an error).
"BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN": "1",
# Ensure our prebuilt Python3 executable is in the PATH to run repository
# rules that invoke Python programs correctly in containers or jails that
# do not expose the system-installed one.
"PATH": f"{python_prebuilt_dir}/bin:{PATH}",
}
if workspace_python_version_file:
bazel_env[
"LOCAL_PREBUILT_PYTHON_VERSION_FILE"
] = workspace_python_version_file
if workspace_clang_version_file:
bazel_env[
"LOCAL_FUCHSIA_CLANG_VERSION_FILE"
] = workspace_clang_version_file
if has_fuchsia_build_dir:
# Pass the location of the Fuchsia build directory to the
# @fuchsia_sdk repository rule. Note that using --action_env will
# not work because this option only affects Bazel actions, and
# not repository rules.
bazel_env["LOCAL_FUCHSIA_PLATFORM_BUILD"] = str(fuchsia_build_dir)
else:
# Pass the location of the Fuchsia IDK archive to the @fuchsia_sdk
# repository rule.
bazel_env["LOCAL_FUCHSIA_IDK_DIRECTORY"] = str(
fuchsia_idk_directory.resolve()
)
# Setting USER is required to run Bazel, so force it to run on infra bots.
if "USER" not in os.environ:
bazel_env["USER"] = "unused-bazel-build-user"
query_target = f"set({args.test_target})"
command_kwargs = {"env": os.environ | bazel_env, "cwd": workspace_dir}
command_args = []
if args.command == "info":
command_args = bazel_startup_args + ["info"] + extra_args
elif args.command == "query":
command_args = (
bazel_startup_args + ["query"] + bazel_common_args + extra_args
)
else:
command_args = (
bazel_startup_args
+ [args.command]
+ bazel_common_args
+ bazel_config_args
+ bazel_test_args
+ ([args.test_target] if args.command == "test" else [])
+ extra_args
)
# If the test failed, exit early with a non-zero error code, (but don't
# raise an exception, because the stack trace printed by that will just be
# noise in the failure output.
_run_command(command_args, check_failure=True, **command_kwargs)
if args.stamp_file:
with open(args.stamp_file, "w") as f:
f.write("")
if args.depfile:
def find_build_files() -> Set[Path]:
# Perform a query to retrieve all build files.
build_files = _get_command_output_lines(
args=(
bazel_startup_args
+ ["query"]
+ bazel_common_args
+ bazel_quiet_args
+ ["buildfiles(deps(%s))" % query_target]
),
extra_env=bazel_env,
cwd=workspace_dir,
)
result = set()
for b in build_files:
resolved = bazel_repo_map.resolve_bazel_path(b)
if resolved:
result.add(resolved)
return result
def find_source_files() -> Set[Path]:
# Perform a cquery to find all input source files.
lines = _get_command_output_lines(
args=(
bazel_startup_args
+ ["cquery"]
+ bazel_common_args
+ bazel_config_args
+ bazel_quiet_args
+ [
"--output=label",
'kind("source file", deps(%s))' % query_target,
]
),
extra_env=bazel_env,
cwd=workspace_dir,
)
source_files = set()
for l in lines:
path, space, label = l.partition(" ")
assert (
space == " " and label == "(null)"
), f"Invalid source file line: {l}"
resolved = bazel_repo_map.resolve_bazel_path(path)
if not resolved:
continue
# If the file is a symlink, find its real location
resolved = resolved.resolve()
source_files.add(resolved)
return source_files
outputs = [args.stamp_file]
implicit_inputs = [
_relative_path(p)
for p in (find_build_files() | find_source_files())
]
with open(args.depfile, "w") as f:
f.write(
"%s: %s\n"
% (
" ".join(_depfile_quote(p) for p in outputs),
" ".join(_depfile_quote(str(p)) for p in implicit_inputs),
)
)
return 0
if __name__ == "__main__":
sys.exit(main())