codewhale_build_support/
lib.rs1use std::{
7 path::{Path, PathBuf},
8 process::Command,
9};
10
11pub fn declare_rerun_conditions(manifest_dir: &Path) {
16 println!("cargo:rerun-if-env-changed=DEEPSEEK_BUILD_SHA");
17 println!("cargo:rerun-if-env-changed=GITHUB_SHA");
18 declare_git_head_rerun(manifest_dir);
19}
20
21pub fn emit_build_version(manifest_dir: &Path, package_version: &str) {
27 let commit = build_commit(manifest_dir);
28 let build_version = commit
29 .as_ref()
30 .and_then(|sha| short_sha(sha.clone()))
31 .map(|sha| format!("{package_version} ({sha})"))
32 .unwrap_or_else(|| package_version.to_string());
33
34 println!("cargo:rustc-env=DEEPSEEK_BUILD_VERSION={build_version}");
35 if let Some(commit) = commit {
36 println!("cargo:rustc-env=CODEWHALE_BUILD_COMMIT={commit}");
37 }
38}
39
40fn declare_git_head_rerun(manifest_dir: &Path) {
53 let workspace_root = manifest_dir.join("..").join("..");
54 let git_meta = workspace_root.join(".git");
55
56 let gitdir = if git_meta.is_dir() {
57 git_meta
58 } else if git_meta.is_file() {
59 println!("cargo:rerun-if-changed={}", git_meta.display());
61 let Ok(contents) = std::fs::read_to_string(&git_meta) else {
62 return;
63 };
64 let Some(rest) = contents.lines().find_map(|l| l.strip_prefix("gitdir:")) else {
65 return;
66 };
67 let trimmed = rest.trim();
68 if Path::new(trimmed).is_absolute() {
69 PathBuf::from(trimmed)
70 } else {
71 workspace_root.join(trimmed)
72 }
73 } else {
74 return;
75 };
76
77 let head = gitdir.join("HEAD");
78 println!("cargo:rerun-if-changed={}", head.display());
79
80 if let Ok(contents) = std::fs::read_to_string(&head)
81 && let Some(target) = parse_symbolic_ref(&contents)
82 {
83 let common_gitdir = git_common_dir(&gitdir);
84 println!(
85 "cargo:rerun-if-changed={}",
86 common_gitdir.join(target).display()
87 );
88 println!(
89 "cargo:rerun-if-changed={}",
90 common_gitdir.join("packed-refs").display()
91 );
92 }
93}
94
95fn git_common_dir(gitdir: &Path) -> PathBuf {
99 let commondir = gitdir.join("commondir");
100 let Ok(contents) = std::fs::read_to_string(commondir) else {
101 return gitdir.to_path_buf();
102 };
103 let trimmed = contents.trim();
104 if trimmed.is_empty() {
105 return gitdir.to_path_buf();
106 }
107 let path = Path::new(trimmed);
108 if path.is_absolute() {
109 path.to_path_buf()
110 } else {
111 gitdir.join(path)
112 }
113}
114
115fn parse_symbolic_ref(head_contents: &str) -> Option<&str> {
118 head_contents
119 .lines()
120 .next()
121 .and_then(|line| line.strip_prefix("ref:"))
122 .map(str::trim)
123 .filter(|s| !s.is_empty())
124}
125
126fn build_commit(manifest_dir: &Path) -> Option<String> {
127 env_commit("DEEPSEEK_BUILD_SHA")
128 .or_else(|| env_commit("GITHUB_SHA"))
129 .or_else(|| git_commit(manifest_dir))
130}
131
132fn env_commit(name: &str) -> Option<String> {
133 std::env::var(name).ok().and_then(full_sha)
134}
135
136fn git_commit(manifest_dir: &Path) -> Option<String> {
137 let top_level_output = Command::new("git")
138 .args(["-C"])
139 .arg(manifest_dir)
140 .args(["rev-parse", "--show-toplevel"])
141 .output()
142 .ok()?;
143 if !top_level_output.status.success() {
144 return None;
145 }
146 let top_level = PathBuf::from(String::from_utf8_lossy(&top_level_output.stdout).trim());
147 if !top_level.join("Cargo.toml").is_file() || !top_level.join("crates/tui").is_dir() {
148 return None;
149 }
150
151 let output = Command::new("git")
152 .args(["-C"])
153 .arg(top_level)
154 .args(["rev-parse", "HEAD"])
155 .output()
156 .ok()?;
157 if !output.status.success() {
158 return None;
159 }
160
161 full_sha(String::from_utf8_lossy(&output.stdout).to_string())
162}
163
164fn full_sha(value: String) -> Option<String> {
165 let trimmed = value.trim().to_ascii_lowercase();
166 if trimmed.len() != 40 || !trimmed.bytes().all(|byte| byte.is_ascii_hexdigit()) {
167 return None;
168 }
169 Some(trimmed)
170}
171
172fn short_sha(value: String) -> Option<String> {
173 let trimmed = value.trim();
174 if trimmed.is_empty() {
175 return None;
176 }
177 Some(trimmed.chars().take(12).collect())
178}
179
180#[cfg(test)]
181mod tests {
182 use super::{full_sha, git_common_dir, parse_symbolic_ref, short_sha};
183 use std::{
184 fs,
185 time::{SystemTime, UNIX_EPOCH},
186 };
187
188 #[test]
189 fn symbolic_ref_strips_prefix_and_whitespace() {
190 assert_eq!(
191 parse_symbolic_ref("ref: refs/heads/main\n"),
192 Some("refs/heads/main")
193 );
194 }
195
196 #[test]
197 fn symbolic_ref_handles_no_trailing_newline() {
198 assert_eq!(
199 parse_symbolic_ref("ref: refs/heads/work/v0.8.26-security"),
200 Some("refs/heads/work/v0.8.26-security")
201 );
202 }
203
204 #[test]
205 fn full_commit_requires_exact_forty_hex_characters() {
206 assert_eq!(
207 full_sha("ABCDEF0123456789ABCDEF0123456789ABCDEF01".to_string()),
208 Some("abcdef0123456789abcdef0123456789abcdef01".to_string())
209 );
210 assert_eq!(full_sha("abc123".to_string()), None);
211 assert_eq!(
212 full_sha("gggggggggggggggggggggggggggggggggggggggg".to_string()),
213 None
214 );
215 assert_eq!(
216 short_sha("abcdef0123456789abcdef0123456789abcdef01".to_string()),
217 Some("abcdef012345".to_string())
218 );
219 }
220
221 #[test]
222 fn detached_head_is_not_a_symbolic_ref() {
223 assert_eq!(
224 parse_symbolic_ref("506343f44e48b9c2c8d6b2d3e8e8e8e8e8e8e8e8\n"),
225 None
226 );
227 }
228
229 #[test]
230 fn empty_input_returns_none() {
231 assert_eq!(parse_symbolic_ref(""), None);
232 assert_eq!(parse_symbolic_ref("ref: \n"), None);
233 }
234
235 #[test]
236 fn linked_worktree_uses_the_common_ref_store() {
237 let unique = SystemTime::now()
238 .duration_since(UNIX_EPOCH)
239 .expect("clock before epoch")
240 .as_nanos();
241 let root = std::env::temp_dir().join(format!(
242 "codewhale-build-support-{}-{unique}",
243 std::process::id()
244 ));
245 let common = root.join(".git");
246 let worktree_gitdir = common.join("worktrees/candidate");
247 fs::create_dir_all(&worktree_gitdir).expect("create worktree gitdir");
248 fs::write(worktree_gitdir.join("commondir"), "../..\n").expect("write commondir");
249
250 assert_eq!(
251 fs::canonicalize(git_common_dir(&worktree_gitdir)).expect("canonical common gitdir"),
252 fs::canonicalize(&common).expect("canonical expected gitdir")
253 );
254
255 fs::remove_dir_all(root).expect("remove isolated test directory");
256 }
257}