Skip to content

Commit ef7d315

Browse files
authored
Make sure search paths inside OUT_DIR precede external paths (#15221)
If a library exists both in an added folder inside OUT_DIR and in the OS, prefer to use the one within OUT_DIR. Folders within OUT_DIR and folders outside OUT_DIR do not change their relative order between themselves. This is accomplished by sorting by whether we think the path is inside the search path or outside. ### What does this PR try to resolve? Fixes #15220. If a Rust crates builds a dynamic library & that same dynamic library is installed in the host OS, the result of the build's success & consistent behavior of executed tools depends on whether or not the user has the conflicting dynamic library in the external search path. If they do, then the host OS library will always be used which is unexpected - updates to your Rust dependency will still have you linking & running against an old host OS library (i.e. someone who doesn't have that library has a different silent behavior). ### How should we test and review this PR? This is what I did to verify my issue got resolved but I'm sure there's a simpler example one could construct. * Make sure Alsa and libllama.so are installed (on Arch I installed alsa-lib and llama.cpp-cuda). * Clone llama-cpp-2 & init llama.cpp submodule & update the submodule to point to ggml-org/llama.cpp#11997 instead. * Add plumbing to expose the new method within llama-cpp-2 as a public facing function on the LlamaModel struct (it's basically the same code as for n_head, just calling n_head_kv from llama.cpp). * Add cpal as a dependency in crate "foo" * Add llama-cpp-2 via path as a dependency in crate "foo" and enable the `dynamic-link` feature. * Add code using the newly expose n_head_kv method in crate "foo" in main.rs. NOTE: Code just needs to compile & be exported, doesn't have to be correct (fn main is probably easiest. * Add some basic code that tries to initialize cpal in crate "foo" in fn main. * Try to build / run crate "foo" Before my change, it fails with a linker error saying it can't find `llama_model_n_head_kv` because /usr/lib appears in the search path before the directory that contains the libllama.so that was built internally by the crate. This is because cpal depends on alsa-sys which uses pkg-config which adds /usr/lib to the search path before the llama-cpp-sys-2 build.rs is run. ### Additional information I'm not sure how to add tests so open to some help on that. I wanted to make sure that this approach is even correct. I coded this to change Cargo minimally and defensively since I don't know the internals of Cargo very well (e.g. I don't know if I have to compare against both `script_out_dir` / `script_out_dir_when_generated` since I don't know the difference & there's not really any explanation on what they are). It's possible this over-complicates the implementation so open to any feedback. Additionally, the sort that happens prior to each build up of the rustc environment is not where I'd ideally place it. I think it would be more efficient to have the list of search paths be free-floating and not tied to a BuildOutput so that they could be kept updated live & resorted only on insertion (since it's changed less frequently than rustc is invoked). Additionally, the generalized sort is correct but pessimistic - maintaining the list sorted could be done efficiently with some minor book keeping (i.e. you'd only need to sort the new paths & then could quickly inject into the middle of a VecDeque). And of course in terms of correctness, I didn't do a thorough job testing across all possible platforms. From first principles this seems directionally correct but it's always possible this breaks someone else's workflow. I'm also uneasy that the relative position of `-L` / `-l` arguments changes in this PR & I'm not sure if that's observable behavior or not (i.e. it used to be -L for a crate followed by `-l` for a crate), but now it's `-L` for all crates, still grouped by crated internally, followed by `-l` by crate).
2 parents 864f74d + 6c6b34e commit ef7d315

File tree

5 files changed

+212
-21
lines changed

5 files changed

+212
-21
lines changed

src/cargo/core/compiler/build_runner/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,9 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
301301
.extend(output.env.iter().cloned());
302302

303303
for dir in output.library_paths.iter() {
304-
self.compilation.native_dirs.insert(dir.clone());
304+
self.compilation
305+
.native_dirs
306+
.insert(dir.clone().into_path_buf());
305307
}
306308
}
307309
Ok(self.compilation)

