Include git commit hash as string into Rust program
Asked Answered
I

6

35

I host a Rust project in git repository and I want to make it print the version on some command. How can I include the version into the program? I thought that the build script could set environment variables which can be used while compiling the project itself, but it does not work:

build.rs:

use std::env;

fn get_git_hash() -> Option<String> {
    use std::process::Command;

    let branch = Command::new("git")
                         .arg("rev-parse")
                         .arg("--abbrev-ref")
                         .arg("HEAD")
                         .output();
    if let Ok(branch_output) = branch {
        let branch_string = String::from_utf8_lossy(&branch_output.stdout);
        let commit = Command::new("git")
                             .arg("rev-parse")
                             .arg("--verify")
                             .arg("HEAD")
                             .output();
        if let Ok(commit_output) = commit {
            let commit_string = String::from_utf8_lossy(&commit_output.stdout);

            return Some(format!("{}, {}",
                        branch_string.lines().next().unwrap_or(""),
                        commit_string.lines().next().unwrap_or("")))
        } else {
            panic!("Can not get git commit: {}", commit_output.unwrap_err());
        }
    } else {
        panic!("Can not get git branch: {}", branch.unwrap_err());
    }
    None
}

fn main() {
    if let Some(git) = get_git_hash() {
        env::set_var("GIT_HASH", git);
    }
}

src/main.rs:

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

fm main() {
    println!("Git hash: {}", GIT_HASH);
}

The error message:

error: environment variable `GIT_HASH` not defined
  --> src/main.rs:10:25
   |
10 | pub const GIT_HASH: &'static str = env!("GIT_HASH");
   |   
                                        ^^^^^^^^^^^^^^^^

Is there a way to pass such data at compile time? How can I communicate between the build script and the source code if not with environment variables? I can only think about writing data to some file, but I think this is overkill for this case.

Impenetrable answered 3/5, 2017 at 7:13 Comment(1)
For anyone interested, I've made a self-contained tutorial with some snippets, about generating version strings.Rafferty
C
6

I can only think about writing data to some file, but I think this is overkill for this case.

That's unfortunate, because that is the only way of doing it. Environment variables can't work because changes to the environment can't "leak" into other, non-child processes.

For simpler things, you can instruct Cargo to define conditional compilation flags, but those aren't powerful enough to communicate a string [1].

The details of generating code from a build script is detailed in the code generation section of the Cargo documentation.


[1]: I mean, unless you feel like breaking the hash into 160 config flags and then re-assembling them in the source being compiled, but that's even more overkill.

Choli answered 3/5, 2017 at 7:29 Comment(6)
I'd love to see an example of the config flag version. A git hash only needs to be 40 characters though.Haynie
into other, non-child processes I think that's the key thing to realize here. The build script is run before the compilation of the library, not around it.Haynie
@Shepmaster: A flag only gives you one bit, though. You have something like #[cfg(bit_0)] const BIT_0: u8 = 1; #[cfg(not(bit_0))] const BIT_0: u8 = 0; 160 times. I may have done something similarly inadvisable in the past... **whistles**Choli
When cargo#3929 is merged you could simply write println!("cargo:rustc-env=GIT_HASH=1fcc849");Holyhead
@Holyhead this deserves an answer I guess. Your comment contains information about one of future solutions. For people who will read this post later this information will be helpful I think.Impenetrable
@VictorPolevoy We could do so when the PR is actually merged, the details are sorted out and can really be used. For now this is too early to put as an answer.Holyhead
H
58

Since Rust 1.19 (cargo 0.20.0), thanks to https://github.com/rust-lang/cargo/pull/3929, you can now define a compile-time environment variable (env!(…)) for rustc and rustdoc via:

println!("cargo:rustc-env=KEY=value");

So OP's program can be written as:

// build.rs
use std::process::Command;
fn main() {
    // note: add error checking yourself.
    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);
}
// main.rs
fn main() {
    println!("{}", env!("GIT_HASH"));
    // output something like:
    // 7480b50f3c75eeed88323ec6a718d7baac76290d
}

Note that you still cannot use this if you still want to support 1.18 or below.

Holyhead answered 7/6, 2017 at 8:38 Comment(5)
the problem here is that if you use incremental build, that part will never be recompiled, leading to false result (in this case having an older GIT_HASH value). Cargo supports change detection using the rerun-if directives, but that's a chicken and egg problem: in order to figure out if you need to re-run the build.rs, you first need to run build.rs to fetch the git hash.Lid
vergen 3.1.0 emits cargo:rustc-rerun-if-changed=.git/HEAD.Holyhead
Additionally to what this answer suggested I added println!("cargo:rustc-rerun-if-changed=.git/HEAD"); to build.rs's fn main() but it's still not updating the GIT_HASH env on cargo build --release. I must be doing some stupid mistake?Archenemy
@Archenemy You have to use cargo:rerun-if-changed instead of cargo:rustc-rerun-if-changedArmstrong
When HEAD is attached to a branch, the file .git/refs/heads/<branch> must be monitored in addition to .git/HEAD. This is correctly handled in vergen (which recently moved git features into separate crates).Jennet
H
23

There is already an existing crate vergen that can calculate the git commit in the build script. As @DK's answer described, the build script cannot modify environment variable before Rust 1.19, so vergen still works by writing the result into OUT_DIR (i.e. vergen still won't solve OP's question, but it should be easier to use).


Usage:

# Cargo.toml
...
[build-dependencies]
vergen = "0.1"
// build.rs
extern crate vergen;
use vergen::*;
fn main() {
    vergen(SHORT_SHA | COMMIT_DATE).unwrap();
}
mod version {
    include!(concat!(env!("OUT_DIR"), "/version.rs"));
}
fn main() {
    println!("commit: {} {}", version::commit_date(), version::short_sha());
    // output something like:
    //        commit: 2017-05-03 a29c7e5
}
Holyhead answered 3/5, 2017 at 10:5 Comment(0)
W
11

There is an easy way to do this without the need for any build.rs logic or custom crates. You simply pass the current git hash directly to the build command as an environment variable, and read it in your program with option_env!("PROJECT_VERSION"), with a env!("CARGO_PKG_VERSION") fallback. These macros read environment variables during build time.

Examples follow that builds this minimal src/main.rs:

fn main() {
    let version = option_env!("PROJECT_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"));
    println!("This binary was built from {}", version);
}

When you build the program and want an accurate git hash, e.g. in your CI/CD configuration, you prefix the cargo command with PROJECT_VERSION=$(git rev-parse --short HEAD). Like this for cargo run (but also works for cargo build and others):

% PROJECT_VERSION=$(git rev-parse --short HEAD) cargo run
This binary was built from 6ca63b2

Personally I prefer $(git describe) over $(git rev-parse) since the former is more descriptive (using cargo build as example now just for variation):

% PROJECT_VERSION=$(git describe) cargo build 
% ./target/debug/your-program
This binary was built from v0.3.0-15-g6ca63b2    # or just 'v0.3.0' if current commit is tagged with that

Since you have a CARGO_PKG_VERSION fallback, your IDE can still build the files on-the-fly for you. Likewise, for development, you can skip passing PROJECT_VERSION. In that case, the version from your Cargo.toml will be used:

% cargo run
This binary was built from 0.3.0
Wiper answered 30/1, 2021 at 19:36 Comment(0)
C
6

I can only think about writing data to some file, but I think this is overkill for this case.

That's unfortunate, because that is the only way of doing it. Environment variables can't work because changes to the environment can't "leak" into other, non-child processes.

For simpler things, you can instruct Cargo to define conditional compilation flags, but those aren't powerful enough to communicate a string [1].

The details of generating code from a build script is detailed in the code generation section of the Cargo documentation.


[1]: I mean, unless you feel like breaking the hash into 160 config flags and then re-assembling them in the source being compiled, but that's even more overkill.

Choli answered 3/5, 2017 at 7:29 Comment(6)
I'd love to see an example of the config flag version. A git hash only needs to be 40 characters though.Haynie
into other, non-child processes I think that's the key thing to realize here. The build script is run before the compilation of the library, not around it.Haynie
@Shepmaster: A flag only gives you one bit, though. You have something like #[cfg(bit_0)] const BIT_0: u8 = 1; #[cfg(not(bit_0))] const BIT_0: u8 = 0; 160 times. I may have done something similarly inadvisable in the past... **whistles**Choli
When cargo#3929 is merged you could simply write println!("cargo:rustc-env=GIT_HASH=1fcc849");Holyhead
@Holyhead this deserves an answer I guess. Your comment contains information about one of future solutions. For people who will read this post later this information will be helpful I think.Impenetrable
@VictorPolevoy We could do so when the PR is actually merged, the details are sorted out and can really be used. For now this is too early to put as an answer.Holyhead
P
6

Uh. (I do not recommend this in production or in testing or in public code or even in private code but I mean, it kinda does the job?)

const REF: &str = include_str!("../.git/HEAD");
const REF_MASTER: &str = include_str!("../.git/refs/heads/master");

// (elsewhere)
if REF == "ref: refs/heads/master" { REF_MASTER } else { REF }

(do not use this unless you're making some sort of codegolf. note that this is 100% untested.)

Polston answered 27/3, 2018 at 0:51 Comment(1)
should this have come in time, it would've been the accepted answer IMHOOfelia
B
0

If you are looking to have the commit hash available from cargo install --locked <crate>, you can check this out https://github.com/pitoniak32/axolotl_git/blob/458b143a8dec2d45abe2e8af7b454ecbbbeafa14/build.rs#L1

There is a file that gets written with the commit hash, it wont work with --allow-dirty flag. But this is my solution for getting the commit hash in my installs from crates.io.

use std::{fs, path::PathBuf, process::Command};

use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct CargoVcsInfo {
    git: GitVcsInfo,
}

#[derive(Debug, Deserialize)]
struct GitVcsInfo {
    sha1: String,
}

const GIT_SHA_SHORT_MIN: usize = 7;

fn main() {
    println!("cargo::rerun-if-changed=.git/HEAD");
    let cargo_vcs_info = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".cargo_vcs_info.json");
    let git_sha_long = if cargo_vcs_info.exists() {
        // not compatible with `cargo package` or `cargo publish` using `--allow-dirty` flag.
        // the `.cargo_vcs_info.json` file is not written
        serde_json::from_str::<CargoVcsInfo>(
            &fs::read_to_string(cargo_vcs_info)
                .expect("should be able to read cargo_vcs_info.json"),
        )
        .expect("cargo_vcs_info.json should contain expected info")
        .git
        .sha1
        .trim()
        .to_string()
    } else {
        String::from_utf8(
            Command::new("git")
                .args(["rev-parse", "HEAD"])
                .output()
                .expect("should be able to get commit hash")
                .stdout,
        )
        .expect("commit hash should be valid utf8")
        .trim()
        .to_string()
    };

    println!("cargo:rustc-env=GIT_SHA_LONG={}", &git_sha_long,);
    println!(
        "cargo:rustc-env=GIT_SHA_SHORT={}",
        &git_sha_long[..GIT_SHA_SHORT_MIN],
    );
}

Hope this helps!

Bouncing answered 16/4 at 2:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.