use std::collections::HashMap;
use std::path::{Path, PathBuf};
use whisker_dev_server::hotpatch::{
build_link_plan, build_obj_plan, library_filename, linker_os_for_host, parse_symbol_table,
run_link_plan, run_obj_plan, CapturedLinkerInvocation, CapturedRustcInvocation,
HotpatchModuleCache, LinkerOs, Patcher,
};
const FIXTURE_CRATE_NAME: &str = "thin_build_fixture";
fn fixture_lib_rs() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/thin-build-fixture/src/lib.rs")
}
fn unique_tempdir(label: &str) -> PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let n = SEQ.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let p = std::env::temp_dir().join(format!("whisker-patcher-{label}-{pid}-{n}"));
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).unwrap();
p
}
fn rustc_path() -> PathBuf {
PathBuf::from(std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()))
}
fn linker_path() -> PathBuf {
if let Some(cc) = std::env::var_os("CC") {
return PathBuf::from(cc);
}
if cfg!(target_os = "macos") {
if let Ok(out) = std::process::Command::new("xcrun")
.args(["-f", "clang"])
.output()
{
if out.status.success() {
let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !path.is_empty() {
return PathBuf::from(path);
}
}
}
return PathBuf::from("clang");
}
PathBuf::from("cc")
}
fn captured_rustc_for_fixture(lib_rs: &Path) -> CapturedRustcInvocation {
CapturedRustcInvocation {
crate_name: FIXTURE_CRATE_NAME.into(),
args: vec![
"--edition=2021".into(),
"--crate-name".into(),
FIXTURE_CRATE_NAME.into(),
"--crate-type".into(),
"rlib".into(),
"--emit=link".into(),
lib_rs.to_string_lossy().into(),
],
timestamp_micros: 0,
}
}
fn captured_linker_for_fixture(output_dylib: &Path) -> CapturedLinkerInvocation {
let mut args = vec![];
if cfg!(target_os = "macos") {
if let Ok(out) = std::process::Command::new("xcrun")
.args(["--show-sdk-path"])
.output()
{
if out.status.success() {
let sdk = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !sdk.is_empty() {
args.push("-isysroot".into());
args.push(sdk);
}
}
}
}
CapturedLinkerInvocation {
output: Some(output_dylib.to_string_lossy().to_string()),
args,
timestamp_micros: 0,
}
}
async fn build_original_via_pipeline(lib_rs: &Path, out_dir: &Path, cwd: &Path) -> PathBuf {
let captured = captured_rustc_for_fixture(lib_rs);
let obj_plan = build_obj_plan(&captured, out_dir);
let object = run_obj_plan(&obj_plan, &rustc_path(), cwd)
.await
.expect("v1 obj");
let dylib = out_dir.join(library_filename(FIXTURE_CRATE_NAME));
let captured_linker = captured_linker_for_fixture(&dylib);
let link_plan = build_link_plan(
&captured_linker.args,
&object,
&dylib,
linker_os_for_host(),
&[],
&[],
);
run_link_plan(&link_plan, &linker_path(), cwd)
.await
.expect("v1 link");
dylib
}
fn find_calculate(table_keys: impl Iterator<Item = String>) -> Option<String> {
table_keys
.into_iter()
.find(|k| k.contains("thin_build_fixture") && k.contains("9calculate"))
}
#[tokio::test]
async fn build_patch_emits_a_jump_table_entry_for_a_mangled_function() {
let work = unique_tempdir("happy");
let lib_rs = work.join("lib.rs");
std::fs::copy(fixture_lib_rs(), &lib_rs).unwrap();
let original_out = work.join("original");
std::fs::create_dir_all(&original_out).unwrap();
let original_dylib = build_original_via_pipeline(&lib_rs, &original_out, &work).await;
let original_table = parse_symbol_table(&original_dylib).expect("parse v1");
let original_calc =
find_calculate(original_table.by_name.keys().cloned()).unwrap_or_else(|| {
panic!(
"no `calculate`-shaped symbol in v1; keys: {:?}",
original_table.by_name.keys().take(20).collect::<Vec<_>>(),
)
});
let original_cache = HotpatchModuleCache::from_path(&original_dylib).expect("cache");
let patch_out = work.join("patches");
let captured_rustc = captured_rustc_for_fixture(&lib_rs);
let mut captured_rustc_args = HashMap::new();
captured_rustc_args.insert(FIXTURE_CRATE_NAME.into(), captured_rustc);
let lib_filename = library_filename(FIXTURE_CRATE_NAME);
let captured_linker = captured_linker_for_fixture(&original_dylib);
let mut captured_linker_args = HashMap::new();
captured_linker_args.insert(lib_filename, captured_linker);
let patcher = Patcher::new(
FIXTURE_CRATE_NAME.into(),
rustc_path(),
linker_path(),
work.clone(),
patch_out.clone(),
match linker_os_for_host() {
LinkerOs::Macos => LinkerOs::Macos,
LinkerOs::Linux => LinkerOs::Linux,
LinkerOs::Other => LinkerOs::Other,
},
original_cache,
captured_rustc_args,
captured_linker_args,
);
let body = std::fs::read_to_string(&lib_rs).unwrap();
let edited = body.replace("x * 2", "x * 3");
assert_ne!(body, edited, "edit must change something");
std::fs::write(&lib_rs, edited).unwrap();
let plan = patcher.build_patch(0, None).await.expect("build_patch");
assert!(
!plan.report.added.iter().any(|n| n == &original_calc),
"calculate shouldn't be in `added`; report: {:?}",
plan.report,
);
assert!(
!plan.report.removed.iter().any(|n| n == &original_calc),
"calculate shouldn't be in `removed` — this would mean rustc \
produced a different mangled name on rebuild. Report: {:?}",
plan.report,
);
let original_calc_info = original_table.by_name.get(&original_calc).unwrap();
let mapped = plan
.table
.map
.get(&original_calc_info.address)
.copied()
.unwrap_or_else(|| {
panic!(
"calculate not in JumpTable map. \
original_addr={:#x}, map keys: {:?}",
original_calc_info.address,
plan.table.map.keys().collect::<Vec<_>>(),
)
});
let _ = mapped;
let _ = std::fs::remove_dir_all(&work);
}
#[tokio::test]
async fn build_patch_errors_when_no_captured_rustc_for_the_package() {
let work = unique_tempdir("missing-rustc");
let lib_rs = work.join("lib.rs");
std::fs::copy(fixture_lib_rs(), &lib_rs).unwrap();
let original_out = work.join("original");
std::fs::create_dir_all(&original_out).unwrap();
let original_dylib = build_original_via_pipeline(&lib_rs, &original_out, &work).await;
let original_cache = HotpatchModuleCache::from_path(&original_dylib).unwrap();
let patcher = Patcher::new(
"package-not-in-cache".into(),
rustc_path(),
linker_path(),
work.clone(),
work.join("patches"),
linker_os_for_host(),
original_cache,
HashMap::new(),
HashMap::new(),
);
let err = patcher.build_patch(0, None).await.unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("no captured rustc invocation"), "{msg}");
let _ = std::fs::remove_dir_all(&work);
}
#[tokio::test]
async fn build_patch_errors_when_captured_linker_is_missing() {
let work = unique_tempdir("missing-linker");
let lib_rs = work.join("lib.rs");
std::fs::copy(fixture_lib_rs(), &lib_rs).unwrap();
let original_out = work.join("original");
std::fs::create_dir_all(&original_out).unwrap();
let original_dylib = build_original_via_pipeline(&lib_rs, &original_out, &work).await;
let original_cache = HotpatchModuleCache::from_path(&original_dylib).unwrap();
let mut captured_rustc_args = HashMap::new();
captured_rustc_args.insert(
FIXTURE_CRATE_NAME.into(),
captured_rustc_for_fixture(&lib_rs),
);
let patcher = Patcher::new(
FIXTURE_CRATE_NAME.into(),
rustc_path(),
linker_path(),
work.clone(),
work.join("patches"),
linker_os_for_host(),
original_cache,
captured_rustc_args,
HashMap::new(), );
let err = patcher.build_patch(0, None).await.unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("no captured linker invocation"), "{msg}");
let _ = std::fs::remove_dir_all(&work);
}