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) {
45 let workspace_root = manifest_dir.join("..").join("..");
46 let git_meta = workspace_root.join(".git");
47
48 let gitdir = if git_meta.is_dir() {
49 git_meta
50 } else if git_meta.is_file() {
51 println!("cargo:rerun-if-changed={}", git_meta.display());
53 let Ok(contents) = std::fs::read_to_string(&git_meta) else {
54 return;
55 };
56 let Some(rest) = contents.lines().find_map(|l| l.strip_prefix("gitdir:")) else {
57 return;
58 };
59 let trimmed = rest.trim();
60 if Path::new(trimmed).is_absolute() {
61 PathBuf::from(trimmed)
62 } else {
63 workspace_root.join(trimmed)
64 }
65 } else {
66 return;
67 };
68
69 let head = gitdir.join("HEAD");
70 println!("cargo:rerun-if-changed={}", head.display());
71
72 if let Ok(contents) = std::fs::read_to_string(&head)
73 && let Some(target) = parse_symbolic_ref(&contents)
74 {
75 println!("cargo:rerun-if-changed={}", gitdir.join(target).display());
76 println!(
77 "cargo:rerun-if-changed={}",
78 gitdir.join("packed-refs").display()
79 );
80 }
81}
82
83fn parse_symbolic_ref(head_contents: &str) -> Option<&str> {
86 head_contents
87 .lines()
88 .next()
89 .and_then(|line| line.strip_prefix("ref:"))
90 .map(str::trim)
91 .filter(|s| !s.is_empty())
92}
93
94fn build_sha(manifest_dir: &Path) -> Option<String> {
95 env_sha("DEEPSEEK_BUILD_SHA")
96 .or_else(|| env_sha("GITHUB_SHA"))
97 .or_else(|| git_sha(manifest_dir))
98}
99
100fn env_sha(name: &str) -> Option<String> {
101 std::env::var(name).ok().and_then(short_sha)
102}
103
104fn git_sha(manifest_dir: &Path) -> Option<String> {
105 let top_level_output = Command::new("git")
106 .args(["-C"])
107 .arg(manifest_dir)
108 .args(["rev-parse", "--show-toplevel"])
109 .output()
110 .ok()?;
111 if !top_level_output.status.success() {
112 return None;
113 }
114 let top_level = PathBuf::from(String::from_utf8_lossy(&top_level_output.stdout).trim());
115 if !top_level.join("Cargo.toml").is_file() || !top_level.join("crates/tui").is_dir() {
116 return None;
117 }
118
119 let output = Command::new("git")
120 .args(["-C"])
121 .arg(top_level)
122 .args(["rev-parse", "--short=12", "HEAD"])
123 .output()
124 .ok()?;
125 if !output.status.success() {
126 return None;
127 }
128
129 short_sha(String::from_utf8_lossy(&output.stdout).to_string())
130}
131
132fn short_sha(value: String) -> Option<String> {
133 let trimmed = value.trim();
134 if trimmed.is_empty() {
135 return None;
136 }
137 Some(trimmed.chars().take(12).collect())
138}
139
140#[cfg(test)]
141mod tests {
142 use super::parse_symbolic_ref;
143
144 #[test]
145 fn symbolic_ref_strips_prefix_and_whitespace() {
146 assert_eq!(
147 parse_symbolic_ref("ref: refs/heads/main\n"),
148 Some("refs/heads/main")
149 );
150 }
151
152 #[test]
153 fn symbolic_ref_handles_no_trailing_newline() {
154 assert_eq!(
155 parse_symbolic_ref("ref: refs/heads/work/v0.8.26-security"),
156 Some("refs/heads/work/v0.8.26-security")
157 );
158 }
159
160 #[test]
161 fn detached_head_is_not_a_symbolic_ref() {
162 assert_eq!(
163 parse_symbolic_ref("506343f44e48b9c2c8d6b2d3e8e8e8e8e8e8e8e8\n"),
164 None
165 );
166 }
167
168 #[test]
169 fn empty_input_returns_none() {
170 assert_eq!(parse_symbolic_ref(""), None);
171 assert_eq!(parse_symbolic_ref("ref: \n"), None);
172 }
173}