Skip to main content

whisker_dev_server/hotpatch/
validate.rs

1//! Pre-flight environment check for the thin-rebuild pipeline.
2//!
3//! Catches the "fat build was for one toolchain, the dev loop is now
4//! using another" class of breakage *before* `subsecond::apply_patch`
5//! runs and segfaults the device. The check is deliberately small —
6//! we only assert what's necessary to keep the same captured rustc
7//! invocation viable:
8//!
9//! 1. The current rustc still supports the target triple the fat
10//!    build was for. If a `rustup toolchain` change between the fat
11//!    build and the first edit dropped the Android target, we want
12//!    a clear error here, not a cryptic linker failure later.
13//!
14//! Things we deliberately do NOT validate:
15//!
16//! - **Exact rustc version match.** Patch dylibs survive across
17//!   patch-level rustc bumps in practice (subsecond is pretty
18//!   tolerant); demanding strict equality would break workflows
19//!   where `rustup update` is a frequent occurrence. Major version
20//!   regressions WILL be surfaced by the thin rebuild itself
21//!   (rustc returns non-zero), so we let that path do the talking.
22//!
23//! - **Sysroot stability.** Same reasoning — rustc verifies its own
24//!   sysroot at compile time. We don't add a redundant probe.
25
26use anyhow::{Context, Result};
27use std::path::Path;
28use std::process::Command;
29
30use super::wrapper::CapturedRustcInvocation;
31
32/// Run the pre-flight checks. Returns Ok(()) if it's safe to call
33/// `thin_rebuild`; otherwise Err with a message a human can act on.
34pub fn validate_environment(
35    captured: &CapturedRustcInvocation,
36    current_rustc: &Path,
37) -> Result<()> {
38    if let Some(triple) = extract_target_triple(&captured.args) {
39        ensure_target_supported(current_rustc, &triple)?;
40    }
41    Ok(())
42}
43
44/// Pull the value passed to `--target` (or `--target=...`) out of a
45/// rustc argv. Pure helper — same shape as `extract_crate_name` in
46/// the shim.
47pub fn extract_target_triple(args: &[String]) -> Option<String> {
48    let mut iter = args.iter();
49    while let Some(arg) = iter.next() {
50        if arg == "--target" {
51            return iter.next().cloned();
52        }
53        if let Some(rest) = arg.strip_prefix("--target=") {
54            return Some(rest.to_string());
55        }
56    }
57    None
58}
59
60/// Spawn `rustc --print=target-list` and verify `triple` shows up.
61/// Synchronous on purpose — runs once per dev-server boot, not per
62/// patch.
63pub fn ensure_target_supported(rustc: &Path, triple: &str) -> Result<()> {
64    let output = Command::new(rustc)
65        .args(["--print=target-list"])
66        .output()
67        .with_context(|| format!("spawn `{} --print=target-list`", rustc.display()))?;
68    if !output.status.success() {
69        anyhow::bail!(
70            "`{} --print=target-list` exited {}",
71            rustc.display(),
72            output.status,
73        );
74    }
75    let stdout = String::from_utf8_lossy(&output.stdout);
76    if stdout.lines().any(|line| line.trim() == triple) {
77        Ok(())
78    } else {
79        anyhow::bail!(
80            "rustc at {} doesn't support target triple `{triple}` \
81             — check `rustup target list --installed` and re-run \
82             the fat build under the same toolchain",
83            rustc.display(),
84        )
85    }
86}
87
88// ============================================================================
89// Tests
90// ============================================================================
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    fn s(v: &[&str]) -> Vec<String> {
97        v.iter().map(|s| s.to_string()).collect()
98    }
99
100    fn captured_with(args: Vec<String>) -> CapturedRustcInvocation {
101        CapturedRustcInvocation {
102            crate_name: "demo".into(),
103            args,
104            timestamp_micros: 0,
105        }
106    }
107
108    fn rustc_path() -> std::path::PathBuf {
109        std::path::PathBuf::from(std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()))
110    }
111
112    /// One target triple we're sure the test runner's rustc supports
113    /// — its own host triple. We discover it dynamically from rustc
114    /// itself so the test is portable across CI runners.
115    fn host_triple() -> String {
116        let out = Command::new(rustc_path())
117            .args(["-vV"])
118            .output()
119            .expect("rustc -vV");
120        let stdout = String::from_utf8_lossy(&out.stdout);
121        stdout
122            .lines()
123            .find_map(|l| l.strip_prefix("host: "))
124            .map(|s| s.trim().to_string())
125            .expect("rustc -vV reports a host:")
126    }
127
128    // ----- extract_target_triple --------------------------------------
129
130    #[test]
131    fn extract_target_triple_from_separated_form() {
132        let args = s(&[
133            "--edition=2021",
134            "--target",
135            "aarch64-apple-darwin",
136            "src/lib.rs",
137        ]);
138        assert_eq!(
139            extract_target_triple(&args).as_deref(),
140            Some("aarch64-apple-darwin")
141        );
142    }
143
144    #[test]
145    fn extract_target_triple_from_equals_form() {
146        let args = s(&["--target=x86_64-unknown-linux-gnu"]);
147        assert_eq!(
148            extract_target_triple(&args).as_deref(),
149            Some("x86_64-unknown-linux-gnu")
150        );
151    }
152
153    #[test]
154    fn extract_target_triple_returns_none_when_absent() {
155        let args = s(&["--edition=2021", "src/lib.rs"]);
156        assert_eq!(extract_target_triple(&args), None);
157    }
158
159    // ----- ensure_target_supported (live rustc spawn) ------------------
160
161    #[test]
162    fn ensure_target_supported_accepts_the_host_triple() {
163        // Whatever rustc reports as its own host MUST be in the
164        // target list — otherwise `rustc -vV` and `rustc
165        // --print=target-list` disagree, which would be a rustc
166        // bug, not ours.
167        let triple = host_triple();
168        ensure_target_supported(&rustc_path(), &triple).expect("host triple supported");
169    }
170
171    #[test]
172    fn ensure_target_supported_rejects_a_made_up_triple() {
173        let result = ensure_target_supported(&rustc_path(), "totally-not-a-real-triple-9999");
174        assert!(result.is_err());
175        let msg = format!("{:#}", result.unwrap_err());
176        assert!(msg.contains("doesn't support target triple"), "got: {msg}",);
177    }
178
179    #[test]
180    fn ensure_target_supported_surfaces_a_missing_rustc_as_err() {
181        let result = ensure_target_supported(
182            std::path::Path::new("/no/such/rustc/anywhere"),
183            "any-triple",
184        );
185        assert!(result.is_err());
186    }
187
188    // ----- validate_environment ---------------------------------------
189
190    #[test]
191    fn validate_passes_when_no_target_triple_in_args() {
192        // Captured args without --target → nothing to check, pass.
193        let captured = captured_with(s(&["--edition=2021", "src/lib.rs"]));
194        validate_environment(&captured, &rustc_path()).expect("ok");
195    }
196
197    #[test]
198    fn validate_passes_with_the_host_triple() {
199        let triple = host_triple();
200        let captured = captured_with(s(&["--target", &triple, "src/lib.rs"]));
201        validate_environment(&captured, &rustc_path()).expect("ok");
202    }
203
204    #[test]
205    fn validate_fails_when_target_triple_is_unsupported() {
206        let captured = captured_with(s(&["--target", "made-up-arch", "src/lib.rs"]));
207        let err = validate_environment(&captured, &rustc_path()).unwrap_err();
208        let msg = format!("{:#}", err);
209        assert!(msg.contains("made-up-arch"), "{msg}");
210    }
211}