Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a way to set arbitrary rpaths. #5077

Open
vext01 opened this issue Feb 26, 2018 · 31 comments
Open

Add a way to set arbitrary rpaths. #5077

vext01 opened this issue Feb 26, 2018 · 31 comments
Labels
A-linkage Area: linker issues, dylib, cdylib, shared libraries, so C-feature-request Category: proposal for a feature. Before PR, ping rust-lang/cargo if this is not `Feature accepted` S-triage Status: This issue is waiting on initial triage.

Comments

@vext01
Copy link
Contributor

vext01 commented Feb 26, 2018

Hi,

Consider this minimal example project:
https://github.com/vext01/rust_link_test

The project has a dependency written in C which is built by the build.rs.

For the examples or tests to work, the shared object for the C code needs to be in the linker path:

     Running target/debug/deps/link_test-8d72230c7bcf790a
/home/vext01/research/link_test/target/debug/deps/link_test-8d72230c7bcf790a: error while loading shared libraries: libcstuff.so: cannot open shared object file: No such file or directory

If I set LD_LIBRARY_PATH to c_lib then everything works. The standard way to work around having to set the LD_LIBRARY_PATH is to encode an rpath into the consuming binary (in this case the example or test binary).

Whilst the example be fixed using, e.g.:

#![link_args="-Wl,-rpath /home/vext01/research/link_test/c_lib"]

At the top of the file, the same doesn't appear to be true of tests.

Another thing that bothers me about using link_args is that it assumes that the path is known, but typically that's a detail that only the build script will know.

Cargo build scripts already have cargo:rustc-link-lib which allows this kind of thing to add linkage to a given library:

println!("cargo:rustc-link-lib=cstuff");

Why not have a more general interface whereby arbitrary linker flags can be passed?

Hope this make sense.

@alexcrichton
Copy link
Member

Does RUSTFLAGS or cargo rustc help for this use case?

@vext01
Copy link
Contributor Author

vext01 commented Feb 27, 2018

Hi Alex.

I'm looking at the --help messages for these commands wondering which flags you mean. I can't see one that would allow my to pass a linker flag.

Even if there were, it would be useful if we could control this stuff in build.rs, rather than having the user have to specify in their env.

@alexcrichton alexcrichton added the C-feature-request Category: proposal for a feature. Before PR, ping rust-lang/cargo if this is not `Feature accepted` label Feb 27, 2018
@vext01
Copy link
Contributor Author

vext01 commented Mar 7, 2018

Any thoughts on this Alex?

@alexcrichton
Copy link
Member

Ah sorry, but RUSTFLAGS is an environment variable that Cargo uses to pass to rustc invocations, and cargo rustc is a way to pass custom flags to the final invocation. Currently build scripts can't embed rpaths because they can't pass along arbitrary flags to rustc.

Currently rustc itself basically doesn't have great support for this unfortunately, the story around passing linker flags and such hasn't been fully fleshed out there.

@vext01
Copy link
Contributor Author

vext01 commented Mar 9, 2018

Thanks Alex.

I'm aware of RUSTFLAGS but I don't think it helps in this scenario.

Indeed, linkage options are a little limited and somewhat inconsistent in Rust.

I notice that #[link_args] is always deemed an "unused attribute" also. I tend to think that #[link_args] would be unnecessary if build.rs were able to set arbitrary linker flags, and this would also solve this issue. I suppose this would require a flag to be added to rustc also.

@vext01
Copy link
Contributor Author

vext01 commented Mar 9, 2018

Just to elaborate on this, I think software authors should be able to specify their own link flags in build.rs without having to ask their downstream users to set environment variables.

@vext01
Copy link
Contributor Author

vext01 commented May 15, 2018

I've been thinking about this again. I think the best thing we could do is allow -C codegen arguments to appear in the Cargo rustc-flags=... directive.

This should be inheritable, like rustc-link-search is, so that consumers of a Rust library which builds (e.g.) C shared objects in a non-standard location need do nothing extra. The Rust library providing the extra libs should be able to have:

println!("rustc-flags='-C link-arg=-Wl,-rpath=...'");

