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}