cli_batteries/
build.rs

1use eyre::{bail, eyre, Result, WrapErr as _};
2use std::{
3    env::{var, VarError},
4    fs,
5    path::Path,
6    process::Command,
7};
8use time::{format_description::well_known::Rfc3339, OffsetDateTime, UtcOffset};
9
10/// Set some compile-time environment variables.
11///
12/// * `TARGET`: The target triple.
13/// * `COMMIT_SHA`: The commit hash.
14/// * `COMMIT_DATE`: The commit date.
15/// * `BUILD_DATE`: The current date.
16///
17/// # Errors
18///
19/// Returns an error if something went wrong, for example `git` failed.
20#[allow(clippy::module_name_repetitions)]
21pub fn build_rs() -> Result<()> {
22    let commit = rerun_if_git_changes().unwrap_or_else(|e| {
23        eprintln!("Warning: {}", e);
24        None
25    });
26
27    println!(
28        "cargo:rustc-env=COMMIT_SHA={}",
29        env_or_cmd("COMMIT_SHA", &["git", "rev-parse", "HEAD"]).unwrap_or_else(|e| {
30            eprintln!("Warning: {}", e);
31            commit.unwrap_or_else(|| "0000000000000000000000000000000000000000".to_string())
32        })
33    );
34    let build_date = OffsetDateTime::now_utc();
35    let commit_date = env_or_cmd("COMMIT_DATE", &[
36        "git",
37        "log",
38        "-n1",
39        "--pretty=format:'%aI'",
40    ])
41    .and_then(|str| Ok(OffsetDateTime::parse(str.trim_matches('\''), &Rfc3339)?))
42    .unwrap_or_else(|e| {
43        eprintln!("Warning: {}", e);
44        OffsetDateTime::UNIX_EPOCH
45    });
46    println!(
47        "cargo:rustc-env=COMMIT_DATE={}",
48        commit_date.to_offset(UtcOffset::UTC).date()
49    );
50    println!(
51        "cargo:rustc-env=BUILD_DATE={}",
52        build_date.to_offset(UtcOffset::UTC).date()
53    );
54    println!(
55        "cargo:rustc-env=TARGET={}",
56        var("TARGET").wrap_err("Fetching environment variable TARGET")?
57    );
58
59    Ok(())
60}
61
62fn env_or_cmd(env: &str, cmd: &[&str]) -> Result<String> {
63    // Try env first
64    match var(env) {
65        Ok(s) => return Ok(s),
66        Err(VarError::NotPresent) => (),
67        Err(e) => bail!(e),
68    };
69
70    // Try command
71    let err = || {
72        format!(
73            "Variable {} is unset and command \"{}\" failed",
74            env,
75            cmd.join(" ")
76        )
77    };
78    let output = Command::new(cmd[0])
79        .args(&cmd[1..])
80        .output()
81        .with_context(err)?;
82    if output.status.success() {
83        Ok(String::from_utf8(output.stdout)?.trim().to_string())
84    } else {
85        bail!(err())
86    }
87}
88
89fn rerun_if_git_changes() -> Result<Option<String>> {
90    let git_head = Path::new(".git/HEAD");
91
92    // Skip if not in a git repo
93    if !git_head.exists() {
94        eprintln!("No .git/HEAD found, not rerunning on git change");
95        return Ok(None);
96    }
97
98    // TODO: Worktree support where `.git` is a file
99    println!("cargo:rerun-if-changed=.git/HEAD");
100
101    // If HEAD contains a ref, then echo that path also.
102    let contents = fs::read_to_string(git_head).wrap_err("Error reading .git/HEAD")?;
103    let head_ref = contents.split(": ").collect::<Vec<_>>();
104    let commit = if head_ref.len() == 2 && head_ref[0] == "ref" {
105        let ref_path = Path::new(".git").join(head_ref[1].trim());
106        let ref_path_str = ref_path
107            .to_str()
108            .ok_or_else(|| eyre!("Could not convert ref path {:?} to string", ref_path))?;
109        println!("cargo:rerun-if-changed={}", ref_path_str);
110        fs::read_to_string(&ref_path).with_context(|| format!("Error reading {}", ref_path_str))?
111    } else {
112        contents
113    };
114    Ok(Some(commit))
115}