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 environment variables to set (e.g. CARGO_PROFILE_RELEASE_STRIP=false).
29    pub env_overrides: Vec<(String, String)>,
30    /// Set RUSTC_BOOTSTRAP=1 so stable rustc accepts -Z flags.
31    pub rustc_bootstrap: bool,
32}
33
34impl GuestBuild {
35    /// Run the inner cargo build. Returns the absolute path to the output ELF.
36    ///
37    /// Emits `cargo:rerun-if-changed` directives for the service source files
38    /// and `cargo:rerun-if-env-changed` for `SKIP_GUEST_BUILD`.
39    ///
40    /// # Panics
41    /// Panics if the build fails or the output artifact is not found.
42    pub fn build(&self) -> PathBuf {
43        // Emit rerun directives
44        let src_dir = self.manifest_dir.join("src");
45        println!("cargo:rerun-if-changed={}", src_dir.display());
46        println!(
47            "cargo:rerun-if-changed={}",
48            self.manifest_dir.join("Cargo.toml").display()
49        );
50        println!("cargo:rerun-if-env-changed=SKIP_GUEST_BUILD");
51
52        // Check skip flag
53        if std::env::var("SKIP_GUEST_BUILD").is_ok() {
54            let elf_path = self.output_elf_path();
55            if elf_path.exists() {
56                return elf_path;
57            }
58            // No cached ELF — must build
59        }
60
61        let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
62        let target_dir = PathBuf::from(&out_dir)
63            .join("guest-build")
64            .join(&self.target_dir_name);
65
66        let manifest_path = self.manifest_dir.join("Cargo.toml");
67
68        let mut cmd = Command::new("cargo");
69        cmd.arg("build")
70            .arg("--release")
71            .arg("--manifest-path")
72            .arg(&manifest_path)
73            .arg("--target")
74            .arg(&self.target_json_path)
75            .arg("-Zbuild-std=core,alloc");
76
77        match &self.build_kind {
78            BuildKind::Bin(name) => {
79                cmd.arg("--bin").arg(name);
80            }
81            BuildKind::Lib => {
82                cmd.arg("--lib");
83            }
84        }
85
86        // Use separate target dir to avoid deadlock
87        cmd.env("CARGO_TARGET_DIR", &target_dir);
88
89        // Use CARGO_ENCODED_RUSTFLAGS to avoid cache invalidation
90        if !self.extra_rustflags.is_empty() {
91            let encoded = self.extra_rustflags.join("\x1f");
92            cmd.env("CARGO_ENCODED_RUSTFLAGS", &encoded);
93        }
94
95        if self.rustc_bootstrap {
96            cmd.env("RUSTC_BOOTSTRAP", "1");
97        }
98
99        for (key, val) in &self.env_overrides {
100            cmd.env(key, val);
101        }
102
103        let output = cmd.output().expect("failed to spawn cargo for guest build");
104
105        if !output.status.success() {
106            let stderr = String::from_utf8_lossy(&output.stderr);
107            let stdout = String::from_utf8_lossy(&output.stdout);
108            panic!(
109                "Guest build failed for {}:\n--- stderr ---\n{}\n--- stdout ---\n{}",
110                self.manifest_dir.display(),
111                stderr,
112                stdout
113            );
114        }
115
116        let elf_path = self.output_elf_path();
117        assert!(
118            elf_path.exists(),
119            "Expected ELF artifact not found at: {}",
120            elf_path.display()
121        );
122        elf_path
123    }
124
125    fn output_elf_path(&self) -> PathBuf {
126        let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
127        let target_dir = PathBuf::from(&out_dir)
128            .join("guest-build")
129            .join(&self.target_dir_name);
130
131        let artifact_name = match &self.build_kind {
132            BuildKind::Bin(name) => name.clone(),
133            BuildKind::Lib => {
134                // cdylib: lib<name>.elf or lib<name>.so depending on target
135                let manifest_path = self.manifest_dir.join("Cargo.toml");
136                let contents =
137                    std::fs::read_to_string(&manifest_path).expect("failed to read Cargo.toml");
138                let lib_name = parse_lib_name(&contents, &self.manifest_dir);
139                lib_name
140            }
141        };
142
143        let release_dir = target_dir
144            .join(&self.target_dir_name)
145            .join("release");
146
147        // Try common artifact patterns
148        let candidates = match &self.build_kind {
149            BuildKind::Bin(_) => vec![
150                release_dir.join(format!("{}.elf", artifact_name)),
151                release_dir.join(&artifact_name),
152            ],
153            BuildKind::Lib => vec![
154                release_dir.join(format!("{}.elf", artifact_name)),
155                release_dir.join(format!("lib{}.elf", artifact_name)),
156            ],
157        };
158
159        for candidate in &candidates {
160            if candidate.exists() {
161                return candidate.clone();
162            }
163        }
164
165        // Return the first candidate as the expected path (for error messages)
166        candidates.into_iter().next().unwrap()
167    }
168}
169
170/// Parse the library name from a Cargo.toml.
171/// Looks for `[lib] name = "..."`, falls back to package name with hyphens replaced.
172fn parse_lib_name(contents: &str, manifest_dir: &Path) -> String {
173    // Simple parsing: look for [lib] section with name = "..."
174    let mut in_lib_section = false;
175    for line in contents.lines() {
176        let trimmed = line.trim();
177        if trimmed == "[lib]" {
178            in_lib_section = true;
179            continue;
180        }
181        if trimmed.starts_with('[') {
182            in_lib_section = false;
183            continue;
184        }
185        if in_lib_section && trimmed.starts_with("name") {
186            if let Some(name) = extract_toml_string_value(trimmed) {
187                return name;
188            }
189        }
190    }
191
192    // Fall back to package name
193    for line in contents.lines() {
194        let trimmed = line.trim();
195        if trimmed.starts_with("name") {
196            if let Some(name) = extract_toml_string_value(trimmed) {
197                return name.replace('-', "_");
198            }
199        }
200    }
201
202    // Last resort: directory name
203    manifest_dir
204        .file_name()
205        .unwrap()
206        .to_str()
207        .unwrap()
208        .replace('-', "_")
209}
210
211fn extract_toml_string_value(line: &str) -> Option<String> {
212    let after_eq = line.split('=').nth(1)?.trim();
213    let unquoted = after_eq.trim_matches('"').trim_matches('\'');
214    Some(unquoted.to_string())
215}
216
217/// Write a target JSON string to OUT_DIR/targets/<filename> and return the path.
218pub fn write_target_json(filename: &str, contents: &str) -> PathBuf {
219    let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
220    let targets_dir = PathBuf::from(&out_dir).join("targets");
221    std::fs::create_dir_all(&targets_dir).expect("failed to create targets dir");
222    let path = targets_dir.join(filename);
223    std::fs::write(&path, contents).expect("failed to write target JSON");
224    path
225}
226
227/// Resolve a relative path against CARGO_MANIFEST_DIR.
228pub fn resolve_manifest_dir(relative_path: &str) -> PathBuf {
229    let manifest_dir =
230        std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
231    let resolved = PathBuf::from(&manifest_dir).join(relative_path);
232    assert!(
233        resolved.exists(),
234        "Service crate not found at: {} (resolved from CARGO_MANIFEST_DIR={})",
235        resolved.display(),
236        manifest_dir
237    );
238    std::fs::canonicalize(&resolved).expect("failed to canonicalize path")
239}