#![allow(clippy::result_large_err)]
use std::path::{Path, PathBuf};
use std::sync::Arc;
use cljrs_eval::GlobalEnv;
use cljrs_project::config::{Dependency, GitDep};
use cljrs_runtime::env::error::{EvalError, EvalResult};
pub const ABI_SYMBOL: &[u8] = b"cljrs_dylib_abi\0";
pub const INIT_SYMBOL: &[u8] = b"cljrs_dylib_init\0";
pub fn abi_fingerprint() -> String {
format!(
"cljrs {}; {}; {}",
env!("CARGO_PKG_VERSION"),
env!("CLJRS_DYLIB_RUSTC"),
host_profile(),
)
}
fn host_profile() -> &'static str {
if cfg!(debug_assertions) {
"debug"
} else {
"release"
}
}
pub fn install(globals: &Arc<GlobalEnv>) {
globals.set_pinned_native_loader(Arc::new(load_pinned));
globals.set_native_require_loader(Arc::new(load_require));
}
fn load_pinned(globals: &Arc<GlobalEnv>, base_ns: &str, commit: &str) -> EvalResult<bool> {
let config = globals.deps_config.read().unwrap().clone();
let Some(config) = config else {
return Ok(false);
};
let Some(git) = find_dylib_dep(&config, base_ns) else {
return Ok(false);
};
let versioned_ns = format!("{base_ns}@{commit}");
if globals.is_loaded(&versioned_ns) {
return Ok(true);
}
let lib_path = build_pinned_wrapper(&git, commit)
.map_err(|e| EvalError::Runtime(format!("pinned native {versioned_ns}: {e}")))?;
load_library(globals, &lib_path, Some(commit))
.map_err(|e| EvalError::Runtime(format!("pinned native {versioned_ns}: {e}")))?;
globals.mark_loaded(&versioned_ns);
eprintln!("[cljrs] loaded pinned native package {versioned_ns}");
Ok(true)
}
fn load_require(globals: &Arc<GlobalEnv>, ns: &str) -> EvalResult<bool> {
let config = globals.deps_config.read().unwrap().clone();
let Some(config) = config else {
return Ok(false);
};
let Some(git) = find_dylib_dep(&config, ns) else {
return Ok(false);
};
if globals.is_loaded(ns) {
return Ok(true);
}
let commit = git.sha.clone();
let lib_path = build_pinned_wrapper(&git, &commit)
.map_err(|e| EvalError::Runtime(format!("native dep {ns}: {e}")))?;
load_library(globals, &lib_path, None)
.map_err(|e| EvalError::Runtime(format!("native dep {ns}: {e}")))?;
eprintln!("[cljrs] loaded native dep {ns} (pinned {commit})");
Ok(true)
}
fn find_dylib_dep(config: &cljrs_project::config::DepsConfig, base_ns: &str) -> Option<GitDep> {
for (name, dep) in &config.deps {
if let Dependency::Git(git) = dep
&& git.rust_load_dylib
&& (name.as_ref() == base_ns
|| base_ns
.strip_prefix(name.as_ref())
.is_some_and(|rest| rest.starts_with('.')))
{
return Some(git.clone());
}
}
None
}
fn build_pinned_wrapper(git: &GitDep, commit: &str) -> Result<PathBuf, String> {
let init_fn = git
.rust_init
.as_deref()
.ok_or("dep has :rust/load :dylib but no :rust/init function")?;
let crate_name = init_fn.split("::").next().unwrap_or(init_fn);
let pkg_ident = crate_name.replace('-', "_");
cljrs_project::vcs::fetch_remote(&git.url, commit).map_err(|e| e.to_string())?;
let checkout =
cljrs_project::vcs::worktree_at_commit(&git.url, commit).map_err(|e| e.to_string())?;
let crate_dir = match git.rust_crate_dir.as_deref() {
Some(sub) => checkout.join(sub),
None => checkout.clone(),
};
if !crate_dir.join("Cargo.toml").exists() {
return Err(format!(
"no Cargo.toml at {} (set :rust/crate if the crate lives in a subdirectory)",
crate_dir.display()
));
}
let fingerprint = abi_fingerprint();
let fp_hash = stable_hash(&format!("{fingerprint}|{}|{commit}", git.url));
let wrapper_dir = dylib_cache_root()
.join(format!("{pkg_ident}@{commit}"))
.join(format!("fp-{fp_hash}"));
let artifact = wrapper_artifact_path(&wrapper_dir);
if artifact.exists() {
return Ok(artifact);
}
write_wrapper_crate(&wrapper_dir, &crate_dir, crate_name, &pkg_ident, init_fn)?;
let offline = find_workspace_root().is_some();
let mut cmd = std::process::Command::new("cargo");
cmd.arg("build").current_dir(&wrapper_dir);
if host_profile() == "release" {
cmd.arg("--release");
}
if offline {
cmd.arg("--offline");
}
eprintln!("[cljrs] building pinned native package {pkg_ident}@{commit}…");
let status = cmd.status().map_err(|e| format!("cargo: {e}"))?;
if !status.success() {
return Err(format!(
"cargo build of pinned wrapper failed (see output above; wrapper at {})",
wrapper_dir.display()
));
}
if !artifact.exists() {
return Err(format!("built wrapper not found at {}", artifact.display()));
}
Ok(artifact)
}
fn write_wrapper_crate(
wrapper_dir: &Path,
crate_dir: &Path,
crate_name: &str,
pkg_ident: &str,
init_fn: &str,
) -> Result<(), String> {
std::fs::create_dir_all(wrapper_dir.join("src")).map_err(|e| e.to_string())?;
let interop_dep = match find_workspace_root() {
Some(root) => format!(
"cljrs-interop = {{ path = \"{}\" }}",
root.join("crates/cljrs-interop").display()
),
None => format!("cljrs-interop = \"={}\"", env!("CARGO_PKG_VERSION")),
};
let cargo_toml = format!(
r#"[package]
name = "cljrs-pinned-wrapper"
version = "{version}"
edition = "2024"
[workspace]
[lib]
crate-type = ["cdylib"]
[dependencies]
{interop_dep}
{crate_name} = {{ path = "{crate_dir}" }}
[profile.release]
panic = "unwind"
"#,
version = env!("CARGO_PKG_VERSION"),
crate_dir = crate_dir.display(),
);
std::fs::write(wrapper_dir.join("Cargo.toml"), cargo_toml).map_err(|e| e.to_string())?;
let build_rs = r#"fn main() {
let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string());
let version = std::process::Command::new(rustc)
.arg("-V")
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default();
println!("cargo:rustc-env=CLJRS_WRAPPER_RUSTC={version}");
}
"#;
std::fs::write(wrapper_dir.join("build.rs"), build_rs).map_err(|e| e.to_string())?;
let lib_rs = format!(
r#"//! Auto-generated pinned-package wrapper (cljrs).
/// ABI fingerprint baked at build time; must equal the host's
/// `cljrs::native::pinned::abi_fingerprint()` exactly (including the build profile —
/// cljrs-gc object headers differ between debug and release).
#[cfg(debug_assertions)]
static ABI: &str = concat!(
"cljrs ",
env!("CARGO_PKG_VERSION"),
"; ",
env!("CLJRS_WRAPPER_RUSTC"),
"; debug\0"
);
#[cfg(not(debug_assertions))]
static ABI: &str = concat!(
"cljrs ",
env!("CARGO_PKG_VERSION"),
"; ",
env!("CLJRS_WRAPPER_RUSTC"),
"; release\0"
);
#[unsafe(no_mangle)]
pub extern "C" fn cljrs_dylib_abi() -> *const std::os::raw::c_char {{
ABI.as_ptr() as *const std::os::raw::c_char
}}
/// Register the pinned package's exports into the host-provided registry.
///
/// # Safety
/// `registry` must be a valid `*mut cljrs_interop::Registry` from a host
/// whose ABI fingerprint matched `cljrs_dylib_abi()`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cljrs_dylib_init(registry: *mut cljrs_interop::Registry) {{
let registry = unsafe {{ &mut *registry }};
// This dylib's own #[export] inventory (separate from the host's).
cljrs_interop::register_exports(registry);
{pkg_ident}::{init_tail}(registry);
}}
"#,
init_tail = init_fn
.split_once("::")
.map(|(_, rest)| rest)
.unwrap_or("cljrs_init"),
);
std::fs::write(wrapper_dir.join("src/lib.rs"), lib_rs).map_err(|e| e.to_string())?;
Ok(())
}
fn wrapper_artifact_path(wrapper_dir: &Path) -> PathBuf {
let stem = "cljrs_pinned_wrapper";
#[cfg(target_os = "macos")]
let file = format!("lib{stem}.dylib");
#[cfg(target_os = "windows")]
let file = format!("{stem}.dll");
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
let file = format!("lib{stem}.so");
wrapper_dir.join("target").join(host_profile()).join(file)
}
fn load_library(
globals: &Arc<GlobalEnv>,
path: &Path,
version: Option<&str>,
) -> Result<(), String> {
unsafe {
let lib = libloading::Library::new(path).map_err(|e| e.to_string())?;
let abi: libloading::Symbol<unsafe extern "C" fn() -> *const std::os::raw::c_char> =
lib.get(ABI_SYMBOL).map_err(|e| e.to_string())?;
let got = std::ffi::CStr::from_ptr(abi())
.to_string_lossy()
.to_string();
let expected = abi_fingerprint();
if got != expected {
return Err(format!(
"ABI fingerprint mismatch: wrapper was built as `{got}` but this binary \
expects `{expected}`; rebuild with the matching toolchain/cljrs version"
));
}
let init: libloading::Symbol<unsafe extern "C" fn(*mut cljrs_interop::Registry)> =
lib.get(INIT_SYMBOL).map_err(|e| e.to_string())?;
let mut registry = match version {
Some(commit) => cljrs_interop::Registry::versioned(globals.clone(), commit),
None => cljrs_interop::Registry::for_require(globals.clone()),
};
init(&mut registry as *mut _);
std::mem::forget(lib);
}
Ok(())
}
fn dylib_cache_root() -> PathBuf {
let home = std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
home.join(".cljrs").join("cache").join("dylibs")
}
fn find_workspace_root() -> Option<PathBuf> {
let validate = |p: PathBuf| -> Option<PathBuf> {
(p.join("Cargo.toml").exists() && p.join("crates/cljrs-interop/Cargo.toml").exists())
.then_some(p)
};
if let Some(root) = std::env::var_os("CLJRS_WORKSPACE_ROOT") {
return validate(PathBuf::from(root));
}
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
validate(manifest_dir.parent()?.parent()?.to_path_buf())
}
fn stable_hash(s: &str) -> String {
use std::hash::{DefaultHasher, Hash as _, Hasher as _};
let mut h = DefaultHasher::new();
s.hash(&mut h);
format!("{:016x}", h.finish())
}