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:

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], Jul 02 2019, 20:44:27)

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);
}

Build Time

Adding the build time can be easily done using the chrono crate. It's important that this is added to the build.rs, and not main.rs. Otherwise the rendered timestamp is the current time, instead of the build time.

Output Example (+ Comments):

versioning 0.1.0 (master:4dd755e+, debug build, windows [x86_64], Jul 02 2019, 20:44:27)
                                                                  \___________________/
                                                                   |
                                                                   +- Build time

Snippet

build.rs

The date and time format is as follows Jul 02 2019, 20:44:27. If another format is desired, then check out chrono's documentation for supported escape sequences.

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

use chrono::prelude::Utc;

#[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,
            Utc::now().format("%b %d %Y, %T"));

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

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

Cargo.toml

Since chrono is used in build.rs, then the dependency must be added to [build-dependencies] instead of [dependencies] in Cargo.toml.

[build-dependencies]
chrono = "0.4.7"