rust编译时注入编译信息

 

背景

不管是什么语言的服务,编译出来的二进制可能都有一个最简单的诉求,那就是可以快速看到这个二进制的一些详细信息。例如:

👉 ./app-bin -v
# 从左到右为: 二进制名, commit, OS/ARCH, rustc版本, cargo版本, 编译时间
👉 app-bin a78a4d3, linux/x86_64, compiled by rustc 1.74.1 (a28077b28 2023-12-04) and cargo 1.74.1 (ecb9851af 2023-10-18), built on 2024.01.26-07:47:09

这个可以帮我们快速掌握这个二进制的信息。

实现

我们可以通过rust自带的特殊方案,构建build.rs,在编译时将我们需要的信息设置成cargo env传递进去。

目录结构

build.rs需要在根目录上:

.
├── build.rs
├── Cargo.toml
├── Makefile
├── src
    ├── main.rs

编译配置

Cargo.toml中需要指定build的文件:

[package]
name = "app-bin"
version = "0.1.0"
edition = "2021"
build = "build.rs"

构造环境变量

// build.rs
use std::process::Command;

// 执行命令行得到对应的结果
fn execute_cmd(cmd: &str, args: &[&str], omit_error: bool) -> String {
    let output = match Command::new(cmd).args(args).output() {
        Ok(output) => output,
        Err(err) => {
            if omit_error {
                return "".to_string();
            } else {
                panic!("fail to execute command [{cmd}] because of [{err}]");
            }
        }
    };
    if !output.status.success() {
        if omit_error {
            return "".to_string();
        }
        panic!(
            "fail to execute command [{cmd}] because of [{}]",
            String::from_utf8_lossy(&output.stderr)
        );
    }
    String::from_utf8_lossy(&output.stdout).to_string()
}

// 设置cargo env 在程序中可以被使用到
fn set_cargo_env(name: &str, value: String) {
    // set cargo env only when it is not defined
    if std::env::var(name).is_err() {
        println!("cargo:rustc-env={name}={value}");
    }
    // rebuild is needed if this env is changed compared with last build
    println!("cargo:rerun-if-env-changed={name}");
}

fn main() {
    let build_date = execute_cmd("date", &["+%Y.%m.%d-%H:%M:%S"], false);
    // we should omit stderr in case git command is not installed
    let build_commit = execute_cmd("git", &["describe", "--always", "--long", "--dirty"], true);
    // we should omit stderr in case this repository has no any tags
    let build_tag = execute_cmd("git", &["describe", "--tags", "--abbrev=0"], true);
    let rustc_build_version = execute_cmd("rustc", &["--version"], false);
    let cargo_build_version = execute_cmd("cargo", &["--version"], false);

    set_cargo_env("BUILD_DATE", build_date);
    set_cargo_env("BUILD_COMMIT", build_commit);
    set_cargo_env("BUILD_TAG", build_tag);
    set_cargo_env("RUSTC_BUILD_VERSION", rustc_build_version);
    set_cargo_env("CARGO_BUILD_VERSION", cargo_build_version);
}

主函数

use std::{env, process};

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() > 1 && args[1].as_str() == "-v" {
        show_version();
    }
}

fn show_version() {
    // cargo编译时自带的环境变量
    let name = env!("CARGO_PKG_NAME");
    // 我们自己添加进来的运行时环境变量
    let build_date = env!("BUILD_DATE");
    let git_tag = env!("BUILD_TAG");
    let git_commit = env!("BUILD_COMMIT");
    let rustc_build_version = env!("RUSTC_BUILD_VERSION");
    let cargo_build_version = env!("CARGO_BUILD_VERSION");

    let git_info = if git_tag.is_empty() {
        format!("{git_commit}")
    } else {
        format!("{git_tag}+{git_commit}")
    };

    let os = env::consts::OS;
    let arch = env::consts::ARCH;

    println!("{name} {git_info}, {os}/{arch}, compiled by {rustc_build_version} and {cargo_build_version}, built on {build_date}");

    process::exit(0);
}