use std::path::{Path, PathBuf};
const EMBEDDED_PACKAGE_JSON: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/package.json"));
fn find_in_roots(roots: &[PathBuf], probe: &str) -> Option<PathBuf> {
roots.iter().find(|r| r.join("node_modules").join(probe).exists()).and_then(|r| r.canonicalize().ok())
}
fn npm_install(root: &Path, npm: &str) -> bool {
match std::process::Command::new(npm).arg("install").current_dir(root).status() {
Ok(status) if status.success() => true,
Ok(status) => {
tracing::warn!("`npm install` in {} exited with {status}", root.display());
false
}
Err(e) => {
tracing::warn!("failed to run `npm install` in {}: {e}", root.display());
false
}
}
}
fn ensure_cache_deps(root: &Path, manifest: &str, npm: &str, probe: &str) -> Option<PathBuf> {
std::fs::create_dir_all(root).ok()?;
let lock = std::fs::File::create(root.join(".install.lock")).ok()?;
lock.lock().ok()?;
let manifest_path = root.join("package.json");
let fresh = std::fs::read_to_string(&manifest_path).is_ok_and(|on_disk| on_disk == manifest);
if fresh && root.join("node_modules").join(probe).exists() {
return Some(root.to_path_buf());
}
if !fresh {
std::fs::write(&manifest_path, manifest).ok()?;
let _ = std::fs::remove_file(root.join("package-lock.json"));
}
if !npm_install(root, npm) {
return None;
}
root.join("node_modules").join(probe).exists().then(|| root.to_path_buf())
}
fn user_cache_root() -> Option<PathBuf> {
if let Ok(x) = std::env::var("XDG_CACHE_HOME") {
if !x.is_empty() {
return Some(PathBuf::from(x).join("pil2-proofman/node-deps"));
}
}
std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache/pil2-proofman/node-deps"))
}
pub(crate) fn ensure_node_deps(probe: &str) -> Option<PathBuf> {
const CRATE_ROOT: &str = env!("CARGO_MANIFEST_DIR");
let mut roots = vec![PathBuf::from(CRATE_ROOT)];
if let Ok(cwd) = std::env::current_dir() {
roots.push(cwd);
}
if let Ok(exe) = std::env::current_exe() {
let mut dir = exe.parent();
while let Some(d) = dir {
roots.push(d.to_path_buf());
dir = d.parent();
}
}
if let Some(root) = find_in_roots(&roots, probe) {
return Some(root);
}
let baked = Path::new(CRATE_ROOT);
let baked = baked.canonicalize().unwrap_or_else(|_| baked.to_path_buf());
if baked.join("package.json").is_file() {
tracing::info!("Node deps not found; running `npm install` in {}", baked.display());
if npm_install(&baked, "npm") && baked.join("node_modules").join(probe).exists() {
return Some(baked);
}
}
let cache = user_cache_root()?;
tracing::info!("Bootstrapping Node deps into {}", cache.display());
ensure_cache_deps(&cache, EMBEDDED_PACKAGE_JSON, "npm", probe)
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
fn fake_npm(dir: &Path, extra: &str) -> String {
let p = dir.join("fake-npm.sh");
std::fs::write(&p, format!("#!/bin/sh\necho run >> runs.log\n{extra}\n")).unwrap();
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755)).unwrap();
p.to_string_lossy().into_owned()
}
fn npm_runs(root: &Path) -> usize {
std::fs::read_to_string(root.join("runs.log")).map(|s| s.lines().count()).unwrap_or(0)
}
#[test]
fn find_in_roots_picks_first_populated_root() {
let tmp = tempfile::tempdir().unwrap();
let empty = tmp.path().join("empty");
let populated = tmp.path().join("populated");
std::fs::create_dir_all(populated.join("node_modules/snarkjs")).unwrap();
std::fs::create_dir_all(&empty).unwrap();
let roots = vec![empty, populated.clone()];
let found = find_in_roots(&roots, "snarkjs").unwrap();
assert_eq!(found, populated.canonicalize().unwrap());
}
#[test]
fn find_in_roots_returns_none_when_probe_missing() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("a/node_modules/other")).unwrap();
let roots = vec![tmp.path().join("a"), tmp.path().join("does-not-exist")];
assert!(find_in_roots(&roots, "snarkjs").is_none());
}
#[test]
fn cache_bootstrap_writes_manifest_and_installs() {
let tmp = tempfile::tempdir().unwrap();
let cache = tmp.path().join("cache");
let npm = fake_npm(tmp.path(), "mkdir -p node_modules/.bin && touch node_modules/.bin/pil2com");
let got = ensure_cache_deps(&cache, "{\"v\":1}", &npm, ".bin/pil2com").unwrap();
assert_eq!(got, cache);
assert_eq!(std::fs::read_to_string(cache.join("package.json")).unwrap(), "{\"v\":1}");
assert_eq!(npm_runs(&cache), 1);
assert!(cache.join("node_modules/.bin/pil2com").is_file());
}
#[test]
fn cache_skips_npm_when_fresh() {
let tmp = tempfile::tempdir().unwrap();
let cache = tmp.path().join("cache");
std::fs::create_dir_all(cache.join("node_modules/.bin")).unwrap();
std::fs::write(cache.join("node_modules/.bin/pil2com"), "").unwrap();
std::fs::write(cache.join("package.json"), "{\"v\":1}").unwrap();
let npm = fake_npm(tmp.path(), "");
let got = ensure_cache_deps(&cache, "{\"v\":1}", &npm, ".bin/pil2com").unwrap();
assert_eq!(got, cache);
assert_eq!(npm_runs(&cache), 0);
}
#[test]
fn cache_reinstalls_when_manifest_stale() {
let tmp = tempfile::tempdir().unwrap();
let cache = tmp.path().join("cache");
std::fs::create_dir_all(cache.join("node_modules/.bin")).unwrap();
std::fs::write(cache.join("node_modules/.bin/pil2com"), "").unwrap();
std::fs::write(cache.join("package.json"), "{\"v\":1}").unwrap();
std::fs::write(cache.join("package-lock.json"), "{}").unwrap();
let npm = fake_npm(tmp.path(), "mkdir -p node_modules/.bin && touch node_modules/.bin/pil2com");
let got = ensure_cache_deps(&cache, "{\"v\":2}", &npm, ".bin/pil2com").unwrap();
assert_eq!(got, cache);
assert_eq!(npm_runs(&cache), 1);
assert_eq!(std::fs::read_to_string(cache.join("package.json")).unwrap(), "{\"v\":2}");
assert!(!cache.join("package-lock.json").exists());
}
#[test]
fn cache_fails_when_npm_fails() {
let tmp = tempfile::tempdir().unwrap();
let cache = tmp.path().join("cache");
let npm = fake_npm(tmp.path(), "exit 1");
assert!(ensure_cache_deps(&cache, "{}", &npm, ".bin/pil2com").is_none());
}
#[test]
#[ignore]
fn real_npm_bootstrap_of_embedded_manifest() {
let tmp = tempfile::tempdir().unwrap();
let cache = tmp.path().join("node-deps");
let got = ensure_cache_deps(&cache, EMBEDDED_PACKAGE_JSON, "npm", ".bin/pil2com").unwrap();
assert!(got.join("node_modules/.bin/pil2com").exists());
assert!(got.join("node_modules/snarkjs").is_dir());
assert!(got.join("node_modules/circomlib/circuits").is_dir());
}
#[test]
fn cache_fails_when_npm_does_not_produce_probe() {
let tmp = tempfile::tempdir().unwrap();
let cache = tmp.path().join("cache");
let npm = fake_npm(tmp.path(), "");
assert!(ensure_cache_deps(&cache, "{}", &npm, ".bin/pil2com").is_none());
}
}