Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rust/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ bzl_library(
"//rust/private:bzl_lib",
"//rust/settings:bzl_lib",
"@bazel_features//:features",
"@rules_cc//cc:defs_bzl",
],
)

Expand Down
6 changes: 5 additions & 1 deletion rust/private/common.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ which exports the `rust_common` struct.
In the Bazel lingo, `rust_common` gives the access to the Rust Sandwich API.
"""

load(":providers.bzl", "CrateGroupInfo", "CrateInfo", "DepInfo", "DepVariantInfo", "StdLibInfo", "TestCrateInfo")
load(":providers.bzl", "CrateGroupInfo", "CrateInfo", "DepInfo", "DepVariantInfo", "RustCrateIdentityInfo", "RustLinkClosureInfo", "StdLibInfo", "TestCrateInfo")

# This constant only represents the default value for attributes and macros
# defined in `rules_rust`. Like any attribute public attribute, it can be
Expand Down Expand Up @@ -67,13 +67,17 @@ def _create_crate_info(**kwargs):
kwargs.update({"root_path": ""})
if not "owner" in kwargs:
kwargs.update({"owner": None})
if not "crate_identity" in kwargs:
kwargs.update({"crate_identity": None})
return CrateInfo(**kwargs)

rust_common = struct(
create_crate_info = _create_crate_info,
crate_info = CrateInfo,
dep_info = DepInfo,
dep_variant_info = DepVariantInfo,
rust_crate_identity_info = RustCrateIdentityInfo,
rust_link_closure_info = RustLinkClosureInfo,
stdlib_info = StdLibInfo,
test_crate_info = TestCrateInfo,
crate_group_info = CrateGroupInfo,
Expand Down
45 changes: 45 additions & 0 deletions rust/private/providers.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ CrateInfo = provider(
"List[str]: The set of enabled cfgs for this crate. Note that this field is populated only " +
"when @rules_rust//rust/settings:collect_cfgs is set."
),
"crate_identity": (
"RustCrateIdentityInfo, optional: The logical Rust library identity and configured crate " +
"instance, if this target carries one. Absent for ordinary handwritten targets."
),
"compile_data": "depset[File]: Compile data required by this crate.",
"compile_data_targets": "depset[Label]: Compile data targets required by this crate.",
"data": "depset[File]: Runtime data associated with the target. Not passed to `Rustc` actions, except for `proc-macro` targets where `Rustc` is the runtime.",
Expand Down Expand Up @@ -71,6 +75,47 @@ DepInfo = provider(
},
)

RustCrateIdentityInfo = provider(
doc = (
"The logical identity of a Rust library together with the specific configured crate " +
"instance that implements it.\n\n" +
"`logical_id` answers \"does this represent the same logical upstream Rust library?\" " +
"For generated Cargo libraries the logical identity is source-qualified: it encodes the " +
"Cargo source/provenance as well as name and version. Two instances with the same name " +
"and version but different sources (e.g. crates.io vs a private registry) therefore have " +
"different logical identities and are treated as distinct libraries; cross-registry " +
"duplication is accepted by design so a private registry may shadow a common name. The " +
"enforced invariant is per identity: within one native link unit there may be at most " +
"one configured crate instance for each source-qualified logical identity.\n\n" +
"The `crate_instance` artifact answers \"is this actually the same compiled instance?\" " +
"Equality of `crate_instance` means the same configured compilation output; target " +
"configuration, enabled features, cfgs, toolchain, transitions, and recursively selected " +
"dependencies are already reflected in the configured action that owns that artifact. " +
"Different artifacts are conservatively treated as different instances even if their " +
"bytes or compilation flags happen to match.\n\n" +
"`owner` and `display_name` are diagnostic-only fields and do not participate in equality."
),
fields = {
"logical_id": "str: Source-qualified logical identity of the upstream Rust library whose runtime/type identity should be unique per link unit.",
"crate_instance": "File: The configured crate output artifact (CrateInfo.output).",
"owner": "Label: The label of the target that produced the crate only for diagnostics.",
"display_name": "str: Short human-readable description for diagnostics only.",
},
)

RustLinkClosureInfo = provider(
doc = (
"The Rust library identity closure absorbed into a native-boundary artifact.\n\n" +
"A native consumer may fold a `static` closure into its own link unit. A `dynamic` " +
"artifact is independently linked and its internal Rust crates must not be combined with " +
"a consuming unit's closure."
),
fields = {
"crates": "depset[RustCrateIdentityInfo]: The Rust library identity closure.",
"linkage": "str: 'static' or 'dynamic'.",
},
)

CrateGroupInfo = provider(
doc = "A provider containing a group of crates.",
fields = {
Expand Down
54 changes: 48 additions & 6 deletions rust/private/rust.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ load(
":rust_allocator_libraries.bzl",
"RUSTC_ALLOCATOR_LIBRARIES_ATTRS",
)
load(":rust_link_validation.bzl", "native_rust_link_validation_aspect")
load(
":rustc.bzl",
"UnstableSelfProfileInfo",
Expand Down Expand Up @@ -815,6 +816,7 @@ _COMMON_ATTRS = {

These must be targets that provide `CrateInfo`, such as `rust_library`.
"""),
aspects = [native_rust_link_validation_aspect],
),
"edition": attr.string(
doc = "The rust edition to use for this crate. Defaults to the edition specified in the rust_toolchain.",
Expand All @@ -826,6 +828,7 @@ _COMMON_ATTRS = {
These are typically `cc_library` targets.
"""),
providers = [[CcInfo], [rust_common.crate_info]],
aspects = [native_rust_link_validation_aspect],
),
"lint_config": attr.label(
doc = "Set of lints to apply when building this crate.",
Expand Down Expand Up @@ -1091,11 +1094,50 @@ _RUST_TEST_ATTRS = {
),
} | _COVERAGE_ATTRS | _EXPERIMENTAL_USE_CC_COMMON_LINK_ATTRS

# `crate_identity` is intentionally restricted to library-producing rules and is
# NOT part of `_COMMON_ATTRS`, so binaries, tests, examples, and build-script
# executables never carry it. A Cargo package can legitimately contain both a
# library and binaries; tagging every Cargo target with only its package ID
# would incorrectly equate distinct Cargo targets.
_CRATE_IDENTITY_ATTRS = {
"crate_identity": attr.string(
doc = dedent("""\
Optional logical identity of the upstream Rust library this target
represents.

When set, the configured library carries a `RustCrateIdentityInfo`
and participates in cross-link-unit crate-instance validation: a
native link unit may contain at most one configured crate instance
for each non-empty logical identity.

The `cargo:` prefix is reserved for generated Cargo library targets.
The crate generator emits a source-qualified identity of the form
`cargo:` + a canonical record encoding the Cargo package's source,
name, and version (e.g.
`cargo:["registry","sparse+https://index.crates.io/","log","0.4.22"]`).
The source is deliberately part of the identity: a Cargo package of
the same name and version pulled from two different registries is two
distinct logical libraries (private-registry shadowing of a common
name is intended), so cross-registry duplication is accepted rather
than flagged. The enforced invariant is per identity: within one
native link unit there may be at most one configured instance of each
source-qualified logical identity.

Handwritten libraries should use a reverse-domain or
repository-qualified identifier, e.g. `com.example:mylib`, and opt
out (or in) consciously.

Leave empty (the default) for ordinary targets that opt out of this
facility.
"""),
),
}

rust_library = rule(
implementation = _rust_library_impl,
provides = COMMON_PROVIDERS,
cfg = per_crate_flag_trim_transition,
attrs = _COMMON_ATTRS | {
attrs = _COMMON_ATTRS | _CRATE_IDENTITY_ATTRS | {
"disable_pipelining": attr.bool(
default = False,
doc = dedent("""\
Expand Down Expand Up @@ -1268,7 +1310,7 @@ _rust_static_library_transition = transition(

rust_static_library = rule(
implementation = _rust_static_library_impl,
attrs = _COMMON_ATTRS | _PLATFORM_ATTRS,
attrs = _COMMON_ATTRS | _CRATE_IDENTITY_ATTRS | _PLATFORM_ATTRS,
fragments = ["cpp"],
cfg = _rust_static_library_transition,
toolchains = [
Expand Down Expand Up @@ -1300,7 +1342,7 @@ def _rust_shared_library_transition_impl(settings, attr):
_PER_CRATE_FLAG_SETTING: per_crate_flags,
}

_rust_shared_library_transition = transition(
_rust_cdylib_library_transition = transition(
implementation = _rust_shared_library_transition_impl,
inputs = [
"//command_line_option:platforms",
Expand Down Expand Up @@ -1330,9 +1372,9 @@ _CC_RUNTIME_LINKAGE_ATTRS = {

rust_cdylib_library = rule(
implementation = _rust_cdylib_library_impl,
attrs = _COMMON_ATTRS | _PLATFORM_ATTRS | _EXPERIMENTAL_USE_CC_COMMON_LINK_ATTRS | _CC_RUNTIME_LINKAGE_ATTRS,
attrs = _COMMON_ATTRS | _CRATE_IDENTITY_ATTRS | _PLATFORM_ATTRS | _EXPERIMENTAL_USE_CC_COMMON_LINK_ATTRS | _CC_RUNTIME_LINKAGE_ATTRS,
fragments = ["cpp"],
cfg = _rust_shared_library_transition,
cfg = _rust_cdylib_library_transition,
toolchains = [
str(Label("//rust:toolchain_type")),
config_common.toolchain_type("@bazel_tools//tools/cpp:toolchain_type", mandatory = False),
Expand Down Expand Up @@ -1362,7 +1404,7 @@ rust_proc_macro = rule(
"_allowlist_function_transition": attr.label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
),
},
} | _CRATE_IDENTITY_ATTRS,
fragments = ["cpp"],
toolchains = [
str(Label("//rust:toolchain_type")),
Expand Down
73 changes: 73 additions & 0 deletions rust/private/rust_crate_identity.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Copyright 2021 The Bazel Authors. All rights reserved.
#
# 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
#
# http://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.

"""Shared Rust crate-instance link validation.

Used by both the intrinsic Rust link rules (`rustc.bzl`) and the native
(C/C++) link-validation aspect so there is a single, consistent definition of
the enforced invariant:

> Within one configured native link unit, each non-empty logical Rust library
> identity may correspond to at most one configured Rust crate instance.
"""

def validate_crate_identity_closure(owner, identities):
"""Fails when one link unit contains incompatible instances of one identity.

Args:
owner (Label): The link unit owner, used only in diagnostics.
identities (list[RustCrateIdentityInfo]): The Rust identity records in
the link unit's static closure.
"""
instances_by_id = {}
for identity in identities:
logical_id = identity.logical_id
if not logical_id:
continue
instances = instances_by_id.get(logical_id)
if instances == None:
instances = {}
instances_by_id[logical_id] = instances
instances[identity.crate_instance] = identity

conflicting_ids = [
logical_id
for logical_id, instances in instances_by_id.items()
if len(instances) > 1
]
if not conflicting_ids:
return

lines = [
"{} would link incompatible instances of the same Rust library:".format(owner),
]
for logical_id in sorted(conflicting_ids):
instances = instances_by_id[logical_id]
lines.extend([
"",
" logical identity: {}".format(logical_id),
])
for identity in sorted(instances.values(), key = lambda i: str(i.owner)):
lines.extend([
"",
" {}".format(identity.owner),
" crate instance: {}".format(identity.crate_instance),
])

lines.extend([
"",
"A native link unit may contain at most one configured Rust crate instance",
"for each logical Rust library identity.",
])
fail("\n".join(lines))
74 changes: 74 additions & 0 deletions rust/private/rust_link_validation.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright 2026 The Bazel Authors. All rights reserved.
#
# 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
#
# http://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.

"""Rust identity-closure aggregation across native dependency edges."""

load(":providers.bzl", "BuildInfo", "CrateInfo", "RustLinkClosureInfo", "TestCrateInfo")
load(":rust_crate_identity.bzl", "validate_crate_identity_closure")

RustLinkAggregationInfo = provider(
doc = "Aggregates static Rust identity closures along native dependency edges.",
fields = {
"crates": "depset[RustCrateIdentityInfo]: Static Rust identity closure reached so far.",
},
)

_NATIVE_DEP_ATTRS = ["deps", "implementation_deps"]
_NATIVE_LINK_UNIT_KINDS = ("cc_binary", "cc_test", "cc_shared_library")

def _rust_link_validation_aspect_impl(target, ctx):
"""Propagates static Rust identity closures across native dependency edges."""
if RustLinkClosureInfo in target:
closure = target[RustLinkClosureInfo]
if closure.linkage != "static":
return []
return [RustLinkAggregationInfo(crates = closure.crates)]

# Rust executables, tests, proc macros, and build scripts are independent
# link/host units.
# Library-producing rules are handled above through RustLinkClosureInfo.
# Do not look through a legacy/custom Rust boundary that lacks that provider.
if BuildInfo in target or CrateInfo in target or TestCrateInfo in target:
return []

crates = []
for attr_name in _NATIVE_DEP_ATTRS:
if not hasattr(ctx.rule.attr, attr_name):
continue
for dep in getattr(ctx.rule.attr, attr_name):
if RustLinkAggregationInfo in dep:
crates.append(dep[RustLinkAggregationInfo].crates)

if not crates:
return []

aggregation = RustLinkAggregationInfo(crates = depset(transitive = crates))
if ctx.rule.kind in _NATIVE_LINK_UNIT_KINDS:
validate_crate_identity_closure(ctx.label, aggregation.crates.to_list())
if ctx.rule.kind == "cc_shared_library":
# The shared library is independently linked. Its internal static
# Rust crates do not belong to a consuming link unit.
return []

return [aggregation]

native_rust_link_validation_aspect = aspect(
implementation = _rust_link_validation_aspect_impl,
attr_aspects = _NATIVE_DEP_ATTRS,
doc = (
"Traverses native dependency edges, accumulates static Rust library identity " +
"closures, validates native terminal link units, and treats shared libraries " +
"as opaque dynamic boundaries."
),
)
Loading
Loading