blob: 4605d3c348a6edb150fa63bb828b8cf8c8d3d731 [file]
# 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.
"""Core logic for expanding and resolving Bazel actions' command lines."""
import dataclasses
import json
import os
import re
import shlex
import sys
from typing import Any
# Manually modify sys.path to allow other modules to import
# this one without worrying about its dependencies.
_SCRIPT_DIR = os.path.dirname(__file__)
sys.path.insert(0, _SCRIPT_DIR)
import build_utils
@dataclasses.dataclass
class ExpandedAction:
"""Models a Bazel action with its fully expanded command-line."""
action: str # Action's description
target: str # Bazel target label
configuration: str # Bazel configuration mnemonic (e.g. k8-fastbuild)
mnemonic: str # The action's mnemonic
args: list[str] # The command line arguments
env_vars: list[str] = dataclasses.field(
default_factory=list
) # A list of VARNAME=value strings. This is kept as a list, instead
# of a dictionary to be as close as possible from the original
# command-line. This helps debugging problematic cases when multiple
# entries assign to the same key (e.g. ["FOO=1", "FOO=2"]).
warnings: list[str] = dataclasses.field(
default_factory=list
) # Warnings encountered during expansion.
def to_dict(self) -> dict[str, Any]:
"""Serializes the expanded action into a dictionary for JSON output."""
res = {
"action": self.action,
"target": self.target,
"configuration": self.configuration,
"mnemonic": self.mnemonic,
"args": self.args,
}
if self.env_vars:
res["environment"] = self.env_vars
if self.warnings:
res["warnings"] = self.warnings
return res
def get_bazel_expanded_actions(
bazel_launcher: build_utils.BazelLauncher,
bazel_execroot: str,
bazel_target: str,
config_args: list[str],
filter_mnemonics: None | list[str] = None,
read_response_files: bool = False,
) -> list[ExpandedAction]:
"""Return the expanded command-line of all actions used to build a given Bazel target.
Args:
bazel_launcher: A BazelLauncher used to run bazel commands.
bazel_execroot: Absolute path to the Bazel execroot.
bazel_target: Bazel target label to get the actions for.
config_args: Additional configuration arguments to pass to Bazel, such as --config=NAME.
filter_mnemonics: Optional list of mnemonics to filter by. If provided only actions
whose mnemonic match these values will be included in the returned list.
read_response_files: Optional flag. When True, the content of response files will be
read directly from the Bazel execroot and no cqueries will be performed. This requires
the target to have been built before calling this function to ensure the response
file's content is correct.
Returns:
A list of ExpandedAction objects with the expanded command lines.
Raises:
RuntimeError if invoking Bazel failed.
"""
# 1. Run a Bazel aquery to get the list of actions with their non-expanded command lines
# and map of response files.
aquery_args = config_args + ["--output=jsonproto", bazel_target]
ret = bazel_launcher.run_query("aquery", aquery_args, ignore_errors=False)
if ret.returncode != 0:
raise RuntimeError(
f"ERROR: aquery failed with exit code {ret.returncode}\n\n{ret.stderr}\n"
)
aquery_result = parse_aquery_output(ret.stdout, filter_mnemonics)
if aquery_result.uses_bazel_params_file:
read_response_files = True
if not read_response_files:
# 1.1 Perform cqueries to extract the BuildFlagsInfo provider values directly,
# without accessing the disk, then use them to expand the response file paths
# in the parsed actions' command lines.
build_args_map = query_build_flags_from_bazel(
aquery_result.get_final_build_flags_target_labels(),
bazel_launcher,
config_args,
)
def expand_action_args(
action: ParsedAction,
) -> ArgumentsExpansionResult:
return expand_args_with_build_args_map(
action.args,
action.env_vars,
aquery_result.response_files_map,
build_args_map,
)
else:
# 1.2 Read the response files directly from the Bazel execroot instead of
# performing cqueries. This is required when Bazel decides to use a .params
# response file to split command lines that are too long.
execroot = os.path.realpath(bazel_execroot)
def expand_action_args(
action: ParsedAction,
) -> ArgumentsExpansionResult:
return expand_args_from_disk(action.args, action.env_vars, execroot)
# 4. Perform Expansion
expanded_actions: list[ExpandedAction] = []
for action in aquery_result.actions:
expanded_result = expand_action_args(action)
# Skip actions that don't have any arguments. These are not useful to inspect.
# This corresponds to Bazel's creation of symlink trees and runfiles manifests.
if not expanded_result.expanded_args:
continue
expanded_actions.append(
ExpandedAction(
action=action.name,
target=action.target,
configuration=action.config,
mnemonic=action.mnemonic,
args=expanded_result.expanded_args,
env_vars=expanded_result.env_vars,
warnings=expanded_result.warnings,
)
)
return expanded_actions
##############################################################################################
##############################################################################################
#####
##### B U I L D F L A G S I M P L E M E N T A T I O N D E T A I L S
#####
# Constants used to identify the type of actions that require build_flags() response.
# LINT.IfChange(action_kinds)
ACTION_KIND_CPP_COMPILE = "cpp_compile"
ACTION_KIND_C_COMPILE = "c_compile"
ACTION_KIND_CPP_LINK = "cpp_link"
ACTION_KIND_RUST_COMPILE = "rust_compile"
ACTION_KINDS = {
ACTION_KIND_CPP_COMPILE,
ACTION_KIND_C_COMPILE,
ACTION_KIND_CPP_LINK,
ACTION_KIND_RUST_COMPILE,
}
# LINT.ThenChange(//build/bazel_sdk/fuchsia_rules_common/build_flags/build_flags.bzl:action_kinds)
# A map from response file suffixes to their corresponding
# action kind. See _generate_response_file() in build_flags.bzl.
_BUILD_FLAGS_RESPONSE_FILE_SUFFIX_MAP = {
f".{kind}.build_flags": kind for kind in ACTION_KINDS
} | {
".rustc_env_file.build_flags": ACTION_KIND_RUST_COMPILE,
}
@dataclasses.dataclass(frozen=True)
class ResolvedBuildArgsFlags:
"""Models the BuildFlagsInfo values extracted from Bazel cqueries."""
label: str
cflags: list[str]
cflags_c: list[str]
cflags_cc: list[str]
defines: list[str]
include_dirs: list[str]
ldflags: list[str]
lib_dirs: list[str]
rustflags: list[str]
rustenv: list[str]
@property
def include_flags(self) -> list[str]:
return [f"-I{include_dir}" for include_dir in self.include_dirs]
@property
def define_flags(self) -> list[str]:
return [f"-D{define}" for define in self.defines]
def get_flags_for(self, kind: str) -> list[str]:
"""Return the list of flags for the given action kind."""
result: list[str] = []
if kind == ACTION_KIND_CPP_COMPILE:
result += self.include_flags
result += self.define_flags
result.extend(self.cflags)
result.extend(self.cflags_cc)
elif kind == ACTION_KIND_C_COMPILE:
result += self.include_flags
result += self.define_flags
result.extend(self.cflags)
result.extend(self.cflags_c)
elif kind == ACTION_KIND_RUST_COMPILE:
result.extend(self.rustflags)
result.extend([f"-Lnative={lib_dir}" for lib_dir in self.lib_dirs])
elif kind == ACTION_KIND_CPP_LINK:
result.extend([f"-Wl,-L{lib_dir}" for lib_dir in self.lib_dirs])
result.extend(self.ldflags)
else:
raise AssertionError(f"Unknown action kind: {kind}")
return result
@staticmethod
def from_json(data: dict[str, Any]) -> "ResolvedBuildArgsFlags":
return ResolvedBuildArgsFlags(
label=data["label"],
cflags=data.get("cflags", []),
cflags_c=data.get("cflags_c", []),
cflags_cc=data.get("cflags_cc", []),
defines=data.get("defines", []),
include_dirs=data.get("include_dirs", []),
ldflags=data.get("ldflags", []),
lib_dirs=data.get("lib_dirs", []),
rustflags=data.get("rustflags", []),
rustenv=data.get("rustenv", []),
)
class ResolvedBuildArgsMap(dict[str, ResolvedBuildArgsFlags]):
"""Map from target labels to their resolved build flags."""
##############################################################################################
##############################################################################################
#####
##### A Q U E R Y P A R S I N G
#####
def _normalize_path(path: str) -> str:
"""Safely strips shell pwd prefixes from Bazel paths to prevent false positives."""
if path.startswith("${pwd}/"):
return path[len("${pwd}/") :]
return path
@dataclasses.dataclass(frozen=True)
class ParsedAction:
"""Models a single parsed action from aquery."""
name: str
target: str
config: str
mnemonic: str
args: list[str]
env_vars: dict[str, str]
# A regular expression to match paths in the Bazel execroot that contain
# bazel-out/<config_dir>/bin/
_BAZEL_OUT_BIN_RE = re.compile(r"bazel-out/[^/]+/bin/")
@dataclasses.dataclass(frozen=True)
class ResponseFileTarget:
"""Models a build_flags() response file target and its type.
The build_flags() implementation relies on the following implementation details of
its wrapper macros:
- Creating a _compute_final_build_flags() target that does not generate anything
but provides the *final* BuildFlagsInfo values to use for the wrapped target.
- Creating one or more _generate_response_file() targets, that depend on the previously
listed one, but *only* generating response files, whose paths are added to the wrapped
target's final command-line.
This type is used to convert a response file path, as it appears on the action's command
line, into the label of the _compute_final_build_flags() target, and the kind of action
the response file corresponds to.
This will allow performing cqueries on the returned target label to extract the final
command-line flags, in order to expand the response file without accessing build artifacts.
"""
label: str # Label of the _compute_final_build_flags() target created by a wrapper macro.
kind: str # Type of action, see ACTION_KINDS defined above.
@staticmethod
def from_execroot_path(path: str) -> "None | ResponseFileTarget":
"""Try to convert a response file path into a ResponseFileTarget instance.
Check the path of a response file as it appears on an action's command-line.
If it happens to be in the Bazel execroot (typically within bazel-out/<config_dir>/bin/)
and uses one of the known file extensions implemented for response files in build_flags.bzl,
then return a new ResponseFileTarget value.
Args:
path: The response file path string (e.g. a compiler argument starting with '@').
Result:
A ResponseFileTarget instance mapping the path to its cquery label and action kind,
or None if the path is not a recognized generated wrapper response file.
"""
match = _BAZEL_OUT_BIN_RE.search(path)
if not match:
return None
rel_path = path[match.end() :]
if rel_path.startswith("external/"):
# This artifact is generated by a target defined in an external repository.
# Its path begins with external/<repo_canonical_repo_name>/
parts = rel_path.split("/")
repo_name = parts[1]
rest = parts[2:]
else:
# This artifact is generated by a target from the root workspace.
repo_name = ""
rest = rel_path.split("/")
target_full_name = rest[-1]
package = "/".join(rest[:-1])
action_kind = None
base_target_name = None
for suffix, kind in _BUILD_FLAGS_RESPONSE_FILE_SUFFIX_MAP.items():
if target_full_name.endswith(suffix):
base_target_name = target_full_name.removesuffix(suffix)
action_kind = kind
break
if not base_target_name:
return None
label = f"@@{repo_name}//{package}:{base_target_name}.final_build_flags"
assert action_kind is not None
return ResponseFileTarget(label, action_kind)
class ResponseFileMap(dict[str, ResponseFileTarget]):
"""A map from response file path to the corresponding ResponseFileTarget value.
Used to record the paths of build_flags.bzl response files found in an
action's original command-line, then extract the list of targets to use
for cqueries later. Usage is:
1) Create instance
2) Call try_path() for each response file path that appears in
the input action's command-line. If this matches the format of a response
file generated by build_flags.bzl wrapper macros, it will be recorded.
"""
def try_path(self, response_path: str) -> None:
target = ResponseFileTarget.from_execroot_path(response_path)
if target:
self[response_path] = target
@dataclasses.dataclass(frozen=True)
class ParsedAqueryResult:
"""The result of parsing aquery outputs.
This provides:
- A list of ParsedAction values corresponding to all non-empty actions returned
by the aquery command.
- A dictionary mapping response file paths (as they appear on actions command lines)
to the matching ResponseFileTarget. These can be used later to perform cqueries
to extract the list of final BuildFlagsInfo values.
- A flag that will be True if any parsed action uses a Bazel .params file.
This happens when an action's command-line is too long and is instead written to
a response file that is generated internally by Bazel and thus cannot be
accessed with queries (it must be read from disk after building the target).
"""
actions: list[ParsedAction]
response_files_map: ResponseFileMap
uses_bazel_params_file: bool
@staticmethod
def new_empty() -> "ParsedAqueryResult":
"""Create a new ParsedAqueryResult with empty values."""
return ParsedAqueryResult([], ResponseFileMap(), False)
def get_final_build_flags_target_labels(self) -> set[str]:
"""Return a set of target labels to use for cquery queries."""
return {t.label for t in self.response_files_map.values()}
def parse_aquery_output(
aquery_output: str,
filter_mnemonics: list[str] | None = None,
) -> ParsedAqueryResult:
"""Parses raw aquery output and extracts commands and response file mappings.
Args:
aquery_output: The raw string output from a 'bazel aquery --output=jsonproto' command.
filter_mnemonics: Optional list of mnemonic names to filter actions early.
Returns:
A ParsedAqueryResult value.
"""
# The following parses the output of `bazel aquery --format=jsonproto` which should
# emit a JSON object that corresponds to the ActionGraphContainer schema from
# https://github.com/bazelbuild/bazel/blob/7278be3f9b0c26842ecb8225f0215c1e4aede5a9/src/main/protobuf/analysis_v2.proto#L189
# Filter out the output from unexpected garbage, for example our main_build.py
# wrapper still appends "Lock acquired, proceeding with build." to stdout when performing
# `fx bazel aquery --config=host --output=jsonproto //build/bazel/host_tests/cc_tests/... 2>/dev/null`
start_idx = aquery_output.find("{")
end_idx = aquery_output.rfind("}")
if start_idx < 0 or end_idx < 0 or start_idx > end_idx:
return ParsedAqueryResult.new_empty()
try:
data = json.loads(aquery_output[start_idx : end_idx + 1])
# data["targets"] is a list of Target dictionaries. Build a map associating
# target unique ids to their label.
targets_map = {t["id"]: t["label"] for t in data.get("targets", [])}
# data["configuration"] is a list of Configuration dicts. Build a map
# association configuration unique ids to the "mnemonic" which is
# something like "k8-fastbuild" (not related to action mnemonics).
configs_map = {
c["id"]: c["mnemonic"] for c in data.get("configuration", [])
}
except Exception as e:
print(f"ERROR: Failed to parse aquery jsonproto: {e}", file=sys.stderr)
return ParsedAqueryResult.new_empty()
actions: list[ParsedAction] = []
response_files_map = ResponseFileMap()
uses_bazel_params_file = False
raw_actions = data.get("actions", [])
for action in raw_actions:
action_mnemonic = action.get("mnemonic", "Unknown Mnemonic")
if filter_mnemonics and action_mnemonic not in filter_mnemonics:
continue
target_id = action.get("targetId", 0)
action_target = targets_map.get(target_id, "Unknown Target")
config_id = action.get("configurationId", 0)
action_config = configs_map.get(config_id, "Unknown Configuration")
cmd_args = action.get("arguments", [])
action_name = f"action '{action_mnemonic} on {action_target}'"
env_vars: dict[str, str] = {
item["key"]: item["value"]
for item in action.get("environmentVariables", [])
# Sometimes there is no value at all, ignore such entries.
if "value" in item
}
for i, arg in enumerate(cmd_args):
if arg == "--env-file" and i + 1 < len(cmd_args):
env_path = _normalize_path(cmd_args[i + 1])
response_files_map.try_path(env_path)
elif arg.startswith("@"):
path = _normalize_path(arg[1:])
if ".params" in os.path.basename(path):
uses_bazel_params_file = True
else:
response_files_map.try_path(path)
actions.append(
ParsedAction(
name=action_name,
target=action_target,
config=action_config,
mnemonic=action_mnemonic,
args=cmd_args,
env_vars=env_vars,
)
)
return ParsedAqueryResult(
actions=actions,
response_files_map=response_files_map,
uses_bazel_params_file=uses_bazel_params_file,
)
##############################################################################################
##############################################################################################
#####
##### A R G U M E N T E X P A N S I O N
#####
@dataclasses.dataclass(frozen=True)
class ArgumentsExpansionResult:
"""Models the result of expanding command line arguments dynamically."""
expanded_args: list[str] # Expanded command-line arguments.
env_vars: list[str] # A list of "VARNAME=value" strings.
warnings: list[str] # Warnings encountered during expansion.
def expand_args_from_disk(
args: list[str],
env_vars: dict[str, str],
execroot: str,
) -> ArgumentsExpansionResult:
"""Recursively expand standard response files and Rust env files from disk iteratively.
This functions takes a list of command-line arguments and will expand response files
that appear in the input to generate a final version. It only works correctly if
these files are in the execroot, hence must be called after building the target
to give correct results.
Args:
args: The raw command line arguments list to expand.
env_vars: The dictionary of environment variables to use for expansion.
execroot: Absolute path to Bazel's execution root.
Returns:
An ArgumentsExpansionResult dataclass containing expanded arguments, environment
variable overrides, and warnings.
"""
import collections
expanded: list[str] = []
env_list: list[str] = [f"{k}={v}" for k, v in env_vars.items()]
warnings: list[str] = []
# The implementation below uses a single double-ended queue to parse
# the arguments recursively. In particular, it recognizes two special
# types of input:
#
# - '@<path>': as a response file usage. The code will read the
# content of <path>, shell-unquote it, and inject its content into
# the queue.
#
# -'--rust-env', '<path>': as a Rust environment file use. The code
# will read <path> which should be a series of NAME=value definitions,
# one per line, which will be recorded in env_list in the order they
# appear (no deduplication is performed).
#
# The operation is recursive (a response file can contain other @<path>
# arguments) and will properly flatten the final result, however, path
# cycles can exist, and the loop below will detect them and generate
# a warning then one is detected, and will also keep the corresponding
# final '@<path>' argument in the output to help debugging.
#
# Each item in the queue is either a string, modelling an incoming
#
# string argument, or a tuple[str, str] which will be ('POP_MARKER', abs_path)
# to model the end of a file scropt. These items are used to pop paths
# from the stack used for cycle detection.
#
# A few example will illustrate how this works, consider this input:
#
# queue = [ "foo", "@path1", "bar" ] output = [] paths = []
#
# The first loop iteration see "foo" and just pulls it to send to the output.
#
# queue = [ "@path1", "bar" ] output = [ "foo" ] paths = []
#
# The second loop iteration will see the "@<path>" format, and will
# push 'path1' to the `paths` stack, read the content of that file
# into a list of unquoted string arguments, followed by a POP_MARKER item,
# the following assumed 'path1' contained "qux", "@path2":
#
# queue = [ "qux", "@path2", (POP_MARKER, "path1"), "bar" ]
# output = [ "foo" ]
# paths = [ "path1" ]
#
# The third loop iteration moves 'qux' to the output, and the fourth one
# will see the new "@path2" statement:
#
# queue = [ "@path2", (POP_MARKER, "path1"), "bar" ]
# output = [ "foo", "qux" ]
# paths = [ "path1" ]
#
# the 'path2' will pushed to the top of the stak, its content read, unquoted
# and prepended at the start of the queue, followed by its POP_MARKER item:
#
# queue = [ "zoo", (POP_MARKER, "path2"), (POP_MARKER, "path1"), "bar" ]
# output = [ "foo", "qux" ]
# paths = [ "path1", "path2" ]
#
# From there, 'zoo' is moved to the output. The first POP_MARKER simply
# removes 'path2' from the top of the `paths` stack, the second one
# removed "path1", then "bar" is appended to the output:
#
# queue = []
# output = [ "foo", "qux", "zoo", "bar" ]
# paths = []
#
# The look also contain special logic to handle "--env-file", "<path>",
# even when this crosses a file scope.
#
work_queue: collections.deque[str | tuple[str, str]] = collections.deque(
args
)
# Active circular dependency path stack (mimics the recursive call stack)
active_path: list[str] = []
def detect_path_cycle(item: str, abs_path: str) -> bool:
"""Return true if a path cycle is detected."""
if abs_path not in active_path:
return False
cycle_idx = active_path.index(abs_path)
cycle_path = [
os.path.relpath(p, execroot) for p in active_path[cycle_idx:]
] + [os.path.relpath(abs_path, execroot)]
cycle_str = " -> ".join(cycle_path)
warnings.append(f"Cycle detected in response file {item}: {cycle_str}")
return True
while work_queue:
item = work_queue.popleft()
# Handle pop marker to unwind active path scope
if isinstance(item, tuple) and item[0] == "POP_MARKER":
pop_path = item[1]
# Consistency checks for safety.
assert (
active_path
), f"Unexpected POP_MARKER for {item[1]} with empty stack!"
assert (
active_path[-1] == pop_path
), f"Unexpected POP_MARKER for {item[1]}, expected {active_path[-1]}"
active_path.pop()
continue
assert isinstance(item, str)
# Handle --env-file <path> for Rust actions
if item == "--env-file":
# If this is the final --env-file, just append it as-is to the output.
# The command-line is likely broken, but should be reported.
if not work_queue:
expanded.append(item)
continue
env_path = work_queue.popleft()
# If the --env-file is followed by a POP_MARKER, swap them in the
# queue and loop again to simplify processing.
if not isinstance(env_path, str):
# --env-file POP_MARKER ... => POP_MARKER --env-file ...
work_queue.appendleft(item)
work_queue.appendleft(env_path)
continue
assert isinstance(env_path, str) # for mypy
normalized_path = _normalize_path(env_path)
abs_path = os.path.join(execroot, normalized_path)
# Tricky case: '@path1' where path1 contains '--env-file path1'
# This is a path cycle, keep the --env-file <path> in the output
# for debugging after the warning (which will include the cycle).
if detect_path_cycle(env_path, abs_path):
expanded += [item, env_path]
continue
if os.path.isfile(abs_path):
try:
with open(abs_path, "r") as f:
envs = [line.strip() for line in f if line.strip()]
env_list.extend(envs)
except Exception as e:
warnings.append(
f"Failed to read env file {env_path} from disk: {e}"
)
expanded += [
item,
env_path,
] # Keep --env-file <path> in output.
else:
warnings.append(
f"Rust environment file {env_path} was not found on disk. "
f"This script requires build outputs to expand arguments. "
f"Please build the target first (using 'fx build') and re-run this script."
)
expanded += [
item,
env_path,
] # Keep --env-file <path> in output.
continue
# Handle @path response files recursively
if item.startswith("@"):
path = item[1:]
normalized_path = _normalize_path(path)
abs_path = os.path.join(execroot, normalized_path)
# Detect path cycles, keep the last @path in the output for debugging
if detect_path_cycle(item, abs_path):
expanded.append(item)
continue
if os.path.isfile(abs_path):
try:
# Read the response file, assume content is shell quoted
# as that's what most tools expect (though there is no
# standard for that), and that it can contain multiple
# arguments per line.
with open(abs_path, "r") as f:
nested_args = []
for line in f:
line = line.strip()
if line:
nested_args.extend(shlex.split(line))
# Prepend POP_MARKER and nested args in original order
work_queue.appendleft(("POP_MARKER", abs_path))
for nested_arg in reversed(nested_args):
work_queue.appendleft(nested_arg)
# Save path in stack.
active_path.append(abs_path)
continue
except OSError as e:
# Something went wrong, keep @path for debugging.
warnings.append(
f"Failed to read parameter file {item} from disk: {e}"
)
else:
warnings.append(
f"Parameter file {item} was not found on disk. "
f"This script requires build outputs to expand arguments. "
f"Please build the target first (using 'fx build') and re-run this script."
)
# Either a normal item, or something went wrong in the @path
# case, and we keep it for debugging the issue.
expanded.append(item)
return ArgumentsExpansionResult(
expanded_args=expanded, env_vars=env_list, warnings=warnings
)
##############################################################################################
##############################################################################################
#####
##### C Q U E R I E S F O R B U I L D F L A G S I N F O
#####
# Path to the .cquery file used by query_build_flags_from_bazel.
_EXPAND_BUILD_ARGS_JSON_CQUERY_PATH = os.path.join(
_SCRIPT_DIR, "../starlark/expand_build_args_json.cquery"
)
def query_build_flags_from_bazel(
final_build_flags_labels: set[str],
bazel_launcher: build_utils.BazelLauncher,
config_args: list[str],
) -> ResolvedBuildArgsMap:
"""Runs cquery to fetch custom build flags statically.
This takes as input a ParseAqueryOutput value and uses it to perform a number
of Bazel cqueries to compute ResolvedBuildArgsFlags values.
Args:
final_build_flags_labels: The labels of the _compute_final_build_flags() target that
will be queries for their provider information by this function. E.g. the result
of ParsedAqueryResult.get_final_build_flags_target_labels().
bazel_launcher: The BazelLauncher instance used to invoke bazel.
config_args: Extra configuration-specific flags (like --config) to pass to cquery.
Returns:
A ResolvedBuildArgsMap value.
Raises:
RuntimeError if there is a problem calling bazel cquery.
"""
flags_map = ResolvedBuildArgsMap()
if final_build_flags_labels:
cquery_expr = f"set({' '.join(sorted(final_build_flags_labels))})"
cquery_file = os.path.realpath(_EXPAND_BUILD_ARGS_JSON_CQUERY_PATH)
print(f"Running cquery for response files flags ...", file=sys.stderr)
cquery_args = [
"--output=starlark",
f"--starlark:file={cquery_file}",
"--consistent_labels",
]
cquery_args.extend(config_args)
cquery_args.append(cquery_expr)
ret = bazel_launcher.run_query(
"cquery", cquery_args, ignore_errors=False
)
if ret.returncode != 0:
raise RuntimeError(f"Error running bazel cquery:\n\n{ret.stderr}\n")
cquery_output = ret.stdout
for line in cquery_output.splitlines():
line = line.strip()
if line.startswith("{") and line.endswith("}"):
try:
data = json.loads(line)
if data: # Ignore empty objects returned by the cquery
flags = ResolvedBuildArgsFlags.from_json(data)
flags_map[flags.label] = flags
except json.JSONDecodeError as e:
raise RuntimeError(
f"Error parsing JSON line: {line}, error: {e}"
)
return flags_map
def expand_args_with_build_args_map(
args: list[str],
env_vars: dict[str, str],
response_files_map: ResponseFileMap,
build_args_map: ResolvedBuildArgsMap,
) -> ArgumentsExpansionResult:
"""Expand command line arguments without touching the filesystem.
This function is similar to expand_args_from_disk() except that it will not read any
file from disk. Instead, it relies on a BuildArgsMap to provide the collected final
BuildFlagsInfo provider values, and map the response files that appear in the input
arguments to the corresponding flags.
Args:
args: The raw command line arguments list to expand.
env_vars: The dictionary of environment variables to use for expansion.
response_files_map: Pre-computed mapping of argument paths to Starlark targets and kinds.
build_args_map: A ResolvedBuildArgsMap value.
Result:
A ArgumentsExpansionResult containing:
- A new list of arguments with all custom build_flags response files expanded recursively.
- A list of resolved environment variables (e.g., "KEY=VALUE") from env files.
- A list of warning strings.
"""
expanded = []
env_list = [f"{k}={v}" for k, v in env_vars.items()]
warnings = []
skip_next = False
for i, arg in enumerate(args):
if skip_next:
skip_next = False
continue
if arg == "--env-file" and i + 1 < len(args):
env_path = _normalize_path(args[i + 1])
res_target = response_files_map.get(env_path)
if res_target:
data = build_args_map.get(res_target.label)
if data is None:
warnings.append(
f"Could not resolve Rust env vars for {env_path} via cquery."
)
else:
env_list.extend(data.rustenv)
skip_next = True
continue
if arg.startswith("@"):
path = _normalize_path(arg[1:])
res_target = response_files_map.get(path)
if not res_target:
expanded.append(arg)
warnings.append(
f"Standard parameter file {arg} cannot be expanded with queries. "
)
continue
data = build_args_map.get(res_target.label)
if data:
flags = data.get_flags_for(res_target.kind)
expanded.extend(flags)
else:
expanded.append(arg)
warnings.append(
f"Could not resolve flags for custom response file {arg} via cquery."
)
else:
expanded.append(arg)
return ArgumentsExpansionResult(
expanded_args=expanded, env_vars=env_list, warnings=warnings
)