whisker_dev_server/hotpatch/
runner.rs1use anyhow::{Context, Result};
19use std::path::{Path, PathBuf};
20
21use super::link_plan::{build_link_plan, LinkPlan, LinkerOs};
22use super::thin_build::{build_obj_plan, ObjBuildPlan};
23use super::wrapper::CapturedRustcInvocation;
24
25pub async fn run_obj_plan(plan: &ObjBuildPlan, rustc_path: &Path, cwd: &Path) -> Result<PathBuf> {
31 std::fs::create_dir_all(&plan.output_dir)
32 .with_context(|| format!("create out dir {}", plan.output_dir.display()))?;
33 let status = tokio::process::Command::new(rustc_path)
34 .args(&plan.args)
35 .current_dir(cwd)
36 .status()
37 .await
38 .with_context(|| format!("spawn {}", rustc_path.display()))?;
39 if !status.success() {
40 anyhow::bail!(
41 "rustc exited {} during obj rebuild (out_dir={})",
42 status,
43 plan.output_dir.display(),
44 );
45 }
46 if !plan.expected_object.is_file() {
47 anyhow::bail!(
48 "rustc succeeded but `{}` was not produced",
49 plan.expected_object.display(),
50 );
51 }
52 Ok(plan.expected_object.clone())
53}
54
55pub async fn run_link_plan(plan: &LinkPlan, linker_path: &Path, cwd: &Path) -> Result<PathBuf> {
63 if let Some(parent) = plan.output.parent() {
64 std::fs::create_dir_all(parent)
65 .with_context(|| format!("create out dir {}", parent.display()))?;
66 }
67 let out = tokio::process::Command::new(linker_path)
71 .args(&plan.args)
72 .current_dir(cwd)
73 .stderr(std::process::Stdio::piped())
74 .output()
75 .await
76 .with_context(|| format!("spawn {}", linker_path.display()))?;
77 if !out.status.success() {
78 let stderr = String::from_utf8_lossy(&out.stderr);
79 anyhow::bail!(
80 "linker `{}` exited {} during patch link (output={})\n\
81 argv: {:?}\n\
82 stderr:\n{}",
83 linker_path.display(),
84 out.status,
85 plan.output.display(),
86 plan.args,
87 stderr.trim_end(),
88 );
89 }
90 if !plan.output.is_file() {
91 anyhow::bail!(
92 "linker succeeded but `{}` was not produced",
93 plan.output.display(),
94 );
95 }
96 Ok(plan.output.clone())
97}
98
99#[allow(clippy::too_many_arguments)]
120pub async fn thin_rebuild_obj(
121 captured_rustc: &CapturedRustcInvocation,
122 captured_linker_args: &[String],
123 output_dir: &Path,
124 output_dylib: &Path,
125 rustc_path: &Path,
126 linker_path: &Path,
127 cwd: &Path,
128 target_os: LinkerOs,
129 aslr_stub: Option<&Path>,
130) -> Result<PathBuf> {
131 let obj_plan = build_obj_plan(captured_rustc, output_dir);
132 let object = run_obj_plan(&obj_plan, rustc_path, cwd).await?;
133 let extras: Vec<PathBuf> = aslr_stub.map(|p| vec![p.to_path_buf()]).unwrap_or_default();
134 let link_plan = build_link_plan(
135 captured_linker_args,
136 &object,
137 output_dylib,
138 target_os,
139 &extras,
140 &[],
141 );
142 run_link_plan(&link_plan, linker_path, cwd).await
143}
144
145#[cfg(test)]
150mod tests {
151 use super::*;
152
153 fn s(v: &[&str]) -> Vec<String> {
154 v.iter().map(|s| s.to_string()).collect()
155 }
156
157 fn unique_tempdir() -> PathBuf {
158 use std::sync::atomic::{AtomicU64, Ordering};
159 static SEQ: AtomicU64 = AtomicU64::new(0);
160 let n = SEQ.fetch_add(1, Ordering::Relaxed);
161 let pid = std::process::id();
162 let p = std::env::temp_dir().join(format!("whisker-runner-test-{pid}-{n}"));
163 let _ = std::fs::remove_dir_all(&p);
164 std::fs::create_dir_all(&p).unwrap();
165 p
166 }
167
168 #[tokio::test]
171 async fn run_obj_plan_surfaces_rustc_failure_with_path_context() {
172 let dir = unique_tempdir();
174 let plan = ObjBuildPlan {
175 args: s(&[
176 "--edition=2021",
177 "--crate-name",
178 "demo",
179 "--crate-type",
180 "rlib",
181 "--out-dir",
182 dir.to_string_lossy().as_ref(),
183 "/nope/this/source/does/not/exist.rs",
184 "--emit",
185 &format!("obj={}/demo.o", dir.display()),
186 ]),
187 output_dir: dir.clone(),
188 expected_object: dir.join("demo.o"),
189 };
190 let res = run_obj_plan(&plan, Path::new("rustc"), &dir).await;
191 let err = res.unwrap_err();
192 let msg = format!("{err:#}");
193 assert!(
194 msg.contains("rustc exited") || msg.contains("spawn"),
195 "msg should mention rustc exit or spawn: {msg}",
196 );
197 let _ = std::fs::remove_dir_all(&dir);
198 }
199
200 #[tokio::test]
201 async fn run_obj_plan_errors_when_rustc_binary_doesnt_exist() {
202 let dir = unique_tempdir();
203 let plan = ObjBuildPlan {
204 args: vec![],
205 output_dir: dir.clone(),
206 expected_object: dir.join("demo.o"),
207 };
208 let res = run_obj_plan(&plan, Path::new("/nope/no-such-rustc"), &dir).await;
209 assert!(res.is_err());
210 let _ = std::fs::remove_dir_all(&dir);
211 }
212
213 #[tokio::test]
216 async fn run_link_plan_creates_output_parent_dir() {
217 let dir = unique_tempdir();
223 let nested_out = dir.join("nested/sub").join("libfoo.dylib");
224 let plan = LinkPlan {
225 args: vec![],
226 output: nested_out.clone(),
227 };
228 let res = run_link_plan(&plan, Path::new("/usr/bin/true"), &dir).await;
229 assert!(res.is_err(), "true returns success but writes no file");
230 let parent = nested_out.parent().unwrap();
231 assert!(parent.is_dir(), "parent should have been created");
232 let _ = std::fs::remove_dir_all(&dir);
233 }
234
235 #[tokio::test]
236 async fn run_link_plan_surfaces_linker_nonzero_exit() {
237 let dir = unique_tempdir();
240 let plan = LinkPlan {
241 args: vec![],
242 output: dir.join("libfoo.dylib"),
243 };
244 let res = run_link_plan(&plan, Path::new("/usr/bin/false"), &dir).await;
245 let err = res.unwrap_err();
246 let msg = format!("{err:#}");
247 assert!(msg.contains("linker"), "msg: {msg}");
248 let _ = std::fs::remove_dir_all(&dir);
249 }
250}