blob: c1c754abf302d38aa0dc38095ef814c313b8277b [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.
import ast
import dataclasses
import json
from pathlib import Path
from typing import Any
# LINT.IfChange(DefaultBuildFlagsSet)
@dataclasses.dataclass(frozen=True)
class DefaultBuildFlagsSet:
"""Models a set of default build_flags() labels to be applied to different target types.
cxx_common_build_flags: A list of build_flags() labels whose flags will be used by
actions of all C/C++ target types.
cxx_executable_build_flags: A list of extra build_flags() labels, used for actions of
C/C++ executable targets only.
cxx_shared_library_build_flags: A list of extra build_flags() labels, used for actions
of C/C++ shared library targets only.
rust_common_build_flags: A list of build_flags() labels whose flags will be used by
actions of all Rust target types.
rust_executable_build_flags: A list of extra build_flags() labels, used for actions of
Rust executable targets only.
rust_shared_library_build_flags: A list of extra build_flags() labels, used for actions
of Rust shared library targets only.
target_compatible_with: A string for a Bazel expression listing Bazel constraint labels
to restrict these definitions to artifacts built in specific build configurations.
"""
cxx_common_build_flags: list[str] = dataclasses.field(default_factory=list)
cxx_executable_build_flags: list[str] = dataclasses.field(
default_factory=list
)
cxx_shared_library_build_flags: list[str] = dataclasses.field(
default_factory=list
)
rust_common_build_flags: list[str] = dataclasses.field(default_factory=list)
rust_executable_build_flags: list[str] = dataclasses.field(
default_factory=list
)
rust_shared_library_build_flags: list[str] = dataclasses.field(
default_factory=list
)
target_compatible_with: str = "[]"
# LINT.ThenChange(//build/bazel_sdk/fuchsia_rules_common/build_flags/providers.bzl:DefaultBuildFlagsSetInfo)
def get_build_flags_targets_from_ast(
content: str, build_file: Path
) -> set[str]:
"""Use Python parser to get build_flags() target names.
Recognizes:
- Direct calls to `build_flags(...)` or any wrapper macro ending with
`_build_flags(...)` (e.g., `compiler_build_flags`,
`riscv64_abi_build_flags`) with a literal string `name = "..."`.
- List comprehensions invoking such macros over constant string
lists/tuples (e.g., `[compiler_build_flags(name = v, ...) for v in
["v9", "v9.1"]]`).
Args:
content: BUILD.bazel content as string.
build_file: Path to BUILD.bazel file.
Returns:
A set of target name strings.
Raises:
Any exception encountered by the Python parser when
processing the input.
"""
targets: set[str] = set()
tree = ast.parse(content, filename=str(build_file))
def _collect_call_targets(
call_node: ast.Call, loop_vars: dict[str, list[str]]
) -> None:
if not isinstance(call_node.func, ast.Name):
return
func_name = call_node.func.id
if func_name not in (
"build_flags",
"extra_linker_flags",
) and not func_name.endswith("_build_flags"):
return
for kw in call_node.keywords:
if kw.arg == "name":
if isinstance(kw.value, ast.Constant) and isinstance(
kw.value.value, str
):
targets.add(kw.value.value)
elif (
isinstance(kw.value, ast.Name) and kw.value.id in loop_vars
):
targets.update(loop_vars[kw.value.id])
for node in ast.walk(tree):
if isinstance(node, ast.ListComp) and isinstance(node.elt, ast.Call):
loop_vars: dict[str, list[str]] = {}
for gen in node.generators:
if isinstance(gen.target, ast.Name) and isinstance(
gen.iter, (ast.List, ast.Tuple)
):
loop_vars[gen.target.id] = [
e.value
for e in gen.iter.elts
if isinstance(e, ast.Constant)
and isinstance(e.value, str)
]
_collect_call_targets(node.elt, loop_vars)
elif isinstance(node, ast.Call):
_collect_call_targets(node, {})
return targets
class BuildFileTargetsCache:
"""Find and cache the build_flags() target definitions in BUILD.bazel files."""
def __init__(self) -> None:
"""Create instance."""
self._cache: dict[Path, set[str]] = {}
def get(self, build_file: Path) -> set[str]:
"""Return the set of build_flags() target names from a given BUILD.bazel path."""
cached = self._cache.get(build_file)
if cached is not None:
return cached
assert build_file.exists(), f"Missing build file: {build_file}"
content = build_file.read_text()
targets = get_build_flags_targets_from_ast(content, build_file)
self._cache[build_file] = targets
return targets
# A global cache mapping paths to BUILD.bazel files to the
# corresponding set of build_flags() target definitions.
_BUILD_FILE_TARGETS_CACHE = BuildFileTargetsCache()
def _check_build_flags_target_exists(fuchsia_dir: Path, label: str) -> bool:
"""Check if a build_flags() target corresponding to the given GN label exists in the Bazel workspace.
The implementation parses the BUILD.bazel as a Python AST. This is fast but
fragile, in particular it requires that name values for build_flags() targets
are string literals, and not computed (e.g. `name = something + ".build_flags"`)
as these definitions will be ignored.
A better way would be to process the result of `bazel query --output=build`
but running these is significantly slower and requires a proper Bazel
workspace setup, which is not always guaranteed when running this code.
Args:
fuchsia_dir: Path to Fuchsia source directory.
label: A label string starting with //, such as //<package>:<target_name>,
or just //<package> (in which case <target_name> is deduced from its basename).
Returns:
True if //<package>/BUILD.bazel exists and defines a `build_flags()` target
with the name <target_name> in it.
"""
if not label.startswith("//"):
return False
label_path = label[2:]
if ":" in label_path:
package_dir, target_name = label_path.split(":", 1)
else:
package_dir = label_path
target_name = label_path.split("/")[-1]
build_file = fuchsia_dir / package_dir / "BUILD.bazel"
if not build_file.exists():
return False
build_file_targets = _BUILD_FILE_TARGETS_CACHE.get(build_file)
return target_name in build_file_targets
class DefaultBuildFlagsMap(dict[str, DefaultBuildFlagsSet]):
"""A { name -> DefaultBuildFlagsSet } map."""
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.missing_configs: list[str] = []
@staticmethod
def new_from_gn_config(
build_dir: Path, fuchsia_dir: Path
) -> "DefaultBuildFlagsMap":
"""Return a new instance matching the current GN build configuration.
Args:
build_dir: Ninja build directory.
fuchsia_dir: Fuchsia source directory.
Returns:
A new DefaultBuildFlagsMap instance.
"""
missing_configs: set[str] = set()
def resolve_and_filter_configs(configs: list[str]) -> list[str]:
filtered = []
for config in configs:
if _check_build_flags_target_exists(fuchsia_dir, config):
filtered.append(config)
else:
missing_configs.add(config)
return filtered
result_map = DefaultBuildFlagsMap()
# The current default only distinguish between Fuchsia and Linux os values,
# but it may be possible to provide different definitions based on additional
# constraints, such as CPU architecture, API level, or the platform/SDK split.
platforms_config = {
"fuchsia": {
"json_file": build_dir / "bazel_default_configs/fuchsia.json",
# NOTE: The same default build flags will be used for both
# fuchsia_platform and fuchsia_sdk artifacts produced in-tree.
"compatible_with": '["@platforms//os:fuchsia"]',
},
"host": {
"json_file": build_dir / "bazel_default_configs/host.json",
"compatible_with": "HOST_OS_CONSTRAINTS",
},
}
for name, config_info in platforms_config.items():
json_file = config_info["json_file"]
assert isinstance(json_file, Path)
target_compatible_with = config_info["compatible_with"]
assert isinstance(target_compatible_with, str)
assert json_file.exists(), f"Missing input file: {json_file}"
with json_file.open("rb") as f:
data = json.load(f)
# LINT.IfChange(DefaultConfigsJsonSchema)
result_map[name] = DefaultBuildFlagsSet(
cxx_common_build_flags=resolve_and_filter_configs(
data.get("cxx_common", [])
),
cxx_executable_build_flags=resolve_and_filter_configs(
data.get("cxx_executable_extra", [])
),
cxx_shared_library_build_flags=resolve_and_filter_configs(
data.get("cxx_shared_library_extra", [])
),
rust_common_build_flags=resolve_and_filter_configs(
data.get("rust_common", [])
),
rust_executable_build_flags=resolve_and_filter_configs(
data.get("rust_executable_extra", [])
),
rust_shared_library_build_flags=resolve_and_filter_configs(
data.get("rust_shared_library_extra", [])
),
target_compatible_with=target_compatible_with,
)
# LINT.ThenChange(//build/bazel/config/BUILD.gn:DefaultConfigsJsonSchema)
result_map.missing_configs = sorted(list(missing_configs))
return result_map
def generate_bazel_toolchain_definitions(self) -> str:
"""Generate a BUILD.bazel fragment that defines Bazel toolchain() targets.
These toolchains carry the default build_flags() labels describing
the default compiler and linker flags to be used when building different
types of C++ and Rust artifacts.
The top-level MODULE.bazel file should call register_toolchains()
with a filegroup() label pointing to them (or use the magic ":all" target
name which picks up all toolchain() targets from a package).
Returns:
A BUILD.bazel text fragment.
"""
content = """# Default build_flags() definition for C++ and Rust toolchains.
load("@fuchsia_rules_common//build_flags:toolchain.bzl", "build_flags_toolchain_instance")
load("@@//build/bazel/platforms:constraints.bzl", "HOST_OS_CONSTRAINTS")
"""
def format_labels(labels: list[str]) -> str:
if not labels:
return "[]"
# Since this BUILD.bazel file is generated inside an external
# repository (@fuchsia_build_info), prepend "@@" to labels starting
# with "//" so Bazel resolves them relative to the main workspace root.
lines = ["["]
for label in labels:
resolved = f"@@{label}" if label.startswith("//") else label
lines.append(f' "{resolved}",')
lines.append(" ]")
return "\n".join(lines)
for name, info in self.items():
content += """
build_flags_toolchain_instance(
name = "{name}_default_build_flags",
cxx_common_build_flags = {cxx_common_build_flags},
cxx_executable_build_flags = {cxx_executable_build_flags},
cxx_shared_library_build_flags = {cxx_shared_library_build_flags},
rust_common_build_flags = {rust_common_build_flags},
rust_executable_build_flags = {rust_executable_build_flags},
rust_shared_library_build_flags = {rust_shared_library_build_flags},
)
toolchain(
name = "{name}_toolchain",
target_compatible_with = {target_compatible_with},
toolchain = ":{name}_default_build_flags",
toolchain_type = "@fuchsia_rules_common//build_flags:toolchain_type",
)
""".format(
name=name,
cxx_common_build_flags=format_labels(
info.cxx_common_build_flags
),
cxx_executable_build_flags=format_labels(
info.cxx_executable_build_flags
),
cxx_shared_library_build_flags=format_labels(
info.cxx_shared_library_build_flags
),
rust_common_build_flags=format_labels(
info.rust_common_build_flags
),
rust_executable_build_flags=format_labels(
info.rust_executable_build_flags
),
rust_shared_library_build_flags=format_labels(
info.rust_shared_library_build_flags
),
target_compatible_with=info.target_compatible_with,
)
if self.missing_configs:
content += """
# The following GN configs were ignored because they do not have a matching
# Bazel build_flags() target definition in the source tree:
"""
for config in self.missing_configs:
content += f"# - {config}\n"
return content