Repository navigation
Expand file tree
/
Copy pathbuild.rs
More file actions
179 lines (169 loc) · 7.26 KB
/
Copy pathbuild.rs
File metadata and controls
179 lines (169 loc) · 7.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Build script for ssh-cli.
//!
//! Embeds the commit hash into the `SSH_CLI_COMMIT_HASH` environment variable.
//! GAP-SSH-REL-002: `-dirty` suffix when the working tree has local changes.
//! GAP-SSH-REL-007: precedence env → git → `.commit_hash` (crates.io pack) → `unknown`.
//!
//! # `.commit_hash` format
//!
//! One line, two space-separated tokens: `<hash> <version>` (for example
//! `a691fc7 0.5.5`). When the second token equals `CARGO_PKG_VERSION` the hash is
//! printed as is; the version is validated, never displayed. A file in the old
//! single-token format counts as "version unknown" and is ignored.
//!
//! Why: a hash that is confidently wrong is worse than a missing one. The 0.5.6 binary
//! once printed the 0.5.5 hash because the file did not say which release it belonged
//! to. A file recorded for another version names the commit this tree was derived from,
//! and the version bump proves the tree changed after it, so it is printed as
//! `<hash>-dirty`: true about the base, explicit about the local changes.
//!
//! # External process policy (G-PROC)
//!
//! The only `std::process::Command` uses in this crate tree at **build** time are
//! optional `git` probes for the short HEAD hash. Runtime product code never
//! spawns local children (SSH is pure `russh`). Missing `git` is non-fatal.
//!
//! Each spawn sets `stdin`/`stdout`/`stderr` explicitly (rules: no implicit
//! inheritance). Arguments are static slices via `args` (no shell, no user input).
use std::process::{Command, Stdio};
fn main() {
let hash = commit_hash_with_dirty();
println!("cargo:rustc-env=SSH_CLI_COMMIT_HASH={hash}");
// Cargo treats a missing `rerun-if-changed` path as changed, which would rebuild the
// crate on every build in a tree without `.git` (crates.io tarball, plain copy).
// Only watch the git files when they exist.
for git_file in [".git/HEAD", ".git/index"] {
if std::path::Path::new(git_file).exists() {
println!("cargo:rerun-if-changed={git_file}");
}
}
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=.commit_hash");
println!("cargo:rerun-if-env-changed=SSH_CLI_COMMIT_HASH");
}
/// Short HEAD hash, with `-dirty` when `git status --porcelain` is non-empty.
///
/// Precedence (REL-007):
/// 1. `SSH_CLI_COMMIT_HASH` env (CI/publish inject)
/// 2. `git rev-parse` when a checkout exists
/// 3. `.commit_hash` file in the package manifest (crates.io tarball, which has no
/// `.git`), only when its recorded version equals `CARGO_PKG_VERSION`
/// 4. `unknown`
///
/// # Why git outranks the file
///
/// The file used to win, on the assumption that it only exists inside a published
/// pack. It does not: `.commit_hash` is tracked in the repository, so every local
/// build of a checkout read a frozen value. Measured on 2026-08-10: the binary
/// reported `f3b98b7` while `HEAD` was `09590ba`, six commits later — provenance that
/// is confidently wrong is worse than provenance that is missing, because nobody
/// double-checks a hash that looks plausible.
///
/// Asking git first fixes the class rather than the instance. The file keeps its
/// stated purpose exactly: a crates.io tarball ships without `.git`, so the git probe
/// finds nothing there and the file answers, as designed.
fn commit_hash_with_dirty() -> String {
if let Ok(env_hash) = std::env::var("SSH_CLI_COMMIT_HASH") {
let t = env_hash.trim();
if !t.is_empty() {
return t.to_string();
}
}
if let Some(hash) = git_head_hash() {
return with_dirty_suffix(hash);
}
if let Some(file_hash) = read_commit_hash_file() {
// G-E2E-06: reached when there is no checkout — the crates.io pack case. The
// `-dirty` suffix is still applied because it is a no-op outside a checkout
// and honest inside one (a source tree that vendored the file).
return with_dirty_suffix(file_hash);
}
"unknown".to_string()
}
/// Short `HEAD` hash from git, or [`None`] when this is not a usable checkout.
///
/// Optional build-time dependency: `git` on PATH. Absent git, absent `.git`, or a
/// failed invocation all mean "no checkout here", which is a normal state for a
/// source build from a published tarball rather than an error.
fn git_head_hash() -> Option<String> {
let output = git_command()
.args(["rev-parse", "--short", "HEAD"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
if hash.is_empty() {
return None;
}
Some(hash)
}
/// Appends `-dirty` when a git checkout has local changes (G-E2E-06).
///
/// No-op when git is missing, not a checkout, or porcelain is empty. Never
/// strips an existing `-dirty` suffix.
fn with_dirty_suffix(mut hash: String) -> String {
if hash.ends_with("-dirty") {
return hash;
}
// Only probe dirty when `.git` exists (crates.io tarball has no git).
let manifest = std::env::var_os("CARGO_MANIFEST_DIR");
let has_git = manifest
.as_ref()
.map(|d| std::path::Path::new(d).join(".git").exists())
.unwrap_or(false);
if !has_git {
return hash;
}
let dirty = git_command()
.args(["status", "--porcelain"])
.output()
.map(|o| o.status.success() && !o.stdout.is_empty())
.unwrap_or(false);
if dirty {
hash.push_str("-dirty");
}
hash
}
/// Builds a non-interactive `git` command with explicit stdio policy.
///
/// - `stdin`: null — never inherit the cargo parent stdin
/// - `stdout`/`stderr`: piped — captured by `.output()`; no terminal noise
///
/// No shell, no env mutation, no user-controlled arguments.
fn git_command() -> Command {
let mut cmd = Command::new("git");
// Explicit stdio (G-PROC-01): do not rely on spawn/status inheritance defaults.
// `.output()` would pipe stdout/stderr and null stdin, but rules require the
// policy to be visible at the call site for every external process.
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
cmd
}
/// Hash from `.commit_hash`, or [`None`] when the file is absent or malformed.
///
/// Expected content is `<hash> <version>` with a 7-40 hex digit hash. When `<version>`
/// equals `CARGO_PKG_VERSION` the hash is returned as is. Any other version means the
/// tree moved past that commit (the version was bumped after it), so the hash comes back
/// as `<hash>-dirty`. A single-token (legacy) file or extra tokens are ignored.
fn read_commit_hash_file() -> Option<String> {
let manifest_dir = std::env::var_os("CARGO_MANIFEST_DIR")?;
let path = std::path::Path::new(&manifest_dir).join(".commit_hash");
let raw = std::fs::read_to_string(path).ok()?;
let pkg_version = std::env::var("CARGO_PKG_VERSION").ok()?;
let mut tokens = raw.split_whitespace();
let hash = tokens.next()?;
let version = tokens.next()?;
let is_hex_hash = (7..=40).contains(&hash.len()) && hash.chars().all(|c| c.is_ascii_hexdigit());
if tokens.next().is_some() || !is_hex_hash {
return None;
}
if version == pkg_version {
Some(hash.to_string())
} else {
Some(format!("{hash}-dirty"))
}
}