or similar in build.rs.

@mdsteele
Copy link

Being able to set a custom rpath is also important when bundling a binary as a Mac OS X app with embedded frameworks -- in this case, you need to encode an rpath of e.g. @executable_path/../Frameworks into the binary. It would be really nice to have a way of doing that from a build.rs script.

@emoon
Copy link

emoon commented Jul 24, 2018

I'm having the exact same issue. In order to get this to work I have to do this in a .cargo/config

[target.x86_64-apple-darwin]
rustflags = ["-C", "link-arg=-Wl,-rpath,/some/path/here"]

Problem is that I either want:

  1. /some/path to be an environment variable or
  2. Be able to pass it from build.rs

But none of these works for me. In my case I would be fine with .cargo/config supporting to expand environment variables but of course passing it directly from build.rs would be ideal.

edit:

Now that I think about it. Maybe #![link_args="-Wl,-rpath /home/vext01/research/link_test/c_lib"] is usable in my case. I could have build.rs generate a file that includes that line where I have expanded the variable myself. This would be a work-around for sure but hopefully it will be helpful in my case but some better support would be nice without this of course :)

gila added a commit to openebs-archive/spdk-sys that referenced this issue Aug 23, 2019
This change makes it such that during bindgen, we will look for the library in
build/. If the user chooses the move (or copy) it to the system default search
paths then we will pick it up during runtime.

If the user does _not_ move the library, (who would?) we must specify the rpath
during linking phase. There are various ways to do this, either by
`RUSTFLAGS=.. cargo build` or by creating a `./cargo/config` and specify per
target the desired flags.

Within the `config` file however, we can not anticipate the absolute path see:

rust-lang/cargo#5077

Instead, Mayastor now has a Makefile that does this for people, so whoever
wants to give it a swirl can do so without having to install "stuff" outside
of the repo.

Lastly, as people might want to use Ubuntu 18.04, the default in the Makefile
is to not include ISA-L and crypto as those require a NASM version not available
in 18.04

Signed-off-by: Jeffry Molanus <jeffry.molanus@gmail.com>
gila added a commit to openebs-archive/spdk-sys that referenced this issue Aug 23, 2019
This change makes it such that during bindgen, we will look for the library in
build/. If the user chooses the move (or copy) it to the system default search
paths then we will pick it up during runtime.

If the user does _not_ move the library, (who would?) we must specify the rpath
during linking phase. There are various ways to do this, either by
`RUSTFLAGS=.. cargo build` or by creating a `./cargo/config` and specify per
target the desired flags.

Within the `config` file however, we can not anticipate the absolute path see:

rust-lang/cargo#5077

Instead, Mayastor now has a Makefile that does this for people, so whoever
wants to give it a swirl can do so without having to install "stuff" outside
of the repo.

Lastly, as people might want to use Ubuntu 18.04, the default in the Makefile
is to not include ISA-L and crypto as those require a NASM version not available
in 18.04

Signed-off-by: Jeffry Molanus <jeffry.molanus@gmail.com>
gila added a commit to openebs-archive/spdk-sys that referenced this issue Aug 24, 2019
This change makes it such that during bindgen, we will look for the library in
build/. If the user chooses the move (or copy) it to the system default search
paths then we will pick it up during runtime.

If the user does _not_ move the library, (who would?) we must specify the rpath
during linking phase. There are various ways to do this, either by
`RUSTFLAGS=.. cargo build` or by creating a `./cargo/config` and specify per
target the desired flags.

Within the `config` file however, we can not anticipate the absolute path see:

rust-lang/cargo#5077

Instead, Mayastor now has a Makefile that does this for people, so whoever
wants to give it a swirl can do so without having to install "stuff" outside
of the repo.

Lastly, as people might want to use Ubuntu 18.04, the default in the Makefile
is to not include ISA-L and crypto as those require a NASM version not available
in 18.04

Signed-off-by: Jeffry Molanus <jeffry.molanus@gmail.com>
gila added a commit to openebs-archive/spdk-sys that referenced this issue Aug 26, 2019
This change makes it such that during bindgen, we will look for the library in
build/. If the user chooses the move (or copy) it to the system default search
paths then we will pick it up during runtime.

