blob: b430a30f9b227bff0581dfae80e089095ae7b0e2 [file]
# Copyright 2022 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("//build/bazel/bazel_inputs.gni")
import("//build/bazel/bazel_workspace.gni")
import("//build/bazel/legacy_ninja_build_outputs.gni")
import("//build/config/compiler.gni")
declare_args() {
# Suppress Bazel non-error output
# This is now disabled by default since bazel_action()
# uses the //:console pool to print its output directly
# to stdout/stderr during the Ninja build.
bazel_quiet = false
}
# Wrap a Bazel command with a GN action.
# Bazel is invoked in a workspace setup by `//build/bazel:bazel_workspace`.
#
# The caller should pass a list of Bazel target patterns for the
# `bazel build` command, as well as a list Bazel output files relative
# to the workspace's root that will be copied into the
# GN target's target_gen_dir by default.
#
# All inputs seen by the Bazel command must be defined in the
# @legacy_ninja_build_outputs Bazel repository, which is populated by
# bazel_input_xxx() targets that must appear as dependencies of the
# //build/bazel:legacy_ninja_build_outputs target.
#
# The bazel-generated files will be copied (possibly hard-linked) into the
# `target_gen_dir` matching the GN action for this template call (since
# GN requires that an action's output file be in either target_gen_dir
# or target_out_dir).
#
# This also takes a list of Bazel target patterns, since Bazel does not
# support building specific files, only targets. For now it is up to the
# caller to know exactly which files are generated by Bazel for a given
# set of target patterns.
#
# Args:
# command: (required)
# The Bazel command, e.g. `build`, `run`, `test`...
# See: `bazel help`.
# Type: string.
#
# bazel_inputs: (optional)
# List of labels to bazel_input_xxx() targets that define
# the set of Ninja-generated inputs that the Bazel build command will
# use. These must be in the dependency tree of
# `//build/bazel:legacy_ninja_build_outputs`. Note that this is only used to
# ensure that said inputs are properly generated by Ninja before this
# action's command is run. The command will be able to see any input
# files exported by the @legacy_ninja_build_outputs Bazel repository,
# as there is no way to restrict them for now.
# Type: list of GN labels.
#
# bazel_targets:
# A list of Bazel target patterns for the `bazel build` command.
# This list cannot be empty since there is no concept of 'default'
# targets in Bazel. Note target sets (i.e. `...`) are not supported.
#
# Relative target names (e.g. ":foo" or "foo:bar") are supported and
# will be resolved relative to the current directory.
#
# Type: list of Bazel target patterns.
#
# copy_outputs: (optional)
# A list of scopes, where each item describes a Bazel output and a
# corresponding Ninja output location (where the Bazel build artifact
# will be copied).
#
# Each scope uses the following format:
#
# {
# bazel = "<bazel_path>"
# ninja = "<ninja_path>"
# }
#
# See technical note below for details about <bazel_path> and <ninja_path>
#
# Type: list of scopes (cannot be empty)
#
# allow_directory_in_outputs (optional):
# If true, directories are allowed in `copy_outputs`, which will allow
# users to send directories as a whole from Bazel to GN.
#
# NOTE: Using directories as outputs can usually lead to incremental build
# issues in GN/Ninja, because timestamps on directories may not correctly
# represent freshness of its contents. See directory_timestamp_outputs below.
#
# Type: boolean
# Default: false
#
# directory_timestamp_outputs: (optional)
#
# When allow_directory_in_outputs is set, a list of one ore more output
# files inside the output directories. Paths are relative to target_gen_dir.
# This is useful to provide Ninja with a file timestamp it can rely on
# (directory timestamps are not usable, and not managed in a consistent way
# across platforms).
#
# IMPORTANT: For incremental build correctness, these timestamps must always
# reflect the freshness of the whole directory content. For example:
#
# bazel_action("foo_product_bundle") {
# command = "build"
# ...
# copy_outputs = [
# {
# bazel = "{{BAZEL_TARGET_OUT_DIR}}/{{BAZEL_TARGET_NAME}}_out"
# ninja = "product_bundle"
# }
# ]
# directory_timestamp_outputs = [
# "product_bundle/product_bundle.json",
# ]
# }
#
# Type: list of paths relative to target_gen_dir.
# Default: empty
#
# extra_bazel_args: (optional)
# A list extra command line arguments to pass to Bazel.
# Type: list of string.
#
# deps:
# metadata:
# no_output_dir_leaks:
# testonly:
# visibility:
# Usual GN meaning.
#
#
# TECHNICAL NOTE ON BAZEL OUTPUTS:
#
# Bazel places its build artifacts under directories that are specific to
# the Bazel build configuration. In practice, these will be different based
# on the current GN toolchain instance where the bazel_action() target
# is defined.
#
# Because the exact Bazel config directories are hard to predict, Bazel
# creates (or updates) a symlink named `bazel-bin` in the main workspace
# directory. This symlink points to the correct config-specific directory,
# and build artifacts are then reachable as
# "$BAZEL_WORKSPACE/bazel-bin/<bazel_target_package>/<output_files>"
#
# However, every time a bazel_action() action is run, the value of the
# bazel-bin symlink target *may* change, so these bazel output paths cannot
# be considered stable or valid once the action has completed.
#
# Hence Bazel build outputs must be copied to stable locations in the
# Ninja build directory, by the script invoked by bazel_action().
#
# The `copy_outputs` argument is used to list all Bazel outputs and their
# corresponding Ninja output path. It is a list of scopes whose format
# is:
#
# {
# bazel = "<bazel_path>"
# ninja = "<ninja_path>"
# }
#
# Where <bazel_path> is a Bazel output path, relative to the Bazel workspace
# that looks like "bazel-bin/<bazel_target_package>/<output_file>", and
# where <ninja_path> is an output path relative to the `target_gen_dir`
# of the current target.
#
# Moreover, the <bazel_path> value supports these special substitution
# expressions:
#
# {{BAZEL_TARGET_NAME}}
# Expands to the target name of the first `bazel_targets` label
#
# {{BAZEL_TARGET_PACKAGE}}
# Expands to the package name of the first `bazel_targets` label
# without a // prefix!
#
# {{BAZEL_TARGET_OUT_DIR}}
# Expands to "bazel-bin/{{BAZEL_TARGET_PACKAGE}}"
#
# {{BAZEL_TARGET_OUT_PATH}}
# Expands to {{BAZEL_TARGET_OUT_DIR}}/{{BAZEL_TARGET_NAME}}.
#
template("bazel_action") {
assert(defined(invoker.command), "command must be defined!")
assert(defined(invoker.bazel_targets), "bazel_targets must be defined!")
assert(invoker.bazel_targets != [], "bazel_targets cannot be empty!")
_bazel_output_list = []
_ninja_output_list = []
assert(invoker.copy_outputs != [], "`copy_outputs` cannot be empty")
foreach(entry, invoker.copy_outputs) {
assert("$entry" != entry,
"`copy_outputs` items must be scopes, string found: \"$entry\"")
assert(defined(entry.bazel),
"`copy_outputs` scope requires bazel key: $entry")
_bazel_output_list += [ entry.bazel ]
assert(defined(entry.ninja),
"`copy_outputs` scope requires ninja key: $entry")
_ninja_output_list += [ entry.ninja ]
}
_allow_directory_in_outputs = false
if (defined(invoker.allow_directory_in_outputs)) {
_allow_directory_in_outputs = invoker.allow_directory_in_outputs
}
_num_bazel_outputs = 0
_bazel_outputs = []
# Compute the value of the TARGET_OUT_XXX shortcuts.
_bazel_targets = invoker.bazel_targets
_bazel_target = get_label_info(_bazel_targets[0], "name")
_bazel_target_package =
rebase_path(get_label_info(_bazel_targets[0], "dir"), "//")
_bazel_target_out_dir = "bazel-bin/" + _bazel_target_package
not_needed([
"_bazel_target",
"_bazel_target_package",
"_bazel_target_out_dir",
])
_bazel_outputs = []
foreach(path, _bazel_output_list) {
assert(rebase_path(path, "//") != path,
"bazel output path must be relative: $path")
assert(string_replace("XX$path", "XX-", "") == "XX$path",
"bazel output path cannot begin with dash: $path")
if (string_replace(path, "{{BAZEL_TARGET_", "") != path) {
path = string_replace(path, "{{BAZEL_TARGET_NAME}}", _bazel_target)
path = string_replace(path,
"{{BAZEL_TARGET_PACKAGE}}",
_bazel_target_package)
path = string_replace(path,
"{{BAZEL_TARGET_OUT_DIR}}",
_bazel_target_out_dir)
path = string_replace(path,
"{{BAZEL_TARGET_OUT_PATH}}",
"${_bazel_target_out_dir}/${_bazel_target}")
}
# In case of typo with a substitution (e.g. writing {{BAZEL_TARGET_O_DIR}})
# GN will complain with an error but will not display the problematic
# so use an assert to provide a user-friendly message.
assert(string_replace(path, "{{", "") == path,
"Unsupported substitution in `copy_outputs` path: $path")
_bazel_outputs += [ path ]
_num_bazel_outputs += 1
}
_ninja_outputs = _ninja_output_list
_outputs = []
foreach(path, _ninja_outputs) {
_outputs += [ target_gen_dir + "/" + path ]
}
_bazel_inputs = []
if (defined(invoker.bazel_inputs)) {
_bazel_inputs = invoker.bazel_inputs
}
_bazel_build_files = []
_bazel_targets = []
foreach(label, invoker.bazel_targets) {
dir = get_label_info(label, "dir")
name = get_label_info(label, "name")
assert(get_path_info(dir, "file") != "...",
"`...` is not supported in bazel_targets")
_bazel_build_files += [ "${dir}/BUILD.bazel" ]
_bazel_targets += [ "${dir}:${name}" ]
}
# Generate a manifest file that references all transitive bazel_input_xxx()
# dependencies from _bazel_inputs, which will be passed to bazel_action.py
# for verification (and print a human-friendly message on error).
#
# NOTE: Place the manifest file under $target_out_dir because $target_gen_dir
# is cleared explicitly by the //build/bazel/tests/build_action/run_tests.py
# script. It also prevents these file to appear as extraneous differences.
_ninja_inputs_manifest = "$target_out_dir/${target_name}.ninja_inputs.json"
_ninja_inputs_target = "${target_name}.ninja_inputs"
generate_bazel_inputs_manifest(_ninja_inputs_target) {
forward_variables_from(invoker, [ "testonly" ])
output = _ninja_inputs_manifest
inputs_deps = _bazel_inputs
}
action(target_name) {
forward_variables_from(invoker,
[
"deps",
"no_output_dir_leaks",
"testonly",
"visibility",
"metadata",
])
script = "//build/bazel/scripts/bazel_action.py"
# NOTE: Do not add bazel_inputs_manifest to the list of inputs
# because *nothing* should depend on the target that generates
# it. Moreoever, it is testonly=true, which would prevent
# non-testonly bazel_action() from being added to the GN graph.
# This is "safe" because the file is generated at GN gen time.
inputs = [ bazel_launcher ] + _bazel_build_files
# Rebuild on any change known to git and let Bazel
# figure out if it actually needs to do work.
# See: fxbug.dev/117818#c2
inputs += [
"//.git/HEAD",
"//.git/index",
]
inputs += [ _ninja_inputs_manifest ]
depfile = "$target_gen_dir/$target_name.d"
outputs = _outputs
if (defined(invoker.directory_timestamp_outputs)) {
assert(
_allow_directory_in_outputs,
"directory_timestamp_outputs is not useful when there are no directory copies (allow_directory_in_outputs=false)")
foreach(path, invoker.directory_timestamp_outputs) {
outputs += [ target_gen_dir + "/" + path ]
}
}
_path_mapping = "${target_gen_dir}/${target_name}.path_mapping"
outputs += [ _path_mapping ]
args = [
"--bazel-launcher",
rebase_path(bazel_launcher, root_build_dir),
"--workspace-dir",
rebase_path(bazel_workspace_dir, root_build_dir),
"--command",
invoker.command,
"--gn-target-label",
get_label_info(":$target_name", "label_no_toolchain"),
"--legacy-inputs-manifest",
rebase_path(bazel_inputs_manifest, root_build_dir),
"--ninja-inputs-manifest",
rebase_path(_ninja_inputs_manifest, root_build_dir),
"--bazel-outputs",
] + _bazel_outputs + [ "--bazel-targets" ] + _bazel_targets +
[ "--ninja-outputs" ] + rebase_path(_outputs, root_build_dir) +
[
"--fuchsia-dir",
rebase_path("//", root_build_dir),
"--depfile",
rebase_path(depfile, root_build_dir),
"--path-mapping",
rebase_path(_path_mapping, root_build_dir),
]
if (_allow_directory_in_outputs) {
args += [ "--allow-directory-in-outputs" ]
}
args += [ "--" ]
# Ensure that the default platform is set correctly when this build action
# is invoked from a Fuchsia or host toolchain. For other ones. Other toolchains
# will inherit the 'default' platforms which may be set to
# //build/bazel/platforms:common in the future, and does not target any
# specific system intentionally.
if (is_host || is_fuchsia) {
args +=
[ "--platforms=//build/bazel/platforms:${current_os}_${current_cpu}" ]
}
if (bazel_quiet) {
args += [ "--config=quiet" ]
} else {
# Disable INFO lines and printing results, this makes Bazel output much
# less chatty when invoked from Ninja. https://fxbug.dev/126548
# Note that --show_result=0 is not supported by `bazel query`.
args += [ "--ui_event_filters=-info" ]
if (invoker.command != "query") {
args += [ "--show_result=0" ]
}
}
if (defined(invoker.extra_bazel_args)) {
args += invoker.extra_bazel_args
}
# Below are extra args to Bazel
# Forward GN sanitizer configuration to Bazel
if (is_asan) {
args += [ "--features=asan" ]
}
if (is_lsan) {
args += [ "--features=lsan" ]
}
if (is_msan) {
args += [ "--features=msan" ]
}
if (is_tsan) {
args += [ "--features=tsan" ]
}
if (is_ubsan) {
args += [ "--features=ubsan" ]
}
# Forward GN test coverage configuration to Bazel
if (is_coverage) {
args += [ "--features=coverage" ]
}
# Forward GN C/C++ compiler optimization configuration to Bazel
if (optimize == "none" || optimize == "default" ||
optimize == "sanitizer" || optimize == "coverage") {
args += [ "--compilation_mode=fastbuild" ]
} else if (optimize == "debug") {
args += [ "--compilation_mode=dbg" ]
} else if (optimize == "size" || optimize == "speed" ||
optimize == "profile") {
args += [ "--compilation_mode=opt" ]
} else {
assert("Unsupported mode optimize = $optimize")
}
if (!defined(deps)) {
deps = []
}
deps += [ ":${_ninja_inputs_target}" ]
deps += [ "//build/bazel:generate_main_workspace($default_toolchain)" ]
# Using the console pool prevents concurrent bazel build actions to run
# (otherwise the `bazel-bin` symlink target would become unpredictable
# resulting in erroneous outputs or errors), as well as allow Bazel to
# print its progress on smart terminals when `fx build` is invoked
# from one, which is handy to follow long-lasting commands.
pool = "//:console($default_toolchain)"
# This action definitely cannot be hermetic. Callers should add
# themselves to the //build:non_hermetic_deps visibility list.
hermetic_deps = false
}
}