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, CapturedRustcInvocation,
};
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-thin-rebuild-obj-{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") {
let out = std::process::Command::new("xcrun")
.args(["-f", "clang"])
.output();
if let Ok(out) = out {
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 host_sdk_args() -> Vec<String> {
if !cfg!(target_os = "macos") {
return vec![];
}
let Ok(out) = std::process::Command::new("xcrun")
.args(["--show-sdk-path"])
.output()
else {
return vec![];
};
if !out.status.success() {
return vec![];
}
let sdk = String::from_utf8_lossy(&out.stdout).trim().to_string();
if sdk.is_empty() {
return vec![];
}
vec!["-isysroot".into(), sdk]
}
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 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 thin_rebuild_obj_plus_dynamic_lookup_link_preserves_mangled_symbols() {
let work = unique_tempdir("happy");
let lib_rs = work.join("lib.rs");
std::fs::copy(fixture_lib_rs(), &lib_rs).unwrap();
let captured = captured_rustc_for_fixture(&lib_rs);
let obj_dir = work.join("obj");
std::fs::create_dir_all(&obj_dir).unwrap();
let obj_plan = build_obj_plan(&captured, &obj_dir);
let object = run_obj_plan(&obj_plan, &rustc_path(), &work)
.await
.expect("rustc --emit=obj should succeed");
assert!(
object.is_file(),
"expected `{}` to exist after run_obj_plan",
object.display(),
);
let dylib = obj_dir.join(library_filename(FIXTURE_CRATE_NAME));
let link_plan = build_link_plan(
&host_sdk_args(),
&object,
&dylib,
linker_os_for_host(),
&[], &[], );
run_link_plan(&link_plan, &linker_path(), &work)
.await
.expect("clang/cc -shared should succeed");
assert!(
dylib.is_file(),
"expected `{}` after run_link_plan",
dylib.display(),
);
let table = parse_symbol_table(&dylib).expect("parse the produced dylib");
let calc = find_calculate(table.by_name.keys().cloned()).unwrap_or_else(|| {
let mut keys: Vec<&String> = table.by_name.keys().collect();
keys.sort();
let preview: Vec<&String> = keys.into_iter().take(30).collect();
panic!(
"mangled `calculate` not found in {} symbols. \
First 30 (sorted): {:?}",
table.by_name.len(),
preview,
)
});
let info = table.by_name.get(&calc).unwrap();
assert!(
!info.is_undefined,
"calculate should be DEFINED in the patch dylib, not undefined: {info:?}",
);
let has_answer = table
.by_name
.keys()
.any(|k| k == "answer" || k == "_answer");
assert!(has_answer, "no `answer` symbol in dylib");
let _ = std::fs::remove_dir_all(&work);
}