crate_git_revision/
lib.rs

1//! Embed the git revision of a crate in its build.
2//!
3//! Supports embedding the version from a local or remote git repository the build
4//! is occurring in, as well as when `cargo install` or depending on a crate
5//! published to crates.io.
6//!
7//! It extracts the git revision in two ways:
8//! - From the `.cargo_vcs_info.json` file embedded in published crates.
9//! - From the git repository the build is occurring from in unpublished crates.
10//!
11//! Injects an environment variable `GIT_REVISION` into the build that contains
12//! the full git revision, with a `-dirty` suffix if the working directory is
13//! dirty.
14//!
15//! Requires the use of a build.rs build script. See [Build Scripts]() for more
16//! details on how Rust build scripts work.
17//!
18//! [Build Scripts]: https://doc.rust-lang.org/cargo/reference/build-scripts.html
19//!
20//! ### Examples
21//!
22//! Add the following to the crate's `Cargo.toml` file:
23//!
24//! ```toml
25//! [build_dependencies]
26//! crate-git-revision = "0.0.2"
27//! ```
28//!
29//! Add the following to the crate's `build.rs` file:
30//!
31//! ```rust
32//! crate_git_revision::init();
33//! ```
34//!
35//! Add the following to the crate's `lib.rs` or `main.rs` file:
36//!
37//! ```ignore
38//! pub const GIT_REVISION: &str = env!("GIT_REVISION");
39//! ```
40
41use std::{fs::read_to_string, path::Path, process::Command, str};
42
43/// Initialize the GIT_REVISION environment variable with the git revision of
44/// the current crate.
45///
46/// Intended to be called from within a build script, `build.rs` file, for the
47/// crate.
48pub fn init() {
49    let _res = __init(&mut std::io::stdout(), &std::env::current_dir().unwrap());
50}
51
52fn __init(w: &mut impl std::io::Write, current_dir: &Path) -> std::io::Result<()> {
53    let mut git_sha: Option<String> = None;
54
55    // Read the git revision from the JSON file embedded by cargo publish. This
56    // will get the version from published crates.
57    if let Ok(vcs_info) = read_to_string(current_dir.join(".cargo_vcs_info.json")) {
58        let vcs_info: Result<CargoVcsInfo, _> = serde_json::from_str(&vcs_info);
59        if let Ok(vcs_info) = vcs_info {
60            git_sha = Some(vcs_info.git.sha1);
61        }
62    }
63
64    // Read the git revision from the git repository containing the code being
65    // built.
66    if git_sha.is_none() {
67        match Command::new("git")
68            .current_dir(current_dir)
69            .arg("rev-parse")
70            .arg("--git-dir")
71            .output()
72            .map(|o| o.stdout)
73        {
74            Err(e) => {
75                writeln!(
76                    w,
77                    "cargo:warning=Error getting git directory to get git revision: {e:?}"
78                )?;
79            }
80            Ok(git_dir) => {
81                let git_dir = String::from_utf8_lossy(&git_dir);
82                let git_dir = git_dir.trim();
83
84                // Require the build script to rerun if relavent git state changes which
85                // changes the current git commit.
86                //  - .git/index: Changes if the index/staged files changes, which will
87                //  cause the repo to be dirty.
88                //  - .git/HEAD: Changes if the ref currently in the working directory,
89                //  and potentially the commit, to change.
90                //  - .git/refs: Changes to any files in refs could cause the current
91                //  commit to have changed if the ref in .git/HEAD is changed.
92                // Note: That changes in the above files may not result in material
93                // changes to the crate, but changes in any should invalidate the
94                // revision since the revision can be changed by any of the above.
95                writeln!(w, "cargo:rerun-if-changed={git_dir}/index")?;
96                writeln!(w, "cargo:rerun-if-changed={git_dir}/HEAD")?;
97                writeln!(w, "cargo:rerun-if-changed={git_dir}/refs")?;
98
99                match Command::new("git")
100                    .current_dir(current_dir)
101                    .arg("describe")
102                    .arg("--always")
103                    .arg("--exclude='*'")
104                    .arg("--long")
105                    .arg("--abbrev=1000")
106                    .arg("--dirty")
107                    .output()
108                    .map(|o| o.stdout)
109                {
110                    Err(e) => {
111                        writeln!(
112                            w,
113                            "cargo:warning=Error getting git revision from {current_dir:?}: {e:?}"
114                        )?;
115                    }
116                    Ok(git_describe) => {
117                        git_sha = str::from_utf8(&git_describe).ok().map(str::to_string);
118                    }
119                }
120            }
121        }
122    }
123
124    if let Some(git_sha) = git_sha {
125        writeln!(w, "cargo:rustc-env=GIT_REVISION={git_sha}")?;
126    }
127
128    Ok(())
129}
130
131#[derive(serde_derive::Serialize, serde_derive::Deserialize, Default)]
132struct CargoVcsInfo {
133    git: CargoVcsInfoGit,
134}
135
136#[derive(serde_derive::Serialize, serde_derive::Deserialize, Default)]
137struct CargoVcsInfoGit {
138    sha1: String,
139}
140
141mod test;