use std::path::Path;
use std::sync::Arc;
use crate::env::env::{Env, GlobalEnv};
use crate::env::error::{EvalError, EvalResult};
use cljrs_value::Value;
pub fn base_ns_name(ns: &str) -> &str {
cljrs_value::symbol::split_version(ns).0
}
pub fn resolve_versioned_value(
globals: &Arc<GlobalEnv>,
defining_ns: &str,
ns_part: Option<&str>,
name: &str,
commit: &str,
) -> EvalResult {
let base_ns: Arc<str> = match ns_part {
Some(p) => {
let resolved = globals
.resolve_alias(defining_ns, p)
.unwrap_or_else(|| Arc::from(p));
Arc::from(base_ns_name(&resolved))
}
None => Arc::from(base_ns_name(defining_ns)),
};
let versioned_ns: Arc<str> = Arc::from(format!("{base_ns}@{commit}"));
if let Some(val) = globals.lookup_in_ns(&versioned_ns, name) {
return Ok(val);
}
if let Some(cached) = globals.get_cached_versioned(&base_ns, name, commit) {
return Ok(cached);
}
if !globals.is_loaded(&versioned_ns) {
if !versioned_source_available(globals, &base_ns, &versioned_ns) {
return pinned_native_or_head_fallback(globals, &base_ns, &versioned_ns, name, commit)
.map_err(|e| {
if globals.versioned_offline() {
EvalError::Runtime(format!(
"versioned namespace {versioned_ns} was not embedded at compile \
time; AOT binaries cannot fetch from git at runtime ({e})"
))
} else {
e
}
});
}
ensure_versioned_ns_loaded(globals, &base_ns, commit)?;
}
if let Some(val) = globals.lookup_in_ns(&versioned_ns, name) {
return Ok(val);
}
pinned_native_or_head_fallback(globals, &base_ns, &versioned_ns, name, commit)
}
fn pinned_native_or_head_fallback(
globals: &Arc<GlobalEnv>,
base_ns: &str,
versioned_ns: &str,
name: &str,
commit: &str,
) -> EvalResult {
let loader = globals.pinned_native_loader.read().unwrap().clone();
if let Some(loader) = loader
&& loader(globals, base_ns, commit)?
{
return globals
.lookup_in_ns(versioned_ns, name)
.ok_or_else(|| EvalError::UnboundSymbol(format!("{versioned_ns}/{name}")));
}
native_head_fallback(globals, base_ns, name, commit)
}
pub fn pin_if_available(globals: &Arc<GlobalEnv>, base_ns: &str, commit: &str) -> EvalResult<bool> {
let versioned_ns = format!("{base_ns}@{commit}");
if !versioned_source_available(globals, base_ns, &versioned_ns) {
return Ok(false);
}
ensure_versioned_ns_loaded(globals, base_ns, commit)?;
Ok(true)
}
fn versioned_source_available(globals: &GlobalEnv, base_ns: &str, versioned_ns: &str) -> bool {
if globals.builtin_source(versioned_ns).is_some() {
return true;
}
if globals.versioned_offline() {
return false;
}
let rel_path = base_ns.replace('.', "/").replace('-', "_");
let src_paths = globals.source_paths.read().unwrap().clone();
match crate::env::loader::find_source_file(&rel_path, &src_paths) {
Some((_, file_path)) => cljrs_vcs::find_repo_root(Path::new(&file_path)).is_some(),
None => false,
}
}
pub fn ensure_versioned_ns_loaded(
globals: &Arc<GlobalEnv>,
base_ns: &str,
commit: &str,
) -> EvalResult<Arc<str>> {
let versioned_ns_name: Arc<str> = Arc::from(format!("{base_ns}@{commit}"));
if globals.is_loaded(&versioned_ns_name) {
return Ok(versioned_ns_name);
}
let should_load = crate::env::loader::claim_or_wait(globals, &versioned_ns_name)?;
if !should_load {
return Ok(versioned_ns_name);
}
let result = do_versioned_load(globals, base_ns, commit, &versioned_ns_name);
globals
.loading
.lock()
.unwrap()
.remove(versioned_ns_name.as_ref());
if result.is_ok() {
globals.mark_loaded(&versioned_ns_name);
}
globals.loading_done.notify_all();
result?;
Ok(versioned_ns_name)
}
fn do_versioned_load(
globals: &Arc<GlobalEnv>,
base_ns: &str,
commit: &str,
versioned_ns_name: &Arc<str>,
) -> EvalResult<()> {
let (src, git_location): (String, Option<(String, String)>) =
if let Some(builtin) = globals.builtin_source(versioned_ns_name) {
(builtin.to_owned(), None)
} else if globals.versioned_offline() {
return Err(EvalError::Runtime(format!(
"versioned namespace {versioned_ns_name} was not embedded at compile time; \
AOT binaries cannot fetch from git at runtime"
)));
} else {
let (src, location) = fetch_versioned_source(globals, base_ns, commit)?;
(src, Some(location))
};
{
use cljrs_value::Namespace;
let ns = cljrs_gc::GcPtr::new(Namespace::new_versioned(versioned_ns_name.as_ref()));
if let Some((ref file_path, ref repo_root)) = git_location {
ns.get().set_source_location(file_path, Some(repo_root));
}
let mut map = globals.namespaces.write().unwrap();
map.entry(versioned_ns_name.clone()).or_insert(ns);
}
globals.refer_all(versioned_ns_name, "clojure.core");
let saved_ns = globals
.lookup_var("clojure.core", "*ns*")
.and_then(|v| crate::env::dynamics::deref_var(&v));
{
let mut env = Env::new_versioned(globals.clone(), versioned_ns_name, commit);
let file_label = format!("<{base_ns}@{commit}>");
let mut parser = cljrs_reader::Parser::new(src, file_label);
let forms = parser.parse_all().map_err(EvalError::Read)?;
for form in forms {
let _alloc_frame = cljrs_gc::push_alloc_frame();
globals
.eval(&form, &mut env)
.map_err(|e| crate::env::loader::annotate(e, versioned_ns_name))?;
}
}
if let Some(saved) = saved_ns
&& let Some(var) = globals.lookup_var("clojure.core", "*ns*")
{
var.get().bind(saved);
}
Ok(())
}
fn fetch_versioned_source(
globals: &Arc<GlobalEnv>,
base_ns: &str,
commit: &str,
) -> EvalResult<(String, (String, String))> {
let rel_path = base_ns.replace('.', "/").replace('-', "_");
let src_paths = globals.source_paths.read().unwrap().clone();
let (_, file_path) =
crate::env::loader::find_source_file(&rel_path, &src_paths).ok_or_else(|| {
EvalError::Runtime(format!(
"Cannot find source for namespace {base_ns} (needed for {base_ns}@{commit})"
))
})?;
let repo_root = cljrs_vcs::find_repo_root(Path::new(&file_path)).ok_or_else(|| {
EvalError::Runtime(format!(
"Namespace {base_ns} (file {file_path}) is not in a git repository; \
cannot resolve {base_ns}@{commit}"
))
})?;
globals.check_commit_signature(&repo_root.to_string_lossy(), commit)?;
let abs_file = Path::new(&file_path);
let rel_file = abs_file.strip_prefix(&repo_root).map_err(|_| {
EvalError::Runtime(format!(
"Cannot compute relative path for {file_path} within {}",
repo_root.display()
))
})?;
let rel_file_str = rel_file.to_string_lossy();
let src = cljrs_vcs::get_file_at_commit(&repo_root, &rel_file_str, commit)
.map_err(|e| EvalError::Runtime(format!("{e}")))?;
globals.record_versioned_source(&format!("{base_ns}@{commit}"), &src);
Ok((src, (file_path, repo_root.display().to_string())))
}
fn native_head_fallback(
globals: &GlobalEnv,
base_ns: &str,
name: &str,
commit: &str,
) -> EvalResult {
match globals.lookup_in_ns(base_ns, name) {
Some(val) if matches!(val, Value::NativeFunction(_)) => {
check_native_provenance(globals, base_ns, commit)?;
globals.cache_versioned(base_ns, name, commit, val.clone());
Ok(val)
}
Some(_) => Err(EvalError::Runtime(format!(
"Cannot find definition of `{name}` in `{base_ns}@{commit}`"
))),
None => Err(EvalError::UnboundSymbol(format!("{base_ns}/{name}"))),
}
}
fn check_native_provenance(globals: &GlobalEnv, base_ns: &str, commit: &str) -> EvalResult<()> {
let recorded = globals.native_provenance_for(base_ns);
if let Some(ref rec) = recorded {
let matches = rec.starts_with(commit) || commit.starts_with(rec.as_ref());
if matches {
return Ok(());
}
}
let described = match &recorded {
Some(rec) => format!("is built from commit {rec}"),
None => "has no recorded provenance".to_string(),
};
if globals.enforce_native_versions() {
return Err(EvalError::Runtime(format!(
"native package `{base_ns}` {described}; cannot satisfy pinned \
`{base_ns}@{commit}` (native functions always come from the current binary)"
)));
}
let warn_key: Arc<str> = Arc::from(format!("{base_ns}@{commit}"));
if globals.provenance_warned.lock().unwrap().insert(warn_key) {
eprintln!(
"cljrs: warning: native package `{base_ns}` {described}; pinned \
`{base_ns}@{commit}` resolves to the current binary's implementation \
(use --enforce-native-versions to make this an error)"
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn offline_load_without_embedded_source_errors() {
let _mutator = cljrs_gc::register_mutator();
let globals = GlobalEnv::new(crate::ExecutionMode::TreeWalk);
globals.set_versioned_offline(true);
let err = ensure_versioned_ns_loaded(&globals, "mylib", "abc1234abcdef")
.expect_err("offline load must fail without an embedded source");
let msg = format!("{err:?}");
assert!(
msg.contains("was not embedded at compile time"),
"unexpected error: {msg}"
);
}
#[test]
fn offline_load_with_embedded_source_succeeds() {
let _mutator = cljrs_gc::register_mutator();
let globals = GlobalEnv::new(crate::ExecutionMode::TreeWalk);
globals.set_versioned_offline(true);
globals.register_builtin_source("mylib@abc1234abcdef", "(def x 1)");
let ns = ensure_versioned_ns_loaded(&globals, "mylib", "abc1234abcdef")
.expect("embedded source must load offline");
assert_eq!(ns.as_ref(), "mylib@abc1234abcdef");
assert!(globals.is_loaded("mylib@abc1234abcdef"));
}
}