If the user does _not_ move the library, (who would?) we must specify the rpath
during linking phase. There are various ways to do this, either by
`RUSTFLAGS=.. cargo build` or by creating a `./cargo/config` and specify per
target the desired flags.

Within the `config` file however, we can not anticipate the absolute path see:

rust-lang/cargo#5077

Instead, Mayastor now has a Makefile that does this for people, so whoever
wants to give it a swirl can do so without having to install "stuff" outside
of the repo.

Lastly, as people might want to use Ubuntu 18.04, the default in the Makefile
is to not include ISA-L and crypto as those require a NASM version not available
in 18.04

Signed-off-by: Jeffry Molanus <jeffry.molanus@gmail.com>
@vext01
Copy link
Contributor Author

vext01 commented Oct 11, 2019

I'm willing to work on this if we can find an inoffesive way for the user to pass down the flags.

What did people think of:

println!("rustc-flags='-C link-arg=-Wl,-rpath=...'");

In build.rs?

@gferon
Copy link
Contributor

gferon commented Nov 25, 2019

@vext01 this is exactly what we're looking for as well (customizing the rpath dynamically when building in a development environment).

@JackLiar
Copy link

CMake could set the rpath of an executable to any location defined by a variable and reset the rpath of an executable to any other locations when running cmake install, which is very convenient for development and production

Hope cargo could implement something similar, or at least support using some variables to set the content of link-arg

@somewheve
Copy link

Is this issue working now? I do not want to set LD_LIBRARY_PATH or add extra command or set the environment.

@iulianR
Copy link

iulianR commented Oct 28, 2020

Hitting this problem as well. For us it's pretty common to build dynamic libraries (it is often the case with Linux-like-embedded, openwrt, buildroot etc) and link other binaries to them. I also thought of copying the library into the path where tests, Rust binaries are, but there's no way of finding out all paths. There's a better way of doing this currently? Thanks!

@LunarLambda
Copy link

LunarLambda commented Dec 25, 2020

Ditto. I'm thinking setting the rpath to $ORIGIN would help, if your build.rs knows where build tests/executables will land, and can copy the built library to the same directory. Don't know how to exactly accomplish this though, especially for downstream consumers of your library. this effects all platforms, including windows

