use std::path::Path;
use super::manifest::{PluginKind, PluginManifest};
use super::store::{InstalledIndex, InstalledPlugin, Store};
use super::{resolver, PkgError, PkgResult};
pub fn add(spec: &str) -> PkgResult<()> {
let store = Store::user_default()?;
store.ensure_layout()?;
let staged = resolver::resolve(spec, &store)?;
let manifest = PluginManifest::load(&staged.dir)?;
let name = manifest
.as_ref()
.map(|m| m.plugin.name.clone())
.filter(|n| !n.is_empty())
.unwrap_or_else(|| staged.name.clone());
let version = manifest
.as_ref()
.map(|m| m.plugin.version.clone())
.filter(|v| !v.is_empty())
.unwrap_or_else(|| "0.0.0".into());
let description = manifest
.as_ref()
.map(|m| m.plugin.description.clone())
.unwrap_or_default();
let kind = PluginKind::detect(&staged.dir, manifest.as_ref())?;
if let PluginKind::Native(spec) = &kind {
prepare_native(&staged.dir, spec, &name)?;
}
let store_path = store.install_dir(&name, &version, &staged.dir)?;
let integrity = super::store_integrity(&store_path)?;
let mut entry = InstalledPlugin {
name: name.clone(),
version: version.clone(),
source: staged.source.clone(),
integrity,
..Default::default()
};
match &kind {
PluginKind::Native(_) => {
entry.kind = "native".into();
entry.lib = find_cdylib(&store_path)
.ok_or_else(|| PkgError::Resolve(format!("{}: no cdylib after build", name)))?;
}
PluginKind::Script(s) => {
entry.kind = "script".into();
entry.source_files = s.source.clone();
entry.fpath = s.fpath.clone();
}
}
let mut index = InstalledIndex::load_from(&store)?;
index.upsert(entry.clone());
index.save_to(&store)?;
if staged.source.starts_with("github:") || staged.source.starts_with("git+") {
let _ = std::fs::remove_dir_all(&staged.dir);
}
load_entry(&store, &entry)?;
let desc = if description.is_empty() {
String::new()
} else {
format!(" — {}", description)
};
println!("znative: added {}@{} ({}){}", name, version, entry.kind, desc);
Ok(())
}
pub fn remove(name: &str) -> PkgResult<()> {
let store = Store::user_default()?;
let mut index = InstalledIndex::load_from(&store)?;
let Some(entry) = index.remove(name) else {
return Err(PkgError::Other(format!("{} is not installed", name)));
};
if entry.kind == "native" {
let _ = crate::plugin_host::unload(name);
}
let dir = store.package_dir(&entry.name, &entry.version);
if dir.is_dir() {
std::fs::remove_dir_all(&dir)
.map_err(|e| PkgError::Io(format!("remove {}: {}", dir.display(), e)))?;
}
index.save_to(&store)?;
println!("znative: removed {}", name);
Ok(())
}
pub fn list() -> PkgResult<()> {
let store = Store::user_default()?;
let index = InstalledIndex::load_from(&store)?;
if index.packages.is_empty() {
println!("znative: no plugins installed");
return Ok(());
}
for p in &index.packages {
println!("{:<24} {:<10} {:<7} {}", p.name, p.version, p.kind, p.source);
}
Ok(())
}
fn dir_size(path: &Path) -> u64 {
let mut total: u64 = 0;
let Ok(entries) = std::fs::read_dir(path) else {
return 0;
};
for entry in entries.flatten() {
let Ok(meta) = entry.metadata() else { continue };
if meta.is_dir() {
total += dir_size(&entry.path());
} else if meta.is_file() {
total += meta.len();
}
}
total
}
pub fn gc(dry_run: bool) -> PkgResult<()> {
let store = Store::user_default()?;
let index = InstalledIndex::load_from(&store)?;
let pinned: std::collections::HashSet<String> = index
.packages
.iter()
.map(|p| format!("{}@{}", p.name, p.version))
.collect();
let mut freed: u64 = 0;
let mut count: usize = 0;
if let Ok(entries) = std::fs::read_dir(store.store_dir()) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
if entry.path().is_dir() && !pinned.contains(&name) {
let bytes = dir_size(&entry.path());
if dry_run {
println!("znative gc: would remove {} ({} KB)", name, (bytes + 512) / 1024);
} else {
std::fs::remove_dir_all(entry.path())
.map_err(|e| PkgError::Io(format!("remove {}: {}", name, e)))?;
println!("znative gc: removed {} ({} KB)", name, (bytes + 512) / 1024);
}
freed += bytes;
count += 1;
}
}
}
let git = store.git_dir();
let git_bytes = dir_size(&git);
if git_bytes > 0 {
if dry_run {
println!("znative gc: would clear git cache ({} KB)", (git_bytes + 512) / 1024);
} else {
let _ = std::fs::remove_dir_all(&git);
println!("znative gc: cleared git cache ({} KB)", (git_bytes + 512) / 1024);
}
freed += git_bytes;
count += 1;
}
if count == 0 {
println!("znative gc: nothing to collect");
} else {
let verb = if dry_run { "would free" } else { "freed" };
println!("znative gc: {} {} KB total", verb, (freed + 512) / 1024);
}
Ok(())
}
pub fn clean() -> PkgResult<()> {
let store = Store::user_default()?;
let mut freed: u64 = 0;
for d in [store.git_dir(), store.cache_dir(), store.bin_dir()] {
if d.exists() {
freed += dir_size(&d);
std::fs::remove_dir_all(&d)
.map_err(|e| PkgError::Io(format!("remove {}: {}", d.display(), e)))?;
}
}
println!("znative clean: cleared {} KB of scratch", (freed + 512) / 1024);
Ok(())
}
pub fn info(name: &str) -> PkgResult<()> {
let store = Store::user_default()?;
let index = InstalledIndex::load_from(&store)?;
let Some(p) = index.find(name) else {
return Err(PkgError::Other(format!("{} is not installed", name)));
};
println!("name {}", p.name);
println!("version {}", p.version);
println!("kind {}", p.kind);
println!("source {}", p.source);
println!("store {}", store.package_dir(&p.name, &p.version).display());
if !p.integrity.is_empty() {
println!("integrity {}", p.integrity);
}
if !p.lib.is_empty() {
println!("lib {}", p.lib);
}
if !p.source_files.is_empty() {
println!("files {}", p.source_files.join(" "));
}
if !p.fpath.is_empty() {
println!("fpath {}", p.fpath.join(" "));
}
Ok(())
}
pub fn load(name: Option<&str>) -> PkgResult<()> {
let store = Store::user_default()?;
let index = InstalledIndex::load_from(&store)?;
match name {
Some(n) => {
if let Some(entry) = index.find(n) {
return load_entry(&store, entry);
}
if let Some(label) = resolver::source_label(n) {
if let Some(entry) = index.packages.iter().find(|p| p.source == label) {
return load_entry(&store, entry);
}
return add(n);
}
Err(PkgError::Other(format!("{} is not installed", n)))
}
None => {
let mut errs = Vec::new();
for p in &index.packages {
if let Err(e) = load_entry(&store, p) {
errs.push(format!("{}: {}", p.name, e));
}
}
if errs.is_empty() {
Ok(())
} else {
Err(PkgError::Other(errs.join("; ")))
}
}
}
}
pub fn update(name: Option<&str>) -> PkgResult<()> {
let store = Store::user_default()?;
let index = InstalledIndex::load_from(&store)?;
let targets: Vec<String> = match name {
Some(n) => vec![n.to_string()],
None => index.packages.iter().map(|p| p.name.clone()).collect(),
};
for n in targets {
let Some(p) = index.find(&n) else {
return Err(PkgError::Other(format!("{} is not installed", n)));
};
let spec = source_to_spec(&p.source);
add(&spec)?;
}
Ok(())
}
fn source_to_spec(source: &str) -> String {
if let Some(rest) = source.strip_prefix("path+file://") {
format!("path:{}", rest)
} else {
source.to_string()
}
}
fn load_entry(store: &Store, p: &InstalledPlugin) -> PkgResult<()> {
let dir = store.package_dir(&p.name, &p.version);
match p.kind.as_str() {
"native" => {
let lib = dir.join(&p.lib);
crate::plugin_host::load(&lib.to_string_lossy())
.map(|_| ())
.map_err(PkgError::Resolve)
}
"script" => {
let mut code = String::new();
if !p.fpath.is_empty() {
let dirs: Vec<String> = p
.fpath
.iter()
.map(|f| shquote(&dir.join(f).to_string_lossy()))
.collect();
code.push_str(&format!("fpath=({} $fpath)\n", dirs.join(" ")));
}
for f in &p.source_files {
let path = dir.join(f);
code.push_str(&format!("source {}\n", shquote(&path.to_string_lossy())));
}
if code.is_empty() {
return Ok(());
}
crate::ported::exec::execute_script(&code)
.map(|_| ())
.map_err(PkgError::Other)
}
other => Err(PkgError::Other(format!(
"{}: unknown plugin kind '{}'",
p.name, other
))),
}
}
fn prepare_native(dir: &Path, spec: &super::manifest::NativeSpec, name: &str) -> PkgResult<()> {
if find_cdylib(dir).is_some() {
return Ok(()); }
let has_cargo = dir.join("Cargo.toml").is_file();
let want_build = spec.build.unwrap_or(has_cargo);
if !want_build {
return Err(PkgError::Resolve(format!(
"{}: native plugin has no prebuilt cdylib and build is disabled",
name
)));
}
if !has_cargo {
return Err(PkgError::Resolve(format!(
"{}: native plugin has neither a cdylib nor a Cargo.toml to build",
name
)));
}
let out = std::process::Command::new("cargo")
.current_dir(dir)
.arg("build")
.arg("--release")
.output()
.map_err(|e| PkgError::Resolve(format!("cargo build: {} (is cargo installed?)", e)))?;
if !out.status.success() {
return Err(PkgError::Resolve(format!(
"{}: cargo build failed:\n{}",
name,
String::from_utf8_lossy(&out.stderr).trim()
)));
}
let rel = dir.join("target").join("release");
let built = find_cdylib(&rel).ok_or_else(|| {
PkgError::Resolve(format!(
"{}: cargo build produced no cdylib in {} (need crate-type=[\"cdylib\"])",
name,
rel.display()
))
})?;
std::fs::copy(rel.join(&built), dir.join(&built))
.map_err(|e| PkgError::Io(format!("stage cdylib: {}", e)))?;
Ok(())
}
fn find_cdylib(dir: &Path) -> Option<String> {
let suffix = std::env::consts::DLL_SUFFIX; let rd = std::fs::read_dir(dir).ok()?;
for entry in rd.flatten() {
let n = entry.file_name().to_string_lossy().into_owned();
if n.ends_with(suffix) && n.starts_with(std::env::consts::DLL_PREFIX) {
return Some(n);
}
}
None
}
fn shquote(s: &str) -> String {
format!("'{}'", s.replace('\'', r"'\''"))
}