1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4pub enum BuildKind {
6 Bin(String),
8 Lib,
10}
11
12pub struct GuestBuild {
17 pub manifest_dir: PathBuf,
19 pub target_json_path: PathBuf,
21 pub target_dir_name: String,
24 pub build_kind: BuildKind,
26 pub extra_rustflags: Vec<String>,
28 pub extra_rustc_args: Vec<String>,
31 pub env_overrides: Vec<(String, String)>,
33 pub rustc_bootstrap: bool,
35}
36
37impl GuestBuild {
38 pub fn build(&self) -> PathBuf {
46 emit_rerun_for_dir(&self.manifest_dir.join("src"));
48 println!(
49 "cargo:rerun-if-changed={}",
50 self.manifest_dir.join("Cargo.toml").display()
51 );
52 println!("cargo:rerun-if-env-changed=SKIP_GUEST_BUILD");
53
54 if std::env::var("SKIP_GUEST_BUILD").is_ok() {
56 let elf_path = self.output_elf_path();
57 if elf_path.exists() {
58 return elf_path;
59 }
60 }
62
63 let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
64 let target_dir = PathBuf::from(&out_dir)
65 .join("guest-build")
66 .join(&self.target_dir_name);
67
68 let manifest_path = self.manifest_dir.join("Cargo.toml");
69
70 let mut cmd = Command::new("cargo");
71 if self.extra_rustc_args.is_empty() {
73 cmd.arg("build");
74 } else {
75 cmd.arg("rustc");
76 }
77 cmd.arg("--release")
78 .arg("--manifest-path")
79 .arg(&manifest_path)
80 .arg("--target")
81 .arg(&self.target_json_path);
82 if cargo_supports_json_target_spec() {
83 cmd.arg("-Zjson-target-spec");
86 }
87 cmd.arg("-Zbuild-std=core,alloc");
88
89 match &self.build_kind {
90 BuildKind::Bin(name) => {
91 cmd.arg("--bin").arg(name);
92 }
93 BuildKind::Lib => {
94 cmd.arg("--lib");
95 }
96 }
97
98 if !self.extra_rustc_args.is_empty() {
99 cmd.arg("--");
100 cmd.args(&self.extra_rustc_args);
101 }
102
103 cmd.env("CARGO_TARGET_DIR", &target_dir);
105
106 cmd.env("BUILD_CRATE_GUEST_BUILD", "1");
109
110 if !self.extra_rustflags.is_empty() {
112 let encoded = self.extra_rustflags.join("\x1f");
113 cmd.env("CARGO_ENCODED_RUSTFLAGS", &encoded);
114 }
115
116 if self.rustc_bootstrap {
117 cmd.env("RUSTC_BOOTSTRAP", "1");
118 }
119
120 for (key, val) in &self.env_overrides {
121 cmd.env(key, val);
122 }
123
124 let output = cmd.output().expect("failed to spawn cargo for guest build");
125
126 if !output.status.success() {
127 let stderr = String::from_utf8_lossy(&output.stderr);
128 let stdout = String::from_utf8_lossy(&output.stdout);
129 panic!(
130 "Guest build failed for {}:\n--- stderr ---\n{}\n--- stdout ---\n{}",
131 self.manifest_dir.display(),
132 stderr,
133 stdout
134 );
135 }
136
137 let elf_path = self.output_elf_path();
138 assert!(
139 elf_path.exists(),
140 "Expected ELF artifact not found at: {}",
141 elf_path.display()
142 );
143 elf_path
144 }
145
146 fn output_elf_path(&self) -> PathBuf {
147 let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
148 let target_dir = PathBuf::from(&out_dir)
149 .join("guest-build")
150 .join(&self.target_dir_name);
151
152 let artifact_name = match &self.build_kind {
153 BuildKind::Bin(name) => name.clone(),
154 BuildKind::Lib => {
155 let manifest_path = self.manifest_dir.join("Cargo.toml");
157 let contents =
158 std::fs::read_to_string(&manifest_path).expect("failed to read Cargo.toml");
159
160 parse_lib_name(&contents, &self.manifest_dir)
161 }
162 };
163
164 let release_dir = target_dir.join(&self.target_dir_name).join("release");
165
166 let candidates = match &self.build_kind {
168 BuildKind::Bin(_) => vec![
169 release_dir.join(format!("{}.elf", artifact_name)),
170 release_dir.join(&artifact_name),
171 ],
172 BuildKind::Lib => vec![
173 release_dir.join(format!("{}.elf", artifact_name)),
174 release_dir.join(format!("lib{}.elf", artifact_name)),
175 ],
176 };
177
178 for candidate in &candidates {
179 if candidate.exists() {
180 return candidate.clone();
181 }
182 }
183
184 candidates.into_iter().next().unwrap()
186 }
187}
188
189fn cargo_supports_json_target_spec() -> bool {
190 let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
191 let Ok(output) = Command::new(cargo).arg("--version").output() else {
192 return false;
193 };
194 if !output.status.success() {
195 return false;
196 }
197
198 let stdout = String::from_utf8_lossy(&output.stdout);
199 let mut parts = stdout.split_whitespace();
200 let _ = parts.next();
201 let Some(version) = parts.next() else {
202 return false;
203 };
204
205 let mut components = version.split('.');
206 let major = components.next().and_then(|part| part.parse::<u64>().ok());
207 let minor = components.next().and_then(|part| part.parse::<u64>().ok());
208
209 matches!((major, minor), (Some(major), Some(minor)) if (major, minor) >= (1, 95))
210}
211
212fn parse_lib_name(contents: &str, manifest_dir: &Path) -> String {
215 let mut in_lib_section = false;
217 for line in contents.lines() {
218 let trimmed = line.trim();
219 if trimmed == "[lib]" {
220 in_lib_section = true;
221 continue;
222 }
223 if trimmed.starts_with('[') {
224 in_lib_section = false;
225 continue;
226 }
227 if in_lib_section
228 && trimmed.starts_with("name")
229 && let Some(name) = extract_toml_string_value(trimmed)
230 {
231 return name;
232 }
233 }
234
235 for line in contents.lines() {
237 let trimmed = line.trim();
238 if trimmed.starts_with("name")
239 && let Some(name) = extract_toml_string_value(trimmed)
240 {
241 return name.replace('-', "_");
242 }
243 }
244
245 manifest_dir
247 .file_name()
248 .unwrap()
249 .to_str()
250 .unwrap()
251 .replace('-', "_")
252}
253
254fn extract_toml_string_value(line: &str) -> Option<String> {
255 let after_eq = line.split('=').nth(1)?.trim();
256 let unquoted = after_eq.trim_matches('"').trim_matches('\'');
257 Some(unquoted.to_string())
258}
259
260pub fn emit_rerun_for_dir(dir: &Path) {
262 if let Ok(entries) = std::fs::read_dir(dir) {
263 for entry in entries.flatten() {
264 let path = entry.path();
265 if path.is_dir() {
266 emit_rerun_for_dir(&path);
267 } else {
268 println!("cargo:rerun-if-changed={}", path.display());
269 }
270 }
271 }
272}
273
274pub fn write_target_json(filename: &str, contents: &str) -> PathBuf {
276 let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
277 let targets_dir = PathBuf::from(&out_dir).join("targets");
278 std::fs::create_dir_all(&targets_dir).expect("failed to create targets dir");
279 let path = targets_dir.join(filename);
280 std::fs::write(&path, contents).expect("failed to write target JSON");
281 path
282}
283
284pub fn resolve_manifest_dir(relative_path: &str) -> PathBuf {
286 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
287 let resolved = PathBuf::from(&manifest_dir).join(relative_path);
288 assert!(
289 resolved.exists(),
290 "Service crate not found at: {} (resolved from CARGO_MANIFEST_DIR={})",
291 resolved.display(),
292 manifest_dir
293 );
294 std::fs::canonicalize(&resolved).expect("failed to canonicalize path")
295}