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 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 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}