Skip to content

Commit 092f8e4

Browse files
committed
feat: introduce link_deps attribute for native library linkage
This change adds a dedicated link_deps attribute to Rust rules, allowing native symbols to be linked without exposing them as Rust crates. Key changes: - Added link_deps to _common_attrs for CcInfo/CrateInfo targets. - Filtered link_deps out of rust_proc_macro to avoid transition complexity. - Implemented transform_link_deps to extract linkage context while hiding crate metadata. - Added a proactive warning for native libraries incorrectly listed in deps. - Added unit tests in test/unit/link_deps to verify correct linkage.
1 parent 7e46ed6 commit 092f8e4

7 files changed

Lines changed: 148 additions & 7 deletions

File tree

rust/private/rust.bzl

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ load(
5252
"get_edition",
5353
"get_import_macro_deps",
5454
"transform_deps",
55+
"transform_link_deps",
5556
"transform_sources",
5657
)
5758

@@ -66,20 +67,49 @@ def _assert_no_deprecated_attributes(_ctx):
6667
pass
6768

6869
def _assert_correct_dep_mapping(ctx):
69-
"""Forces a failure if proc_macro_deps and deps are mixed inappropriately
70+
"""Ensures dependencies are correctly mapped between 'deps', 'proc_macro_deps', and 'link_deps'.
71+
72+
This function validates that procedural macros and native libraries are listed in
73+
their appropriate attributes to maintain the rules_rust dependency model.
7074
7175
Args:
7276
ctx (ctx): The current rule's context object
7377
"""
7478
for dep in ctx.attr.deps:
75-
if rust_common.crate_info in dep:
76-
if dep[rust_common.crate_info].type == "proc-macro":
79+
# Identify if this is a Rust-related target using any known Rust provider.
80+
is_rust_target = (
81+
rust_common.crate_info in dep or
82+
rust_common.crate_group_info in dep or
83+
rust_common.test_crate_info in dep or
84+
rust_common.dep_info in dep or
85+
BuildInfo in dep
86+
)
87+
88+
if is_rust_target:
89+
if rust_common.crate_info in dep and dep[rust_common.crate_info].type == "proc-macro":
7790
fail(
7891
"{} listed {} in its deps, but it is a proc-macro. It should instead be in the bazel property proc_macro_deps.".format(
7992
ctx.label,
8093
dep.label,
8194
),
8295
)
96+
97+
continue
98+
99+
# If it's not a known Rust target but provides CcInfo, it's a native library
100+
# that should ideally be in 'link_deps'.
101+
if CcInfo in dep:
102+
# buildifier: disable=print
103+
print(
104+
("\nWARNING: Target {dep} in 'deps' of {target} is a C++ library. " +
105+
"Only Rust targets are allowed in 'deps'. " +
106+
"Please use 'link_deps' for manual FFI linkage or 'cc_deps' for binding generation. " +
107+
"Support for C++ libraries in 'deps' is deprecated and will be removed in a future release.").format(
108+
dep = dep.label,
109+
target = ctx.label,
110+
),
111+
)
112+
83113
for dep in ctx.attr.proc_macro_deps:
84114
if CrateInfo in dep:
85115
types = [dep[CrateInfo].type]
@@ -217,6 +247,8 @@ def _rust_library_common(ctx, crate_type):
217247
)
218248

219249
deps = transform_deps(ctx.attr.deps)
250+
if hasattr(ctx.attr, "link_deps"):
251+
deps += transform_link_deps(ctx.attr.link_deps)
220252
proc_macro_deps = transform_deps(ctx.attr.proc_macro_deps + get_import_macro_deps(ctx))
221253

222254
return rustc_compile_action(
@@ -269,6 +301,8 @@ def _rust_binary_impl(ctx):
269301
output = ctx.actions.declare_file(output_filename + toolchain.binary_ext)
270302

271303
deps = transform_deps(ctx.attr.deps)
304+
if hasattr(ctx.attr, "link_deps"):
305+
deps += transform_link_deps(ctx.attr.link_deps)
272306
proc_macro_deps = transform_deps(ctx.attr.proc_macro_deps + get_import_macro_deps(ctx))
273307

274308
crate_root = getattr(ctx.file, "crate_root", None)
@@ -356,6 +390,8 @@ def _rust_test_impl(ctx):
356390

357391
crate_type = "bin"
358392
deps = transform_deps(ctx.attr.deps)
393+
if hasattr(ctx.attr, "link_deps"):
394+
deps += transform_link_deps(ctx.attr.link_deps)
359395
proc_macro_deps = transform_deps(ctx.attr.proc_macro_deps + get_import_macro_deps(ctx))
360396

361397
if ctx.attr.crate and ctx.attr.srcs:
@@ -713,15 +749,22 @@ _COMMON_ATTRS = {
713749
),
714750
"deps": attr.label_list(
715751
doc = dedent("""\
716-
List of other libraries to be linked to this library target.
752+
List of other Rust libraries to be linked to this library target.
717753
718-
These can be either other `rust_library` targets or `cc_library` targets if
719-
linking a native library.
754+
These must be targets that provide `CrateInfo`, such as `rust_library`.
720755
"""),
721756
),
722757
"edition": attr.string(
723758
doc = "The rust edition to use for this crate. Defaults to the edition specified in the rust_toolchain.",
724759
),
760+
"link_deps": attr.label_list(
761+
doc = dedent("""\
762+
List of other native libraries to be linked to this library target.
763+
764+
These are typically `cc_library` targets.
765+
"""),
766+
providers = [[CcInfo], [rust_common.crate_info]],
767+
),
725768
"lint_config": attr.label(
726769
doc = "Set of lints to apply when building this crate.",
727770
providers = [LintsInfo],
@@ -1128,7 +1171,7 @@ rust_proc_macro = rule(
11281171
# need to declare `_allowlist_function_transition`, see
11291172
# https://docs.bazel.build/versions/main/skylark/config.html#user-defined-transitions.
11301173
attrs = dict(
1131-
_COMMON_ATTRS.items(),
1174+
{name: value for name, value in _COMMON_ATTRS.items() if name != "link_deps"}.items(),
11321175
_allowlist_function_transition = attr.label(
11331176
default = Label("//tools/allowlists/function_transition_allowlist"),
11341177
),

rust/private/utils.bzl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,23 @@ def transform_deps(deps):
530530
crate_group_info = dep[CrateGroupInfo] if CrateGroupInfo in dep else None,
531531
) for dep in deps]
532532

533+
def transform_link_deps(link_deps):
534+
"""Transforms a [Target] into [DepVariantInfo] for native symbol linkage.
535+
536+
Args:
537+
link_deps (list of Targets): Dependencies coming from ctx.attr.link_deps
538+
539+
Returns:
540+
list of DepVariantInfos with only CcInfo populated.
541+
"""
542+
return [DepVariantInfo(
543+
crate_info = None,
544+
dep_info = None,
545+
build_info = None,
546+
cc_info = dep[CcInfo] if CcInfo in dep else None,
547+
crate_group_info = None,
548+
) for dep in link_deps]
549+
533550
def get_import_macro_deps(ctx):
534551
"""Returns a list of targets to be added to proc_macro_deps.
535552

