Skip to main content

build_crate/
lib.rs

1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4/// What kind of artifact to build.
5pub enum BuildKind {
6    /// Build a binary target: `--bin <name>`.
7    Bin(String),
8    /// Build a library target: `--lib`.
9    Lib,
10}
11
12/// Configuration for a guest (cross-compiled) cargo build.
13///
14/// Spawns a separate `cargo build` subprocess with its own `CARGO_TARGET_DIR`
15/// to avoid deadlocking with the outer cargo process running build.rs.
16pub struct GuestBuild {
17    /// Absolute path to the service crate directory (containing Cargo.toml).
18    pub manifest_dir: PathBuf,
19    /// Absolute path to the target JSON file.
20    pub target_json_path: PathBuf,
21    /// Name used as subdirectory in cargo's target dir (e.g. "riscv64em-javm").
22    /// This is the directory name cargo creates under `target/` for the custom target.
23    pub target_dir_name: String,
24    /// Whether to build a binary or library.
25    pub build_kind: BuildKind,
26    /// Extra flags appended to CARGO_ENCODED_RUSTFLAGS.
27    pub extra_rustflags: Vec<String>,
28    /// Extra arguments passed to rustc after `--` (e.g. `--crate-type cdylib`).
29    /// When non-empty, uses `cargo rustc` instead of `cargo build`.
30    pub extra_rustc_args: Vec<String>,
31    /// Extra environment variables to set (e.g. CARGO_PROFILE_RELEASE_STRIP=false).
32    pub env_overrides: Vec<(String, String)>,
33    /// Set RUSTC_BOOTSTRAP=1 so stable rustc accepts -Z flags.
34    pub rustc_bootstrap: bool,
35}
36
37impl GuestBuild {
38    /// Run the inner cargo build. Returns the absolute path to the output ELF.
39    ///
40    /// Emits `cargo:rerun-if-changed` directives for the service source files
41    /// and `cargo:rerun-if-env-changed` for `SKIP_GUEST_BUILD`.
42    ///
43    /// # Panics
44    /// Panics if the build fails or the output artifact is not found.
45    pub fn build(&self) -> PathBuf {
46        // Emit rerun directives — watch all source files recursively
47        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        // Check skip flag
55        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            // No cached ELF — must build
61        }
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        // Use `cargo rustc` when we need to pass extra args to rustc (e.g. --crate-type).
72        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            // Rust 1.95 requires an explicit cargo unstable flag when
84            // building against a custom target JSON.
85            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        // Use separate target dir to avoid deadlock
104        cmd.env("CARGO_TARGET_DIR", &target_dir);
105
106        // Signal to nested build.rs scripts that they're inside a guest build.
107        // This prevents recursive guest builds (e.g., javm-guest-tests building itself).
108        cmd.env("BUILD_CRATE_GUEST_BUILD", "1");
109
110        // Use CARGO_ENCODED_RUSTFLAGS to avoid cache invalidation
111        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                // cdylib: lib<name>.elf or lib<name>.so depending on target
156                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        // Try common artifact patterns
167        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        // Return the first candidate as the expected path (for error messages)
185        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
212/// Parse the library name from a Cargo.toml.
213/// Looks for `[lib] name = "..."`, falls back to package name with hyphens replaced.
214fn parse_lib_name(contents: &str, manifest_dir: &Path) -> String {
215    // Simple parsing: look for [lib] section with name = "..."
216    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    // Fall back to package name
236    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    // Last resort: directory name
246    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
260/// Recursively emit `cargo:rerun-if-changed` for all files in a directory.
261pub 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
274/// Write a target JSON string to `OUT_DIR/targets/<filename>` and return the path.
275pub 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
284/// Resolve a relative path against CARGO_MANIFEST_DIR.
285pub 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}