blob: 81b001ec31d32e9468540e33235cd8b2db6fbcb7 [file] [log] [blame]
#!/usr/bin/env python3
# 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.
"Implement the running of a Bazel command from Ninja and copying the outputs back into the ninja outdir."
import dataclasses
import json
import os
import shlex
import sys
from functools import partial
from pathlib import Path
import thread_pool_helpers
_SCRIPT_DIR = os.path.dirname(__file__)
sys.path.insert(0, _SCRIPT_DIR)
# LINT.IfChange(imports)
import bazel_action_utils
import build_utils
import stdio_redirection
from bazel_action_file_copy_utils import (
FilePath,
check_if_need_to_copy_file,
copy_directory_if_changed,
hardlink_or_copy_writable,
write_file_if_changed,
)
# LINT.ThenChange(//build/bazel/bazel_action.gni:bazel_action_impl_imports)
# Set this to True to debug operations locally in this script.
# IMPORTANT: Setting this to True will result in Ninja timeouts in CQ
# due to the stdout/stderr logs being too large.
_DEBUG = False
# Set this to True to debug the bazel query cache.
_DEBUG_BAZEL_QUERIES = _DEBUG
def debug(msg: str) -> None:
# Print debug message to stderr if _DEBUG is True.
if _DEBUG:
print("BAZEL_ACTION_DEBUG: " + msg, file=sys.stderr)
# The prefix that appears in Bazel stderr DEBUG lines, generated by
# the debug symbol aspect.
# LINT.IfChange(debug_symbols_manifest_prefix)
_BAZEL_DEBUG_SYMBOLS_MANIFEST_PATH_PREFIX = b"DEBUG_SYMBOLS_MANIFEST_PATH="
# LINT.ThenChange(//build/bazel/debug_symbols/aspects.bzl:debug_symbols_manifest_prefix)
# Directory where to find Starlark input files.
_STARLARK_DIR = os.path.join(os.path.dirname(_SCRIPT_DIR), "starlark")
class BazelActionError(Exception):
"""A Bazel action error was encountered."""
@dataclasses.dataclass
class BazelActionOutputs(object):
"""The outputs to copy from Bazel to GN for a given Bazel action."""
files: list[bazel_action_utils.FileOutput]
directories: list[bazel_action_utils.DirectoryOutput]
packages: list[bazel_action_utils.PackageOutput]
final_symlinks: list[bazel_action_utils.FinalSymlinkOutput]
@dataclasses.dataclass
class BazelExtraOutputs(object):
"""A collection of optional outputs to write as debugging aids, and the paths to write them at.
Fields:
build_event_json_file: A build event json file from Bazel
command_file: The Bazel command itself
command_profile: A command profile tgz file
explain_file: A file which explains why Bazel re-ran each action
"""
build_event_json_file: Path | None = None
command_file: Path | None = None
command_profile: Path | None = None
explain_file: Path | None = None
@dataclasses.dataclass
class BazelActionResult(object):
"""Wrapper for results of executing a bazel action.
Fields:
configured_args: A list of args usable for bazel queries in the same configuration.
debug_symbol_manifest_paths: A list of paths to debug symbol manifests
output_files: A list of the output paths that were actually copied to (updated)
by this action.
"""
configured_args: list[str]
debug_symbol_manifest_paths: list[str]
output_files: list[Path]
class BazelActionRunner(object):
"""Used to run Bazel actions and copy all the outputs back to the Ninja outdir.
This class is configured based on the global context of the build (both static
args set via GN metadata and build-specific environment variables). It can
then be used to run multiple Bazel actions in sequence, with that global
configuration.
See the `run()` method for per-action calling.
"""
def __init__(
self,
bazel_paths: build_utils.BazelPaths,
global_args: bazel_action_utils.BazelGlobalArguments,
query_cache: build_utils.BazelQueryCache,
) -> None:
self.paths = bazel_paths
self.global_args = global_args
self.launcher = build_utils.BazelLauncher(
bazel_paths.launcher,
log_err=lambda msg: (
print(f"BAZEL_ACTION_ERROR: {msg}", file=sys.stderr)
if _DEBUG
else None
),
)
self.rbe_settings = (
bazel_action_utils.BazelRbeSettings.create_from_build_dir(
bazel_paths.ninja_build_dir
)
)
self.query_cache = query_cache
def run(
self,
command: str,
platform_config: str,
platform_label: str,
targets: list[str],
outputs: BazelActionOutputs,
extra_outputs: BazelExtraOutputs,
time_profile: build_utils.TimeProfile,
) -> BazelActionResult:
"""Run a bazel command.
Args:
targets: The list of bazel target labels to build/test/etc.
extra_args: (temporary) list of bazel cli args to append to the command
Returns:
(temporary) A list of debug symbol manifest paths
Raises:
BazelActionError on any failure.
"""
time_profile.start(
"construct_cmd_args",
"Constructing the command-line arguments for Bazel",
)
# Calculate the platform configuration needed for the build and subsequent queries.
configured_args = calculate_platform_config_args(
platform_config,
platform_label,
self.rbe_settings,
)
# Create the base command-line from the command to run (build, etc.), the platform
# configuration, and the set of targets to run the command against.
cmd_args: list[str] = [
command,
*configured_args,
*targets,
]
# If a build event json file is requested, tell Bazel to create one.
if extra_outputs.build_event_json_file:
# Create parent directory to avoid Bazel complaining it cannot
# write the events log file.
extra_outputs.build_event_json_file.parent.mkdir(
parents=True, exist_ok=True
)
cmd_args += [
"--build_event_json_file",
str(extra_outputs.build_event_json_file.resolve()),
]
# If an explain file is requested, tell Bazel to create one.
if extra_outputs.explain_file:
cmd_args += [
"--explain",
self.paths.ninja_build_dir / extra_outputs.explain_file,
]
# If a command profile is requested, tell Bazel to create one.
if extra_outputs.command_profile:
cmd_args += [
"--profile",
self.paths.ninja_build_dir / extra_outputs.command_profile,
]
# if build-event uploading is enabled in global args, then append the config for that
if self.global_args.upload_build_events:
cmd_args += [f"--config={self.global_args.upload_build_events}"]
jobs = calculate_jobs_param(self.rbe_settings)
if jobs:
cmd_args += [jobs]
if self.global_args.quiet:
cmd_args += ["--config=quiet"]
else:
# Normal builds always need to print INFO level messages so that
# warnings can be emitted from bazel ctx.run_shell() commands,
# otherwise any stderr/stdout warnings from tools are eaten by
# the build.
#
# Known uses:
# - size checker outputs
# - developer overrides warnings for assembly
#
cmd_args += ["--config=verbose"]
cmd_args += [
# Ensure that all debug symbols are properly generated during the build
# The aspect will also generate extra manifests whose paths will be
# printed to Bazel's stderr DEBUG lines, which will be filtered
# below to retrieve them (and hide the output from the user).
#
# Doing so avoids doing an extra `bazel build` command to get the
# same data. See https://fxbug.dev/452591388
"--output_groups=+debug_symbol_files",
"--aspects=//build/bazel/debug_symbols:aspects.bzl%generate_manifest",
]
# Add --sandbox_debug if enabled in the build environment.
if self.global_args.sandbox_debug:
cmd_args += ["--sandbox_debug"]
# Always enable verbose failures
cmd_args += ["--verbose_failures"]
# Now that there's a complete command string calculated, print it to debug or the command
# file output if we have one.
if _DEBUG:
# This is all arguments on one line, so that they can be run via cut/paste.
debug(
"BUILD_CMD: "
+ build_utils.cmd_args_to_string(
[self.paths.launcher] + cmd_args
)
)
if extra_outputs.command_file:
# This file is one argument per line.
write_file_if_changed(
extra_outputs.command_file,
" \\\n ".join(
shlex.quote(str(c))
for c in [self.paths.launcher] + cmd_args
)
+ "\n",
)
if self.global_args.quiet:
# Still capture stdout to print its content if there is an error
# running the build command below.
stdout_sink = stdio_redirection.BytesOutputSink()
stderr_sink = stdio_redirection.BytesOutputSink()
is_stdout_pty = False
is_stderr_pty = False
else:
# Send output to regular stdout / stderr instead.
stdout_sink = stdio_redirection.StdoutOutputSink()
stderr_sink = stdio_redirection.StderrOutputSink()
is_stdout_pty = os.isatty(sys.stdout.fileno())
is_stderr_pty = os.isatty(sys.stderr.fileno())
# The Bazel stderr must always be captured to extract DEBUG statements
# that include the paths to debug symbol manifests. However, they should
# be only printed when quiet is False.
debug_symbol_manifest_paths: list[str] = []
debug_symbol_manifest_filtering_sink = (
bazel_action_utils.BazelStderrDebugLineFilter(
stderr_sink,
partial(bazel_debug_line_filter, debug_symbol_manifest_paths),
)
)
with stdio_redirection.PipeOutputSink(
debug_symbol_manifest_filtering_sink, use_pty=is_stderr_pty
) as pty_stderr:
# This makes mypy happy, as it can't detect the type correctly in the 'with' statement
assert isinstance(pty_stderr, stdio_redirection.PipeOutputSink)
with stdio_redirection.PipeOutputSink(
stdout_sink, use_pty=is_stdout_pty
) as pty_stdout:
# This makes mypy happy, as it can't detect the type correctly in the 'with' statement
assert isinstance(pty_stdout, stdio_redirection.PipeOutputSink)
time_profile.start(
"run_bazel_command", "Run the Bazel command."
)
ret = self.launcher.run_bazel_command(
cmd_args,
stdout=pty_stdout.get_write_fd(),
stderr=pty_stderr.get_write_fd(),
)
if ret.returncode != 0:
if self.global_args.quiet:
# Assert that the output sinks are the expected types for quiet mode.
assert isinstance(
stdout_sink, stdio_redirection.BytesOutputSink
)
assert isinstance(
stderr_sink, stdio_redirection.BytesOutputSink
)
# Print the captured outputs in quiet mode to help debugging build errors.
if stdout_sink.data:
sys.stdout.buffer.write(stdout_sink.data)
sys.stdout.flush()
if stderr_sink.data:
sys.stderr.buffer.write(stderr_sink.data)
sys.stderr.flush()
# Detect the error message corresponding to a Bazel target
# referencing a @gn_targets//<dir>:<name> label
# that does not exist. This happens when the GN bazel_action()
# fails to depend on the proper bazel_input_file() or
# bazel_input_directory() dependency.
#
# NOTE: Path to command.log should be stable, because we explicitly set
# output_base. See https://bazel.build/run/scripts#command-log.
if verify_unknown_gn_targets(
(self.paths.output_base / "command.log")
.read_text()
.splitlines(),
targets,
):
raise BazelActionError()
# This is a different error, just print it as is.
#
# Note most build users are not interested in executing bazel directly, so hiding this
# message bechind a flag.
print(
"\nERROR when calling Bazel. To reproduce, run this in the Ninja output directory:\n\n %s\n"
% " ".join(shlex.quote(c) for c in ret.args),
file=sys.stderr,
)
raise BazelActionError()
# If we're building, and have packages, query to get the paths to the package archives,
# as they need to be copied to the Ninja outdir along with any other files.
if command == "build" and outputs.packages:
time_profile.start(
"package_info",
"Run cquery to extract Fuchsia package archiveinformation",
)
package_archive_files = self.query_for_package_archives(
configured_args, outputs.packages
)
outputs.files.extend(package_archive_files)
# Now copy all the outputs. This is a separate function to help break up the run()
# functionality for better clarity.
output_copier = _BazelOutputCopier(self.paths)
all_output_files = output_copier.copy(outputs, time_profile)
return BazelActionResult(
configured_args=configured_args,
debug_symbol_manifest_paths=debug_symbol_manifest_paths,
output_files=all_output_files,
)
def query_for_package_archives(
self,
configured_args: list[str],
packages: list[bazel_action_utils.PackageOutput],
) -> list[bazel_action_utils.FileOutput]:
"""Given a list of PackageOutputs, query to find all the archive files to copy.
This basically is converting PackageOutput entries into FileOutput entries.
"""
# Query against all package output targets at once. The query results
# embed the target labels so that they can be matched with the ninja
# output paths.
query_result = run_starlark_cquery(
self.query_cache,
self.launcher,
configured_args,
[entry.package_label for entry in packages],
"FuchsiaPackageInfo_archive.cquery",
)
# The results are lines of json, so split them and parse each. They're
# each a dict of:
# target: "@@<the bazel target>""
# archive_path: "<path to the archive file>""
results: dict[str, str] = {
p["target"].removeprefix("@@"): p["archive_path"]
for p in [json.loads(line) for line in query_result]
}
# Now match each package output label to its results and add to the file
# copies to perform.
package_archive_paths: list[bazel_action_utils.FileOutput] = []
for entry in packages:
bazel_archive_path = results.get(entry.package_label)
# This is just in case we don't get a result for a label, or they
# change format on us.
assert bazel_archive_path
package_archive_paths.append(
bazel_action_utils.FileOutput(
bazel_path=str(self.paths.execroot / bazel_archive_path),
ninja_path=entry.archive_path,
)
)
return package_archive_paths
def calculate_platform_config_args(
platform_config: str,
platform_label: str,
rbe_settings: bazel_action_utils.BazelRbeSettings,
) -> list[str]:
"""Constructs the CLI args that define the 'common platform configuration' as needed by the command and any subsequent queries.
This is an internal helper that creates the cli args, including the RBE
settings, so that any post-build queries can use the same platform config
as the action itself did.
Args:
platform_config: The name of the bazel config to enable for the platform
platform_label: The bazel label of the platform
rbe_settings: The global RBE settings for the build
Returns:
List of strings that are the CLI args that together configure Bazel
correctly for the build and for any subsequent queries.
"""
platform_config_args = [
f"--config={platform_config}",
f"--platforms={platform_label}",
]
# When remote builds are enabled, append the right build arguments.
# These must appear on the Bazel command-line otherwise remote builds
# will fail on infra (the reason being that the Bazel wrapper script
# detects these options to add infra-specific proxy configuration
# the the final command-line).
#
# RBE settings are included in the platform config so that the remote cache can be
# utilized for queries.
if rbe_settings.enabled:
assert rbe_settings.exec_strategy != None
# If RBE is enabled, append the chosen RBE config to
# the command line.
platform_config_args += [f"--config={rbe_settings.exec_strategy}"]
return platform_config_args
def calculate_jobs_param(
rbe_settings: bazel_action_utils.BazelRbeSettings,
) -> str | None:
"""Given the RBE settings and the environment vars, determine what --jobs param to use, if any."""
jobs = None
# When running jobs remotely, increase the number of allowed jobs to 10x
# when running jobs locally. This is different from the reclient config
# because this controls the _running_ of jobs, not the checking of the
# cache for jobs.
if rbe_settings.enabled and rbe_settings.exec_strategy == "remote":
cpus = os.cpu_count()
if cpus:
jobs = 10 * cpus
if jobs is None:
# If an explicit job count was passed to `fx build`, tell Bazel to respect it.
# See https://fxbug.dev/351623259
job_count = os.environ.get("FUCHSIA_BAZEL_JOB_COUNT")
if job_count:
jobs = int(job_count)
return f"--jobs={jobs}" if jobs else None
def bazel_debug_line_filter(manifest_paths: list[str], line: bytes) -> bool:
"""Filters a DEBUG line from Bazel stderr and extracts debug manifest paths.
This function is used as a filter for `BazelStderrDebugLineFilter`. See
BazelStderrDebugLineFilter documentation for details.
Args:
manifest_paths: A list of strings. If a debug symbol manifest path is found
in the `line`, it will be decoded and appended to this list. This list
is mutated by the function.
line: The bytes line read from Bazel's stderr.
Returns:
True if the line contains a debug symbol manifest path and should be
filtered out (i.e., not printed to the actual stderr). False otherwise.
"""
path_prefix = _BAZEL_DEBUG_SYMBOLS_MANIFEST_PATH_PREFIX
pos = line.find(path_prefix)
if pos < 0:
return False # Keep this line
manifest_paths.append(
line[pos + len(path_prefix) :].decode("utf-8").strip()
)
return True # Skip this line
def verify_unknown_gn_targets(
build_files_error: list[str],
bazel_targets: list[str],
) -> int:
"""Check for unknown @gn_targets// dependencies.
Args:
build_files_error: list of error lines from bazel build or query.
bazel_targets: list of Bazel targets invoked by the GN bazel_action() target.
Returns:
On success, simply return 0. On failure, print a human friendly
error message explaining the situation to stderr, then return 1.
"""
missing_ninja_outputs = set()
missing_ninja_packages = set()
for error_line in build_files_error:
if not ("ERROR: " in error_line and "@gn_targets" in error_line):
continue
pos = error_line.find("@@gn_targets")
if pos < 0:
# Should not happen, do not assert and let the caller print the full error
# after this.
print(f"UNSUPPORTED ERROR LINE: {error_line}", file=sys.stderr)
return 0
ending_pos = error_line.find("'", pos)
if ending_pos < 0:
print(f"UNSUPPORTED ERROR LINE: {error_line}", file=sys.stderr)
return 0
label = error_line[pos + 1 : ending_pos] # skip first @.
if error_line[:pos].endswith(": no such package '"):
# The line looks like the following when the BUILD.bazel references a label
# that does not belong to a @gn_targets package.
#
# ERROR: <abspath>/BUILD.bazel:<line>:<column>: no such package '@@gn_targets//<dir>': ...
#
# This happens when the GN bazel_action() targets fails to dependon the corresponding
# bazel_input_file() or bazel_input_directory() target, and that none of its other
# dependencies expose other targets from the same package / directory. The error message
# does not give any information about the target name being evaluated by the query though.
missing_ninja_packages.add(label)
elif error_line[:pos].endswith(": no such target '"):
# The line looks like this when the BUILD.bazel files references the wrong
# label from a package exposed in @gn_targets//:
# ERROR: <abspath>/BUILD.bazel:<line>:<column>: no such target '@@gn_targets//<dir>:<name>' ...
missing_ninja_outputs.add(label)
if not missing_ninja_outputs and not missing_ninja_packages:
return 0
missing_outputs = sorted(missing_ninja_outputs)
missing_packages = sorted(missing_ninja_packages)
_ERROR = """
BAZEL_ACTION_ERROR: Unknown @gn_targets targets.
The following Bazel target(s): {bazel_targets}
""".format(
bazel_targets=" ".join(bazel_targets),
)
if not missing_packages:
_ERROR += """Which reference the following unknown @gn_targets labels:
{missing_bazel_labels}
To fix this, ensure that bazel_input_file() or bazel_input_directory()
targets are defined in the GN graph for:
{missing_gn_labels}
Then ensure that the GN target depends on them transitively.
""".format(
missing_bazel_labels="\n ".join(missing_outputs),
missing_gn_labels="\n ".join(
f"//{o.removeprefix('@gn_targets//')}" for o in missing_outputs
),
)
else:
missing_labels = missing_outputs + missing_packages
missing_build_files = set()
for label in missing_labels:
label = label.removeprefix("@gn_targets//")
gn_dir, sep, gn_name = label.partition(":")
if sep != ":":
gn_dir = label
missing_build_files.add(f"//{gn_dir}/BUILD.gn")
_ERROR += """Which reference the following unknown @gn_targets labels or packages:
{missing_bazel_labels}
To fix this, ensure that bazel_input_file() or bazel_input_directory()
targets are defined in the following build files:
{missing_build_files}
Then ensure that the GN target depends on them transitively.
""".format(
missing_bazel_labels="\n ".join(
missing_outputs + missing_packages
),
missing_build_files="\n ".join(sorted(missing_build_files)),
)
print(_ERROR, file=sys.stderr)
return 1
def run_starlark_cquery(
query_cache: build_utils.BazelQueryCache,
bazel_launcher: build_utils.BazelLauncher,
configured_args: list[str],
query_targets: list[str],
starlark_filename: str,
) -> list[str]:
"""Run a Bazel cquery and process its output with a starlark file.
Args:
query_targets: A list of Bazel targets to run the query over.
starlark_filename: Name of starlark file from //build/bazel/starlark.
Returns:
A list of output lines.
Raises:
AssertionError in case of failure.
"""
result = run_bazel_query(
query_cache,
bazel_launcher,
"cquery",
[
"--config=quiet",
"--output=starlark",
"--starlark:file",
get_input_starlark_file_path(starlark_filename),
]
+ configured_args
+ ["set(%s)" % " ".join(query_targets)],
)
assert result is not None
return result
def get_input_starlark_file_path(filename: FilePath) -> str:
"""Return the path of a input starlark file for Bazel queries.
Args:
filename: File name, searched in //build/bazel/starlark/
Returns:
file path to the corresponding file.
"""
result = os.path.join(_STARLARK_DIR, filename)
assert os.path.isfile(result), f"Missing starlark input file: {result}"
return result
def run_bazel_query(
query_cache: build_utils.BazelQueryCache,
bazel_launcher: build_utils.BazelLauncher,
query_cmd: str,
query_args: list[str],
) -> list[str] | None:
"""Run a Bazel query, return output as list of lines.
Args:
query_cmd: Query command ("query", "cquery" or "aquery").
query_args: Query arguments.
Returns:
On success, a list of output lines. On failure return None.
"""
return query_cache.get_query_output(
query_cmd,
query_args,
bazel_launcher,
log=lambda m: (
print(f"DEBUG: {m}", file=sys.stderr)
if _DEBUG_BAZEL_QUERIES
else None
),
)
class _BazelOutputCopier(object):
"""This exists to consolidate together all of the Bazel output-copying logic."""
def __init__(self, paths: build_utils.BazelPaths):
self.paths = paths
def copy(
self,
outputs: BazelActionOutputs,
time_profile: build_utils.TimeProfile,
) -> list[Path]:
"""Copy all the given outputs.
Returns a list of all output destination files that were copied to, including tracked
files in DirectoryOutputs.
"""
output_files = self._copy_files(outputs.files, time_profile)
tracked_files = self._copy_directories(
outputs.directories, time_profile
)
self._make_final_symlinks(outputs.final_symlinks, time_profile)
return output_files + tracked_files
def _copy_files(
self,
file_outputs: list[bazel_action_utils.FileOutput],
time_profile: build_utils.TimeProfile,
) -> list[Path]:
"""Perform all of the file-copying logic.
Returns a list of the destination file paths that were copied to.
"""
time_profile.start(
"check_outputs_for_copying",
"Validate output files to copy are actually files.",
)
file_copies: list[tuple[Path, Path]] = []
unwanted_dirs = []
for file_output in file_outputs:
src_path = self.paths.workspace / file_output.bazel_path
if src_path.is_dir():
unwanted_dirs.append(src_path)
continue
dst_path = Path(file_output.ninja_path)
file_copies.append((src_path, dst_path))
if unwanted_dirs:
raise BazelActionError(
"\nDirectories are not allowed in --file-outputs Bazel paths, got directories:\n\n%s\n"
% "\n".join(str(d) for d in unwanted_dirs)
)
if file_copies:
time_profile.start(
"check_copy_files",
"Check to see if files need to be copied or not.",
)
files_to_copy = [
file_copy
for file_copy in thread_pool_helpers.filter_threaded(
check_if_need_to_copy_file, file_copies
)
if file_copy
]
if files_to_copy:
time_profile.start(
"copy_files",
"Copy Bazel output files to Ninja build directory",
)
thread_pool_helpers.starmap_threaded(
hardlink_or_copy_writable,
[
(src, dst, self.paths.output_base)
for src, dst in files_to_copy
],
)
return [dst for _, dst in file_copies]
def _copy_directories(
self,
directory_outputs: list[bazel_action_utils.DirectoryOutput],
time_profile: build_utils.TimeProfile,
) -> list[Path]:
time_profile.start(
"check_output_directories",
"Validate that output directories are ready to be copied.",
)
dir_copies: list[tuple[Path, Path, list[FilePath]]] = []
missing_directories: list[Path] = []
unwanted_files: list[Path] = []
invalid_tracked_files: list[Path] = []
for dir_output in directory_outputs:
src_path = self.paths.workspace / dir_output.bazel_path
if not src_path.exists():
missing_directories.append(src_path)
continue
if not src_path.is_dir():
unwanted_files.append(src_path)
continue
for tracked_file in dir_output.tracked_files:
tracked_file = src_path / tracked_file
if not tracked_file.is_file():
invalid_tracked_files.append(tracked_file)
dst_path = Path(dir_output.ninja_path)
dir_copies.append(
(
src_path,
dst_path,
[Path(f) for f in dir_output.tracked_files],
)
)
if missing_directories:
raise BazelActionError(
"\nError: Directory provided to --directory-outputs is missing, got:\n\n%s\n"
% "\n".join(str(d) for d in missing_directories)
)
if unwanted_files:
raise BazelActionError(
"\nError: Non-directories are not allowed in --directory-outputs Bazel path, got:\n\n%s\n"
% "\n".join(str(f) for f in unwanted_files)
)
if invalid_tracked_files:
raise BazelActionError(
"\nError: Missing or non-directory tracked files from --directory-outputs Bazel path:\n\n%s\n"
% "\n".join(str(f) for f in invalid_tracked_files)
)
if dir_copies:
time_profile.start(
"copy_directories",
"Copy Bazel output directories to Ninja build directory",
)
for src_path, dst_path, tracked_files in dir_copies:
copy_directory_if_changed(src_path, dst_path, tracked_files)
# Return all of the full paths to the tracked files in the directories, as these are
# the "destination output files" of these directories.
all_tracked_files: list[Path] = []
for dst, _, tracked_files in dir_copies:
all_tracked_files.extend([dst / file for file in tracked_files])
return all_tracked_files
def _make_final_symlinks(
self,
final_symlink_outputs: list[bazel_action_utils.FinalSymlinkOutput],
time_profile: build_utils.TimeProfile,
) -> None:
final_symlinks: list[tuple[Path, Path]] = []
for final_symlink_output in final_symlink_outputs:
src_path = self.paths.workspace / final_symlink_output.bazel_path
target_path = src_path.resolve()
link_path = Path(final_symlink_output.ninja_path)
final_symlinks.append((target_path, link_path))
if final_symlinks:
time_profile.start("symlink_outputs", "Symlink output files.")
# This doesn't need to use a thread pool because as of today there's only ever
# one file, in one action, that uses this codepath.
for target_path, link_path in final_symlinks:
build_utils.force_symlink(link_path, target_path)