blob: 5d0720849b612735b2fc094bc6f06021f1c64f9f [file]
# Copyright 2026 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Bazel rule for creating incrementally-linked hermetic C/C++ libraries.
Derived from
https://cs.opensource.google/fuchsia/fuchsia/+/main:build/toolchain/hermetic_source_set.gni
An incrementally-linked hermetic library takes compiled objects from its `srcs`
and private dependencies (`deps`), performs a relocatable link (`ld -r`), and
localizes all internal symbols using `objcopy --keep-global-symbol`. Only
symbols explicitly listed in `global_symbols` (or `global_symbols_file`) remain
globally visible. Public dependencies and headers passed via `public_deps` /
`hdrs` are propagated to consumers without localization.
"""
load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES")
load(
"@bazel_tools//tools/cpp:toolchain_utils.bzl",
"find_cpp_toolchain",
"use_cpp_toolchain",
)
load("@rules_cc//cc/common:cc_common.bzl", "cc_common")
load("@rules_cc//cc/common:cc_info.bzl", "CcInfo")
def _get_static_library_name(name):
prefix = "" if name.startswith("lib") else "lib"
return prefix + name + ".a"
def _pw_hermetic_cc_library_impl(ctx):
cc_toolchain = find_cpp_toolchain(ctx)
feature_configuration = cc_common.configure_features(
ctx = ctx,
cc_toolchain = cc_toolchain,
requested_features = ctx.features,
unsupported_features = ctx.disabled_features,
)
use_pic = cc_common.is_enabled(
feature_configuration = feature_configuration,
feature_name = "pic",
)
# Compile srcs and collect compilation context
compilation_contexts = []
for dep in ctx.attr.deps + ctx.attr.public_deps:
if CcInfo in dep:
compilation_contexts.append(dep[CcInfo].compilation_context)
compilation_context, compilation_outputs = cc_common.compile(
name = ctx.label.name,
actions = ctx.actions,
feature_configuration = feature_configuration,
cc_toolchain = cc_toolchain,
srcs = ctx.files.srcs,
public_hdrs = ctx.files.hdrs,
includes = ctx.attr.includes,
strip_include_prefix = ctx.attr.strip_include_prefix,
defines = ctx.attr.defines,
user_compile_flags = ctx.attr.copts,
compilation_contexts = compilation_contexts,
)
src_objects = []
if use_pic and compilation_outputs.pic_objects:
src_objects = compilation_outputs.pic_objects
elif compilation_outputs.objects:
src_objects = compilation_outputs.objects
# Collect linking inputs from private `deps`
dep_objects = []
dep_static_libs = []
for dep in ctx.attr.deps:
if CcInfo in dep:
linking_context = dep[CcInfo].linking_context
for linker_input in linking_context.linker_inputs.to_list():
for lib in linker_input.libraries:
if use_pic:
# PIC: Prefer PIC inputs
if lib.pic_static_library:
dep_static_libs.append(lib.pic_static_library)
elif lib.static_library:
dep_static_libs.append(lib.static_library)
elif lib.pic_objects:
dep_objects.extend(lib.pic_objects)
elif lib.objects:
dep_objects.extend(lib.objects)
else:
# Non-PIC: Prefer non-PIC inputs
if lib.static_library:
dep_static_libs.append(lib.static_library)
elif lib.pic_static_library:
dep_static_libs.append(lib.pic_static_library)
elif lib.objects:
dep_objects.extend(lib.objects)
elif lib.pic_objects:
dep_objects.extend(lib.pic_objects)
# Deduplicate dependencies while preserving order
dep_static_libs = depset(dep_static_libs).to_list()
dep_objects = depset(dep_objects).to_list()
# Perform relocatable link (ld -r / CC -r)
linker_path = cc_common.get_tool_for_action(
feature_configuration = feature_configuration,
action_name = ACTION_NAMES.cpp_link_executable,
)
relocatable_o = ctx.actions.declare_file(ctx.label.name + ".relocatable.o")
link_inputs = list(src_objects) + list(dep_objects) + list(dep_static_libs)
if not link_inputs:
fail("""pw_hermetic_cc_library target %s has no source or dependency
object files to link.""" % ctx.label)
link_variables = cc_common.create_link_variables(
feature_configuration = feature_configuration,
cc_toolchain = cc_toolchain,
)
toolchain_link_flags = cc_common.get_memory_inefficient_command_line(
feature_configuration = feature_configuration,
action_name = ACTION_NAMES.cpp_link_executable,
variables = link_variables,
)
link_args = list(toolchain_link_flags) + [
"-r",
"-nostdlib",
# Toolchain executable link flags include runtime library options (e.g.
# -pthread, --rtlib=compiler-rt, --unwindlib=libunwind) that clang++ warns
# about as unused when performing a relocatable link (-r -nostdlib).
"-Wno-unused-command-line-argument",
# Override -Wl,--gc-sections in toolchain config, as that is applicable
# only for a true, final link.
"-Wl,--no-gc-sections",
]
if ctx.attr.linker_flags:
link_args.extend(ctx.attr.linker_flags)
# Require global symbols to be defined / kept
for sym in ctx.attr.global_symbols:
link_args.append("-Wl,--undefined=" + sym)
if dep_static_libs:
link_args.append("-Wl,--whole-archive")
for lib in dep_static_libs:
link_args.append(lib.path)
link_args.append("-Wl,--no-whole-archive")
for obj in src_objects + dep_objects:
link_args.append(obj.path)
link_args.extend(["-o", relocatable_o.path])
ctx.actions.run(
outputs = [relocatable_o],
inputs = depset(
direct = link_inputs,
transitive = [cc_toolchain.all_files],
),
executable = linker_path,
arguments = link_args,
mnemonic = "HermeticLink",
progress_message = "Hermetic partial link for %s" % ctx.label,
)
# Localize symbols with objcopy
if (not ctx.attr.global_symbols and not ctx.file.global_symbols_file and
not ctx.attr.localize_hidden):
fail(
"pw_hermetic_cc_library target {}".format(ctx.label) +
" must specify 'global_symbols', 'global_symbols_file', or set" +
" 'localize_hidden = True'.",
)
objcopy_path = cc_common.get_tool_for_action(
feature_configuration = feature_configuration,
action_name = ACTION_NAMES.objcopy_embed_data,
)
hermetic_o = ctx.actions.declare_file(ctx.label.name + ".hermetic.o")
objcopy_args = []
objcopy_inputs = [relocatable_o]
if ctx.file.global_symbols_file:
objcopy_args.append("--keep-global-symbols=" +
ctx.file.global_symbols_file.path)
objcopy_inputs.append(ctx.file.global_symbols_file)
if ctx.attr.global_symbols:
for sym in ctx.attr.global_symbols:
objcopy_args.append("--keep-global-symbol=" + sym)
elif not ctx.file.global_symbols_file and ctx.attr.localize_hidden:
objcopy_args.append("--localize-hidden")
if ctx.attr.objcopy_flags:
objcopy_args.extend(ctx.attr.objcopy_flags)
objcopy_args.extend([relocatable_o.path, hermetic_o.path])
ctx.actions.run(
outputs = [hermetic_o],
inputs = depset(
direct = objcopy_inputs,
transitive = [cc_toolchain.all_files],
),
executable = objcopy_path,
arguments = objcopy_args,
mnemonic = "HermeticObjcopy",
progress_message = "Localizing symbols for %s" % ctx.label,
)
# Undefined symbols and init/fini section verification
validation_outputs = []
verify_stamp = ctx.actions.declare_file(
ctx.label.name + ".verify.stamp",
)
args = ctx.actions.args()
args.add("--elf-file", hermetic_o.path)
args.add("--stamp-file", verify_stamp.path)
args.add("--label", str(ctx.label))
if ctx.attr.allow_init:
args.add("--allow-init")
if ctx.attr.allow_fini:
args.add("--allow-fini")
for sym in ctx.attr.undefined_symbols:
args.add("--allowed-symbol", sym)
ctx.actions.run(
outputs = [verify_stamp],
inputs = [hermetic_o],
executable = ctx.executable._verifier,
arguments = [args],
mnemonic = "VerifyHermetic",
progress_message = (
"Verifying hermetic library constraints for %s" % ctx.label
),
)
validation_outputs.append(verify_stamp)
# Archive into .a library
ar_path = cc_common.get_tool_for_action(
feature_configuration = feature_configuration,
action_name = ACTION_NAMES.cpp_link_static_library,
)
output_a_name = _get_static_library_name(ctx.label.name)
output_a = ctx.actions.declare_file(output_a_name)
ctx.actions.run(
outputs = [output_a],
inputs = depset(
direct = [hermetic_o],
transitive = [cc_toolchain.all_files],
),
executable = ar_path,
arguments = ["rcs", output_a.path, hermetic_o.path],
mnemonic = "HermeticArchive",
progress_message = "Archiving hermetic library for %s" % ctx.label,
)
# Build CcInfo
library_to_link = cc_common.create_library_to_link(
actions = ctx.actions,
feature_configuration = feature_configuration,
cc_toolchain = cc_toolchain,
static_library = output_a,
alwayslink = ctx.attr.alwayslink,
)
linker_input = cc_common.create_linker_input(
owner = ctx.label,
libraries = depset([library_to_link]),
)
public_linking_contexts = [
dep[CcInfo].linking_context
for dep in ctx.attr.public_deps
if CcInfo in dep
]
new_linking_context = cc_common.create_linking_context(
linker_inputs = depset(
direct = [linker_input],
transitive = [lc.linker_inputs for lc in public_linking_contexts],
),
)
public_compilation_contexts = [
dep[CcInfo].compilation_context
for dep in ctx.attr.public_deps
if CcInfo in dep
]
merged_compilation_context = cc_common.merge_compilation_contexts(
compilation_contexts = [compilation_context] + public_compilation_contexts,
)
return [
DefaultInfo(files = depset([output_a])),
OutputGroupInfo(_validation = depset(validation_outputs)),
CcInfo(
compilation_context = merged_compilation_context,
linking_context = new_linking_context,
),
]
pw_hermetic_cc_library = rule(
implementation = _pw_hermetic_cc_library_impl,
doc = """Produces an incrementally-linked hermetic C/C++ static library.
An incrementally-linked hermetic library compiles sources from `srcs`,
takes private dependencies from `deps`, performs a relocatable link
(`ld -r`), and localizes all internal symbols using `objcopy`.
Only linkage symbols explicitly listed in `global_symbols` (or
`global_symbols_file`) remain globally exported. Public dependencies and
headers specified in `public_deps` and `hdrs` are propagated to downstream
consumers without localization.
This rule only supports ELF-based toolchains.
""",
attrs = {
"allow_fini": attr.bool(
default = False,
doc = """If True, allow .fini_array section
(global destructors).""",
),
"allow_init": attr.bool(
default = False,
doc = """If True, allow .init_array section
(global constructors).""",
),
"alwayslink": attr.bool(
default = False,
doc = "If True, force linking of this library into binaries.",
),
"copts": attr.string_list(
doc = "Compiler flags for compilation.",
),
"defines": attr.string_list(
doc = "Preprocessor defines.",
),
"deps": attr.label_list(
providers = [CcInfo],
doc = """Private dependencies to be partially linked into the
hermetic library and localized.""",
),
"global_symbols": attr.string_list(
doc = """List of linkage symbol names to retain as global symbols.
All other defined symbols will be localized.""",
),
"global_symbols_file": attr.label(
allow_single_file = True,
doc = """File containing symbol names to retain as global symbols
(one per line).""",
),
"hdrs": attr.label_list(
allow_files = True,
doc = "Public header files to expose to consumers.",
),
"includes": attr.string_list(
doc = "Include directories.",
),
"linker_flags": attr.string_list(
default = ["-Wl,--force-group-allocation"],
doc = "Additional linker flags for the relocatable link step.",
),
"localize_hidden": attr.bool(
default = False,
doc = """If True and global_symbols is empty, localize all
hidden symbols.""",
),
"objcopy_flags": attr.string_list(
doc = "Additional flags for objcopy.",
),
"public_deps": attr.label_list(
providers = [CcInfo],
doc = """Public dependencies propagated to consumers without
localization.""",
),
"srcs": attr.label_list(
allow_files = True,
doc = "Source files to compile into the hermetic library.",
),
"strip_include_prefix": attr.string(
doc = "Prefix to strip from header paths.",
),
"undefined_symbols": attr.string_list(
doc = "List of allowed undefined symbol names.",
),
"_verifier": attr.label(
default = "//pw_build/py:verify_hermetic_elf",
executable = True,
cfg = "exec",
),
},
provides = [CcInfo],
fragments = ["cpp"],
toolchains = use_cpp_toolchain(),
)