Skip to main content

vs_installer/
lib.rs

1//! Transactional runtime installs for `vs`.
2
3mod fs;
4mod install;
5mod receipt;
6
7use std::path::PathBuf;
8
9use thiserror::Error;
10
11pub use install::Installer;
12pub use receipt::InstallReceipt;
13
14/// Errors returned by installer services.
15#[derive(Debug, Error)]
16pub enum InstallerError {
17    /// An I/O operation failed.
18    #[error(transparent)]
19    Io(#[from] std::io::Error),
20    /// A download failed.
21    #[error("artifact download failed: {0}")]
22    Download(String),
23    /// A directory walk failed.
24    #[error("failed to walk directory tree: {0}")]
25    Walk(String),
26    /// The source directory was not found.
27    #[error("install source does not exist: {0}")]
28    MissingSource(PathBuf),
29    /// JSON data could not be parsed.
30    #[error("failed to parse JSON file at {path}: {message}")]
31    Json { path: PathBuf, message: String },
32    /// The install validation step failed.
33    #[error("install validation failed: {0}")]
34    Validation(String),
35    /// An archive could not be unpacked.
36    #[error(transparent)]
37    Archive(#[from] zip::result::ZipError),
38}
39
40#[cfg(test)]
41mod tests {
42    use std::error::Error;
43    use std::fs;
44
45    use tempfile::TempDir;
46    use vs_plugin_api::{InstallArtifact, InstallPlan, InstallSource};
47    use zip::ZipWriter;
48    use zip::write::SimpleFileOptions;
49
50    use super::Installer;
51
52    #[test]
53    fn install_should_rollback_when_validation_fails() -> Result<(), Box<dyn Error>> {
54        let temp_dir = TempDir::new()?;
55        let source = temp_dir.path().join("source");
56        fs::create_dir_all(&source)?;
57        fs::write(source.join(".vs-fail-install"), "")?;
58
59        let installer = Installer::new(temp_dir.path().join("home"));
60        let plan = InstallPlan {
61            plugin: String::from("nodejs"),
62            version: String::from("20.11.1"),
63            main: InstallArtifact {
64                name: String::from("nodejs"),
65                version: String::from("20.11.1"),
66                source: InstallSource::Directory { path: source },
67                note: None,
68                checksum: None,
69            },
70            additions: Vec::new(),
71            legacy_filenames: Vec::new(),
72        };
73
74        let error = match installer.install(&plan) {
75            Ok(_) => {
76                return Err(Box::new(std::io::Error::other(
77                    "install unexpectedly succeeded",
78                )));
79            }
80            Err(error) => error,
81        };
82        assert!(error.to_string().contains("validation failed"));
83        assert!(!installer.install_dir("nodejs", "20.11.1").exists());
84        Ok(())
85    }
86
87    #[test]
88    fn install_should_preserve_flat_archive_layouts() -> Result<(), Box<dyn Error>> {
89        let temp_dir = TempDir::new()?;
90        let archive = write_zip(
91            &temp_dir,
92            "flat.zip",
93            &[("bin/node", b"#!/bin/sh\necho node\n".as_slice())],
94        )?;
95
96        let installer = Installer::new(temp_dir.path().join("home"));
97        let plan = install_plan_from_archive(&archive);
98        let installed = installer.install(&plan)?;
99
100        assert!(installed.main.path.join("bin/node").exists());
101        Ok(())
102    }
103
104    #[test]
105    fn install_should_collapse_single_wrapped_archive_root() -> Result<(), Box<dyn Error>> {
106        let temp_dir = TempDir::new()?;
107        let archive = write_zip(
108            &temp_dir,
109            "wrapped.zip",
110            &[("package/bin/node", b"#!/bin/sh\necho node\n".as_slice())],
111        )?;
112
113        let installer = Installer::new(temp_dir.path().join("home"));
114        let plan = install_plan_from_archive(&archive);
115        let installed = installer.install(&plan)?;
116
117        assert!(installed.main.path.join("bin/node").exists());
118        assert!(!installed.main.path.join("package").exists());
119        Ok(())
120    }
121
122    fn install_plan_from_archive(path: &std::path::Path) -> InstallPlan {
123        InstallPlan {
124            plugin: String::from("nodejs"),
125            version: String::from("20.11.1"),
126            main: InstallArtifact {
127                name: String::from("nodejs"),
128                version: String::from("20.11.1"),
129                source: InstallSource::File {
130                    path: path.to_path_buf(),
131                },
132                note: None,
133                checksum: None,
134            },
135            additions: Vec::new(),
136            legacy_filenames: Vec::new(),
137        }
138    }
139
140    fn write_zip(
141        temp_dir: &TempDir,
142        file_name: &str,
143        entries: &[(&str, &[u8])],
144    ) -> Result<std::path::PathBuf, Box<dyn Error>> {
145        let path = temp_dir.path().join(file_name);
146        let file = fs::File::create(&path)?;
147        let mut zip = ZipWriter::new(file);
148        let options = SimpleFileOptions::default();
149
150        for (name, contents) in entries {
151            zip.start_file(name, options)?;
152            std::io::Write::write_all(&mut zip, contents)?;
153        }
154
155        zip.finish()?;
156        Ok(path)
157    }
158}