"rerun-if-changed-env" does not trigger a rerun?

I'm having a hard time figuring out how to use rerun-if-env-changed in build scripts.
I have a minimal project setup like this:

inner
  - src
    - lib.rs
  - build.rs
  - Cargo.toml
src
  - main.rs
build.rs
Cargo.toml

In the outer Cargo.toml I define inner as a dependency:

[package]
name = "outer"
version = "0.1.0"
edition = "2018"
build = "build.rs"

[dependencies]
inner = { path = "./inner" }

The build script for outer defines an env var FOO, which I'd like inner to use during its build:

// outer/build.rs
fn main() {
    println!("cargo:rustc-env=FOO=1");
}

And the inner build script:

// inner/build.rs

use std::env;
use std::fs::File;
use std::io::Write;
use std::path::Path;

fn main() {
    println!("cargo:rerun-if-env-changed=FOO");

    let out_dir = env::var("OUT_DIR").unwrap();
    let dest = Path::new(&out_dir).join("build_constants.rs");
    let mut file = File::create(&dest).unwrap();

    let foo: i32 = option_env!("FOO")
        .map_or(Ok(-1), str::parse)
        .expect("failed to parse env variable");

    write!(&mut file, "const BUILD_FOO: i32 = {};", foo).unwrap();
}

The inner crate publicly exports a the constant, which can be imported by outer:

// inner/src/lib.rs
include!(concat!(env!("OUT_DIR"), "/build_constants.rs"));
pub const FOO: i32 = BUILD_FOO;

I would expect this to work like this:
If outer is build first, then FOO is set to 1 and when inner is built, the constant is consequently set to 1.
If inner is build first, it is then built again (rerun), since FOO has changed during the build of outer.
However, this does not happen and the pub const FOO is always set to -1, the default value defined in the inner build script.

Am I doing something wrong here or is there a better way to pass around environment variables between crates?

I think rerun-if-env changed is irrelevant here. What matters is passing environment variables from one crate to another. Cargo doesn't have a general solution for this problem IIRC. In particular, println!("cargo:rustc-env=FOO=1"); will set FOO for rustc only for the outer crate itself, it wont affect dependencies or dependent crates. If you need to set env variables globally, per-build, you need to do this outside of Cargo.

1 Like

I was a afraid that would be the case...
Is there some other way to pass information from one build script to another one? All the hints and documentation I find on this topic are extremely vague and usually related to *-sys crates.

You can pass info from dependencies to dependent crates (that is, from inner to outer). The relevant docs are here: Build Scripts - The Cargo Book

As mentioned above in the output format, each build script can generate an arbitrary set of metadata in the form of key-value pairs. This metadata is passed to the build scripts of dependent packages. For example, if libbar depends on libfoo , then if libfoo generates key=value as part of its metadata, then the build script of libbar will have the environment variables DEP_FOO_KEY=value .

Note that metadata is only passed to immediate dependents, not transitive dependents. The motivation for this metadata passing is outlined in the linking to system libraries case study below.

I am afraid that does not help me.

What I am trying to do is to allow users of a library to configure some constants at compile-time. So I need to be able to pass variables from a dependent crate down to a dependency. Cargo features are also insufficient for this, as they don't allow specifying values (yet?). Passing environment variables directly to cargo is also not really a solution, if the library needs to be used by another library.

If outer is build first,

It's not. Your crate's dependencies are always built before your crate, and you can't make env vars time travel.

And dependencies are deduplicated across the entire dependency graph where possible, so your dependencies aren't exclusively yours.

Ok, but in this case my assumption was that the crate would be rebuilt, since the specified environment var would have been changed in the build script of the dependent crate.

If that is not possible, I don't even know what purpose the cargo:rerun-if-env-changed=FOO could possibly serve.

Build scripts can read environment variables set by the user when building the crate.

So is there any other way how to achieve what I want or do I have to give up? I'm surprised that this is apparently so difficult, given that it is so trivial to achieve in C and C++ and also very common.

