返回 CodeWhale
lib.rs
1 //! Shared build-script helpers for the `codewhale-cli`, `codewhale-tui`, and
2 //! `codewhale-telemetry` build scripts: rerun-condition declarations, the
3 //! embedded `DEEPSEEK_BUILD_VERSION` metadata, and the release-only build sha.
4 //! Only call these functions from a build script — they emit `cargo:`
5 //! directives on stdout.
6 //!
7 //! Two different shas live here and they are not interchangeable.
8 //! `DEEPSEEK_BUILD_VERSION`/`CODEWHALE_BUILD_COMMIT` describe *the build the
9 //! environment asked for* (`DEEPSEEK_BUILD_SHA`/`GITHUB_SHA`); an unstamped
10 //! local build renders a `(dev)` marker instead.
11 //! `CODEWHALE_RELEASE_BUILD_SHA` describes a *published* binary and has no
12 //! fallback at all, because it leaves the machine.
13 //!
14 //! ## Why the stamp never reads the local checkout (#5245)
15 //!
16 //! These helpers used to watch `.git/HEAD`/refs and fall back to
17 //! `git rev-parse HEAD`, so every local commit invalidated the two largest
18 //! compile units in the workspace (a ~14-minute release rebuild with zero
19 //! code changes). And the alternative — resolving the sha at *runtime* —
20 //! would lie: the binary runs inside users' repositories, and a stale binary
21 //! would report whatever the checkout's HEAD is *now*, which breaks the
22 //! dogfood-receipt identity `scripts/release/install-dogfood.sh` verifies.
23 //! So the contract is: a sha appears in the version string only when the
24 //! build environment supplied one (`DEEPSEEK_BUILD_SHA` wins over
25 //! `GITHUB_SHA`), the build script reruns only when those variables change,
26 //! and a build nobody stamped says `(dev)`. CI and release builds are
27 //! byte-identical to the old behavior; dogfood builds pass the sha
28 //! explicitly (the install script prints the exact command).
29
30 use std::path::Path;
31
32 /// Declare the rerun conditions for the build-metadata directives: the two
33 /// SHA-override environment variables, and deliberately nothing about the
34 /// local checkout — watching `.git` files is what made every local commit
35 /// rebuild the whole crate (#5245).
36 ///
37 /// `manifest_dir` is accepted (and ignored) so build scripts keep one call
38 /// shape; it documents that the decision is per-crate, not global state.
39 pub fn declare_rerun_conditions(_manifest_dir: &Path) {
40 println!("cargo:rerun-if-env-changed=DEEPSEEK_BUILD_SHA");
41 println!("cargo:rerun-if-env-changed=GITHUB_SHA");
42 }
43
44 /// Emit `cargo:rustc-env=DEEPSEEK_BUILD_VERSION=...` — the package version,
45 /// suffixed with the short build SHA when the environment supplied one
46 /// (`DEEPSEEK_BUILD_SHA`, then `GITHUB_SHA`), or with the literal `dev`
47 /// marker when it did not. `CODEWHALE_BUILD_COMMIT` is emitted only in the
48 /// stamped case.
49 ///
50 /// `package_version` is the calling build script's `CARGO_PKG_VERSION`;
51 /// `manifest_dir` is accepted for call-shape stability.
52 pub fn emit_build_version(_manifest_dir: &Path, package_version: &str) {
53 let commit = build_commit();
54 let build_version = commit
55 .as_ref()
56 .and_then(|sha| short_sha(sha.clone()))
57 .map(|sha| format!("{package_version} ({sha})"))
58 .unwrap_or_else(|| format!("{package_version} (dev)"));
59
60 println!("cargo:rustc-env=DEEPSEEK_BUILD_VERSION={build_version}");
61 if let Some(commit) = commit {
62 println!("cargo:rustc-env=CODEWHALE_BUILD_COMMIT={commit}");
63 }
64 }
65
66 /// Declare the rerun conditions for [`emit_release_build_sha`] alone: the two
67 /// release-CI SHA variables, and nothing about the local checkout.
68 ///
69 /// Deliberately not [`declare_rerun_conditions`]: watching `.git/HEAD` would
70 /// make the build script rerun on every local commit, for a value that is
71 /// `None` on every local build by design.
72 pub fn declare_release_sha_rerun() {
73 println!("cargo:rerun-if-env-changed=DEEPSEEK_BUILD_SHA");
74 println!("cargo:rerun-if-env-changed=GITHUB_SHA");
75 }
76
77 /// Emit `cargo:rustc-env=CODEWHALE_RELEASE_BUILD_SHA=...` — the first 12 hex
78 /// characters of the build sha — **only** when the build environment supplied
79 /// one.
80 ///
81 /// This is provenance for a *published* binary, and it is the only sha a
82 /// telemetry payload may carry. There is deliberately no fallback to the local
83 /// checkout:
84 ///
85 /// - `CODEWHALE_BUILD_COMMIT` historically fell back to the builder's own
86 /// private `HEAD` on every local build; since #5245 it is env-only too,
87 /// but this value keeps its own name and rule because it is the only sha
88 /// a telemetry payload may carry.
89 /// - The "was this a published release" gate proposed earlier cannot be built:
90 /// `codewhale_release::latest_release_tag_{async,blocking}` are **network
91 /// calls** to `api.github.com` that return *tag names*, not shas, so the only
92 /// available comparison is version-vs-version — and a private tree at the
93 /// same version compares equal.
94 ///
95 /// Build-time provenance is deterministic, network-free, and verifiable from
96 /// the repository. Absent the release environment the value is simply absent,
97 /// and `option_env!` in the consuming crate yields `None`.
98 pub fn emit_release_build_sha() {
99 if let Some(sha) = release_build_sha(|name| std::env::var(name).ok()) {
100 println!("cargo:rustc-env=CODEWHALE_RELEASE_BUILD_SHA={sha}");
101 }
102 }
103
104 /// The decision behind [`emit_release_build_sha`], with the environment
105 /// injected so it can be tested without mutating the process.
106 ///
107 /// `DEEPSEEK_BUILD_SHA` wins over `GITHUB_SHA`; both must be a full 40-hex sha
108 /// to be believed, and the result is the first 12 characters.
109 #[must_use]
110 pub fn release_build_sha(read_env: impl Fn(&str) -> Option<String>) -> Option<String> {
111 read_env("DEEPSEEK_BUILD_SHA")
112 .and_then(full_sha)
113 .or_else(|| read_env("GITHUB_SHA").and_then(full_sha))
114 .and_then(short_sha)
115 }
116
117 fn build_commit() -> Option<String> {
118 build_commit_with(|name| std::env::var(name).ok())
119 }
120
121 /// The stamping decision with the environment injected, so the no-local-
122 /// fallback contract is testable without mutating the process (#5245).
123 fn build_commit_with(read_env: impl Fn(&str) -> Option<String>) -> Option<String> {
124 read_env("DEEPSEEK_BUILD_SHA")
125 .and_then(full_sha)
126 .or_else(|| read_env("GITHUB_SHA").and_then(full_sha))
127 }
128
129 fn full_sha(value: String) -> Option<String> {
130 let trimmed = value.trim().to_ascii_lowercase();
131 if trimmed.len() != 40 || !trimmed.bytes().all(|byte| byte.is_ascii_hexdigit()) {
132 return None;
133 }
134 Some(trimmed)
135 }
136
137 fn short_sha(value: String) -> Option<String> {
138 let trimmed = value.trim();
139 if trimmed.is_empty() {
140 return None;
141 }
142 Some(trimmed.chars().take(12).collect())
143 }
144
145 #[cfg(test)]
146 mod tests {
147 use super::{full_sha, release_build_sha, short_sha};
148
149 #[test]
150 fn full_commit_requires_exact_forty_hex_characters() {
151 assert_eq!(
152 full_sha("ABCDEF0123456789ABCDEF0123456789ABCDEF01".to_string()),
153 Some("abcdef0123456789abcdef0123456789abcdef01".to_string())
154 );
155 assert_eq!(full_sha("abc123".to_string()), None);
156 assert_eq!(
157 full_sha("gggggggggggggggggggggggggggggggggggggggg".to_string()),
158 None
159 );
160 assert_eq!(
161 short_sha("abcdef0123456789abcdef0123456789abcdef01".to_string()),
162 Some("abcdef012345".to_string())
163 );
164 }
165
166 #[test]
167 fn the_release_build_sha_is_absent_for_every_local_build() {
168 // No release environment: nothing is emitted, so `option_env!` in the
169 // consuming crate is `None` and a telemetry payload carries `git_sha:
170 // null`. This is the property that keeps a maintainer's private HEAD
171 // out of a shipped binary.
172 assert_eq!(release_build_sha(|_| None), None);
173 }
174
175 #[test]
176 fn the_release_build_sha_comes_only_from_a_release_environment() {
177 let ci = "abcdef0123456789abcdef0123456789abcdef01";
178 assert_eq!(
179 release_build_sha(|name| (name == "GITHUB_SHA").then(|| ci.to_string())),
180 Some("abcdef012345".to_string())
181 );
182 // The Codewhale variable wins over the GitHub one.
183 assert_eq!(
184 release_build_sha(|name| match name {
185 "DEEPSEEK_BUILD_SHA" => Some("f".repeat(40)),
186 "GITHUB_SHA" => Some(ci.to_string()),
187 _ => None,
188 }),
189 Some("f".repeat(12))
190 );
191 // A value that is not a full sha is not believed, and does not fall
192 // through to the local checkout.
193 assert_eq!(
194 release_build_sha(|name| (name == "DEEPSEEK_BUILD_SHA").then(|| "abc123".to_string())),
195 None
196 );
197 // `CODEWHALE_BUILD_COMMIT` is a different value with a different rule
198 // and is never a source here.
199 assert_eq!(
200 release_build_sha(|name| (name == "CODEWHALE_BUILD_COMMIT").then(|| ci.to_string())),
201 None
202 );
203 }
204
205 /// #5245 contract: the version stamp reads ONLY the two environment
206 /// variables. There is no fallback to the local checkout, so a plain
207 /// local build renders `(dev)` and — the actual point — the build script
208 /// declares no `.git` rerun paths, meaning `git commit` no longer
209 /// invalidates the two largest compile units in the workspace.
210 #[test]
211 fn the_build_commit_never_reads_the_local_checkout() {
212 // This test runs inside the real repository; if a git fallback still
213 // existed it would resolve a sha here. Absent env vars must mean
214 // absent commit, in the repo or out of it.
215 assert_eq!(super::build_commit_with(|_| None), None);
216 let ci = "abcdef0123456789abcdef0123456789abcdef01";
217 assert_eq!(
218 super::build_commit_with(|name| (name == "GITHUB_SHA").then(|| ci.to_string())),
219 Some(ci.to_string())
220 );
221 assert_eq!(
222 super::build_commit_with(|name| match name {
223 "DEEPSEEK_BUILD_SHA" => Some("f".repeat(40)),
224 "GITHUB_SHA" => Some(ci.to_string()),
225 _ => None,
226 }),
227 Some("f".repeat(40))
228 );
229 }
230 }
231
231 lines RUST