/ RUST

High-cardinality values for build flags in Rust

While working on my demo on WebAssembly and Kubernetes, I wanted to create three different binaries based on the same code:

  • Native: compile the Rust code to regular native code as a baseline
  • Embed: compile to WebAssembly and use the WasmEdge runtime image as the base Docker image
  • Runtime: compile to WebAssembly, use a base scratch image as my base image, and set the runtime when running the code

The code itself is an HTTP server that offers a single endpoint. For the sake of the demo, I wanted it to return the flavour of the underlying image.

curl localhost:3000/get
{"source": "native", "data": {}}

The idea is to have a single codebase that I can compile to native or WebAssembly. I solved this requirement by using a cfg compile flag.

#[cfg(flavor = "native")]
const FLAVOR: &str = "native";

#[cfg(flavor = "embed")]
const FLAVOR: &str = "embed";

#[cfg(flavor = "runtime")]
const FLAVOR: &str = "runtime";

// Use FLAVOR later in the returned HTTP response

In my case, I can live with three different values. But what if I had to deal with a high cardinality, say, fifteen? It would be quite a bore to define them manually. I searched for a solution and found Build Scripts.

Some packages need to compile third-party non-Rust code, for example C libraries. Other packages need to link to C libraries which can either be located on the system or possibly need to be built from source. Others still need facilities for functionality such as code generation before building (think parser generators).

Cargo does not aim to replace other tools that are well-optimized for these tasks, but it does integrate with them with custom build scripts. Placing a file named build.rs in the root of a package will cause Cargo to compile that script and execute it just before building the package.

Let’s try with a simple build.script at the root of the crate:

fn main() {
    println!("Hello from build script!");
}
cargo build
   Compiling high-cardinality-cfg-compile v0.1.0 (/Users/nico/projects/private/high-cardinality-cfg-compile)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s

There isn’t any log–nothing seems to happen. The documentation reveals the reason:

The output of the script is hidden from the terminal during normal compilation. If you would like to see the output directly in your terminal, invoke Cargo as "very verbose" with the -vv flag.

Let’s compile again with the verbose flag:

cargo build --vv

The output is what we expect:

...
[high-cardinality-cfg-compile 0.1.0] Hello from build script!
...

Now is the time to do something useful: I’ll use the build script to replace the hard-coded constants above. For that, I’ll generate a Rust code file that contains the value passed for flavor.

Note the build script runs before compilation. Hence, it doesn’t have access to the cfg flags. Instead, I’ll pass the value as a Cargo.toml package metadata key value.

[package.metadata]
flavor = "foobar"

For our build script to read the Cargo.toml metadata, we need to add a build dependency:

[build-dependencies]
cargo_metadata = "0.19.1"

The code is straightforward:

fn main() {
    let metadata = MetadataCommand::new()                                         (1)
        .exec()
        .expect("Failed to fetch cargo metadata");

    let package = metadata.root_package().expect("No root package found");        (2)

    let flavor = package                                                          (3)
        .metadata
        .get("flavor")
        .and_then(|f| f.as_str())
        .expect("flavor is not set in Cargo.toml under [package.metadata]");

    let dest_path = Path::new("src").join("flavor.rs");                           (4)

    fs::write(&dest_path, format!("pub const FLAVOR: &str = \"{}\";\n", flavor))  (5)
        .expect("Failed to write flavor.rs");

    println!("cargo:rerun-if-changed=Cargo.toml");
    println!("cargo:warning=FLAVOR written to {}", dest_path.display());
}
1 Get the metadata
2 Get the package where we want to create the new file
3 Read the flavor value
4 Reference src/flavor.rs
5 Write pub const FLAVOR: &str = <flavor>

On the code side, we only need to use the FLAVOR const from the flavor module:

mod flavor;

fn main() {
    println!("Hello from flavor {}", flavor::FLAVOR);
}

Running the program outputs the flavor configured in the Cargo.toml file:

cargo run
Hello from flavor foobar

Conclusion

In this, I’ve shown how to use high-cardinality build parameters from the package metadata. I could have achieved the same with environment variables, but I wanted to learn about the metadata section of the Cargo.toml file.

The build.rs file is a handy trick for achieving goals that aren’t possible with regular Cargo features.

To go further:

Nicolas Fränkel

Nicolas Fränkel

Nicolas Fränkel is a technologist focusing on cloud-native technologies, DevOps, CI/CD pipelines, and system observability. His focus revolves around creating technical content, delivering talks, and engaging with developer communities to promote the adoption of modern software practices. With a strong background in software, he has worked extensively with the JVM, applying his expertise across various industries. In addition to his technical work, he is the author of several books and regularly shares insights through his blog and open-source contributions.

Read More
High-cardinality values for build flags in Rust
Share this