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], 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"