Skip to main content

slox_core/
lib.rs

1mod activate;
2mod env;
3mod package;
4mod store;
5mod term;
6
7use slox_cli::{Commands, EnvCommand, PkgCommand};
8use store::StorePaths;
9
10pub fn report_error(error: &str) {
11    term::report_error(error);
12}
13
14pub fn run(command: Commands) -> Result<(), String> {
15    run_with_store(command, &StorePaths::default())
16}
17
18fn run_with_store(command: Commands, store: &StorePaths) -> Result<(), String> {
19    match command {
20        Commands::Env { command } => handle_env(store, command),
21        Commands::Activate { command } => {
22            println!("{}", activate::activation_script(store, command)?);
23            Ok(())
24        }
25        Commands::Pkg { command } => handle_pkg(store, command),
26    }
27}
28
29fn handle_env(store: &StorePaths, cmd: EnvCommand) -> Result<(), String> {
30    match cmd {
31        EnvCommand::Add { path } => env::add_env(store, &path),
32        EnvCommand::Remove { path } => env::remove_env(store, &path),
33        EnvCommand::Set { path } => env::set_env(store, &path),
34    }
35}
36
37fn handle_pkg(store: &StorePaths, cmd: PkgCommand) -> Result<(), String> {
38    match cmd {
39        PkgCommand::Add { path } => package::download(store, &package::parse_pkg(&path)?),
40        PkgCommand::Remove { path } => {
41            term::print_status(
42                "WARN",
43                term::YELLOW,
44                &format!("Package removal is not implemented yet for `{path}`."),
45            );
46            Ok(())
47        }
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::{
54        activate::activation_script,
55        env::{active_bin_dir, active_env_name, add_env, remove_env, set_env},
56        package::{Pkg, install_built_binary, parse_pkg, run_build_script},
57        store::StorePaths,
58    };
59    use crate::activate;
60    use slox_cli::ActivateCommand;
61    use std::fs;
62    use std::path::PathBuf;
63    use std::time::{SystemTime, UNIX_EPOCH};
64
65    fn make_temp_dir(name: &str) -> PathBuf {
66        let unique = SystemTime::now()
67            .duration_since(UNIX_EPOCH)
68            .expect("time went backwards")
69            .as_nanos();
70        let dir = std::env::temp_dir().join(format!("slox-{name}-{unique}"));
71        fs::create_dir_all(&dir).expect("failed to create temp dir");
72        dir
73    }
74
75    fn make_store() -> StorePaths {
76        StorePaths {
77            base: make_temp_dir("store"),
78        }
79    }
80
81    #[test]
82    fn parse_github_package() {
83        let pkg = parse_pkg("devwon/slox@github").expect("github package should parse");
84        match pkg {
85            Pkg::GitHub { user, repo } => {
86                assert_eq!(user, "devwon");
87                assert_eq!(repo, "slox");
88            }
89            Pkg::Regit { .. } => panic!("expected github package"),
90        }
91    }
92
93    #[test]
94    fn parse_regit_package() {
95        let pkg = parse_pkg("plur@sloxpkgs").expect("sloxpkgs package should parse");
96        match pkg {
97            Pkg::Regit { pkg } => assert_eq!(pkg, "plur"),
98            Pkg::GitHub { .. } => panic!("expected sloxpkgs package"),
99        }
100    }
101
102    #[test]
103    fn reject_invalid_package_format() {
104        let error = parse_pkg("invalid-package").expect_err("package parse should fail");
105        assert!(error.contains("user/repo@github"));
106    }
107
108    #[test]
109    fn activation_script_uses_root_bin_path() {
110        let store = StorePaths {
111            base: PathBuf::from("/tmp/slox-store"),
112        };
113
114        let script = activation_script(&store, ActivateCommand::Zsh)
115            .expect("root activation script should render");
116        assert_eq!(
117            script,
118            concat!(
119                "_slox_store='/tmp/slox-store'\n",
120                "_slox_bin='/tmp/slox-store/bin'\n",
121                "_slox_filtered_path=\n",
122                "_slox_old_ifs=$IFS\n",
123                "IFS=:\n",
124                "for _slox_entry in $PATH; do\n",
125                "  case \"$_slox_entry\" in\n",
126                "    \"$_slox_store\"/bin|\"$_slox_store\"/bin/)\n",
127                "      ;;\n",
128                "    '')\n",
129                "      ;;\n",
130                "    *)\n",
131                "      if [ -n \"$_slox_filtered_path\" ]; then\n",
132                "        _slox_filtered_path=\"$_slox_filtered_path:$_slox_entry\"\n",
133                "      else\n",
134                "        _slox_filtered_path=\"$_slox_entry\"\n",
135                "      fi\n",
136                "      ;;\n",
137                "  esac\n",
138                "done\n",
139                "IFS=$_slox_old_ifs\n",
140                "if [ -n \"$_slox_filtered_path\" ]; then\n",
141                "  export PATH=\"$_slox_bin:$_slox_filtered_path\"\n",
142                "else\n",
143                "  export PATH=\"$_slox_bin\"\n",
144                "fi\n",
145                "unset _slox_store _slox_bin _slox_filtered_path _slox_old_ifs _slox_entry"
146            )
147        );
148    }
149
150    #[test]
151    fn add_and_remove_env_under_store() {
152        let store = make_store();
153        let env_dir = store.env_dir("demo");
154
155        add_env(&store, "demo").expect("env should be created");
156        assert!(env_dir.is_dir());
157        assert!(store.env_bin_dir("demo").is_dir());
158
159        remove_env(&store, "demo").expect("env should be removed");
160        assert!(!env_dir.exists());
161
162        fs::remove_dir_all(store.base).expect("failed to clean store dir");
163    }
164
165    #[test]
166    fn set_env_changes_active_bin_dir_and_activation_script() {
167        let store = make_store();
168        add_env(&store, "demo").expect("env should be created");
169
170        set_env(&store, "demo").expect("env should be set");
171        assert_eq!(
172            active_env_name(&store).expect("active env should be readable"),
173            Some("demo".to_string())
174        );
175        assert_eq!(
176            active_bin_dir(&store).expect("active bin dir should resolve"),
177            store.env_bin_dir("demo")
178        );
179
180        let script = activation_script(&store, ActivateCommand::Bash)
181            .expect("env activation script should render");
182        assert_eq!(
183            script,
184            format!(
185                concat!(
186                    "_slox_store={store}\n",
187                    "_slox_bin={bin}\n",
188                    "_slox_filtered_path=\n",
189                    "_slox_old_ifs=$IFS\n",
190                    "IFS=:\n",
191                    "for _slox_entry in $PATH; do\n",
192                    "  case \"$_slox_entry\" in\n",
193                    "    \"$_slox_store\"/bin|\"$_slox_store\"/bin/)\n",
194                    "      ;;\n",
195                    "    '')\n",
196                    "      ;;\n",
197                    "    *)\n",
198                    "      if [ -n \"$_slox_filtered_path\" ]; then\n",
199                    "        _slox_filtered_path=\"$_slox_filtered_path:$_slox_entry\"\n",
200                    "      else\n",
201                    "        _slox_filtered_path=\"$_slox_entry\"\n",
202                    "      fi\n",
203                    "      ;;\n",
204                    "  esac\n",
205                    "done\n",
206                    "IFS=$_slox_old_ifs\n",
207                    "if [ -n \"$_slox_filtered_path\" ]; then\n",
208                    "  export PATH=\"$_slox_bin:$_slox_filtered_path\"\n",
209                    "else\n",
210                    "  export PATH=\"$_slox_bin\"\n",
211                    "fi\n",
212                    "unset _slox_store _slox_bin _slox_filtered_path _slox_old_ifs _slox_entry"
213                ),
214                store = activate::shell_single_quote(&store.base.display().to_string()),
215                bin = activate::shell_single_quote(&store.shim_bin_dir().display().to_string())
216            )
217        );
218
219        fs::remove_dir_all(store.base).expect("failed to clean store dir");
220    }
221
222    #[test]
223    fn removing_active_env_clears_active_env_file() {
224        let store = make_store();
225        add_env(&store, "demo").expect("env should be created");
226        set_env(&store, "demo").expect("env should be set");
227
228        remove_env(&store, "demo").expect("env should be removed");
229        assert_eq!(
230            active_env_name(&store).expect("active env should be readable"),
231            None
232        );
233        assert_eq!(
234            active_bin_dir(&store).expect("root bin should be used"),
235            store.root_bin_dir()
236        );
237
238        fs::remove_dir_all(store.base).expect("failed to clean store dir");
239    }
240
241    #[test]
242    fn build_and_install_github_package_layout() {
243        let repo_name = "demo";
244        let repo_dir = make_temp_dir("repo");
245        let root_bin_dir = make_temp_dir("root-bin");
246
247        fs::write(
248            repo_dir.join("build"),
249            format!(
250                "#!/bin/sh\nmkdir -p bin\nprintf '#!/bin/sh\\necho installed\\n' > bin/{repo_name}\nchmod +x bin/{repo_name}\n"
251            ),
252        )
253        .expect("failed to write build script");
254
255        run_build_script(&repo_dir).expect("build script should succeed");
256        let installed_binary = install_built_binary(&repo_dir, repo_name, &root_bin_dir)
257            .expect("install should succeed");
258
259        assert!(installed_binary.is_file());
260        let contents =
261            fs::read_to_string(installed_binary).expect("failed to read installed binary");
262        assert!(contents.contains("installed"));
263
264        fs::remove_dir_all(repo_dir).expect("failed to clean repo dir");
265        fs::remove_dir_all(root_bin_dir).expect("failed to clean root bin dir");
266    }
267
268    #[test]
269    fn install_target_follows_active_env_bin() {
270        let store = make_store();
271        let repo_name = "demo";
272        let repo_dir = make_temp_dir("repo");
273
274        add_env(&store, "demo-env").expect("env should be created");
275        set_env(&store, "demo-env").expect("env should be set");
276
277        fs::write(
278            repo_dir.join("build"),
279            format!(
280                "#!/bin/sh\nmkdir -p bin\nprintf '#!/bin/sh\\necho active-env\\n' > bin/{repo_name}\nchmod +x bin/{repo_name}\n"
281            ),
282        )
283        .expect("failed to write build script");
284
285        run_build_script(&repo_dir).expect("build script should succeed");
286        let install_dir = active_bin_dir(&store).expect("active bin dir should resolve");
287        let installed_binary = install_built_binary(&repo_dir, repo_name, &install_dir)
288            .expect("install should succeed");
289
290        assert_eq!(
291            installed_binary,
292            store.env_bin_dir("demo-env").join(repo_name)
293        );
294        assert!(installed_binary.is_file());
295
296        fs::remove_dir_all(repo_dir).expect("failed to clean repo dir");
297        fs::remove_dir_all(store.base).expect("failed to clean store dir");
298    }
299
300    #[test]
301    fn sync_active_shims_points_to_active_env_bins() {
302        let store = make_store();
303        add_env(&store, "demo").expect("env should be created");
304
305        fs::create_dir_all(store.env_bin_dir("demo")).expect("env bin dir should exist");
306        fs::write(
307            store.env_bin_dir("demo").join("demo"),
308            "#!/bin/sh\necho env\n",
309        )
310        .expect("binary should be written");
311
312        set_env(&store, "demo").expect("env should be set");
313
314        let shim_path = store.shim_bin_dir().join("demo");
315        assert!(shim_path.is_file());
316        let shim_contents = fs::read_to_string(&shim_path).expect("shim should be readable");
317        assert!(
318            shim_contents.contains(&store.env_bin_dir("demo").join("demo").display().to_string())
319        );
320
321        fs::remove_dir_all(store.base).expect("failed to clean store dir");
322    }
323
324    #[test]
325    fn clearing_active_env_refreshes_shims_to_root_bin() {
326        let store = make_store();
327        add_env(&store, "demo").expect("env should be created");
328        fs::create_dir_all(store.root_bin_dir()).expect("root bin dir should exist");
329        fs::write(
330            store.root_bin_dir().join("root-tool"),
331            "#!/bin/sh\necho root\n",
332        )
333        .expect("root binary should be written");
334        fs::write(
335            store.env_bin_dir("demo").join("env-tool"),
336            "#!/bin/sh\necho env\n",
337        )
338        .expect("env binary should be written");
339
340        set_env(&store, "demo").expect("env should be set");
341        remove_env(&store, "demo").expect("env should be removed");
342
343        let shim_dir = store.shim_bin_dir();
344        assert!(shim_dir.join("root-tool").is_file());
345        assert!(!shim_dir.join("env-tool").exists());
346
347        fs::remove_dir_all(store.base).expect("failed to clean store dir");
348    }
349}