fedimint_build/
lib.rs

1#![deny(clippy::pedantic)]
2
3//! Fedimint build scripts
4//!
5//! Implements detection of build hash. Used both internally in all
6//! public-consumed binaries of `fedimint`, and also in custom builds
7//! that can included 3rd party Fedimint modules.
8//!
9//! To use include:
10//!
11//! ```norust
12//! [build-dependencies]
13//! fedimint-build = { version = "=0.4.0-alpha", path = "../fedimint-build" }
14//! ```
15//!
16//! in `Cargo.toml`, and:
17//!
18//! ```ignore
19//! fn main() {
20//!     fedimint_build::set_code_version();
21//! }
22//! ```
23//!
24//! in `build.rs` script.
25//!
26//! This will define `FEDIMINT_BUILD_CODE_VERSION` at the build time, which can
27//! be accessed via `fedimint_build_code_version_env!()` and passed to binary
28//! builders like `FedimintCli::new`.
29pub mod envs;
30
31use std::env;
32use std::path::Path;
33use std::process::Command;
34
35use crate::envs::{FEDIMINT_BUILD_CODE_VERSION_ENV, FORCE_GIT_HASH_ENV};
36
37fn set_code_version_inner() -> Result<(), String> {
38    println!("cargo:rerun-if-env-changed={FORCE_GIT_HASH_ENV}");
39
40    if let Ok(hash) = env::var(FORCE_GIT_HASH_ENV) {
41        eprintln!("Forced hash via {FORCE_GIT_HASH_ENV} to {hash}");
42        println!("cargo:rustc-env={FEDIMINT_BUILD_CODE_VERSION_ENV}={hash}");
43        return Ok(());
44    }
45
46    // In case we are compiling a released crate we don't have a git directory, but
47    // can read the version hash from .cargo_vcs_info.json
48    if let Ok(file) = std::fs::File::open("./.cargo_vcs_info.json") {
49        let info: serde_json::Value = serde_json::from_reader(file)
50            .map_err(|e| format!("Failed to parse .cargo_vcs_info.json: {e}"))?;
51        let hash = info["git"]["sha1"].as_str().ok_or_else(|| {
52            format!("Failed to parse .cargo_vcs_info.json: no `.git.sha` field: {info:?}")
53        })?;
54        println!("cargo:rustc-env={FEDIMINT_BUILD_CODE_VERSION_ENV}={hash}");
55        return Ok(());
56    }
57
58    // built somewhere in the `$HOME/.cargo/...`, probably detecting it and
59    // using a release version instead.
60
61    // Note: best effort approach to force a re-run when the git hash in
62    // the local repo changes without wrecking the incremental compilation
63    // completely.
64    for base in [
65        // The relative path of git files might vary, so we just try a lot of cases.
66        // If you go deeper than that, you're silly.
67        ".",
68        "..",
69        "../..",
70        "../../..",
71        "../../../..",
72        "../../../../..",
73    ] {
74        let p = &format!("{base}/.git/HEAD");
75        if Path::new(&p).exists() {
76            println!("cargo:rerun-if-changed={p}");
77        }
78        // Common(?) `git workdir` setup
79        let p = &format!("{base}/HEAD");
80        if Path::new(&p).exists() {
81            println!("cargo:rerun-if-changed={p}");
82        }
83    }
84
85    let hash = call_cmd("git", &["rev-parse", "HEAD"])?;
86
87    let dirty = !call_cmd("git", &["status", "--porcelain"])?.is_empty();
88
89    let hash = if dirty {
90        // Since our hash needs to be constant, mark the dirty
91        // state by replacing the middle with 0s. This should
92        // be noticeable enough, while letting find out the
93        // root commit anyway.
94        format!("{}00000000{}", &hash[0..16], &hash[(40 - 16)..40])
95    } else {
96        hash
97    };
98
99    println!("cargo:rustc-env={FEDIMINT_BUILD_CODE_VERSION_ENV}={hash}");
100
101    Ok(())
102}
103
104fn call_cmd(cmd: &str, args: &[&str]) -> Result<String, String> {
105    let output = match Command::new(cmd).args(args).output() {
106        Ok(output) => output,
107        Err(e) => {
108            return Err(format!("Failed to execute `git` command: {e}"));
109        }
110    };
111
112    if !output.status.success() {
113        return Err(format!(
114            "`git` command failed: stderr: {}; stdout: {}",
115            String::from_utf8_lossy(&output.stderr),
116            String::from_utf8_lossy(&output.stdout)
117        ));
118    }
119
120    match String::from_utf8(output.stdout) {
121        Ok(o) => Ok(o.trim().to_string()),
122        Err(e) => Err(format!("Invalid UTF-8 sequence detected: {e}")),
123    }
124}
125
126/// Run from a `build.rs` script to detect code version. See [`crate`] for
127/// description.
128pub fn set_code_version() {
129    if let Err(e) = set_code_version_inner() {
130        eprintln!(
131            "Failed to detect git hash version: {e}. Set {FORCE_GIT_HASH_ENV} to enforce the version and skip auto-detection."
132        );
133        println!(
134            "cargo:rustc-env={FEDIMINT_BUILD_CODE_VERSION_ENV}=0000000000000000000000000000000000000000"
135        );
136    }
137}