(on windows, you can sort of get around it by copying the dll to the crate root, as long as you're issuing your cargo run commands from there. Maybe this will work on linux if you LD_LIBRARY_PATH=.)

@vext01
Copy link
Contributor Author

vext01 commented Feb 1, 2021

I've finally found a way to add an rpath in a build script.

If you invoke nightly cargo with -Z extra-link-arg, then you can use cargo:rustc-link-arg in your build script.

Using that you can do stuff like:

println!("cargo:rustc-link-arg=-Wl,-rpath={}", lib_dir);

This seems to work, and you could load lib_dir from the environment if you so wished (so I think this helps @emoon above).

I suspect the reason we've all missed this is because -Z extra-link-arg is missing from-Z help.

What's slightly annoying is that you have to remember to manually type -Z extra-link-arg when invoking cargo. You can't re-alias cargo build in .cargo/config to add it automatically, as you are not allowed to shadow built-in targets.

The best I've come up with so far is a cargo wrapper like this:

#!/bin/sh
cargo -Z extra-link-arg $*

Then run ./cargo.sh instead of cargo.

I also looked at making cargo's cargo:rustc-flags accept arbitrary arguments, but it looks hard. Cargo passes different arguments to different crate types and at different stages of the build, so arbitrary flaggage isn't going to fit well, I don't think.

I see there is another issue relating to arbitrary flags already.

@problame
Copy link

problame commented Feb 2, 2021

Some of the "don't want to set LD_LIBRARY_PATH" use cases can be covered by

// build.rs

// make cargo run and cargo test work without setting LD_LIBRARY_PATH
println!("cargo:rustc-env=LD_LIBRARY_PATH={}", p.display());

Not sure why this works or was ever intended to work, but it does make things bearable for me.


Regardless, I would like to see this feature happen as well because it might make my use case a little easier.

My use case:

  • I have an existing C project that has a bunch of internal libraries and a C based CLI.
  • I am implementing a new binary in Rust that should link against the project's internal C libraries, i.e., those C libraries are in the builddir but not installed on the system.
  • We want to enable in-tree development to be seamless:
    • cargo run and cargo test should just work
      • above trick makes this work
    • rust-gdb target/debug/deps/sometestbinary-1234656425 should just work
      • I think setting rpath would do it, but just for test binaries.
    • Predefined linker flags should be inherited from the build system (autotools in that case) -
      • Right now we generate a .cargo/config.toml with [target....] rustflags = $generated before we invoke cargo build from the autotools-generated Makefile. Had to really bend over backwards to get this to fly and it's a dirty hack.
      • I don't think the scope of this issue is sufficient for this use case. We'd need to set arbitrary linker flags and that proposal seems to have been abandoned.

Notes:

  • I think we'd need the build script be able to detect whether it builds a test so that it can ...-rpath={}... only in that case. (After all we don't want the binaries we build for distribution to contain the build machine's rpath.)

@vext01
Copy link
Contributor Author

vext01 commented Feb 2, 2021

println!("cargo:rustc-env=LD_LIBRARY_PATH={}", p.display());

That'll work if you run your program via cargo, but not if you want your binary to be stand-alone.

We'd need to set arbitrary linker flags and that proposal seems to have been abandoned.

Agreed, and this is particularly acute for embedded applications, where folks often have to do linker gymnastics.

@alexxbb
Copy link

alexxbb commented May 14, 2021

I need this quite often. Yes, .cargo/config helps, but I think most would agree that it should be available in build.rs too.

@luke-titley
Copy link

plus one on this, rpath is really the only way to neatly package up shared libs.

@davepacheco
Copy link
Contributor

I've finally found a way to add an rpath in a build script.

If you invoke nightly cargo with -Z extra-link-arg, then you can use cargo:rustc-link-arg in your build script.

Using that you can do stuff like:

println!("cargo:rustc-link-arg=-Wl,-rpath={}", lib_dir);

This seems to work, and you could load lib_dir from the environment if you so wished (so I think this helps @emoon above).

@vext01 I'm trying to do something similar. I've found that this works for example if you build a binary directly out of a *-sys crate. But if I use that crate from a different crate, it doesn't work. From what I can tell, the linker args are only included when building the *-sys crate (which makes sense), but when that's built from the top-level crate, we're not building a dynamic object -- we're building an archive file. So we lose this information. Have you worked around this? Or are you doing something different?

@davepacheco
Copy link
Contributor

Relatedly, if my last comment is correct, then I think that's a reason not to do this with generic linker flags, but to first-class the idea of rpaths instead. Say you have a crate A that depends on b-sys (a binding crate for libb). The build for b-sys will often use pkg-config or something similar to locate the library. b-sys needs to expose metadata that says when building A, the resulting binaries should have have path P on their rpath. The solution with generic linker flags allows this to work for binaries built as part of b-sys (like examples, tests, etc.) but not anything that uses b-sys, unless I've misunderstood something.

@nsabovic
Copy link

nsabovic commented Nov 3, 2021

I'm having the exact same problem. A build script for OpenCV uses libclang, which on macOS lives where ever you installed Xcode. So the chain of opencv build.rs -> clang -> clang-sys doesn't work. clang-sys finds libclang.dylib just fine but has no way of propagating rpath back up to clang and OpenCV's build.rs. As a library maintainer, the idea that your library's environment variables requirement are propagated to all the library users sucks.

I would expect that setting rpath on the non-dylib targets causes it to be picked up by the target, and if the target is not a dylib/executable I'd expect it to go up another level until it's finds a dylib/executable the exact same way link libs do. I don't see a way of mimicking that with any flag wizardry—cargo has to be aware of rpaths for its targets and propagate accordingly.

I'd love to take a stab at this. I didn't understand from the Contributor's Guide what's the procedure for approving feature requests?

@alexxbb
Copy link

alexxbb commented Feb 1, 2022

Is there a technical reason why this hasn't been addressed yet by the Cargo team? It's been 4 years now,
Really really missing this feature.

@Be-ing
Copy link

Be-ing commented Oct 14, 2022

The unstable -l link-arg=arbitrary_linker_arguments argument to rustc would be passed from cargo via cargo:rustc-link-lib rather than cargo:rustc-link-arg, so I think could resolve the issue with cargo:rustc-link-arg not being transitively passed down to reverse dependencies.

However, looking into how C/C++ build systems work, I don't think Cargo build scripts should be setting rpaths. There are several distinct use cases for setting rpaths:

  1. When C/C++ executables depend on libraries built as shared libraries within the same project, CMake and Meson set rpaths while in the build directory so the executables are runnable without installing them (autotools does something similar but kludgier by replacing executables with shell scripts that set the LD_LIBRARY_PATH environment variable when running the actual executable). Then, in the installation process, the build system removes the rpaths it automatically added. This satisfies Linux distribution requirements that forbid rpaths.
  2. Sometimes C/C++ projects want to install shared libraries to unusual locations relative to the installation root, for example $INSTALL_PREFIX/lib/project_specific_subdirectory. In these cases, the build scripts need to set the rpath to a path relative to the executable's path, for example $ORIGIN/../lib/project_specific_subdirectory.
  3. Sometimes developers/users want to link to libraries installed in nonstandard paths. In order for the executables to be runnable in the build directory without installing them first, rpaths need to be set. A similar situation happens with NixOS, which installs every package to its own directory and relies on rpaths for installed system packages to be runnable.

I don't think use cases 1 & 2 are relevant for Rust because Rust dylibs aren't very useful without a stable ABI and Rust crates can't link to cdylib crates like normal Rust crates. If a Rust crate is built as a cdylib, that's probably the only artifact that's being distributed.

Please correct me if I'm misunderstanding, but I think most people commenting here are interested in use case 3, as I am.

@davepacheco

Relatedly, if my last comment is correct, then I think that's a reason not to do this with generic linker flags, but to first-class the idea of rpaths instead. Say you have a crate A that depends on b-sys (a binding crate for libb). The build for b-sys will often use pkg-config or something similar to locate the library. b-sys needs to expose metadata that says when building A, the resulting binaries should have have path P on their rpath. The solution with generic linker flags allows this to work for binaries built as part of b-sys (like examples, tests, etc.) but not anything that uses b-sys, unless I've misunderstood something.
smklein, knewt, Be-ing, and LexouDuck reacted with thumbs up emoji

This seemed to me like a good idea at first, but the more I think about it, I don't think Cargo build scripts setting arbitrary rpaths would be a great solution. Cargo already has the information it needs to set rpaths; it was given via cargo:rustc-link-search. Currently cargo sets LD_LIBRARY_PATH/DYLD_FALLBACK_LIBRARY_PATH/PATH for cargo run and cargo test, but only for paths within Cargo's target directory. Cargo documentation explicitly states: "It is the responsibility of the user running Cargo to properly set the environment if additional libraries on the system are needed in the search path."

There are two problems with this:

  1. Setting those environment variables is a blunt instrument which can interfere with external executables run via std::process::Command, which by default inherits the current process's environment. This can lead to nasty surprises. rpaths do not have this problem.
  2. It makes dynamic libraries that aren't part of the OS impractical to use with Rust. On Linux, you can document how to install libraries from the system package manager, but that doesn't help on Windows or macOS.

Currently it seems the only practical way to use C/C++ libraries with Rust is to link statically, rely on Linux system package managers, or only link DLLs (Windows)/frameworks (macOS) which are parts of the OS that are guaranteed to be there.

To change this, I propose:

  1. The current paths which are set in LD_LIBRARY_PATH/DYLD_FALLBACK_LIBRARY_PATH automatically by cargo should be replaced by rpaths. Linux distros might not like this, so there might need to be a command line option to disable this. cargo install could remove these rpaths automatically like C/C++ build systems do when installing, but I'm not sure if cargo install is usable for Linux distro packages.
  2. All paths passed via cargo:rustc-link-search would be set as rpaths automatically, unless they're a system path (/usr/lib, /lib, maybe more?). I don't think this should interfere with Linux distro packaging because they'll be linking to libraries in the system paths.

Unfortunately Windows does not have rpaths or anything directly analogous, so setting PATH is the best that could be done unless a feature is added to copy linked DLLs into the build directory and do this recursively for all those DLLs' dependencies.

@LexouDuck
Copy link

Great response by @Be-ing - I wholeheartedly agree with the problems highlighted, and the solution proposal given.

To expand on one point:

Unfortunately Windows does not have rpaths or anything directly analogous, so setting PATH is the best that could be done unless a feature is added to copy linked DLLs into the build directory and do this recursively for all those DLLs' dependencies.

Indeed, and I would much rather recommend the latter of the two options, since the typical use-case/design-pattern for dynamic libraries on Windows is to have all the required .dll files in the same folder as the .exe which uses them - because the Windows dynamic linker will always look in . first, before looking through the PATH environment variable. If the other solution is chosen (messing with the PATH environment variable on Windows), that would likely lead to many of the same nasty hard-to-find issues you mentioned previously in your reply.

@Be-ing
Copy link

Be-ing commented Oct 17, 2022

Yes, copying the DLLs to the same directory as the executable would be ideal for Windows, but I think it would be harder to implement than using the PATH environment variable. I am baffled that neither Windows nor the MSVC toolchain provides a tool to get the DLLs linked to an executable and recursively all the DLLs those DLLs are linked to considering this is the way Microsoft designed Windows to handle DLLs. Such a tool does exist, but it's written in C# so that doesn't really help Cargo. :/ So if someone wants this feature in Cargo, I suggest starting by writing a Rust library which can recursively find all the DLLs linked to an executable.

@ChrisDenton
Copy link
Contributor

The closest analogy to rpath that Windows has is setting a probing path (see Application Configuration Files). However, it's limited to nine paths and is relative to the application directory. Also Cargo does not support application configuration files at this time.

@davepacheco
Copy link
Contributor

@Be-ing I think that might leave out several important use cases. (I apologize in advance if I've misunderstood something!)

The unstable -l link-arg=arbitrary_linker_arguments argument to rustc would be passed from cargo via cargo:rustc-link-lib rather than cargo:rustc-link-arg, so I think could resolve the issue with cargo:rustc-link-arg not being transitively passed down to reverse dependencies.

Can you say more about this?

However, looking into how C/C++ build systems work, I don't think Cargo build scripts should be setting rpaths. There are several distinct use cases for setting rpaths:

1. When C/C++ executables depend on libraries built as shared libraries within the same project, CMake and Meson set rpaths _while in the build directory_ so the executables are runnable without installing them (autotools does something similar but kludgier by replacing executables with shell scripts that set the LD_LIBRARY_PATH environment variable when running the actual executable). Then, in the installation process, the build system removes the rpaths it automatically added. This satisfies [Linux distribution requirements that forbid rpaths](https://docs.fedoraproject.org/en-US/packaging-guidelines/#_beware_of_rpath).

2. Sometimes C/C++ projects want to install shared libraries to unusual locations relative to the installation root, for example `$INSTALL_PREFIX/lib/project_specific_subdirectory`. In these cases, the build scripts need to set the rpath to a path relative to the executable's path, for example `$ORIGIN/../lib/project_specific_subdirectory`.

3. Sometimes developers/users want to link to libraries installed in nonstandard paths. In order for the executables to be runnable in the build directory without installing them first, rpaths need to be set. A similar situation happens with NixOS, which installs every package to its own directory and relies on rpaths for installed system packages to be runnable.

I don't think use cases 1 & 2 are relevant for Rust because Rust dylibs aren't very useful without a stable ABI and Rust crates can't link to cdylib crates like normal Rust crates. If a Rust crate is built as a cdylib, that's probably the only artifact that's being distributed. Please correct me if I'm misunderstanding, but I think most people commenting here are interested in use case 3, as I am.

I think there are other use cases:

  1. I've got a Rust executable that links to a native shared library using a *-sys crate. I intend to ship the executable onto a system that already provides the native library in a known but non-default path (e.g., /opt/$COMPANY/lib). I want to be able to run the executable both in the build directory (e.g., cargo run) as well as on the production system. People often do this by delivering the native library in the same path on both the build system and production system and adding an rpath entry to the executable that specifies that path. But it's also desirable sometimes for the paths to be different: as a developer in this example I might have the library in $HOME/lib.
  2. I've got a Rust executable that links to a native shared library using a *-sys crate. I intend to ship a bundle that includes both the executable and the shared library. If I assemble the build directory the same way as the production bundle, then I can use one rpath entry (referencing $ORIGIN) and the executable will work both in the build directory and in production. (There are tradeoffs here, though.) This is basically your use case 2. I'm not sure why you feel this isn't relevant for Rust.
  3. I've got a Rust library crate C1 that links to a native shared library using a *-sys crate. I do not know who my consuming crates are. They may be other library crates or executable crates. I don't know how they want to link to the native shared library. Even my consuming crates that are themselves Rust library crates don't necessarily know where the native library will be at runtime. Only the top-level executable crate knows that. Maybe the author of that crate wants to assume it's in a default location (installed by the package manager, for example), or they want to put the library into a known, unbundled, non-default path (like my use case 1), or maybe they'll want to bundle the library (like my use case 2). That should be their choice.
  4. Relatedly: I'm building a *-sys crate that provides Rust interfaces for a native library. While working on it, I might be fine assuming the library is in a default location. But I might like to allow the consuming executable to control where the library is found at build-time, whether it's linked statically or dynamically, and if dynamic, then where the library is found at runtime. The choices of build-time location may involve various discovery mechanisms (e.g., pkg-config) or an explicit override.

Cross-compilation is another use case that brings these issues into sharp relief. One might want to build a Rust crate against a set of headers and libraries for a target system that's wholly different than the build system. In this case, the build-time library search path is definitely different than the runtime one.

To me, the high order bit is that only the top-level executable builder necessarily knows where the native shared library should be found, both at build time and runtime. Admittedly, some people don't care and don't want to have to think about any of this. They want to be able to install libfoo with the system package manager and depend on foo-sys and have that work. That's fine -- that can be made to work out of the box using default behavior while still allowing people with these other use cases to choose more precisely what they want.

@davepacheco

Relatedly, if my last comment is correct, then I think that's a reason not to do this with generic linker flags, but to first-class the idea of rpaths instead. Say you have a crate A that depends on b-sys (a binding crate for libb). The build for b-sys will often use pkg-config or something similar to locate the library. b-sys needs to expose metadata that says when building A, the resulting binaries should have have path P on their rpath. The solution with generic linker flags allows this to work for binaries built as part of b-sys (like examples, tests, etc.) but not anything that uses b-sys, unless I've misunderstood something.
smklein, knewt, Be-ing, and LexouDuck reacted with thumbs up emoji

This seemed to me like a good idea at first, but the more I think about it, I don't think Cargo build scripts setting arbitrary rpaths would be a great solution. Cargo already has the information it needs to set rpaths; it was given via cargo:rustc-link-search. Currently cargo sets LD_LIBRARY_PATH/DYLD_FALLBACK_LIBRARY_PATH/PATH for cargo run and cargo test, but only for paths within Cargo's target directory. Cargo documentation explicitly states: "It is the responsibility of the user running Cargo to properly set the environment if additional libraries on the system are needed in the search path."

There are two problems with this:

1. Setting those environment variables is a blunt instrument which can interfere with external executables run via std::process::Command, which [by default inherits the current process's environment](https://doc.rust-lang.org/std/process/struct.Command.html#method.new). This can lead to [nasty surprises](https://github.com/rust-lang/cargo/issues/2888#issuecomment-431049264). rpaths do not have this problem.

2. It makes dynamic libraries that aren't part of the OS impractical to use with Rust. On Linux, you can document how to install libraries from the system package manager, but that doesn't help on Windows or macOS.

Currently it seems the only practical way to use C/C++ libraries with Rust is to link statically, rely on Linux system package managers, or only link DLLs (Windows)/frameworks (macOS) which are parts of the OS that are guaranteed to be there.

To change this, I propose:

1. The current paths which are set in LD_LIBRARY_PATH/DYLD_FALLBACK_LIBRARY_PATH automatically by cargo should be replaced by rpaths. Linux distros might not like this, so there might need to be a command line option to disable this. `cargo install` could remove these rpaths automatically like C/C++ build systems do when installing, but I'm not sure if `cargo install` is usable for Linux distro packages.

To be clear, you mean that: when cargo run and cargo test currently set LD_LIBRARY_PATH and friends (based on their values when you ran cargo), you're proposing instead that cargo create rpath entries in built executables that include whatever happened to be on LD_LIBRARY_PATH when cargo was run? That's a little implicit to me. LD_LIBRARY_PATH is by nature transient, applying only to the current shell and children that don't override it. It's a little surprising to bake that into built binaries. This seems almost certainly wrong when cross-compiling, since the build system may look nothing like the runtime system. It wouldn't help if cargo install removes these because (as I understand it) that's not part of the process people use to build and package executables to deploy elsewhere.

2. All paths passed via `cargo:rustc-link-search` would be set as rpaths automatically, unless they're a system path (`/usr/lib`, `/lib`, maybe more?). I don't think this should interfere with Linux distro packaging because they'll be linking to libraries in the system paths.

I mentioned above why I think this has problems -- the runtime path is not the search path, and the *-sys crate is not the ultimate decider of where the native shared library should be found at runtime. At most, it could inform the top-level crate where it found the library on the build system. (You could flip it around so that the top-level crate can tell the *-sys crate where to find the library at runtime and then the *-sys crate emits instructions that affect linking of the top-level crate, but it seems a lot more convoluted than just letting the top-level crate specify what it wants.)


I don't necessarily recommend the following but we've done this with some success:

  • The *-sys crate we're using already has multiple discovery mechanisms, including pkg-config and an explicit override via environment variable.
  • We modified the *-sys crate to emit a piece of metadata saying where it found the library, using metadata key-value pairs.
  • Our top-level executable crates include a build script that reads this metadata and emits the corresponding cargo:rustc-link-arg instructions. (We abstracted this into its own crate that can go into the top-level crates' build-dependencies.)
  • These top-level executable crates (which mostly depend on the *-sys crate indirectly) have an extra explicit dependency on the *-sys crate version *, solely to be able to consume the metadata emitted by the *-sys crate in their build scripts.

I mention this by way of example, not because I think Rust should first-class this approach. It's annoying in a few obvious ways. But it has the effect that the top-level builder can specify where to find the library and the right rpath entries get added. And we can still use the discovery mechanism built into the *-sys crate. There are many ways Rust could make this easier, but it seems like being able to specify the desired rpath entries needs to be at least part of the answer.

@RJVB
Copy link

RJVB commented Apr 26, 2023

What if the traditional LDFLAGS were used, simply?

In MacPorts we work around this kind of problem by writing compiler and linker wrapper scripts that get injected into the path ahead of whatever would otherwise be used. Kludgy, but it's often the only way to make certain our choice of compiler and options are used...

@nsabovic
Copy link

Just a note that for build scripts rustc-link-search doesn't help because paths outside the target directory are removed:

Cargo includes the following paths:

Search paths included from any build script with the rustc-link-search instruction. Paths outside of the target directory are removed. It is the responsibility of the user running Cargo to properly set the environment if additional libraries on the system are needed in the search path.
The base output directory, such as target/debug, and the “deps” directory. This is mostly for legacy support of rustc compiler plugins.
The rustc sysroot library path. This generally is not important to most users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-linkage Area: linker issues, dylib, cdylib, shared libraries, so C-feature-request Category: proposal for a feature. Before PR, ping rust-lang/cargo if this is not `Feature accepted` S-triage Status: This issue is waiting on initial triage.
Projects
None yet
Development

No branches or pull requests