build.rs (4095B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2025, 2026 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 use std::env; 18 use std::fs; 19 use std::path::{Path, PathBuf}; 20 use std::process::Command; 21 22 fn git(root: &Path, args: &[&str]) -> Result<String, String> { 23 let output = Command::new("git") 24 .args(args) 25 .current_dir(root) 26 .env_remove("GIT_DIR") 27 .env_remove("GIT_WORK_TREE") 28 .env_remove("GIT_COMMON_DIR") 29 .output() 30 .map_err(|e| format!("could not run git: {e}"))?; 31 if !output.status.success() { 32 return Err(format!( 33 "git {}: {}", 34 args.join(" "), 35 String::from_utf8_lossy(&output.stderr).trim() 36 )); 37 } 38 String::from_utf8(output.stdout) 39 .map(|value| value.trim().to_owned()) 40 .map_err(|e| format!("invalid Git output: {e}")) 41 } 42 43 fn watch(path: &Path) { 44 println!("cargo:rerun-if-changed={}", path.display()); 45 } 46 47 fn git_version(root: &Path) -> Result<String, String> { 48 // An archive can be unpacked inside another checkout. Only use our own Git 49 // metadata, including the .git file used by linked worktrees/submodules. 50 let marker = root.join(".git"); 51 if !marker.exists() { 52 return Err("source tree has no Git metadata".into()); 53 } 54 if marker.is_file() { 55 watch(&marker); 56 } 57 let toplevel = git(root, &["rev-parse", "--show-toplevel"])?; 58 if Path::new(&toplevel) 59 .canonicalize() 60 .map_err(|e| e.to_string())? 61 != root 62 { 63 return Err("Git metadata belongs to another source tree".into()); 64 } 65 66 // Resolve paths through Git: worktree HEAD is private, while refs and 67 // packed-refs live in the common Git directory. Watching all loose refs 68 // also catches new/deleted tags, even when HEAD itself does not move. 69 for name in ["HEAD", "refs", "packed-refs", "shallow"] { 70 let path = PathBuf::from(git( 71 root, 72 &["rev-parse", "--path-format=absolute", "--git-path", name], 73 )?); 74 // refs exists even when empty. Packing refs removes loose files there, 75 // which reruns this script and adds the newly created packed-refs file. 76 if path.exists() || name == "HEAD" { 77 watch(&path); 78 } 79 } 80 git(root, &["describe", "--tags", "--always", "--abbrev=8"]) 81 } 82 83 fn main() -> Result<(), String> { 84 let manifest = 85 PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").ok_or("CARGO_MANIFEST_DIR is missing")?); 86 let root = manifest 87 .join("../..") 88 .canonicalize() 89 .map_err(|e| format!("could not locate source root: {e}"))?; 90 watch(&manifest.join("build.rs")); 91 let version = match git_version(&root) { 92 Ok(version) => version, 93 Err(git_error) => { 94 let path = root.join(".version"); 95 watch(&path); 96 // Recheck Git availability on later builds of an extracted tree. 97 watch(&root.join(".git")); 98 fs::read_to_string(&path).map_err(|e| format!( 99 "cannot determine program version: {git_error}; could not read {}: {e}. Source archives must contain a .version file", 100 path.display() 101 ))? 102 } 103 }; 104 let version = version.trim(); 105 if version.is_empty() || version.contains(['\n', '\r', '\0']) { 106 return Err("program version must be a nonempty single line".into()); 107 } 108 println!("cargo:rustc-env=BUILD_VERSION={version}"); 109 Ok(()) 110 }