From 83887c48339f7be1e267199455e2574183d39c93 Mon Sep 17 00:00:00 2001 From: Xiaoyi Shi Date: Tue, 23 Jun 2026 02:49:57 -0700 Subject: [PATCH] cargo_build_script: propagate cdylib and binary link args A build script's cargo::rustc-cdylib-link-arg and cargo::rustc-link-arg-bins directives were dropped with a warning, so flags like napi-rs's `-undefined dynamic_lookup` never reached the linker. Route them through BuildInfo to the targets cargo applies them to: * rustc-cdylib-link-arg is collected into BuildInfo.cdylib_link_flags and applied to cdylib crates. Matching cargo, it propagates transitively: a cdylib picks up the cdylib link args of every build script in its transitive dependencies (rust-lang/cargo#9562). * rustc-link-arg-bins is collected into BuildInfo.bin_link_flags and applied to binary crates. Each set is written to its own flag file, added to the relevant rustc action as an --arg-file, so a non-matching crate type never reads it. The per-binary cargo::rustc-link-arg-bin=BIN=FLAG stays unsupported: a build script's outputs are shared by every target that depends on it, so a flag cannot be scoped to one named binary. Fixes #1062. --- cargo/private/cargo_build_script.bzl | 8 ++ .../private/cargo_build_script_runner/bin.rs | 24 ++++ .../private/cargo_build_script_runner/lib.rs | 62 +++++++- .../cdylib_bin_link_args/BUILD.bazel | 3 + .../cdylib_bin_link_args/bin.rs | 1 + .../cdylib_bin_link_args/build.rs | 13 ++ .../cdylib_bin_link_args_test.bzl | 136 ++++++++++++++++++ .../cdylib_bin_link_args/lib.rs | 1 + rust/private/providers.bzl | 2 + rust/private/rustc.bzl | 15 ++ 10 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 cargo/tests/cargo_build_script/cdylib_bin_link_args/BUILD.bazel create mode 100644 cargo/tests/cargo_build_script/cdylib_bin_link_args/bin.rs create mode 100644 cargo/tests/cargo_build_script/cdylib_bin_link_args/build.rs create mode 100644 cargo/tests/cargo_build_script/cdylib_bin_link_args/cdylib_bin_link_args_test.bzl create mode 100644 cargo/tests/cargo_build_script/cdylib_bin_link_args/lib.rs diff --git a/cargo/private/cargo_build_script.bzl b/cargo/private/cargo_build_script.bzl index a14eafe07e..8fbe9af7ca 100644 --- a/cargo/private/cargo_build_script.bzl +++ b/cargo/private/cargo_build_script.bzl @@ -335,6 +335,8 @@ def _cargo_build_script_impl(ctx): flags_out = ctx.actions.declare_file(ctx.label.name + ".flags") link_flags = ctx.actions.declare_file(ctx.label.name + ".linkflags") link_search_paths = ctx.actions.declare_file(ctx.label.name + ".linksearchpaths") # rustc-link-search, propagated from transitive dependencies + cdylib_link_flags = ctx.actions.declare_file(ctx.label.name + ".cdyliblinkflags") # rustc-cdylib-link-arg, applied to cdylib crates (incl. transitively) + bin_link_flags = ctx.actions.declare_file(ctx.label.name + ".binlinkflags") # rustc-link-arg-bins, applied to binary crates compilation_mode_opt_level = get_compilation_mode_opts(ctx, toolchain).opt_level script_data = [] @@ -549,6 +551,8 @@ def _cargo_build_script_impl(ctx): args.add(flags_out, format = "--flags_out=%s") args.add(link_flags, format = "--link_flags=%s") args.add(link_search_paths, format = "--link_search_paths=%s") + args.add(cdylib_link_flags, format = "--cdylib_link_flags=%s") + args.add(bin_link_flags, format = "--bin_link_flags=%s") args.add(dep_env_out, format = "--dep_env_out=%s") args.add(ctx.attr.rundir, format = "--rundir=%s") @@ -613,6 +617,8 @@ def _cargo_build_script_impl(ctx): flags_out, link_flags, link_search_paths, + cdylib_link_flags, + bin_link_flags, dep_env_out, runfiles_dir, ] + extra_output, @@ -640,6 +646,8 @@ def _cargo_build_script_impl(ctx): flags = flags_out, linker_flags = link_flags, link_search_paths = link_search_paths, + cdylib_link_flags = cdylib_link_flags, + bin_link_flags = bin_link_flags, compile_data = depset([runfiles_dir] + extra_output, transitive = script_data), ), OutputGroupInfo( diff --git a/cargo/private/cargo_build_script_runner/bin.rs b/cargo/private/cargo_build_script_runner/bin.rs index 84b80cf919..261745811a 100644 --- a/cargo/private/cargo_build_script_runner/bin.rs +++ b/cargo/private/cargo_build_script_runner/bin.rs @@ -43,6 +43,8 @@ fn run_buildrs() -> Result<(), String> { compile_flags_file, link_flags_file, link_search_paths_file, + cdylib_link_flags_file, + bin_link_flags_file, output_dep_env_path, stdout_path, stderr_path, @@ -215,6 +217,8 @@ fn run_buildrs() -> Result<(), String> { compile_flags, link_flags, link_search_paths, + cdylib_link_flags, + bin_link_flags, } = BuildScriptOutput::outputs_to_flags( &buildrs_outputs, &exec_root.to_string_lossy(), @@ -231,6 +235,14 @@ fn run_buildrs() -> Result<(), String> { link_search_paths_file, e ) }); + write(&cdylib_link_flags_file, cdylib_link_flags.as_bytes()).unwrap_or_else(|e| { + panic!( + "Unable to write file {:?}: {:#?}", + cdylib_link_flags_file, e + ) + }); + write(&bin_link_flags_file, bin_link_flags.as_bytes()) + .unwrap_or_else(|e| panic!("Unable to write file {:?}: {:#?}", bin_link_flags_file, e)); if !exec_root_links.is_empty() { for link in exec_root_links { @@ -397,6 +409,8 @@ struct Args { compile_flags_file: String, link_flags_file: String, link_search_paths_file: String, + cdylib_link_flags_file: String, + bin_link_flags_file: String, output_dep_env_path: String, stdout_path: Option, stderr_path: Option, @@ -420,6 +434,10 @@ impl Args { Err("Argument `link_flags_file` not provided".to_owned()); let mut link_search_paths_file: Result = Err("Argument `link_search_paths_file` not provided".to_owned()); + let mut cdylib_link_flags_file: Result = + Err("Argument `cdylib_link_flags_file` not provided".to_owned()); + let mut bin_link_flags_file: Result = + Err("Argument `bin_link_flags_file` not provided".to_owned()); let mut output_dep_env_path: Result = Err("Argument `output_dep_env_path` not provided".to_owned()); let mut stdout_path = None; @@ -444,6 +462,10 @@ impl Args { link_flags_file = Ok(arg.split_off("--link_flags=".len())); } else if arg.starts_with("--link_search_paths=") { link_search_paths_file = Ok(arg.split_off("--link_search_paths=".len())); + } else if arg.starts_with("--cdylib_link_flags=") { + cdylib_link_flags_file = Ok(arg.split_off("--cdylib_link_flags=".len())); + } else if arg.starts_with("--bin_link_flags=") { + bin_link_flags_file = Ok(arg.split_off("--bin_link_flags=".len())); } else if arg.starts_with("--dep_env_out=") { output_dep_env_path = Ok(arg.split_off("--dep_env_out=".len())); } else if arg.starts_with("--stdout=") { @@ -469,6 +491,8 @@ impl Args { compile_flags_file: compile_flags_file.unwrap(), link_flags_file: link_flags_file.unwrap(), link_search_paths_file: link_search_paths_file.unwrap(), + cdylib_link_flags_file: cdylib_link_flags_file.unwrap(), + bin_link_flags_file: bin_link_flags_file.unwrap(), output_dep_env_path: output_dep_env_path.unwrap(), stdout_path, stderr_path, diff --git a/cargo/private/cargo_build_script_runner/lib.rs b/cargo/private/cargo_build_script_runner/lib.rs index 5faee0a1f8..55ee42bb69 100644 --- a/cargo/private/cargo_build_script_runner/lib.rs +++ b/cargo/private/cargo_build_script_runner/lib.rs @@ -24,6 +24,13 @@ pub struct CompileAndLinkFlags { pub compile_flags: String, pub link_flags: String, pub link_search_paths: String, + /// `-Clink-arg`s that apply only when the crate is built as a cdylib + /// (`cargo::rustc-cdylib-link-arg`). The consuming rule gates these on the + /// crate type, and propagates them transitively to cdylibs (matching cargo). + pub cdylib_link_flags: String, + /// `-Clink-arg`s that apply only when the crate is built as a binary + /// (`cargo::rustc-link-arg-bins`). Gated on the crate type by the rule. + pub bin_link_flags: String, } /// Enum containing all the considered return value from the script @@ -39,6 +46,10 @@ pub enum BuildScriptOutput { Flags(String), /// cargo::rustc-link-arg LinkArg(String), + /// cargo::rustc-cdylib-link-arg + CdylibLinkArg(String), + /// cargo::rustc-link-arg-bins + BinLinkArg(String), /// cargo::rustc-env Env(String), /// cargo::VAR=VALUE @@ -100,10 +111,12 @@ impl BuildScriptOutput { Some(BuildScriptOutput::DepEnv(format!("METADATA={}", param))) } } - "rustc-cdylib-link-arg" | "rustc-link-arg-bin" | "rustc-link-arg-bins" => { - // cargo::rustc-cdylib-link-arg=FLAG — Passes custom flags to a linker for cdylib crates. + // cargo::rustc-cdylib-link-arg=FLAG — Passes custom flags to a linker for cdylib crates. + "rustc-cdylib-link-arg" => Some(BuildScriptOutput::CdylibLinkArg(param)), + // cargo::rustc-link-arg-bins=FLAG – Passes custom flags to a linker for binaries. + "rustc-link-arg-bins" => Some(BuildScriptOutput::BinLinkArg(param)), + "rustc-link-arg-bin" => { // cargo::rustc-link-arg-bin=BIN=FLAG – Passes custom flags to a linker for the binary BIN. - // cargo::rustc-link-arg-bins=FLAG – Passes custom flags to a linker for binaries. eprint!( "Warning: build script returned unsupported directive `{}`", split[0] @@ -208,12 +221,18 @@ impl BuildScriptOutput { let mut compile_flags = Vec::new(); let mut link_flags = Vec::new(); let mut link_search_paths = Vec::new(); + let mut cdylib_link_flags = Vec::new(); + let mut bin_link_flags = Vec::new(); for flag in outputs { match flag { BuildScriptOutput::Cfg(e) => compile_flags.push(format!("--cfg={e}")), BuildScriptOutput::Flags(e) => compile_flags.push(e.to_owned()), BuildScriptOutput::LinkArg(e) => compile_flags.push(format!("-Clink-arg={e}")), + BuildScriptOutput::CdylibLinkArg(e) => { + cdylib_link_flags.push(format!("-Clink-arg={e}")) + } + BuildScriptOutput::BinLinkArg(e) => bin_link_flags.push(format!("-Clink-arg={e}")), BuildScriptOutput::LinkLib(e) => link_flags.push(format!("-l{e}")), BuildScriptOutput::LinkSearch(e) => link_search_paths.push(format!("-L{e}")), _ => {} @@ -228,6 +247,12 @@ impl BuildScriptOutput { exec_root, out_dir, ), + cdylib_link_flags: Self::redact_flags( + &cdylib_link_flags.join("\n"), + exec_root, + out_dir, + ), + bin_link_flags: Self::redact_flags(&bin_link_flags.join("\n"), exec_root, out_dir), } } @@ -336,6 +361,8 @@ mod tests { .to_owned(), link_flags: "-lsdfsdf".to_owned(), link_search_paths: "-L${pwd}/bleh".to_owned(), + cdylib_link_flags: "".to_owned(), + bin_link_flags: "".to_owned(), } ); } @@ -364,6 +391,33 @@ non-assignment-instructions-are-ignored", from_read_buffer_to_env_and_flags_test_impl(buff); } + #[test] + fn test_cdylib_and_bin_link_args() { + let buff = Cursor::new( + " +cargo::rustc-cdylib-link-arg=-undefined +cargo::rustc-link-arg-bins=-Wl,--whole-archive +cargo::rustc-link-arg-bin=mybin=-Wl,--per-bin", + ); + let result = BuildScriptOutput::outputs_from_reader(BufReader::new(buff)); + + // `rustc-link-arg-bin` (the per-binary form) is unsupported and dropped. + assert_eq!( + result, + vec![ + BuildScriptOutput::CdylibLinkArg("-undefined".to_owned()), + BuildScriptOutput::BinLinkArg("-Wl,--whole-archive".to_owned()), + ] + ); + + let flags = BuildScriptOutput::outputs_to_flags(&result, "/some/absolute/path", ""); + assert_eq!(flags.cdylib_link_flags, "-Clink-arg=-undefined".to_owned()); + assert_eq!( + flags.bin_link_flags, + "-Clink-arg=-Wl,--whole-archive".to_owned() + ); + } + /// Demonstrate that the old style single colon flags are all parsable #[test] fn test_legacy_from_read_buffer_to_env_and_flags() { @@ -489,6 +543,8 @@ cargo::rustc-link-search=/abs/exec_root/other/path link_flags: "".to_owned(), link_search_paths: "-L${pwd}/${bazel-out/cfg/bin/pkg/_bs.out_dir}\n-L${pwd}/other/path".to_owned(), + cdylib_link_flags: "".to_owned(), + bin_link_flags: "".to_owned(), } ); } diff --git a/cargo/tests/cargo_build_script/cdylib_bin_link_args/BUILD.bazel b/cargo/tests/cargo_build_script/cdylib_bin_link_args/BUILD.bazel new file mode 100644 index 0000000000..1333961ff4 --- /dev/null +++ b/cargo/tests/cargo_build_script/cdylib_bin_link_args/BUILD.bazel @@ -0,0 +1,3 @@ +load(":cdylib_bin_link_args_test.bzl", "cdylib_bin_link_args_test_suite") + +cdylib_bin_link_args_test_suite(name = "cdylib_bin_link_args_test_suite") diff --git a/cargo/tests/cargo_build_script/cdylib_bin_link_args/bin.rs b/cargo/tests/cargo_build_script/cdylib_bin_link_args/bin.rs new file mode 100644 index 0000000000..f328e4d9d0 --- /dev/null +++ b/cargo/tests/cargo_build_script/cdylib_bin_link_args/bin.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/cargo/tests/cargo_build_script/cdylib_bin_link_args/build.rs b/cargo/tests/cargo_build_script/cdylib_bin_link_args/build.rs new file mode 100644 index 0000000000..3962c71303 --- /dev/null +++ b/cargo/tests/cargo_build_script/cdylib_bin_link_args/build.rs @@ -0,0 +1,13 @@ +fn main() { + // Emit a benign linker flag (an extra library search path) as the cdylib/bin + // link arg. It exercises the real pipeline end to end -- the consumers below + // link with it applied -- while being a no-op that doesn't break any linker. + let out_dir = std::env::var("OUT_DIR").unwrap(); + let search = if std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc") { + format!("/LIBPATH:{out_dir}") + } else { + format!("-L{out_dir}") + }; + println!("cargo::rustc-cdylib-link-arg={search}"); + println!("cargo::rustc-link-arg-bins={search}"); +} diff --git a/cargo/tests/cargo_build_script/cdylib_bin_link_args/cdylib_bin_link_args_test.bzl b/cargo/tests/cargo_build_script/cdylib_bin_link_args/cdylib_bin_link_args_test.bzl new file mode 100644 index 0000000000..0153a56f88 --- /dev/null +++ b/cargo/tests/cargo_build_script/cdylib_bin_link_args/cdylib_bin_link_args_test.bzl @@ -0,0 +1,136 @@ +"""Tests for `cargo::rustc-cdylib-link-arg` and `cargo::rustc-link-arg-bins`.""" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") +load("//cargo:defs.bzl", "cargo_build_script") +load("//rust:defs.bzl", "rust_binary", "rust_library", "rust_shared_library") +load("//test/unit:common.bzl", "assert_action_mnemonic") + +# The build script's `.cdyliblinkflags` / `.binlinkflags` outputs are passed to +# rustc as `--arg-file ` (see `_process_build_scripts`). A flag only reaches +# a consumer when its crate type matches, so the presence of a specific build +# script's arg-file in the Rustc command line is what distinguishes a correctly +# gated (and, for cdylibs, transitively propagated) consumer. +def _has_arg_file(argv, suffix): + for i in range(len(argv) - 1): + if argv[i] == "--arg-file" and argv[i + 1].endswith(suffix): + return True + return False + +def _link_args_test_impl(ctx): + env = analysistest.begin(ctx) + tut = analysistest.target_under_test(env) + + action = None + for a in tut.actions: + if a.mnemonic == "Rustc": + action = a + break + asserts.true(env, action != None, "Expected a Rustc action") + assert_action_mnemonic(env, action, "Rustc") + + for suffix in ctx.attr.expect_arg_files: + asserts.true(env, _has_arg_file(action.argv, suffix), "expected --arg-file ending with '{}'".format(suffix)) + for suffix in ctx.attr.unexpected_arg_files: + asserts.false(env, _has_arg_file(action.argv, suffix), "unexpected --arg-file ending with '{}'".format(suffix)) + + return analysistest.end(env) + +_link_args_test = analysistest.make( + _link_args_test_impl, + attrs = { + "expect_arg_files": attr.string_list(), + "unexpected_arg_files": attr.string_list(), + }, +) + +def cdylib_bin_link_args_test_suite(name): + """Test that build-script cdylib/bin link args reach only the matching crate type. + + Args: + name: Name of the test suite. + """ + + # A build script reached only transitively, via `dep_lib`. + cargo_build_script( + name = "transitive_build_script", + srcs = ["build.rs"], + tags = ["manual"], + ) + + rust_library( + name = "dep_lib", + srcs = ["lib.rs"], + deps = [":transitive_build_script"], + tags = ["manual"], + ) + + # The consumers' own (direct) build script. + cargo_build_script( + name = "direct_build_script", + srcs = ["build.rs"], + tags = ["manual"], + ) + + rust_shared_library( + name = "cdylib", + srcs = ["lib.rs"], + deps = [":direct_build_script", ":dep_lib"], + tags = ["manual"], + ) + + rust_binary( + name = "bin", + srcs = ["bin.rs"], + deps = [":direct_build_script", ":dep_lib"], + tags = ["manual"], + ) + + rust_library( + name = "lib", + srcs = ["lib.rs"], + deps = [":direct_build_script"], + tags = ["manual"], + ) + + # A cdylib gets its own cdylib link args AND those of a transitive build + # script, but not the bin link args. + _link_args_test( + name = "cdylib_consumer_test", + target_under_test = ":cdylib", + expect_arg_files = [ + "direct_build_script.cdyliblinkflags", + "transitive_build_script.cdyliblinkflags", + ], + unexpected_arg_files = ["direct_build_script.binlinkflags"], + ) + + # A binary gets its own bin link args, but no cdylib link args (direct or + # transitive). + _link_args_test( + name = "bin_consumer_test", + target_under_test = ":bin", + expect_arg_files = ["direct_build_script.binlinkflags"], + unexpected_arg_files = [ + "direct_build_script.cdyliblinkflags", + "transitive_build_script.cdyliblinkflags", + ], + ) + + # An rlib gets neither. + _link_args_test( + name = "lib_consumer_test", + target_under_test = ":lib", + unexpected_arg_files = [ + "direct_build_script.cdyliblinkflags", + "direct_build_script.binlinkflags", + ], + ) + + native.test_suite( + name = name, + tests = [ + ":cdylib_consumer_test", + ":bin_consumer_test", + ":lib_consumer_test", + ], + ) diff --git a/cargo/tests/cargo_build_script/cdylib_bin_link_args/lib.rs b/cargo/tests/cargo_build_script/cdylib_bin_link_args/lib.rs new file mode 100644 index 0000000000..a6ba46c3eb --- /dev/null +++ b/cargo/tests/cargo_build_script/cdylib_bin_link_args/lib.rs @@ -0,0 +1 @@ +pub fn noop() {} diff --git a/rust/private/providers.bzl b/rust/private/providers.bzl index a531884d19..f33a19ceea 100644 --- a/rust/private/providers.bzl +++ b/rust/private/providers.bzl @@ -79,6 +79,8 @@ CrateGroupInfo = provider( BuildInfo = provider( doc = "A provider containing `rustc` build settings for a given Crate.", fields = { + "bin_link_flags": "Optional[File]: file of `-Clink-arg`s to pass to rustc only when the crate is built as a binary (`cargo::rustc-link-arg-bins`).", + "cdylib_link_flags": "Optional[File]: file of `-Clink-arg`s to pass to rustc only when the crate is built as a cdylib (`cargo::rustc-cdylib-link-arg`); propagated transitively to cdylibs.", "compile_data": "Depset[File]: Compile data provided by the build script that was not copied into `out_dir`.", "dep_env": "Optional[File]: extra build script environment variables to be set to direct dependencies.", "flags": "Optional[File]: file containing additional flags to pass to rustc", diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl index c278b5f9f1..9e36cf6924 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -831,6 +831,7 @@ def collect_inputs( build_script_compile_inputs, out_dir, build_env_file, build_flags_files = _process_build_scripts( build_info = build_info, dep_info = dep_info, + crate_type = crate_info.type, include_link_flags = include_link_flags, include_transitive_data = not toolchain._incompatible_do_not_include_transitive_data_in_compile_inputs, ) @@ -2253,6 +2254,7 @@ def add_edition_flags(args, crate): def _process_build_scripts( build_info, dep_info, + crate_type, include_link_flags = True, include_transitive_data = False): """Gathers the outputs from a target's `cargo_build_script` action. @@ -2297,6 +2299,12 @@ def _process_build_scripts( build_flags_files.append(build_info.linker_flags) direct_inputs.append(build_info.linker_flags) + # `cargo::rustc-link-arg-bins` applies only to binary targets, and (like + # cargo) only from the crate's own build script — not transitively. + if crate_type == "bin" and getattr(build_info, "bin_link_flags", None): + build_flags_files.append(build_info.bin_link_flags) + direct_inputs.append(build_info.bin_link_flags) + transitive_inputs.append(build_info.compile_data) # We include transitive dep build_infos because cargo build scripts may generate files which get linked into the final binary. @@ -2306,6 +2314,13 @@ def _process_build_scripts( direct_inputs.append(dep_build_info.out_dir) transitive_inputs.append(dep_build_info.compile_data) + # `cargo::rustc-cdylib-link-arg` applies only to cdylib targets, and (like + # cargo, see rust-lang/cargo#9562) propagates transitively to them. The + # crate's own build script is included here via `transitive_build_infos`. + if crate_type == "cdylib" and getattr(dep_build_info, "cdylib_link_flags", None): + build_flags_files.append(dep_build_info.cdylib_link_flags) + direct_inputs.append(dep_build_info.cdylib_link_flags) + out_dir_compile_inputs = depset( direct_inputs, transitive = transitive_inputs,