Skip to main content

stryke/pkg/
store.rs

1//! Global store layout: `~/.stryke/{store,cache,git,bin,index}/`.
2//!
3//! Paths are human-readable (`name@version`) per RFC §"Global Store" — we get
4//! Nix-grade reproducibility from the lockfile's content hashes without
5//! Nix-grade opaque path UX.
6
7use std::path::{Path, PathBuf};
8
9use super::{PkgError, PkgResult};
10
11/// Resolves and (lazily) creates the standard `~/.stryke/...` layout.
12pub struct Store {
13    root: PathBuf,
14}
15
16impl Store {
17    /// Construct a [`Store`] rooted at `~/.stryke/`. Honors the `STRYKE_HOME`
18    /// environment variable for tests and CI sandboxes.
19    pub fn user_default() -> PkgResult<Store> {
20        if let Ok(custom) = std::env::var("STRYKE_HOME") {
21            return Ok(Store {
22                root: PathBuf::from(custom),
23            });
24        }
25        let home = std::env::var("HOME")
26            .map_err(|_| PkgError::Other("HOME environment variable not set".into()))?;
27        Ok(Store {
28            root: PathBuf::from(home).join(".stryke"),
29        })
30    }
31
32    /// Construct a [`Store`] rooted at an explicit path (used by tests).
33    pub fn at(root: impl Into<PathBuf>) -> Store {
34        Store { root: root.into() }
35    }
36
37    pub fn root(&self) -> &Path {
38        &self.root
39    }
40    pub fn store_dir(&self) -> PathBuf {
41        self.root.join("store")
42    }
43    pub fn cache_dir(&self) -> PathBuf {
44        self.root.join("cache")
45    }
46    pub fn git_dir(&self) -> PathBuf {
47        self.root.join("git")
48    }
49    pub fn bin_dir(&self) -> PathBuf {
50        self.root.join("bin")
51    }
52    pub fn index_dir(&self) -> PathBuf {
53        self.root.join("index")
54    }
55
56    /// Path where a package extraction lives: `~/.stryke/store/{name}@{version}/`.
57    pub fn package_dir(&self, name: &str, version: &str) -> PathBuf {
58        self.store_dir().join(format!("{}@{}", name, version))
59    }
60
61    /// Ensure the full directory layout exists. Idempotent. Called eagerly by
62    /// `s install`; tests exercise it directly.
63    pub fn ensure_layout(&self) -> PkgResult<()> {
64        for d in [
65            self.store_dir(),
66            self.cache_dir(),
67            self.git_dir(),
68            self.bin_dir(),
69            self.index_dir(),
70        ] {
71            std::fs::create_dir_all(&d)
72                .map_err(|e| PkgError::Io(format!("create {}: {}", d.display(), e)))?;
73        }
74        Ok(())
75    }
76
77    /// True if a `name@version` extraction already exists in the store.
78    pub fn has_package(&self, name: &str, version: &str) -> bool {
79        self.package_dir(name, version).is_dir()
80    }
81
82    /// Recursively copy a directory tree into the store as `name@version`. Used
83    /// for path deps where the source is a local directory the user maintains.
84    /// Existing destination is removed first so re-installs see fresh content.
85    pub fn install_path_dep(&self, name: &str, version: &str, src: &Path) -> PkgResult<PathBuf> {
86        let dst = self.package_dir(name, version);
87        if dst.exists() {
88            std::fs::remove_dir_all(&dst)
89                .map_err(|e| PkgError::Io(format!("clear {}: {}", dst.display(), e)))?;
90        }
91        std::fs::create_dir_all(&dst)?;
92        copy_dir(src, &dst)?;
93        Ok(dst)
94    }
95}
96
97/// Recursive directory copy. Symlinks are copied as symlinks; files preserve
98/// permissions when the OS supports it.
99fn copy_dir(src: &Path, dst: &Path) -> PkgResult<()> {
100    for entry in std::fs::read_dir(src)? {
101        let entry = entry?;
102        let from = entry.path();
103        let name = entry.file_name();
104        let to = dst.join(&name);
105        let meta = entry.metadata()?;
106        if meta.file_type().is_symlink() {
107            #[cfg(unix)]
108            {
109                let target = std::fs::read_link(&from)?;
110                std::os::unix::fs::symlink(target, &to)
111                    .map_err(|e| PkgError::Io(format!("symlink {}: {}", to.display(), e)))?;
112            }
113            #[cfg(not(unix))]
114            std::fs::copy(&from, &to)
115                .map_err(|e| PkgError::Io(format!("copy {}: {}", from.display(), e)))?;
116        } else if meta.is_dir() {
117            std::fs::create_dir_all(&to)?;
118            copy_dir(&from, &to)?;
119        } else {
120            std::fs::copy(&from, &to)
121                .map_err(|e| PkgError::Io(format!("copy {}: {}", from.display(), e)))?;
122        }
123    }
124    Ok(())
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    fn tempdir() -> PathBuf {
132        let pid = std::process::id();
133        let nanos = std::time::SystemTime::now()
134            .duration_since(std::time::UNIX_EPOCH)
135            .unwrap()
136            .subsec_nanos();
137        let p = std::env::temp_dir().join(format!("stryke-store-test-{}-{}", pid, nanos));
138        let _ = std::fs::remove_dir_all(&p);
139        std::fs::create_dir_all(&p).unwrap();
140        p
141    }
142
143    #[test]
144    fn ensure_layout_creates_subdirs() {
145        let root = tempdir();
146        let s = Store::at(&root);
147        s.ensure_layout().unwrap();
148        assert!(s.store_dir().is_dir());
149        assert!(s.cache_dir().is_dir());
150        assert!(s.git_dir().is_dir());
151        assert!(s.bin_dir().is_dir());
152        assert!(s.index_dir().is_dir());
153    }
154
155    #[test]
156    fn package_dir_path_shape() {
157        let s = Store::at("/x");
158        assert_eq!(
159            s.package_dir("http", "1.0.0"),
160            PathBuf::from("/x/store/http@1.0.0")
161        );
162    }
163
164    #[test]
165    fn install_path_dep_round_trip() {
166        let store_root = tempdir();
167        let src = tempdir();
168        std::fs::create_dir_all(src.join("lib")).unwrap();
169        std::fs::write(src.join("lib/Foo.stk"), b"sub foo { 1 }").unwrap();
170        std::fs::write(
171            src.join("stryke.toml"),
172            "[package]\nname = \"foo\"\nversion = \"0.1.0\"\n",
173        )
174        .unwrap();
175        let s = Store::at(&store_root);
176        s.ensure_layout().unwrap();
177        let dst = s.install_path_dep("foo", "0.1.0", &src).unwrap();
178        assert!(dst.is_dir());
179        assert!(dst.join("lib/Foo.stk").is_file());
180        assert!(dst.join("stryke.toml").is_file());
181    }
182}