test/unit/link_deps/BUILD.bazel

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
load("@rules_cc//cc:defs.bzl", "cc_library")
2+
load("//rust:defs.bzl", "rust_binary", "rust_library")
3+
load(":link_deps_test.bzl", "link_deps_test")
4+
5+
# A dummy library to be used as a dependency
6+
rust_library(
7+
name = "leaf_lib",
8+
srcs = ["lib.rs"],
9+
)
10+
11+
# Test 1: rust_library supports link_deps
12+
rust_library(
13+
name = "main_lib",
14+
srcs = ["lib.rs"],
15+
link_deps = [":leaf_lib"],
16+
tags = ["manual"],
17+
)
18+
19+
link_deps_test(
20+
name = "link_deps_success_test",
21+
target_under_test = ":main_lib",
22+
)
23+
24+
# Test 2: Build test linking a C library via link_deps
25+
cc_library(
26+
name = "c_lib",
27+
srcs = ["c_lib.c"],
28+
)
29+
30+
rust_binary(
31+
name = "rust_bin",
32+
srcs = ["main.rs"],
33+
link_deps = [":c_lib"],
34+
)
35+
36+
build_test(
37+
name = "rust_bin_link_deps_test",
38+
target_under_test = ":rust_bin",
39+
)

test/unit/link_deps/c_lib.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
int c_function() {
2+
return 42;
3+
}

test/unit/link_deps/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
load("//rust/private:common.bzl", "rust_common")
2+
load("@rules_cc//cc/common:cc_info.bzl", "CcInfo")
3+
load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
4+
5+
def _link_deps_test_impl(ctx):
6+
env = analysistest.begin(ctx)
7+
target = analysistest.target_under_test(env)
8+
9+
# Verify that CcInfo (symbols) is present
10+
asserts.true(env, CcInfo in target, "Target should provide CcInfo from link_deps")
11+
12+
dep_info = target[rust_common.dep_info]
13+
14+
# Verify that leaf_lib is NOT in direct_crates
15+
for direct_crate in dep_info.direct_crates.to_list():
16+
asserts.not_equals(env, "leaf_lib", direct_crate.name, "link_deps should not be added to direct_crates")
17+
18+
# Verify that transitive_noncrates specifically contains leaf_lib as a linking input
19+
linker_inputs = dep_info.transitive_noncrates.to_list()
20+
found_leaf_lib = False
21+
for linker_input in linker_inputs:
22+
if linker_input.owner == Label("//test/unit/link_deps:leaf_lib"):
23+
found_leaf_lib = True
24+
break
25+
asserts.true(env, found_leaf_lib, "link_deps should pass leaf_lib as a linker input")
26+
27+
return analysistest.end(env)
28+
29+
link_deps_test = analysistest.make(_link_deps_test_impl)

test/unit/link_deps/main.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
extern "C" {
2+
fn c_function() -> i32;
3+
}
4+
5+
fn main() {
6+
unsafe {
7+
assert_eq!(c_function(), 42);
8+
}
9+
}

0 commit comments

Comments
 (0)