Skip to main content

zsh/extensions/pkg/
mod.rs

1//! zshrs plugin package manager (`znative`) — a GLOBAL-only package manager for
2//! zsh script plugins AND native (Rust cdylib) plugins.
3//!
4//! Ported from strykelang's package manager (`strykelang/pkg/`), retargeted
5//! from `.stk` modules to zshrs plugins and simplified to the global-store
6//! model (no per-project manifest/lockfile — the global install index at
7//! `$ZSHRS_HOME/pkg/installed.toml` is the single source of truth).
8//!
9//! World's first: a compiled Unix shell whose package manager installs and
10//! loads BOTH interpreted (zsh) and compiled (Rust cdylib) plugins from one
11//! content-addressed global store, with SHA-256 integrity pinning.
12//!
13//! Surface:
14//! - [`manifest`] — a plugin's optional `znative.toml` (`[plugin]`/`[native]`/
15//!   `[script]`); auto-detected when absent.
16//! - [`store`]    — `$ZSHRS_HOME/pkg/{store,cache,git,bin}/` layout + the
17//!   `installed.toml` global index.
18//! - [`resolver`] — turn a source spec (`owner/repo`, `git+URL`, `path:DIR`)
19//!   into a staged directory ready to install.
20//! - [`commands`] — `znative {add,remove,list,info,load,update}` implementations.
21//! - [`builtin`]  — the `znative` builtin dispatcher (wired from `fusevm_bridge`).
22
23pub mod builtin;
24pub mod commands;
25pub mod manifest;
26pub mod resolver;
27pub mod store;
28
29/// Result alias used throughout the package manager. Errors are stringly-typed
30/// (one user-facing diagnostic per failure path), emitted to stderr as
31/// `znative: <reason>` with exit code 1 — matching the shell's terse error style.
32pub type PkgResult<T> = Result<T, PkgError>;
33
34/// Errors emitted by the package manager. `Display` produces the one-line
35/// reason (no `znative:` prefix — the builtin adds it).
36#[derive(Debug)]
37pub enum PkgError {
38    /// File I/O — read/write/create/copy.
39    Io(String),
40    /// Manifest parse error (bad TOML in a plugin's `znative.toml`).
41    Manifest(String),
42    /// Resolver error — unknown source form, clone/build/download failure.
43    Resolve(String),
44    /// The plugin kind could not be determined (no `znative.toml`, no
45    /// `*.plugin.zsh`, no cdylib/`Cargo.toml`).
46    Unknown(String),
47    /// Generic runtime error.
48    Other(String),
49}
50
51impl std::fmt::Display for PkgError {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        match self {
54            PkgError::Io(s)
55            | PkgError::Manifest(s)
56            | PkgError::Resolve(s)
57            | PkgError::Unknown(s)
58            | PkgError::Other(s) => write!(f, "{}", s),
59        }
60    }
61}
62
63impl From<std::io::Error> for PkgError {
64    fn from(e: std::io::Error) -> Self {
65        PkgError::Io(e.to_string())
66    }
67}
68
69/// Deterministic SHA-256 of a directory tree, `sha256-<hex>`. Ported from
70/// strykelang `pkg/lockfile.rs::integrity_for_directory`. Files are walked in
71/// sorted order so the hash is stable regardless of filesystem iteration; each
72/// file contributes `<relpath>\0F\0<len>\n<bytes>\n`, symlinks their target.
73/// Recorded in the install index for change detection / audit.
74pub fn store_integrity(root: &std::path::Path) -> PkgResult<String> {
75    use sha2::{Digest, Sha256};
76    let mut hasher = Sha256::new();
77    let mut entries: Vec<std::path::PathBuf> = Vec::new();
78    fn walk(
79        root: &std::path::Path,
80        cur: &std::path::Path,
81        out: &mut Vec<std::path::PathBuf>,
82    ) -> PkgResult<()> {
83        for entry in std::fs::read_dir(cur)? {
84            let entry = entry?;
85            let path = entry.path();
86            let meta = entry.metadata()?;
87            if meta.is_dir() && !meta.file_type().is_symlink() {
88                walk(root, &path, out)?;
89            } else {
90                out.push(path.strip_prefix(root).unwrap_or(&path).to_path_buf());
91            }
92        }
93        Ok(())
94    }
95    walk(root, root, &mut entries)?;
96    entries.sort();
97    for rel in &entries {
98        let abs = root.join(rel);
99        let meta = std::fs::symlink_metadata(&abs)?;
100        let rel_s = rel.to_string_lossy();
101        if meta.file_type().is_symlink() {
102            let target = std::fs::read_link(&abs)?;
103            hasher.update(rel_s.as_bytes());
104            hasher.update(b"\0L\0");
105            hasher.update(target.to_string_lossy().as_bytes());
106            hasher.update(b"\n");
107        } else if meta.is_file() {
108            let bytes = std::fs::read(&abs)?;
109            hasher.update(rel_s.as_bytes());
110            hasher.update(b"\0F\0");
111            hasher.update(bytes.len().to_string().as_bytes());
112            hasher.update(b"\n");
113            hasher.update(&bytes);
114            hasher.update(b"\n");
115        }
116    }
117    Ok(format!("sha256-{:x}", hasher.finalize()))
118}