build.rs (4085B)
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::{ 18 env, fs, 19 path::{Path, PathBuf}, 20 process::Command, 21 }; 22 23 fn git(root: &Path, args: &[&str]) -> Result<String, String> { 24 let output = Command::new("git") 25 .args(args) 26 .current_dir(root) 27 .env_remove("GIT_DIR") 28 .env_remove("GIT_WORK_TREE") 29 .env_remove("GIT_COMMON_DIR") 30 .output() 31 .map_err(|e| format!("could not run git: {e}"))?; 32 if !output.status.success() { 33 return Err(format!( 34 "git {}: {}", 35 args.join(" "), 36 String::from_utf8_lossy(&output.stderr).trim() 37 )); 38 } 39 String::from_utf8(output.stdout) 40 .map(|value| value.trim().to_owned()) 41 .map_err(|e| format!("invalid Git output: {e}")) 42 } 43 44 fn watch(path: &Path) { 45 println!("cargo:rerun-if-changed={}", path.display()); 46 } 47 48 fn git_version(root: &Path) -> Result<String, String> { 49 // An archive can be unpacked inside another checkout. Only use our own Git 50 // metadata, including the .git file used by linked worktrees/submodules. 51 let marker = root.join(".git"); 52 if !marker.exists() { 53 return Err("source tree has no Git metadata".into()); 54 } 55 if marker.is_file() { 56 watch(&marker); 57 } 58 let toplevel = git(root, &["rev-parse", "--show-toplevel"])?; 59 if Path::new(&toplevel) 60 .canonicalize() 61 .map_err(|e| e.to_string())? 62 != root 63 { 64 return Err("Git metadata belongs to another source tree".into()); 65 } 66 67 // Resolve paths through Git: worktree HEAD is private, while refs and 68 // packed-refs live in the common Git directory. Watching all loose refs 69 // also catches new/deleted tags, even when HEAD itself does not move. 70 for name in ["HEAD", "refs", "packed-refs", "shallow"] { 71 let path = PathBuf::from(git( 72 root, 73 &["rev-parse", "--path-format=absolute", "--git-path", name], 74 )?); 75 // refs exists even when empty. Packing refs removes loose files there, 76 // which reruns this script and adds the newly created packed-refs file. 77 if path.exists() || name == "HEAD" { 78 watch(&path); 79 } 80 } 81 git(root, &["describe", "--tags", "--always", "--abbrev=8"]) 82 } 83 84 fn main() -> Result<(), String> { 85 let manifest = 86 PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").ok_or("CARGO_MANIFEST_DIR is missing")?); 87 let root = manifest 88 .join("../..") 89 .canonicalize() 90 .map_err(|e| format!("could not locate source root: {e}"))?; 91 watch(&manifest.join("build.rs")); 92 let version = match git_version(&root) { 93 Ok(version) => version, 94 Err(git_error) => { 95 let path = root.join(".version"); 96 watch(&path); 97 // Recheck Git availability on later builds of an extracted tree. 98 watch(&root.join(".git")); 99 fs::read_to_string(&path).map_err(|e| format!( 100 "cannot determine program version: {git_error}; could not read {}: {e}. Source archives must contain a .version file", 101 path.display() 102 ))? 103 } 104 }; 105 let version = version.trim(); 106 if version.is_empty() || version.contains(['\n', '\r', '\0']) { 107 return Err("program version must be a nonempty single line".into()); 108 } 109 println!("cargo:rustc-env=BUILD_VERSION={version}"); 110 Ok(()) 111 }