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