fedimint_build/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#![deny(clippy::pedantic)]

//! Fedimint build scripts
//!
//! Implements detection of build hash. Used both internally in all
//! public-consumed binaries of `fedimint`, and also in custom builds
//! that can included 3rd party Fedimint modules.
//!
//! To use include:
//!
//! ```norust
//! [build-dependencies]
//! fedimint-build = { version = "=0.4.0-alpha", path = "../fedimint-build" }
//! ```
//!
//! in `Cargo.toml`, and:
//!
//! ```ignore
//! fn main() {
//!     fedimint_build::set_code_version();
//! }
//! ```
//!
//! in `build.rs` script.
//!
//! This will define `FEDIMINT_BUILD_CODE_VERSION` at the build time, which can
//! be accessed via `fedimint_build_code_version_env!()` and passed to binary
//! builders like `FedimintCli::new`.
pub mod envs;

use std::env;
use std::path::Path;
use std::process::Command;

use crate::envs::{FEDIMINT_BUILD_CODE_VERSION_ENV, FORCE_GIT_HASH_ENV};

fn set_code_version_inner() -> Result<(), String> {
    println!("cargo:rerun-if-env-changed={FORCE_GIT_HASH_ENV}");

    if let Ok(hash) = env::var(FORCE_GIT_HASH_ENV) {
        eprintln!("Forced hash via {FORCE_GIT_HASH_ENV} to {hash}");
        println!("cargo:rustc-env={FEDIMINT_BUILD_CODE_VERSION_ENV}={hash}");
        return Ok(());
    }

    // In case we are compiling a released crate we don't have a git directory, but
    // can read the version hash from .cargo_vcs_info.json
    if let Ok(file) = std::fs::File::open("./.cargo_vcs_info.json") {
        let info: serde_json::Value = serde_json::from_reader(file)
            .map_err(|e| format!("Failed to parse .cargo_vcs_info.json: {e}"))?;
        let hash = info["git"]["sha1"].as_str().ok_or_else(|| {
            format!("Failed to parse .cargo_vcs_info.json: no `.git.sha` field: {info:?}")
        })?;
        println!("cargo:rustc-env={FEDIMINT_BUILD_CODE_VERSION_ENV}={hash}");
        return Ok(());
    }

    // built somewhere in the `$HOME/.cargo/...`, probably detecting it and
    // using a release version instead.

    // Note: best effort approach to force a re-run when the git hash in
    // the local repo changes without wrecking the incremental compilation
    // completely.
    for base in [
        // The relative path of git files might vary, so we just try a lot of cases.
        // If you go deeper than that, you're silly.
        ".",
        "..",
        "../..",
        "../../..",
        "../../../..",
        "../../../../..",
    ] {
        let p = &format!("{base}/.git/HEAD");
        if Path::new(&p).exists() {
            println!("cargo:rerun-if-changed={p}");
        }
        // Common(?) `git workdir` setup
        let p = &format!("{base}/HEAD");
        if Path::new(&p).exists() {
            println!("cargo:rerun-if-changed={p}");
        }
    }

    let hash = call_cmd("git", &["rev-parse", "HEAD"])?;

    let dirty = !call_cmd("git", &["status", "--porcelain"])?.is_empty();

    let hash = if dirty {
        // Since our hash needs to be constant, mark the dirty
        // state by replacing the middle with 0s. This should
        // be noticeable enough, while letting find out the
        // root commit anyway.
        format!("{}00000000{}", &hash[0..16], &hash[(40 - 16)..40])
    } else {
        hash
    };

    println!("cargo:rustc-env={FEDIMINT_BUILD_CODE_VERSION_ENV}={hash}");

    Ok(())
}

fn call_cmd(cmd: &str, args: &[&str]) -> Result<String, String> {
    let output = match Command::new(cmd).args(args).output() {
        Ok(output) => output,
        Err(e) => {
            return Err(format!("Failed to execute `git` command: {e}"));
        }
    };

    if !output.status.success() {
        return Err(format!(
            "`git` command failed: stderr: {}; stdout: {}",
            String::from_utf8_lossy(&output.stderr),
            String::from_utf8_lossy(&output.stdout)
        ));
    }

    match String::from_utf8(output.stdout) {
        Ok(o) => Ok(o.trim().to_string()),
        Err(e) => Err(format!("Invalid UTF-8 sequence detected: {e}")),
    }
}

/// Run from a `build.rs` script to detect code version. See [`crate`] for
/// description.
pub fn set_code_version() {
    if let Err(e) = set_code_version_inner() {
        eprintln!("Failed to detect git hash version: {e}. Set {FORCE_GIT_HASH_ENV} to enforce the version and skip auto-detection.");
        println!("cargo:rustc-env={FEDIMINT_BUILD_CODE_VERSION_ENV}=0000000000000000000000000000000000000000");
    }
}