Skip to main content

tycho_build_info/
lib.rs

1use std::path::{Path, PathBuf};
2use std::process::{Command, Stdio};
3
4use anyhow::{Context, Result, anyhow};
5
6/// Controls how [`git_describe`] formats the reported git revision.
7pub enum DescribeFormat {
8    /// Uses `git describe --dirty --tags`
9    Reach,
10    /// Only emit the abbreviated commit hash, matching `git rev-parse HEAD`
11    CommitOnly,
12}
13
14/// Returns the current git version formatted according to [`DescribeFormat`].
15///
16/// * [`DescribeFormat::Reach`] produces output identical to `git describe --always --dirty --tags`.
17///   `[tag]-[number of commits since tag]-g[short hash][-modified if dirty]` Eg v0.3.2-20-gedb15c6f7-modified
18/// * [`DescribeFormat::CommitOnly`] emits full commit hash from `git rev-parse`.
19pub fn git_describe(describe_type: DescribeFormat) -> Result<String> {
20    let manifest_dir = manifest_dir()?;
21    let git_dir = git_dir(&manifest_dir)?;
22    register_git_paths(&git_dir);
23
24    match describe_type {
25        DescribeFormat::Reach => run_git(&manifest_dir, [
26            "describe",
27            "--always",
28            "--dirty=-modified",
29            "--tags",
30            "--match=v[0-9]*",
31        ]),
32        DescribeFormat::CommitOnly => run_git(&manifest_dir, ["rev-parse", "HEAD"]),
33    }
34}
35
36fn manifest_dir() -> Result<PathBuf> {
37    std::env::var_os("CARGO_MANIFEST_DIR")
38        .map(PathBuf::from)
39        .ok_or_else(|| anyhow!("missing 'CARGO_MANIFEST_DIR' environment variable"))
40}
41
42fn git_dir(manifest_dir: &Path) -> Result<PathBuf> {
43    let raw = run_git(manifest_dir, ["rev-parse", "--git-dir"])?;
44    let path = PathBuf::from(raw.trim());
45    Ok(if path.is_absolute() {
46        path
47    } else {
48        manifest_dir.join(path)
49    })
50}
51
52fn register_git_paths(git_dir: &Path) {
53    for subpath in ["HEAD", "logs/HEAD", "index"] {
54        if let Ok(path) = git_dir.join(subpath).canonicalize() {
55            println!("cargo:rerun-if-changed={}", path.display());
56        }
57    }
58}
59
60fn run_git(cwd: &Path, args: impl IntoIterator<Item = &'static str>) -> Result<String> {
61    let args: Vec<&'static str> = args.into_iter().collect();
62    run_command(cwd, "git", &args)
63}
64
65pub fn run_command(cwd: &Path, program: &str, args: &[&str]) -> Result<String> {
66    println!("cargo:rerun-if-env-changed=PATH");
67    let output = Command::new(program)
68        .args(args)
69        .current_dir(cwd)
70        .stderr(Stdio::inherit())
71        .output()
72        .with_context(|| format!("{program} {:?} failed", args))?;
73
74    if output.status.success() {
75        let mut text = String::from_utf8(output.stdout)
76            .with_context(|| format!("{program} {:?} printed invalid utf-8", args))?;
77        while matches!(text.as_bytes().last(), Some(b'\n' | b'\r')) {
78            text.pop();
79        }
80        Ok(text)
81    } else if let Some(code) = output.status.code() {
82        Err(anyhow!("{program} {:?}: exited with {code}", args))
83    } else {
84        Err(anyhow!("{program} {:?}: terminated by signal", args))
85    }
86}