I've seen some of your commits in the openssl crate, which features quite extensive/complex build scripts, so I'm asking you directly :wink:

The expected solution in Rust is something along the lines of:

export FOO=1 
cargo build

Your build script can also mess with your crate, but not other crates. So if you want to define some constants dynamically, define them in your crate. It's trivial to do within your crate.

it is so trivial to achieve in C and C++

Dependencies are not trivial in C/C++. Ability to complect builds in arbitrary ways also comes with the cost of making every C/C++ project a snowflake with its own choice of build system and its configuration, and no single command that can correctly build an arbitrary project. Having cargo build work for 27000 crates requires defining a one manageable way to build them all.

Thanks for the suggestion, but as I've outlined, this is not realistic in my case.

I've written a concurrent memory management library, that would only be useful to other library crates implementing a data structure, which could then be used by yet some other crate, e.g. rayon. I want to let users of my crate to define some numeric parameters for performance fine-tuning, which is not really possible with features, unless I define a feature for every possible value or at least a reasonable range of values, which is fairly unergonomic.
And I can not really expect any user of e.g. rayon to globally define some environment variables, which affect some crate deep down in the dependency hierarchy.

I was also not suggesting dependency management with C/C++ build systems is trivial, I was merely talking about defining compile time parameters (e.g. add_compile_definitions in CMake).

I’ve written a concurrent memory management library, that would only be useful to other library crates implementing a data structure, which could then be used by yet some other crate, e.g. rayon . I want to let users of my crate to define some numeric parameters for performance fine-tuning, which is not really possible with features, unless I define a feature for every possible value or at least a reasonable range of values, which is fairly unergonomic.
And I can not really expect any user of e.g. rayon to globally define some environment variables, which affect some crate deep down in the dependency hierarchy.

This sounds like a job for const generics. If the parameters are integers, you can do this today with typenum.

Const generics would probably work, but I would personally prefer a build system solution, since squeezing a bunch of parameters into fairly unrelated type definitions would kind of muddle the interface in my eyes.
Since these parameters would also affect array sizes, typenum would also not work entirely, if I remember correctly.

See generic_array.

(and one day, when const generics are finally stable, we won't need these hacks)

Sorry, I should have been more precise, I am actually using arrayvec internally.
But even if it were possible I would consider the negative impact on ergonomics unacceptable. It would leave me with type definitions like Atomic<T, const MARK_BITS: usize, const CACHE_SIZE: usize, const CHECK_THRESHOLD: u32, const ADVANCE_THRESHOLD: u32>, of which only N (for which I currently use typenum) would actually be relevant for the type itself and the rest would be parameters for the underlying memory management scheme.

Yeah, the features system is too weak for this. The features are supposed to be additive for all users of the crate. What if one binary used two libraries, each using your library, but configuring it with different values?

Interesting question, I honestly don't know.
But I'd assume that it would be handled in the same way separate usages of the same library with different features enabled or disabled is handled.

The way that this is handled is by compiling the library once with the union of all of the requested features. So... it wouldn't work.

(A crate can appear in the dependency graph multiple times if and only if the versions that appear are all mutually semver incompatible. For each such version, the features requested by all upstream crates depending on that specific version are unioned)

I see, but that could easily lead to a compile error, and rightly so, since features can be mutually exclusive, too:

#[cfg(all(not(feature = "var_is_1), not(feature = "var_is_2")))]
const VAR: i32 = -1;
#[cfg(all(feature = "var_is_1", not(feature = "var_is_2"))]
const VAR: i32 = 1;
#[cfg(all(feature = "var_is_2", not(feature = "var_is_1"))]
const VAR: i32 = 2;

While cargo may not know these features are mutually exclusive, it is not invalid to use them that way, so if the union of "var_is_1" and "var_is_2" would be used, neither crate would compile (assuming VAR is used somewhere).

It is explicitly invalid to use them in that way because Cargo assumes that features are additive.