src/cargo/core/compiler/custom_build.rs

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
//! [`CompileMode::RunCustomBuild`]: super::CompileMode
3232
//! [instructions]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#outputs-of-the-build-script
3333
34-
use super::{fingerprint, BuildRunner, Job, Unit, Work};
34+
use super::{fingerprint, get_dynamic_search_path, BuildRunner, Job, Unit, Work};
3535
use crate::core::compiler::artifact;
3636
use crate::core::compiler::build_runner::UnitHash;
3737
use crate::core::compiler::fingerprint::DirtyReason;
@@ -75,11 +75,76 @@ pub enum Severity {
7575

7676
pub type LogMessage = (Severity, String);
7777

78+
/// Represents a path added to the library search path.
79+
///
80+
/// We need to keep track of requests to add search paths within the cargo build directory
81+
/// separately from paths outside of Cargo. The reason is that we want to give precedence to linking
82+
/// against libraries within the Cargo build directory even if a similar library exists in the
83+
/// system (e.g. crate A adds `/usr/lib` to the search path and then a later build of crate B adds
84+
/// `target/debug/...` to satisfy its request to link against the library B that it built, but B is
85+
/// also found in `/usr/lib`).
86+
///
87+
/// There's some nuance here because we want to preserve relative order of paths of the same type.
88+
/// For example, if the build process would in declaration order emit the following linker line:
89+
/// ```bash
90+
/// -L/usr/lib -Ltarget/debug/build/crate1/libs -L/lib -Ltarget/debug/build/crate2/libs)
91+
/// ```
92+
///
93+
/// we want the linker to actually receive:
94+
/// ```bash
95+
/// -Ltarget/debug/build/crate1/libs -Ltarget/debug/build/crate2/libs) -L/usr/lib -L/lib
96+
/// ```
97+
///
98+
/// so that the library search paths within the crate artifacts directory come first but retain
99+
/// relative ordering while the system library paths come after while still retaining relative
100+
/// ordering among them; ordering is the order they are emitted within the build process,
101+
/// not lexicographic order.
102+
///
103+
/// WARNING: Even though this type implements PartialOrd + Ord, this is a lexicographic ordering.
104+
/// The linker line will require an explicit sorting algorithm. PartialOrd + Ord is derived because
105+
/// BuildOutput requires it but that ordering is different from the one for the linker search path,
106+
/// at least today. It may be worth reconsidering & perhaps it's ok if BuildOutput doesn't have
107+
/// a lexicographic ordering for the library_paths? I'm not sure the consequence of that.
108+
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
109+
pub enum LibraryPath {
110+
/// The path is pointing within the output folder of the crate and takes priority over
111+
/// external paths when passed to the linker.
112+
CargoArtifact(PathBuf),
113+
/// The path is pointing outside of the crate's build location. The linker will always
114+
/// receive such paths after `CargoArtifact`.
115+
External(PathBuf),
116+
}
117+
118+
impl LibraryPath {
119+
fn new(p: PathBuf, script_out_dir: &Path) -> Self {
120+
let search_path = get_dynamic_search_path(&p);
121+
if search_path.starts_with(script_out_dir) {
122+
Self::CargoArtifact(p)
123+
} else {
124+
Self::External(p)
125+
}
126+
}
127+
128+
pub fn into_path_buf(self) -> PathBuf {
129+
match self {
130+
LibraryPath::CargoArtifact(p) | LibraryPath::External(p) => p,
131+
}
132+
}
133+
}
134+
135+
impl AsRef<PathBuf> for LibraryPath {
136+
fn as_ref(&self) -> &PathBuf {
137+
match self {
138+
LibraryPath::CargoArtifact(p) | LibraryPath::External(p) => p,
139+
}
140+
}
141+
}
142+
78143
/// Contains the parsed output of a custom build script.
79144
#[derive(Clone, Debug, Hash, Default, PartialEq, Eq, PartialOrd, Ord)]
80145
pub struct BuildOutput {
81146
/// Paths to pass to rustc with the `-L` flag.
82-
pub library_paths: Vec<PathBuf>,
147+
pub library_paths: Vec<LibraryPath>,
83148
/// Names and link kinds of libraries, suitable for the `-l` flag.
84149
pub library_links: Vec<String>,
85150
/// Linker arguments suitable to be passed to `-C link-arg=<args>`
@@ -239,7 +304,7 @@ fn emit_build_output(
239304
let library_paths = output
240305
.library_paths
241306
.iter()
242-
.map(|l| l.display().to_string())
307+
.map(|l| l.as_ref().display().to_string())
243308
.collect::<Vec<_>>();
244309

245310
let msg = machine_message::BuildScript {
@@ -886,10 +951,16 @@ impl BuildOutput {
886951
"rustc-flags" => {
887952
let (paths, links) = BuildOutput::parse_rustc_flags(&value, &whence)?;
888953
library_links.extend(links.into_iter());
889-
library_paths.extend(paths.into_iter());
954+
library_paths.extend(
955+
paths
956+
.into_iter()
957+
.map(|p| LibraryPath::new(p, script_out_dir)),
958+
);
890959
}
891960
"rustc-link-lib" => library_links.push(value.to_string()),
892-
"rustc-link-search" => library_paths.push(PathBuf::from(value)),
961+
"rustc-link-search" => {
962+
library_paths.push(LibraryPath::new(PathBuf::from(value), script_out_dir))
963+
}
893964
"rustc-link-arg-cdylib" | "rustc-cdylib-link-arg" => {
894965
if !targets.iter().any(|target| target.is_cdylib()) {
895966
log_messages.push((

src/cargo/core/compiler/mod.rs

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ pub use self::compilation::{Compilation, Doctest, UnitOutput};
7979
pub use self::compile_kind::{CompileKind, CompileKindFallback, CompileTarget};
8080
pub use self::crate_type::CrateType;
8181
pub use self::custom_build::LinkArgTarget;
82-
pub use self::custom_build::{BuildOutput, BuildScriptOutputs, BuildScripts};
82+
pub use self::custom_build::{BuildOutput, BuildScriptOutputs, BuildScripts, LibraryPath};
8383
pub(crate) use self::fingerprint::DirtyReason;
8484
pub use self::job_queue::Freshness;
8585
use self::job_queue::{Job, JobQueue, JobState, Work};
@@ -497,16 +497,39 @@ fn rustc(
497497
current_id: PackageId,
498498
mode: CompileMode,
499499
) -> CargoResult<()> {
500+
let mut library_paths = vec![];
501+
502+
for key in build_scripts.to_link.iter() {
503+
let output = build_script_outputs.get(key.1).ok_or_else(|| {
504+
internal(format!(
505+
"couldn't find build script output for {}/{}",
506+
key.0, key.1
507+
))
508+
})?;
509+
library_paths.extend(output.library_paths.iter());
510+
}
511+
512+
// NOTE: This very intentionally does not use the derived ord from LibraryPath because we need to
513+
// retain relative ordering within the same type (i.e. not lexicographic). The use of a stable sort
514+
// is also important here because it ensures that paths of the same type retain the same relative
515+
// ordering (for an unstable sort to work here, the list would need to retain the idx of each element
516+
// and then sort by that idx when the type is equivalent.
517+
library_paths.sort_by_key(|p| match p {
518+
LibraryPath::CargoArtifact(_) => 0,
519+
LibraryPath::External(_) => 1,
520+
});
521+
522+
for path in library_paths.iter() {
523+
rustc.arg("-L").arg(path.as_ref());
524+
}
525+
500526
for key in build_scripts.to_link.iter() {
501527
let output = build_script_outputs.get(key.1).ok_or_else(|| {
502528
internal(format!(
503529
"couldn't find build script output for {}/{}",
504530
key.0, key.1
505531
))
506532
})?;
507-
for path in output.library_paths.iter() {
508-
rustc.arg("-L").arg(path);
509-
}
510533

511534
if key.0 == current_id {
512535
if pass_l_flag {
@@ -654,7 +677,7 @@ fn add_plugin_deps(
654677
.get(*metadata)
655678
.ok_or_else(|| internal(format!("couldn't find libs for plugin dep {}", pkg_id)))?;
656679
search_path.append(&mut filter_dynamic_search_path(
657-
output.library_paths.iter(),
680+
output.library_paths.iter().map(AsRef::as_ref),
658681
root_output,
659682
));
660683
}
@@ -663,6 +686,13 @@ fn add_plugin_deps(
663686
Ok(())
664687
}
665688

689+
fn get_dynamic_search_path(path: &Path) -> &Path {
690+
match path.to_str().and_then(|s| s.split_once("=")) {
691+
Some(("native" | "crate" | "dependency" | "framework" | "all", path)) => Path::new(path),
692+
_ => path,
693+
}
694+
}
695+
666696
// Determine paths to add to the dynamic search path from -L entries
667697
//
668698
// Strip off prefixes like "native=" or "framework=" and filter out directories
@@ -674,12 +704,9 @@ where
674704
{
675705
let mut search_path = vec![];
676706
for dir in paths {
677-
let dir = match dir.to_str().and_then(|s| s.split_once("=")) {
678-
Some(("native" | "crate" | "dependency" | "framework" | "all", path)) => path.into(),
679-
_ => dir.clone(),
680-
};
707+
let dir = get_dynamic_search_path(dir);
681708
if dir.starts_with(&root_output) {
682-
search_path.push(dir);
709+
search_path.push(dir.to_path_buf());
683710
} else {
684711
debug!(
685712
"Not including path {} in runtime library search path because it is \

src/cargo/util/context/target.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::{ConfigKey, ConfigRelativePath, GlobalContext, OptValue, PathAndArgs, StringList, CV};
2-
use crate::core::compiler::{BuildOutput, LinkArgTarget};
2+
use crate::core::compiler::{BuildOutput, LibraryPath, LinkArgTarget};
33
use crate::util::CargoResult;
44
use serde::Deserialize;
55
use std::collections::{BTreeMap, HashMap};
@@ -167,7 +167,9 @@ fn parse_links_overrides(
167167
let flags = value.string(key)?;
168168
let whence = format!("target config `{}.{}` (in {})", target_key, key, flags.1);
169169
let (paths, links) = BuildOutput::parse_rustc_flags(flags.0, &whence)?;
170-
output.library_paths.extend(paths);
170+
output
171+
.library_paths
172+
.extend(paths.into_iter().map(LibraryPath::External));
171173
output.library_links.extend(links);
172174
}
173175
"rustc-link-lib" => {
@@ -178,9 +180,11 @@ fn parse_links_overrides(
178180
}
179181
"rustc-link-search" => {
180182
let list = value.list(key)?;
181-
output
182-
.library_paths
183-
.extend(list.iter().map(|v| PathBuf::from(&v.0)));
183+
output.library_paths.extend(
184+
list.iter()
185+
.map(|v| PathBuf::from(&v.0))
186+
.map(LibraryPath::External),
187+
);
184188
}
185189
"rustc-link-arg-cdylib" | "rustc-cdylib-link-arg" => {
186190
let args = extra_link_args(LinkArgTarget::Cdylib, key, value)?;

tests/testsuite/build_script.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6084,3 +6084,90 @@ fn directory_with_leading_underscore() {
60846084
.with_status(0)
60856085
.run();
60866086
}
6087+
6088+
#[cargo_test]
6089+
fn linker_search_path_preference() {
6090+
// This isn't strictly the exact scenario that causes the issue, but it's the shortest demonstration
6091+
// of the issue.
6092+
let p = project()
6093+
.file(
6094+
"Cargo.toml",
6095+
r#"
6096+
[package]
6097+
name = "foo"
6098+
version = "0.1.0"
6099+
edition = "2024"
6100+
build = "build.rs"
6101+
6102+
[dependencies]
6103+
a = { path = "a" }
6104+
b = { path = "b" }
6105+
"#,
6106+
)
6107+
.file(
6108+
"build.rs",
6109+
r#"
6110+
fn main() {
6111+
let out_dir = std::env::var("OUT_DIR").unwrap();
6112+
println!("cargo::rustc-link-search=/usr/lib");
6113+
println!("cargo::rustc-link-search={}/libs2", out_dir);
6114+
println!("cargo::rustc-link-search=/lib");
6115+
println!("cargo::rustc-link-search={}/libs1", out_dir);
6116+
}
6117+
"#,
6118+
)
6119+
.file("src/main.rs", "fn main() {}")
6120+
.file(
6121+
"a/Cargo.toml",
6122+
r#"
6123+
[package]
6124+
name = "a"
6125+
version = "0.1.0"
6126+
edition = "2024"
6127+
build = "build.rs"
6128+
"#,
6129+
)
6130+
.file("a/src/lib.rs", "")
6131+
.file(
6132+
"a/build.rs",
6133+
r#"
6134+
fn main() {
6135+
let out_dir = std::env::var("OUT_DIR").unwrap();
6136+
println!("cargo::rustc-link-search=/usr/lib3");
6137+
println!("cargo::rustc-link-search={}/libsA.2", out_dir);
6138+
println!("cargo::rustc-link-search=/lib3");
6139+
println!("cargo::rustc-link-search={}/libsA.1", out_dir);
6140+
}
6141+
"#,
6142+
)
6143+
.file(
6144+
"b/Cargo.toml",
6145+
r#"
6146+
[package]
6147+
name = "b"
6148+
version = "0.1.0"
6149+
edition = "2024"
6150+
build = "build.rs"
6151+
"#,
6152+
)
6153+
.file("b/src/lib.rs", "")
6154+
.file(
6155+
"b/build.rs",
6156+
r#"
6157+
fn main() {
6158+
let out_dir = std::env::var("OUT_DIR").unwrap();
6159+
println!("cargo::rustc-link-search=/usr/lib2");
6160+
println!("cargo::rustc-link-search={}/libsB.1", out_dir);
6161+
println!("cargo::rustc-link-search=/lib2");
6162+
println!("cargo::rustc-link-search={}/libsB.2", out_dir);
6163+
}
6164+
"#,
6165+
)
6166+
.build();
6167+
6168+
p.cargo("build -v").with_stderr_data(str![[r#"
6169+
...
6170+
[RUNNING] `rustc --crate-name foo [..] -L [ROOT]/foo/target/debug/build/foo-[HASH]/out/libs2 -L [ROOT]/foo/target/debug/build/foo-[HASH]/out/libs1 -L [ROOT]/foo/target/debug/build/a-[HASH]/out/libsA.2 -L [ROOT]/foo/target/debug/build/a-[HASH]/out/libsA.1 -L [ROOT]/foo/target/debug/build/b-[HASH]/out/libsB.1 -L [ROOT]/foo/target/debug/build/b-[HASH]/out/libsB.2 -L /usr/lib -L /lib -L /usr/lib3 -L /lib3 -L /usr/lib2 -L /lib2`
6171+
...
6172+
"#]]).run();
6173+
}

0 commit comments

Comments
 (0)