KeiruaProd

Including the build version inside a rust binary

A recent problem was to include the build version of a rust binary, so that $ program --version answers with the appropriate version number.

We’ll look at various solutions.

Starting with an empty example

Here is a snippet of such a program:

// bin/empty.rs
extern crate clap; 
use clap::App; 
 
fn main() { 
    App::new("")
       .version("1.0")
       .get_matches(); 
}

in Cargo.toml, you’ll need to add clap = "*" as a dependency. It’s a popular crate that does Command Line Argument Parsing, you’ll encounter it often if you write command line programs.

If you run this example, it’ll output our hardcoded version:

$ cargo run --bin empty -- --version
 1.0

It’s not very useful for now since it’s static, but that’s a start.

A note about the command syntax:

It’s very similar as running cargo build && ./target/debug/empty --version.

Using cargo built-in variables

Let’s improve this program. A first way to do is it to use cargo’s built-in variables:

// bin/cargopkg.rs
fn main() { 
    App::new("")
       .version(env!("CARGO_PKG_VERSION"))
       .get_matches(); 
}

If we run this program, we have a version:

$ cargo run --bin cargopkg -- --version
 0.1.0

Where does this 0.1.0 come from ? std::env! is a macro that fetches the value of an environment variable during compile time. CARGO_PKG_VERSION gives us the version number set in Cargo.toml.

A few other environment variables are available.

If you go in this direction, you’ll need to update your version every time you make a release. There are pros and cons to this approach.

Using a build script

Another approach is to use a build script. There are many parts so it is a bit hard to grasp at first:

One of the things we can tell Cargo is “let’s add a new environment variable, with the hash of the latest commit”. Let’s do that.

# Cargo.toml
[package]
# …
build = "build.rs"

Cargo only supports one build script per project at the moment, so everything we do will be available to all the binary (should they care to use it). Here is our build.rs script:

// build.rs
use std::process::Command;
fn main() {
    // taken from https://stackoverflow.com/questions/43753491/include-git-commit-hash-as-string-into-rust-program
    let output = Command::new("git")
        .args(&["rev-parse", "HEAD"])
        .output()
        .unwrap();
    let git_hash = String::from_utf8(output.stdout).unwrap();
    println!("cargo:rustc-env=GIT_HASH={}", git_hash);
}

So we write a program that fetches the last commit hash via git rev-parse HEAD. git rev-parse provides the SHA1 of a revision.

Now we can make use of the GIT_HASH environment variable in our main program:

// bin/gitcommit.rs
extern crate clap; 
use clap::App; 
 
fn main() { 
    App::new("")
       .version(env!("GIT_HASH"))
       .get_matches(); 
}

Now, lets run this program:

$ cargo run --bin gitcommit -- --version
909d20fe5bf4cfc3ae784c78401455b42bdf02d2

This is the approach we went with for ds_proxy

Using a dedicated crate

Well, parsing the git output ourselves in order to feed it to a special cargo string can be a bit tedious. As often in the rust ecosystem, there is a crate for that. vergen exposes a bunch of environment variables, like the build date, the sha1 of the commit or the target architecture.

See a typo ? You can suggest a modification on Github.