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 build_version = build_sha(manifest_dir)
28 .map(|sha| format!("{package_version} ({sha})"))
29 .unwrap_or_else(|| package_version.to_string());
30
31 println!("cargo:rustc-env=DEEPSEEK_BUILD_VERSION={build_version}");
32}
33
34fn declare_git_head_rerun(manifest_dir: &Path) {
47 let workspace_root = manifest_dir.join("..").join("..");
48 let git_meta = workspace_root.join(".git");
49
50 let gitdir = if git_meta.is_dir() {
51 git_meta
52 } else if git_meta.is_file() {
53 println!("cargo:rerun-if-changed={}", git_meta.display());
55 let Ok(contents) = std::fs::read_to_string(&git_meta) else {
56 return;
57 };
58 let Some(rest) = contents.lines().find_map(|l| l.strip_prefix("gitdir:")) else {
59 return;
60 };
61 let trimmed = rest.trim();
62 if Path::new(trimmed).is_absolute() {
63 PathBuf::from(trimmed)
64 } else {
65 workspace_root.join(trimmed)
66 }
67 } else {
68 return;
69 };
70
71 let head = gitdir.join("HEAD");
72 println!("cargo:rerun-if-changed={}", head.display());
73
74 if let Ok(contents) = std::fs::read_to_string(&head)
75 && let Some(target) = parse_symbolic_ref(&contents)
76 {
77 let common_gitdir = git_common_dir(&gitdir);
78 println!(
79 "cargo:rerun-if-changed={}",
80 common_gitdir.join(target).display()
81 );
82 println!(
83 "cargo:rerun-if-changed={}",
84 common_gitdir.join("packed-refs").display()
85 );
86 }
87}
88
89fn git_common_dir(gitdir: &Path) -> PathBuf {
93 let commondir = gitdir.join("commondir");
94 let Ok(contents) = std::fs::read_to_string(commondir) else {
95 return gitdir.to_path_buf();
96 };
97 let trimmed = contents.trim();
98 if trimmed.is_empty() {
99 return gitdir.to_path_buf();
100 }
101 let path = Path::new(trimmed);
102 if path.is_absolute() {
103 path.to_path_buf()
104 } else {
105 gitdir.join(path)
106 }
107}
108
109fn parse_symbolic_ref(head_contents: &str) -> Option<&str> {
112 head_contents
113 .lines()
114 .next()
115 .and_then(|line| line.strip_prefix("ref:"))
116 .map(str::trim)
117 .filter(|s| !s.is_empty())
118}
119
120fn build_sha(manifest_dir: &Path) -> Option<String> {
121 env_sha("DEEPSEEK_BUILD_SHA")
122 .or_else(|| env_sha("GITHUB_SHA"))
123 .or_else(|| git_sha(manifest_dir))
124}
125
126fn env_sha(name: &str) -> Option<String> {
127 std::env::var(name).ok().and_then(short_sha)
128}
129
130fn git_sha(manifest_dir: &Path) -> Option<String> {
131 let top_level_output = Command::new("git")
132 .args(["-C"])
133 .arg(manifest_dir)
134 .args(["rev-parse", "--show-toplevel"])
135 .output()
136 .ok()?;
137 if !top_level_output.status.success() {
138 return None;
139 }
140 let top_level = PathBuf::from(String::from_utf8_lossy(&top_level_output.stdout).trim());
141 if !top_level.join("Cargo.toml").is_file() || !top_level.join("crates/tui").is_dir() {
142 return None;
143 }
144
145 let output = Command::new("git")
146 .args(["-C"])
147 .arg(top_level)
148 .args(["rev-parse", "--short=12", "HEAD"])
149 .output()
150 .ok()?;
151 if !output.status.success() {
152 return None;
153 }
154
155 short_sha(String::from_utf8_lossy(&output.stdout).to_string())
156}
157
158fn short_sha(value: String) -> Option<String> {
159 let trimmed = value.trim();
160 if trimmed.is_empty() {
161 return None;
162 }
163 Some(trimmed.chars().take(12).collect())
164}
165
166#[cfg(test)]
167mod tests {
168 use super::{git_common_dir, parse_symbolic_ref};
169 use std::{
170 fs,
171 time::{SystemTime, UNIX_EPOCH},
172 };
173
174 #[test]
175 fn symbolic_ref_strips_prefix_and_whitespace() {
176 assert_eq!(
177 parse_symbolic_ref("ref: refs/heads/main\n"),
178 Some("refs/heads/main")
179 );
180 }
181
182 #[test]
183 fn symbolic_ref_handles_no_trailing_newline() {
184 assert_eq!(
185 parse_symbolic_ref("ref: refs/heads/work/v0.8.26-security"),
186 Some("refs/heads/work/v0.8.26-security")
187 );
188 }
189
190 #[test]
191 fn detached_head_is_not_a_symbolic_ref() {
192 assert_eq!(
193 parse_symbolic_ref("506343f44e48b9c2c8d6b2d3e8e8e8e8e8e8e8e8\n"),
194 None
195 );
196 }
197
198 #[test]
199 fn empty_input_returns_none() {
200 assert_eq!(parse_symbolic_ref(""), None);
201 assert_eq!(parse_symbolic_ref("ref: \n"), None);
202 }
203
204 #[test]
205 fn linked_worktree_uses_the_common_ref_store() {
206 let unique = SystemTime::now()
207 .duration_since(UNIX_EPOCH)
208 .expect("clock before epoch")
209 .as_nanos();
210 let root = std::env::temp_dir().join(format!(
211 "codewhale-build-support-{}-{unique}",
212 std::process::id()
213 ));
214 let common = root.join(".git");
215 let worktree_gitdir = common.join("worktrees/candidate");
216 fs::create_dir_all(&worktree_gitdir).expect("create worktree gitdir");
217 fs::write(worktree_gitdir.join("commondir"), "../..\n").expect("write commondir");
218
219 assert_eq!(
220 fs::canonicalize(git_common_dir(&worktree_gitdir)).expect("canonical common gitdir"),
221 fs::canonicalize(&common).expect("canonical expected gitdir")
222 );
223
224 fs::remove_dir_all(root).expect("remove isolated test directory");
225 }
226}