将git提交哈希值作为字符串包含在Rust程序中。

29

我在git存储库中托管了一个Rust项目,希望它在一些命令中能够打印版本号。我该如何将版本包含到程序中?我的想法是构建脚本可以设置环境变量,然后在编译项目时使用这些环境变量,但是这并不起作用:

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

错误信息:

error: environment variable `GIT_HASH` not defined
  --> src/main.rs:10:25
   |
10 | pub const GIT_HASH: &'static str = env!("GIT_HASH");
   |   
                                        ^^^^^^^^^^^^^^^^
有没有一种方法可以在编译时传递这样的数据?如果不能使用环境变量来进行构建脚本和源代码之间的通信,那该怎么办?我只能想到将数据写入某个文件,但我认为对于这种情况来说这太过繁琐了。

有没有办法在编译时传递数据?如果不能用环境变量来通信,如何在构建脚本和源代码之间进行通信?只能把数据写到文件中,但对于此案例来说似乎略显复杂。


1
对于任何感兴趣的人,我已经制作了一个自包含的教程,其中包含一些片段,关于生成版本字符串 - vallentin
5个回答

54

自从Rust 1.19(cargo 0.20.0)以来,由于https://github.com/rust-lang/cargo/pull/3929,您现在可以通过以下方式为rustcrustdoc定义编译时环境变量(env!(...)):

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

所以OP的程序可以写成:

// 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
}
注意,如果您仍然想支持1.18或更低版本,则仍然无法使用此功能。

2
问题在于,如果使用增量构建,那部分代码将永远不会被重新编译,导致结果错误(在这种情况下,GIT_HASH 值较旧)。Cargo 支持使用 rerun-if 指令进行更改检测,但这是一个先有鸡还是先有蛋的问题:为了确定是否需要重新运行 build.rs,您首先需要运行 build.rs 来获取 git 哈希。 - vasilakisfil
4
vergen 3.1.0会输出cargo:rustc-rerun-if-changed=.git/HEAD。这表示如果.git/HEAD文件被修改了,Cargo将重新运行编译。 - kennytm
除了这个答案建议的之外,我还在build.rsfn main()中添加了println!("cargo:rustc-rerun-if-changed=.git/HEAD");,但是在cargo build --release中仍然没有更新GIT_HASH环境变量。我一定是犯了一些愚蠢的错误吧? - undefined

22

已经存在一个名为vergen的crate可以在构建脚本中计算git commit。正如@DK的回答所描述的,在Rust 1.19之前,构建脚本不能修改环境变量,因此vergen仍然通过将结果写入OUT_DIR来工作(即vergen仍无法解决OP的问题,但使用起来应该更容易)。


用法:

# 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
}

8

有一种简单的方法可以做到这一点,无需使用任何build.rs逻辑或自定义的crate。你只需要将当前的git哈希作为环境变量直接传递给构建命令,并使用option_env!("PROJECT_VERSION")读取它,在程序中如果没有则会回退到env!("CARGO_PKG_VERSION")。这些宏在编译时读取环境变量。

以下是构建此最小src/main.rs的示例:

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

当您构建程序并需要一个准确的 Git 哈希值,例如在 CI/CD 配置中,您可以在 cargo 命令前缀中添加 PROJECT_VERSION=$(git rev-parse --short HEAD)。像这样使用 cargo run(但也适用于 cargo build 和其他命令):

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

就我个人而言,我更喜欢$(git describe)而非$(git rev-parse),因为前者更具描述性(以下是以使用cargo build为例子,仅出于变化考虑):

% 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

由于您有一个CARGO_PKG_VERSION的回退版本,因此您的IDE仍然可以实时为您构建文件。同样,在开发过程中,您可以跳过传递PROJECT_VERSION。在这种情况下,将使用您的Cargo.toml中的版本:

% cargo run
This binary was built from 0.3.0

6

哎呀。(我不建议在生产环境、测试环境、公共代码甚至私有代码中使用,但是我是说,它还算能胜任工作吧?)

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 }

除非你在做某种代码高尔夫比赛,否则不要使用这个功能。请注意,这是100%未经测试的。


如果及时得到这个答案,我认为它会被接受。 - zertyz

6
我只能想到将数据写入某个文件,但我认为这对于这种情况来说有些过度。不幸的是,那确实是唯一的方法。环境变量无法工作,因为环境的更改无法“泄漏”到其他非子进程中。对于简单的事情,您可以指示Cargo定义条件编译标志,但这些标志不足以传达字符串[1]。从构建脚本生成代码的详细信息在Cargo文档的代码生成部分中说明。
[1]: 我的意思是,除非你想把哈希值分成160个配置标志,然后在编译源代码时重新组装它们,但这甚至更加过度。

我很想看到配置标志版本的示例。但是,Git哈希只需要40个字符。 - Shepmaster
3
“进入其他非子进程”我认为这是需要意识到的关键点。构建脚本在库编译之前运行,而不是在其周围运行。 - Shepmaster
2
@Shepmaster: 不过标记只给你一个比特,你需要类似这样的东西:#[cfg(bit_0)] const BIT_0: u8 = 1; #[cfg(not(bit_0))] const BIT_0: u8 = 0; 这样做了160次。 我可能曾经也犯过类似的错误... *口哨* - DK.
3
cargo#3929被合并后,您可以简单地编写println!("cargo:rustc-env=GIT_HASH=1fcc849"); - kennytm
@kennytm 这个问题值得回答,我想。你的评论包含了一个未来解决方案的信息。对于那些以后会阅读这篇文章的人来说,这个信息将会很有帮助。 - Victor Polevoy
@VictorPolevoy 当PR实际合并、细节解决并且确实可以使用时,我们可以这样做。目前来说,现在还为时过早,不能作为答案。 - kennytm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接