Skip to main content

vs_core/service/
use_tool.rs

1use vs_plugin_api::InstalledRuntime;
2use vs_shell::{global_current_dir, link_directory, project_sdk_dir};
3
4use crate::{App, CoreError, InstalledVersion, UseScope};
5
6impl App {
7    /// Activates an installed tool version for a given scope.
8    pub fn use_tool(
9        &self,
10        plugin_name: &str,
11        version: &str,
12        scope: UseScope,
13        unlink: bool,
14    ) -> Result<InstalledVersion, CoreError> {
15        let entry = self.resolve_registry_entry(plugin_name)?;
16        let plugin = self.load_plugin(&entry)?;
17        let previous_version = self
18            .current_tool(plugin_name)?
19            .map(|current| current.version);
20        let installed_runtimes = self.load_installed_runtimes(plugin_name)?;
21
22        let requested_version =
23            self.resolve_requested_use_version(&*plugin, plugin_name, version)?;
24        let resolved_version = plugin
25            .pre_use(
26                &requested_version,
27                scope.as_str(),
28                &self.cwd,
29                previous_version.as_deref(),
30                &installed_runtimes,
31            )?
32            .unwrap_or(requested_version);
33        let runtime = self
34            .load_installed_runtime(plugin_name, &resolved_version)?
35            .ok_or_else(|| {
36                CoreError::Unsupported(format!(
37                    "{plugin_name}@{resolved_version} is not installed. Please run `vs install {plugin_name}@{resolved_version}` first"
38                ))
39            })?;
40        let installed = InstalledVersion {
41            plugin: plugin_name.to_string(),
42            version: runtime.version.clone(),
43            install_dir: runtime.root_dir.clone(),
44        };
45
46        match scope {
47            UseScope::Global => {
48                self.write_tool_assignment(
49                    &vs_config::global_tools_file(self.home()),
50                    plugin_name,
51                    Some(&runtime.version),
52                )?;
53                link_directory(
54                    &installed.install_dir,
55                    &global_current_dir(self.home(), plugin_name),
56                )?;
57            }
58            UseScope::Project => {
59                self.write_tool_assignment(
60                    &self.preferred_project_file(),
61                    plugin_name,
62                    Some(&runtime.version),
63                )?;
64                if !unlink {
65                    link_directory(
66                        &installed.install_dir,
67                        &project_sdk_dir(&self.cwd, plugin_name),
68                    )?;
69                }
70            }
71            UseScope::Session => {
72                let session_file = self.session_file()?;
73                self.write_tool_assignment(&session_file, plugin_name, Some(&runtime.version))?;
74            }
75        }
76        Ok(installed)
77    }
78
79    /// Returns installed versions for a single plugin, sorted from newest-looking to oldest-looking.
80    pub fn installed_versions_for_plugin(
81        &self,
82        plugin_name: &str,
83    ) -> Result<Vec<InstalledVersion>, CoreError> {
84        let mut installed = self
85            .list_installed_versions()?
86            .into_iter()
87            .filter(|installed| installed.plugin == plugin_name)
88            .collect::<Vec<_>>();
89        installed.sort_by(|left, right| right.version.cmp(&left.version));
90        Ok(installed)
91    }
92
93    fn load_installed_runtimes(
94        &self,
95        plugin_name: &str,
96    ) -> Result<Vec<InstalledRuntime>, CoreError> {
97        let mut runtimes = Vec::new();
98        for installed in self.installed_versions_for_plugin(plugin_name)? {
99            if let Some(runtime) = self.load_installed_runtime(plugin_name, &installed.version)? {
100                runtimes.push(runtime);
101            }
102        }
103        Ok(runtimes)
104    }
105
106    fn resolve_requested_use_version(
107        &self,
108        plugin: &dyn vs_plugin_api::Plugin,
109        plugin_name: &str,
110        version: &str,
111    ) -> Result<String, CoreError> {
112        if version != "latest" {
113            return Ok(version.to_string());
114        }
115
116        plugin
117            .available_versions(&[])?
118            .into_iter()
119            .next()
120            .map(|available| available.version)
121            .ok_or_else(|| {
122                CoreError::Unsupported(format!(
123                    "plugin {plugin_name} does not expose any available versions"
124                ))
125            })
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use std::error::Error;
132    use std::fs;
133
134    use tempfile::TempDir;
135    use vs_config::HomeLayout;
136    use vs_plugin_api::PluginBackendKind;
137
138    use crate::{App, UseScope};
139
140    #[cfg(feature = "lua")]
141    #[test]
142    fn use_tool_should_apply_pre_use_resolution() -> Result<(), Box<dyn Error>> {
143        let temp_dir = TempDir::new()?;
144        let home = temp_dir.path().join("home");
145        let cwd = temp_dir.path().join("project");
146        fs::create_dir_all(&cwd)?;
147
148        let app = App::new(
149            HomeLayout {
150                active_home: home,
151                migration_candidates: Vec::new(),
152            },
153            cwd.clone(),
154            Some(String::from("session")),
155        )?;
156
157        let source = temp_dir.path().join("nodejs-lua");
158        write_pre_use_fixture(&source)?;
159        app.add_plugin(
160            Some("nodejs"),
161            Some(source.display().to_string()),
162            Some(PluginBackendKind::Lua),
163            None,
164        )?;
165        app.install_plugin_version("nodejs", Some("20.11.1"))?;
166
167        let installed = app.use_tool("nodejs", "lts", UseScope::Project, false)?;
168
169        assert_eq!(installed.version, "20.11.1");
170        let config = fs::read_to_string(cwd.join(".vs.toml"))?;
171        assert!(config.contains("nodejs = \"20.11.1\""));
172        Ok(())
173    }
174
175    #[cfg(feature = "lua")]
176    #[test]
177    fn use_tool_should_fail_when_requested_version_is_not_installed() -> Result<(), Box<dyn Error>>
178    {
179        let temp_dir = TempDir::new()?;
180        let home = temp_dir.path().join("home");
181        let cwd = temp_dir.path().join("project");
182        fs::create_dir_all(&cwd)?;
183
184        let app = App::new(
185            HomeLayout {
186                active_home: home,
187                migration_candidates: Vec::new(),
188            },
189            cwd,
190            Some(String::from("session")),
191        )?;
192
193        let source = temp_dir.path().join("nodejs-lua");
194        write_pre_use_fixture(&source)?;
195        app.add_plugin(
196            Some("nodejs"),
197            Some(source.display().to_string()),
198            Some(PluginBackendKind::Lua),
199            None,
200        )?;
201
202        let error = match app.use_tool("nodejs", "20.11.1", UseScope::Project, false) {
203            Ok(_) => {
204                return Err(Box::new(std::io::Error::other(
205                    "use should fail without a matching installed runtime",
206                )));
207            }
208            Err(error) => error,
209        };
210        assert!(
211            error
212                .to_string()
213                .contains("Please run `vs install nodejs@20.11.1` first")
214        );
215        Ok(())
216    }
217
218    #[cfg(feature = "lua")]
219    fn write_pre_use_fixture(root: &std::path::Path) -> Result<(), Box<dyn Error>> {
220        fs::create_dir_all(root.join("hooks"))?;
221        fs::create_dir_all(root.join("packages/20.11.1/bin"))?;
222        fs::write(
223            root.join("metadata.lua"),
224            "PLUGIN = {}\nPLUGIN.name = 'nodejs'\nPLUGIN.version = '0.1.0'\n",
225        )?;
226        fs::write(
227            root.join("hooks/pre_install.lua"),
228            "function PLUGIN:PreInstall(ctx)\n  return { version = '20.11.1', url = 'packages/20.11.1' }\nend\n",
229        )?;
230        fs::write(
231            root.join("hooks/available.lua"),
232            "function PLUGIN:Available(ctx)\n  return { { version = '20.11.1' } }\nend\n",
233        )?;
234        fs::write(
235            root.join("hooks/env_keys.lua"),
236            "function PLUGIN:EnvKeys(ctx)\n  return { { key = 'PATH', value = ctx.path .. '/bin' } }\nend\n",
237        )?;
238        fs::write(
239            root.join("hooks/pre_use.lua"),
240            "function PLUGIN:PreUse(ctx)\n  if ctx.version == 'lts' then\n    return { version = '20.11.1' }\n  end\n  return { version = ctx.version }\nend\n",
241        )?;
242        Ok(())
243    }
244}