blob: 18f56c35ff1df8897cafb11c6207e08f5782cb66 [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.
load("@fuchsia_rules_common//:local_actions.bzl", "LOCAL_ONLY_ACTION_KWARGS")
load(
"@fuchsia_rules_common//assembly:product_configuration.bzl",
"BUILD_TYPES",
"FEATURE_SET_LEVELS",
)
load("@fuchsia_rules_common//assembly:providers.bzl", "AssemblyInputBundleInfo")
load("@fuchsia_rules_common//packages:providers.bzl", "FuchsiaPackageInfo")
def _assembly_input_bundle_impl(ctx):
out_dir = ctx.actions.declare_directory(ctx.label.name)
args = ctx.actions.args()
args.add("create")
args.add("--outdir", out_dir.path)
if ctx.attr.experimental:
args.add("--experimental")
# Validate and add the allowed_in, scrutiny_required, and auto_include_in values.
for value in ctx.attr.allowed_in:
_validate_allowed_in_value("allowed_in", value)
args.add("--allowed-in", value)
for value in ctx.attr.scrutiny_required:
_validate_allowed_in_value("scrutiny_required", value)
args.add("--scrutiny-required", value)
for value in ctx.attr.auto_include_in:
_validate_allowed_in_value("auto_include_in", value)
args.add("--auto-include-in", value)
inputs = []
# Handle package lists
package_sets = {
"base_packages": "--base-pkg-list",
"cache_packages": "--cache-pkg-list",
"flexible_packages": "--flexible-pkg-list",
"system_packages": "--system-pkg-list",
"bootfs_packages": "--bootfs-pkg-list",
"bootfs_or_base_packages": "--bootfs-or-base-pkg-list",
"on_demand_packages": "--on-demand-pkg-list",
"anchored_automatic_packages": "--anchored-automatic-pkg-list",
"anchored_on_demand_packages": "--anchored-on-demand-pkg-list",
}
for attr_name, arg_name in package_sets.items():
deps = getattr(ctx.attr, attr_name)
if deps:
manifests = [dep[FuchsiaPackageInfo].package_manifest.path for dep in deps]
list_file = ctx.actions.declare_file(ctx.label.name + "_" + attr_name + ".json")
ctx.actions.write(
output = list_file,
content = json.encode(manifests),
)
args.add(arg_name, list_file)
inputs.append(list_file)
# Add all inputs from the packages
for dep in deps:
inputs.extend(dep[FuchsiaPackageInfo].files)
if ctx.attr.kernel_cmdline:
cmdline_file = ctx.actions.declare_file(ctx.label.name + "_kernel_cmdline.json")
ctx.actions.write(
output = cmdline_file,
content = json.encode(ctx.attr.kernel_cmdline),
)
args.add("--kernel-cmdline", cmdline_file)
inputs.append(cmdline_file)
if ctx.attr.shell_commands:
parsed_shell_commands = json.decode(ctx.attr.shell_commands)
for shell_cmd in parsed_shell_commands:
if "package" not in shell_cmd:
fail("shell_command entries must specify a package name: %s" % shell_cmd)
if "components" not in shell_cmd:
fail("shell_command components must be a list of strings pointing to binaries that are components in the package that make up the package: %s" % shell_cmd)
for key in shell_cmd.keys():
if key not in ["package", "components", "bootfs_package"]:
fail("unknown field in shell_command entry: %s" % key)
shell_commands_file = ctx.actions.declare_file(ctx.label.name + "shell_commands.json")
ctx.actions.write(
output = shell_commands_file,
content = json.encode_indent(parsed_shell_commands),
)
args.add("--shell-cmds-list", shell_commands_file)
inputs.append(shell_commands_file)
if ctx.files.memory_buckets:
for memory_bucket_file in ctx.files.memory_buckets:
args.add("--memory-buckets", memory_bucket_file)
inputs.append(memory_bucket_file)
if ctx.attr.bootfs_files_package:
dep = ctx.attr.bootfs_files_package
args.add("--bootfs-files-package", dep[FuchsiaPackageInfo].package_manifest.path)
inputs.append(dep[FuchsiaPackageInfo].package_manifest)
inputs.extend(dep[FuchsiaPackageInfo].files)
if ctx.file.qemu_kernel:
args.add("--qemu-kernel", ctx.file.qemu_kernel.path)
inputs.append(ctx.file.qemu_kernel)
if ctx.file.kernel:
args.add("--kernel", ctx.file.kernel.path)
inputs.append(ctx.file.kernel)
# Handle shards and generate compiled packages JSON
compiled_packages = {}
if ctx.attr.compiled_packages:
parsed = json.decode(ctx.attr.compiled_packages)
# Build map from canonical label to path.
# Since str(dep.label) returns the canonical label string, we rely on the macro
# having canonicalized the labels in the JSON to match this format.
path_map = {}
for dep in ctx.attr.compiled_packages_inputs:
lbl = str(dep.label)
if FuchsiaPackageInfo in dep:
# For package inputs, map the label to the path of its package manifest,
# which is what the AIB creation tool expects.
path_map[lbl] = dep[FuchsiaPackageInfo].package_manifest.path
# Add all the package files (including the manifest) to the action's inputs.
inputs.extend(dep[FuchsiaPackageInfo].files)
else:
# For non-package inputs, they need to be a single file.
files = dep.files.to_list()
if len(files) > 1:
fail(
("Label '%s' resolved to multiple files, but only single " +
"files are supported in 'shards' and 'component_includes' " +
"fields of 'compiled_packages'.") % lbl,
)
if not files:
fail("Label '%s' did not resolve to any files." % lbl)
path_map[lbl] = files[0].path
# Add the file to the action's inputs.
inputs.extend(files)
# Update parsed structure with paths
for pkg in parsed:
if "packages" in pkg:
pkg["packages"] = [path_map[lbl] for lbl in pkg["packages"]]
components = {}
if "components" in pkg:
for comp in pkg["components"]:
if "shards" in comp:
comp["shards"] = [path_map[lbl] for lbl in comp["shards"]]
components[comp["component_name"]] = comp
pkg["components"] = components
if "component_includes" in pkg:
for inc in pkg["component_includes"]:
if "source" in inc:
inc["source"] = path_map[inc["source"]]
compiled_packages[pkg["name"]] = pkg
shard_configs = [
("bootstrap_shards", "bootstrap", True),
("core_shards", "core", False),
("root_shards", "root", True),
("toolbox_shards", "toolbox", True),
]
for attr_name, name, is_bootfs in shard_configs:
shards = getattr(ctx.attr, attr_name)
if shards:
shard_paths = [f.path for f in getattr(ctx.files, attr_name)]
# Add the files from the hardcoded shards to the action's inputs.
inputs.extend(getattr(ctx.files, attr_name))
# Get or create package
pkg = compiled_packages.setdefault(name, {
"name": name,
"components": {},
"bootfs_package": is_bootfs,
})
# Get or create component
comp = pkg["components"].setdefault(name, {
"component_name": name,
"shards": [],
})
comp["shards"].extend(shard_paths)
if compiled_packages:
# Convert components dict to a list for JSON.
compiled_packages_list = []
for pkg in compiled_packages.values():
new_pkg = dict(pkg)
new_pkg["components"] = list(pkg["components"].values())
compiled_packages_list.append(new_pkg)
compiled_packages_file = ctx.actions.declare_file(ctx.label.name + "_compiled_packages.json")
ctx.actions.write(
output = compiled_packages_file,
content = json.encode_indent(compiled_packages_list),
)
args.add("--compiled-packages", compiled_packages_file)
inputs.append(compiled_packages_file)
# Handle drivers
drivers_list = []
if ctx.attr.drivers:
parsed_drivers = json.decode(ctx.attr.drivers)
path_map = {}
for dep in ctx.attr.drivers_inputs:
lbl = str(dep.label)
path_map[lbl] = dep[FuchsiaPackageInfo].package_manifest.path
inputs.extend(dep[FuchsiaPackageInfo].files)
for drv in parsed_drivers:
if "package_target" not in drv:
fail("driver entries must specify a package_target: %s" % drv)
if "driver_components" not in drv:
fail("driver entries must specify driver_components: %s" % drv)
if "set" not in drv:
fail("driver entries must specify set: %s" % drv)
lbl = drv["package_target"]
if lbl in path_map:
drv["package_target"] = path_map[lbl]
else:
fail("Driver package %s not found in inputs" % lbl)
drivers_list.append(drv)
if drivers_list:
drivers_file = ctx.actions.declare_file(ctx.label.name + "_drivers.json")
ctx.actions.write(
output = drivers_file,
content = json.encode(drivers_list),
)
args.add("--drivers-list", drivers_file)
inputs.append(drivers_file)
# Handle config_data
config_data_list = []
if ctx.attr.config_data:
parsed_config_data = json.decode(ctx.attr.config_data)
path_map = {}
for dep in ctx.attr.config_data_inputs:
lbl = str(dep.label)
files = dep.files.to_list()
if len(files) > 1:
fail(
("Label '%s' resolved to multiple files, but only single " +
"files are supported in the 'source' field of 'config_data'.") % lbl,
)
if not files:
fail("Label '%s' did not resolve to any files in config_data" % lbl)
path_map[lbl] = files[0].path
inputs.extend(files)
for pkg in parsed_config_data:
package_name = pkg["package_name"]
for file in pkg["files"]:
lbl = file["source"]
if lbl in path_map:
source_path = path_map[lbl]
else:
fail("Config data source %s not found in inputs" % lbl)
config_data_list.append({
"package_name": package_name,
"source": source_path,
"destination": file["destination"],
})
if config_data_list:
config_data_file = ctx.actions.declare_file(ctx.label.name + "_config_data.json")
ctx.actions.write(
output = config_data_file,
content = json.encode(config_data_list),
)
args.add("--config-data-list", config_data_file)
inputs.append(config_data_file)
ctx.actions.run(
inputs = inputs,
outputs = [out_dir],
executable = ctx.executable._tool,
arguments = [args],
mnemonic = "AssemblyInputBundle",
progress_message = "Creating Assembly Input Bundle %s" % ctx.label.name,
**LOCAL_ONLY_ACTION_KWARGS
)
return [
DefaultInfo(files = depset([out_dir])),
AssemblyInputBundleInfo(
name = ctx.label.name,
directory = out_dir.path,
),
]
_assembly_input_bundle = rule(
implementation = _assembly_input_bundle_impl,
attrs = {
"allowed_in": attr.string_list(),
"scrutiny_required": attr.string_list(),
"auto_include_in": attr.string_list(),
"experimental": attr.bool(),
"base_packages": attr.label_list(providers = [FuchsiaPackageInfo]),
"cache_packages": attr.label_list(providers = [FuchsiaPackageInfo]),
"flexible_packages": attr.label_list(providers = [FuchsiaPackageInfo]),
"system_packages": attr.label_list(providers = [FuchsiaPackageInfo]),
"bootfs_packages": attr.label_list(providers = [FuchsiaPackageInfo]),
"bootfs_or_base_packages": attr.label_list(providers = [FuchsiaPackageInfo]),
"on_demand_packages": attr.label_list(providers = [FuchsiaPackageInfo]),
"anchored_automatic_packages": attr.label_list(providers = [FuchsiaPackageInfo]),
"anchored_on_demand_packages": attr.label_list(providers = [FuchsiaPackageInfo]),
"kernel_cmdline": attr.string_list(),
"shell_commands": attr.string(),
"memory_buckets": attr.label_list(allow_files = True),
"bootstrap_shards": attr.label_list(allow_files = True),
"core_shards": attr.label_list(allow_files = True),
"root_shards": attr.label_list(allow_files = True),
"toolbox_shards": attr.label_list(allow_files = True),
"compiled_packages": attr.string(),
# Internal attribute to pass labels extracted from compiled_packages by the macro.
"compiled_packages_inputs": attr.label_list(allow_files = True),
"drivers": attr.string(),
"drivers_inputs": attr.label_list(providers = [FuchsiaPackageInfo]),
"config_data": attr.string(),
"config_data_inputs": attr.label_list(allow_files = True),
"bootfs_files_package": attr.label(providers = [FuchsiaPackageInfo]),
"qemu_kernel": attr.label(allow_single_file = True),
"kernel": attr.label(allow_single_file = True),
"_tool": attr.label(
default = "//build/assembly/scripts:assembly_input_bundle_tool",
executable = True,
cfg = "exec",
),
},
)
def assembly_input_bundle(
name,
allowed_in = [],
scrutiny_required = [],
auto_include_in = [],
experimental = False,
base_packages = [],
cache_packages = [],
flexible_packages = [],
system_packages = [],
bootfs_packages = [],
bootfs_or_base_packages = [],
on_demand_packages = [],
anchored_automatic_packages = [],
anchored_on_demand_packages = [],
kernel_cmdline = [],
shell_commands = [],
memory_buckets = [],
bootstrap_shards = [],
core_shards = [],
root_shards = [],
toolbox_shards = [],
compiled_packages = [],
drivers = [],
config_data = [],
qemu_kernel = None,
kernel = None,
bootfs_files_package = None,
**kwargs):
"""Creates an Assembly Input Bundle.
Args:
name: The name of the target.
experimental: [boolean, default False]
Whether this AIB is experimental and should be excluded from the scrutiny goldens.
Experimental AIBs must be available in userdebug and never in user. Experimental AIBs
are allowed in any feature set level.
The typical way to mark an AIB as available in userdebug but not user is to
set `experimental = true` and `allowed_in = ["userdebug", "eng"]`.
allowed_in: [list of strings] Which feature set + build type combinations this AIB is
allowed to be included in. Assembly asserts during product assembly that
the AIB is not included in any feature set + build type combinations
that are not in this list.
Options:
- "everything"
- "standard", "utility", "bootstrap", "embeddable"
- "user", "userdebug", "eng"
- "standard::user", "utility::eng", etc.
scrutiny_required [list of strings]
Which feature set + build type combinations to expect the contents of this AIB to be
added to. Adding to this list will force the contents of this AIB to be listed as
required in scrutiny goldens. Valid values are the same as for 'allowed_in'.
auto_include_in: [list of strings]
Which feature set + build type combinations to automatically include this AIB in. Valid
values are the same as for 'allowed_in'
base_packages: [list of labels] Package targets to include in the base package set.
cache_packages: [list of labels] Package targets to include in the cache package set.
flexible_packages: [list of labels] Package targets that assembly may choose to put in base,
cache, or elsewhere.
system_packages: [list of labels] Package targets to include in the system package set.
bootfs_packages: [list of labels] Package targets to include in the bootfs package set.
bootfs_files_package: [label] A label pointing to a package target
containing bootfs files to include in the assembly input bundle.
This package is typically generated in GN to handle resource/binary
aggregation.
bootfs_or_base_packages: [list of labels] Package targets to include in the bootfs_or_base
package set.
on_demand_packages: [list of labels] Package targets to include in the on-demand package
set.
anchored_automatic_packages: [list of labels] Package targets to include in the anchored
automatic package set.
anchored_on_demand_packages: [list of labels] Package targets to include in the anchored
on-demand package set.
kernel_cmdline: [list of strings] Kernel cmdline arguments.
shell_commands: [list of dicts] A list of dicts that describe the shell commands for each
listed package
Example:
shell_commands = [
{
package = "//third_party/sbase"
components = [ "ls" ]
},
]
memory_buckets: [list of labels] Paths to memory bucket configs that should get merged
and passed to memory monitor.
bootstrap_shards: [list of labels] A list of CML shard files to add to the "bootstrap"
component of the "bootstrap" compiled package. This package is a bootfs package.
core_shards: [list of labels] A list of CML shard files to add to the "core"
component of the "core" compiled package.
root_shards: [list of labels] A list of CML shard files to add to the "root"
component of the "root" compiled package. This package is a bootfs package.
toolbox_shards: [list of labels] A list of CML shard files to add to the "toolbox"
component of the "toolbox" compiled package. This package is a bootfs package.
compiled_packages: [list of dicts] List of dicts that describe packages that are to be built
dynamically by Assembly, for example, the `core` package.
Example:
compiled_packages = [
{
name = "core"
packages = [ "//path/to:package" ]
components = [
{
component_name = "core"
shards = [
"//src/sys/process-resolver:process_resolver.core_shard.cml",
]
},
]
component_includes = [ ... ]
},
]
shards [optional]
[list of labels] List of CML files to merge together when
compiling the component.
cmc_features [optional]
[list of strings] List of CMC features to enable for component
compilation.
component_includes [optional]
[list of dicts] List of source/destination pairs related to a
compiled package in the compiled_packages list that specifies cml files
that can be included by any cml shards in any platform AIB for the given
package. These files will be included in the Assembly Input Bundle.
drivers: [list of dicts] List of dicts that describe driver packages to include in the assembly input bundle.
Example:
drivers = [
{
package_target = "//path/to:driver_package"
driver_components = [ "meta/driver.cm" ]
set = "base"
}
]
config_data: [list of dicts] List of dicts that describe config data associated with a given package.
Example:
config_data = [
{
"package_name": "example_package",
"files": [
{
"source": "//src/path_to_config:config1.json",
"destination": "config1.json"
},
]
}
]
qemu_kernel: [label] Optional label to a single file to use as the qemu kernel.
**kwargs: Other arguments to pass to the rule.
"""
# Extract labels from compiled_packages to pass to the rule.
#
# We also canonicalize the labels here (converting them to strings using str(Label(...))).
# This is done because the rule implementation needs to look up these labels in the
# compiled_packages_inputs list to get their file paths. Since str(dep.label) in the
# implementation returns a canonical label string, we must ensure the strings in our JSON
# match that canonical format, regardless of how the user wrote the label (e.g., relative
# vs absolute).
#
# e.g. //some/path/to:foo/bar.cml => "@@//some/path/to:foo/bar.cml"
compiled_packages_inputs = []
for pkg in compiled_packages:
if "packages" in pkg:
packages_labels = [str(Label(lbl)) for lbl in pkg["packages"]]
pkg["packages"] = packages_labels
# Add the package labels to the inputs list so Bazel tracks them as dependencies
# and makes their files available to the action.
compiled_packages_inputs.extend(packages_labels)
if "components" in pkg:
for comp in pkg["components"]:
if "shards" in comp:
shards_labels = [str(Label(lbl)) for lbl in comp["shards"]]
comp["shards"] = shards_labels
# Add the shard labels to the inputs list so Bazel tracks them as dependencies.
compiled_packages_inputs.extend(shards_labels)
if "component_includes" in pkg:
for inc in pkg["component_includes"]:
if "source" in inc:
source_label = str(Label(inc["source"]))
inc["source"] = source_label
# Add the component include source labels to the inputs list so Bazel tracks them as dependencies.
compiled_packages_inputs.append(source_label)
compiled_packages_inputs = depset(compiled_packages_inputs).to_list()
# Extract labels from drivers to pass to the rule and canonicalize them.
#
# We canonicalize the labels here (converting them to strings using str(Label(...))).
# This is done because the rule implementation needs to look up these labels in the
# drivers_inputs list to get their file paths. Since str(dep.label) in the
# implementation returns a canonical label string, we must ensure the strings in our JSON
# match that canonical format.
#
# e.g. //some/path/to:foo/bar.cml => "@@//some/path/to:foo/bar.cml"
drivers_inputs = []
for drv in drivers:
if "package_target" in drv:
driver_label = str(Label(drv["package_target"]))
drv["package_target"] = driver_label
drivers_inputs.append(driver_label)
drivers_inputs = depset(drivers_inputs).to_list()
# Extract labels from config_data to pass to the rule and canonicalize them.
#
# We canonicalize the labels here (converting them to strings using str(Label(...))).
# This is done because the rule implementation needs to look up these labels in the
# config_data_inputs list to get their file paths. Since str(dep.label) in the
# implementation returns a canonical label string, we must ensure the strings in our JSON
# match that canonical format.
#
# e.g. //some/path/to:foo/bar.cml => "@@//some/path/to:foo/bar.cml"
config_data_inputs = []
for pkg in config_data:
if "files" in pkg:
for file in pkg["files"]:
if "source" in file:
source_label = str(Label(file["source"]))
file["source"] = source_label
config_data_inputs.append(source_label)
config_data_inputs = depset(config_data_inputs).to_list()
_assembly_input_bundle(
name = name,
experimental = experimental,
allowed_in = allowed_in,
scrutiny_required = scrutiny_required,
auto_include_in = auto_include_in,
base_packages = base_packages,
cache_packages = cache_packages,
flexible_packages = flexible_packages,
system_packages = system_packages,
bootfs_packages = bootfs_packages,
bootfs_or_base_packages = bootfs_or_base_packages,
on_demand_packages = on_demand_packages,
anchored_automatic_packages = anchored_automatic_packages,
anchored_on_demand_packages = anchored_on_demand_packages,
kernel_cmdline = kernel_cmdline,
shell_commands = json.encode_indent(shell_commands),
memory_buckets = memory_buckets,
bootstrap_shards = bootstrap_shards,
core_shards = core_shards,
root_shards = root_shards,
toolbox_shards = toolbox_shards,
compiled_packages = json.encode(compiled_packages),
compiled_packages_inputs = compiled_packages_inputs,
drivers = json.encode(drivers),
drivers_inputs = drivers_inputs,
config_data = json.encode(config_data),
config_data_inputs = config_data_inputs,
qemu_kernel = qemu_kernel,
kernel = kernel,
bootfs_files_package = bootfs_files_package,
**kwargs
)
# Construct the lists of valid build types and feature set levels. Only do this once, not each
# time we validate a value.
_build_types = json.decode(json.encode(BUILD_TYPES)).values()
_feature_set_levels = json.decode(json.encode(FEATURE_SET_LEVELS)).values()
def _validate_allowed_in_value(list_name, value):
if value == "everything":
return
if value in _build_types:
return
if value in _feature_set_levels:
return
tokens = value.split("::")
if len(tokens) == 2:
if tokens[0] in _feature_set_levels and tokens[1] in _build_types:
return
fail("'%s' is an invalid value for '%s'. The valid values are '" % (value, list_name) + "', '".join(_build_types + _feature_set_levels) + "' or a '<feature_set_level>::<build_type>'.")
def _assembly_resources_directory_impl(ctx):
out_dir = ctx.actions.declare_directory(ctx.label.name)
# We're using a shell script to create the resources directory because we cannot have
# Bazel directly copy files into a directory that we declare with declare_directory().
cmds = [
"mkdir -p %s" % out_dir.path,
"printf '{}' > %s/assembly_config.json" % out_dir.path,
]
for f in ctx.files.sources:
dest_name = f.basename
if dest_name == "latest_commit_date.txt" or dest_name == "build_info_version.txt":
dest_name = "version.txt"
cmds.append("cp %s %s/%s" % (f.path, out_dir.path, dest_name))
ctx.actions.run_shell(
inputs = ctx.files.sources,
outputs = [out_dir],
command = "\n".join(cmds),
mnemonic = "AssemblyResourcesDirectory",
progress_message = "Creating Assembly Resources Directory %s" % ctx.label.name,
)
return [
DefaultInfo(files = depset([out_dir])),
AssemblyInputBundleInfo(
name = ctx.label.name,
directory = out_dir.path,
),
]
assembly_resources_directory = rule(
implementation = _assembly_resources_directory_impl,
attrs = {
"sources": attr.label_list(
allow_files = True,
mandatory = True,
doc = "List of resource files to be added to the directory.",
),
},
)