build.rs (4070B)
1 // This file is part of Robocop 2 // 3 // Robocop is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU General Public License as published by 5 // the Free Software Foundation, either version 3 of the License, or 6 // (at your option) any later version. 7 // 8 // Robocop is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU General Public License for more details. 12 // 13 // You should have received a copy of the GNU General Public License 14 // along with this program. If not, see <https://www.gnu.org/licenses/>. 15 // 16 // Copyright (C) 2026 Taler Systems SA 17 18 use std::env; 19 use std::fs; 20 use std::path::{Path, PathBuf}; 21 use std::process::Command; 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 .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 }