Using Cargo and build scripts it's possible to generate a version string, that includes information related to the state of the Git repository at the time of building.

Such as:

  • Commit hash
  • Current branch name
  • Was the project build with a clean working tree?

This becomes more and more useful as the project grows over time. Since it makes it easier to checkout specific versions when tracking and debugging issues.

This post includes snippets for creating version strings that include the aforementioned things. Here's an example of the final version string:

versioning 0.1.0 (master:4dd755e+, debug build, windows [x86_64])

Simple Version String

This simple version string doesn't require any build script. It gets the OS and architecture information from std::env::consts, and the Cargo.toml package information through the CARGO_PKG_* environment variables.

Output Example (+ Comments):

versioning 0.1.0 (debug build, windows [x86_64])
\________/ \___/  \_________/  \_____/  \____/
 |          |      |            |        |
 |          |      |            |        +- std::env::consts::ARCH
 |          |      |            +- std::env::consts::OS
 |          |      +- Checks debug_assertions
 |          +- Package version from Cargo.toml
 +- Package name from Cargo.toml

Snippet

main.rs

use std::env::consts::{OS, ARCH};

#[cfg(debug_assertions)]
const BUILD_TYPE: &'static str = "debug";
#[cfg(not(debug_assertions))]
const BUILD_TYPE: &'static str = "release";

fn main() {
    println!("{} {} ({} build, {} [{}])",
        env!("CARGO_PKG_NAME"),
        env!("CARGO_PKG_VERSION"),
        BUILD_TYPE,
        OS, ARCH);
}

Repository State

Embedding the state of the Git repository requires a build script. Such that the information can be fetched during building and stored into the executable.

Output Example (+ Comments):

versioning 0.1.0 (master:4dd755e+, debug build, windows [x86_64])
                  \____/ \_____/|
                   |      |     +- Adds a "+" if the working tree is not clean
                   |      +- Commit hash
                   +- Current branch name

Snippet

The new snippet moves all previous logic from main.rs to build.rs.

build.rs

Create build.rs in the same directory as Cargo.toml, and not in src/.

use std::env::{self, consts::{OS, ARCH}};
use std::process::Command;
use std::path::Path;
use std::fs;

#[cfg(debug_assertions)]
const BUILD_TYPE: &'static str = "debug";
#[cfg(not(debug_assertions))]
const BUILD_TYPE: &'static str = "release";

fn main() {
    let out_dir = env::var("OUT_DIR").unwrap();
    let version_path = Path::new(&out_dir).join("version");

    let version_string =
        format!("{} {} ({}:{}{}, {} build, {} [{}])",
            env!("CARGO_PKG_NAME"),
            env!("CARGO_PKG_VERSION"),
            get_branch_name(),
            get_commit_hash(),
            if is_working_tree_clean() { "" } else { "+" },
            BUILD_TYPE,
            OS, ARCH);

    fs::write(version_path, version_string).unwrap();
}

fn get_commit_hash() -> String {
    let output = Command::new("git")
        .arg("log")
        .arg("-1")
        .arg("--pretty=format:%h") // Abbreviated commit hash
        // .arg("--pretty=format:%H") // Full commit hash
        .current_dir(env!("CARGO_MANIFEST_DIR"))
        .output()
        .unwrap();

    assert!(output.status.success());

    String::from_utf8_lossy(&output.stdout).to_string()
}

fn get_branch_name() -> String {
    let output = Command::new("git")
        .arg("rev-parse")
        .arg("--abbrev-ref")
        .arg("HEAD")
        .current_dir(env!("CARGO_MANIFEST_DIR"))
        .output()
        .unwrap();

    assert!(output.status.success());

    String::from_utf8_lossy(&output.stdout).trim_end().to_string()
}

fn is_working_tree_clean() -> bool {
    let status = Command::new("git")
        .arg("diff")
        .arg("--quiet")
        .arg("--exit-code")
        .current_dir(env!("CARGO_MANIFEST_DIR"))
        .status()
        .unwrap();

    status.code().unwrap() == 0
}

main.rs

const VERSION_STRING: &'static str = include_str!(concat!(env!("OUT_DIR"), "/version"));

fn main() {
    println!("{}", VERSION_STRING);
}

Cargo.toml

Lastly a build item must be added to the [package] section in the Cargo.toml.

[package]
build = "build.rs"

Outputs of the Build Script

Alternatively instead of writing to a file, it is also possible to output in a way that is interpreted by Cargo (Outputs of the Build Script). Making it possible to set environment variables by doing println!("cargo:rustc-env=KEY=VAL").

Snippet

build.rs

The Git related functions remain the same, and have been left out of the snippet.

use std::env::consts::{OS, ARCH};
use std::process::Command;

#[cfg(debug_assertions)]
const BUILD_TYPE: &'static str = "debug";
#[cfg(not(debug_assertions))]
const BUILD_TYPE: &'static str = "release";

fn main() {
    let version_string =
        format!("{} {} ({}:{}{}, {} build, {} [{}])",
            env!("CARGO_PKG_NAME"),
            env!("CARGO_PKG_VERSION"),
            get_branch_name(),
            get_commit_hash(),
            if is_working_tree_clean() { "" } else { "+" },
            BUILD_TYPE,
            OS, ARCH);

    println!("cargo:rustc-env=VERSION_STRING={}", version_string);
}

main.rs

const VERSION_STRING: &'static str = env!("VERSION_STRING");

fn main() {
    println!("{}", VERSION_STRING);
}