How to refer to a linker script provided in dependent crate

Hi there,

I've created a crate that will act as "core" for any further binaries built on top of this crate. As this core crates depends on specific linker symbols provided by a linker script the building of the final binary should always use the linker script provided by the core crate.

I'm able to pass the link script by copy it from the git repository or the src folder where cargo put the core crate after downloading it from crates.io to the actual project directory and use the rustflag -C linker-args=-T./link.ld

As I'm sure cargo will download the crate source to some fixed location, or a location following some patterns, so is there a way to get the src folder location of the dependent crate in an environment variable to pass this to the rustflags when building the final binary? This would ensure always the correct and latest version of the link script is used for linking the binary.

I've checked that there are some related questions about passing linker arguments from build scripts etc, but I could not find any solution or hint how this could be achieved if at all possible.

Thx in advance for any hint.

I'v also checked the #[linker_args]attribute which is working fine if it is part of the crate building the binary, but this does not work if it is part of the dependent crate.

So the summary is:
A crate kernel depends on a crate core. Crate corecontains a link.ld link script. This shall be used while building kernel crate, preferably some sort of "automatically" as soon as the dependency to core crate is defined in the cargo.toml of kernel crate. May be "secured" by a feature gate of the corecrate to ensure one could explicitly configure when not to use this provided link script.

Thanks in advance for any hint ...

Best regards.

A dependency can communicate with its parents from its build.rs by telling Cargo to set env vars. You could use that to specify path to the link script.

  1. The dependency must use links attribute in Cargo.toml. That attribute doesn't actually link anything. It's just required to ensure there's only one copy of the dependency.

  2. print cargo:foo=value and the parent crate will see it as DEP_<links name>_FOO

https://doc.rust-lang.org/cargo/reference/build-scripts.html#the-links-manifest-key

1 Like

Hey,
thanks for the quick response. But ....
I set the linksattribute in the cargo.toml file of the kernelcrate right? So:

[package]
links = "core"

[dependency]
core = { path = "../core" }

The build.rsfile of the core crate does now pass a variable through the "stdout bridge":

let script_location = env::current_dir().unwrap();
println!("cargo:linker_script={}\\link64.ld", script_location.display());

In the output file of the core crate I can see the variable is set:
cargo:linker_script=.........\boot\link64.ld.

But when printing all env. variables available to the kernelcrate in it's build script like so:

env::vars_os().for_each(|var| {
        println!("OS-ENV: {:?}", var);
    });

    env::vars().for_each(|var| {
        println!("ENV: {:?}", var);
    });

There is nothing mentioning of my linker_scriptvariable....

But even if this variable would be there, how to pass it as -T ? Could I use the #[link_args] attribute and pass this env-variable in?

No, links doesn't refer to any crate. It's really badly named. It is an arbitrary unique identifier, totally meaningless and not technically related to anything anywhere, that a dependency uses to prevent itself from being used in multiple versions in the dependency tree. That uniqueness guarantee is also a requirement for being able to communicate with other crates via env vars (so that multiple versions of the same crate don't overwrite each other's env vars).

And it needs to be inside of Cargo.toml of the core crate:

[package]
name = "core"
links = "im_the_core_crate"
build = "build_exports_the_path.rs"

so the build_exports_the_path.rs can say cargo:the_path_is=foo, and then users of this crate will see DEP_IM_THE_CORE_CRATE_THE_PATH_IS set to foo.

Hey,
well ok, the documentation is than quite false here isn't it:

The links field (optional)
This field specifies the name of a native library that is being linked to.

Nevertheless, also with those changes, listing the links = "core"in the core crate does not change anything. While building the kernelcrate there is no environment variable listed with the pattern DEP_*_LINKER_SCRIPT. I seem to clearely miss something here... I'm building with nightly rust on a windows machine if this makes any difference :wink:

This works for me (you can create these via patch command):

diff --git child/Cargo.toml child/Cargo.toml
new file mode 100644
index 0000000..2bc4cdc
--- /dev/null
+++ child/Cargo.toml
@@ -0,0 +1,5 @@
+[package]
+name = "child"
+version = "0.1.0"
+links = "child_links_name"
+build = "build.rs"
diff --git child/build.rs child/build.rs
new file mode 100644
index 0000000..b2c18b6
--- /dev/null
+++ child/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+    println!("cargo:hello=world");
+}
diff --git child/src/lib.rs child/src/lib.rs
new file mode 100644
index 0000000..e69de29
diff --git parent/Cargo.toml parent/Cargo.toml
new file mode 100644
index 0000000..c2e1e7a
--- /dev/null
+++ parent/Cargo.toml
@@ -0,0 +1,6 @@
+[package]
+name = "parent"
+version = "0.1.0"
+
+[dependencies]
+child = { path = "../child" }
diff --git parent/build.rs parent/build.rs
new file mode 100644
index 0000000..15af8ff
--- /dev/null
+++ parent/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+    panic!("{:#?}", std::env::var("DEP_CHILD_LINKS_NAME_HELLO"));
+}
diff --git parent/src/main.rs parent/src/main.rs
new file mode 100644
index 0000000..e7a11a9
--- /dev/null
+++ parent/src/main.rs
@@ -0,0 +1,3 @@
+fn main() {
+    println!("Hello, world!");
+}

Hey thanks a ton for taking your time.
I double checked the setup on my end and I could now confirm that it is working as expected. I might have had "tomatoes on my eyes" :smiley: ...

The next question now would be how to pass this variable to set the linker script in the parent crate.
using the build script and writing

println!("cargo:rustc-link-arg=-T{:?}", env::var("DEP_CORE_LINKERSCRIPT").unwrap());

does not seem to pass this flag to the invocation of the linker. As per documentation this there are only -l and -L linker args passed.

Any idea on this one? Or could I use the #[link_args] attribute and access this env variable there in some kind?

Oh, sorry. I forgot that the linker flags from build scripts are limited like that. I don't know how to work around that one :frowning:

There's cargo:rustc-cdylib-link-arg= if you're building cdylib.

Another option is to build a static library with Cargo, and then do final linking outside of Cargo.

Well I was able to solve this with a "trick". I left the parameter passed to the linker of the final binary the same, like -C link-arg=-T./link.ld and created a build.rs file in the binary crate to copy the linker script from the dependent crate (based on the custom env-attribute) to the current build folder.

fn main() {
    // copy the linker script from the boot crate to the current directory
    // so it will be invoked by the linker
    let ld_source = env::var_os("DEP_RUSPIRO_BOOT_LINKERSCRIPT")
        .expect("error in ruspiro build, `ruspiro-boot` not a dependency?");
    let src_file = Path::new(&ld_source);
    let trg_file = format!("{}/{}",
        env::current_dir().unwrap().display(),
        src_file.file_name().unwrap().to_str().unwrap());
    println!("Copy linker script from {:?}, to {:?}", src_file, trg_file);
    fs::copy(src_file, trg_file).unwrap();
}

But I've noticed some interesting behavior:
My crate (dependent and depending) are part of a "workspace". In this scenario, even when building inside the crates folder, the invoked linker tries to find the linker file in the workspace root instead of the crates root. As soon as I exclude the current binary crate from the workspace the "working" directory seem to be the one cargo build was invoked from.
BUT: in both cases, the env::current_dir() is giving the path of to the current build crate.
Is this a bug that, when a crate is part of a workspace the invoked linker is using the workspace path as current directory instead of the crates directory?