1use anyhow::{Result, anyhow};
2use std::env;
3use std::fs;
4use std::path::PathBuf;
5use walkdir::WalkDir;
6use zoi_core::config;
7use zoi_core::types::Scope;
8use zoi_core::utils::{ask_for_confirmation, symlink_file};
9use zoi_db as db;
10use zoi_plugins::PluginManager;
11use zoi_project as project;
12use zoi_resolver::{local, resolve};
13#[cfg(target_os = "linux")]
14use zoi_sandbox as sandbox;
15
16pub fn run_shim(
17 bin_name: &str,
18 args: Vec<String>,
19 plugin_manager: Option<&PluginManager>,
20 auto_install: Option<&dyn Fn(&str, &str) -> Result<()>>,
21) -> Result<()> {
22 let bin_path = resolve_to_installed_bin(bin_name, plugin_manager, auto_install)?;
23
24 #[cfg(target_os = "linux")]
25 {
26 let mut current = bin_path.parent();
27 let mut manifest: Option<zoi_core::types::InstallManifest> = None;
28 let mut pkg_version_dir = None;
29
30 while let Some(path) = current {
31 let mut manifest_path = None;
32 if let Ok(entries) = fs::read_dir(path) {
33 for entry in entries.flatten() {
34 let name = entry.file_name().to_string_lossy().to_string();
35 if name.starts_with("manifest") && name.ends_with(".yaml") {
36 manifest_path = Some(entry.path());
37 break;
38 }
39 }
40 }
41
42 if let Some(mp) = manifest_path
43 && let Ok(content) = fs::read_to_string(mp)
44 && let Ok(m) = serde_yaml::from_str::<zoi_core::types::InstallManifest>(&content)
45 {
46 manifest = Some(m);
47 pkg_version_dir = Some(path.to_path_buf());
48 break;
49 }
50 current = path.parent();
51 }
52
53 if let Some(m) = manifest
54 && let Some(sandbox) = m.sandbox
55 && sandbox.enabled
56 && let Some(version_dir) = pkg_version_dir
57 {
58 let mut cmd = sandbox::wrap_command(&bin_path, &args, &sandbox, &version_dir)?;
59 use std::os::unix::process::CommandExt;
60 let err = cmd.exec();
61 return Err(anyhow!(
62 "Failed to execute sandboxed binary '{}': {}",
63 bin_name,
64 err
65 ));
66 }
67 }
68
69 let mut cmd = std::process::Command::new(bin_path);
70 cmd.args(args);
71
72 #[cfg(unix)]
73 {
74 use std::os::unix::process::CommandExt;
75 let err = cmd.exec();
76 Err(anyhow!("Failed to execute binary '{}': {}", bin_name, err))
77 }
78
79 #[cfg(windows)]
80 {
81 let mut child = cmd.spawn()?;
82 let status = child.wait()?;
83 std::process::exit(status.code().unwrap_or(0));
84 }
85}
86
87pub fn resolve_to_installed_bin(
88 bin_name: &str,
89 plugin_manager: Option<&PluginManager>,
90 auto_install: Option<&dyn Fn(&str, &str) -> Result<()>>,
91) -> Result<PathBuf> {
92 let desired_version = get_desired_version(bin_name, plugin_manager)?;
93
94 let providers = db::find_provides("local", bin_name)?;
95
96 if let Some(version) = &desired_version {
97 for (pkg, _) in &providers {
98 if let Some(path) = search_store_for_version(&pkg.name, version, bin_name)? {
99 return Ok(path);
100 }
101 }
102
103 if let Some(install) = auto_install
104 && ask_for_confirmation(
105 &format!(
106 "Binary '{}' v{} is required but not installed. Install it now?",
107 bin_name, version
108 ),
109 false,
110 )
111 {
112 install(bin_name, version)?;
113 let providers = db::find_provides("local", bin_name)?;
114 for (pkg, _) in &providers {
115 if let Some(path) = search_store_for_version(&pkg.name, version, bin_name)? {
116 return Ok(path);
117 }
118 }
119 }
120 }
121
122 if providers.is_empty() {
123 return Err(anyhow!(
124 "No installed package provides binary '{}'. Run 'zoi provides {}' to find providers.",
125 bin_name,
126 bin_name
127 ));
128 }
129
130 if let Some(version) = &desired_version {
131 for (pkg, _) in &providers {
132 if let Some(path) = search_store_for_version(&pkg.name, version, bin_name)? {
133 return Ok(path);
134 }
135 }
136 }
137
138 let (pkg, _) = &providers[0];
139
140 if let Some(path) = search_store_for_version(&pkg.name, "latest", bin_name)? {
141 return Ok(path);
142 }
143
144 let version = pkg
145 .version
146 .as_deref()
147 .ok_or_else(|| anyhow!("Package '{}' has no version info in DB", pkg.name))?;
148
149 if let Some(path) = search_store_for_version(&pkg.name, version, bin_name)? {
150 return Ok(path);
151 }
152
153 for scope in [Scope::Project, Scope::User, Scope::System] {
154 let store_root = local::get_store_base_dir(scope)?;
155 if !store_root.exists() {
156 continue;
157 }
158
159 for entry in fs::read_dir(store_root)? {
160 let entry = entry?;
161 let path = entry.path();
162 if !path.is_dir() {
163 continue;
164 }
165
166 if let Some(dir_name) = path.file_name().and_then(|s| s.to_str())
167 && dir_name.ends_with(&format!("-{}", pkg.name))
168 {
169 let latest_dir = path.join("latest");
170 if latest_dir.exists()
171 && let Some(p) = find_bin_in_dir(&latest_dir, bin_name)
172 {
173 return Ok(p);
174 }
175 }
176 }
177 }
178
179 Err(anyhow!(
180 "Could not locate binary '{}' in the Zoi store. Try reinstalling the provider package.",
181 bin_name
182 ))
183}
184
185fn find_tool_versions_version(bin_name: &str) -> Result<Option<String>> {
186 let mut current_dir = env::current_dir()?;
187 loop {
188 let tool_versions_path = current_dir.join(".tool-versions");
189 if tool_versions_path.exists() {
190 let content = fs::read_to_string(&tool_versions_path)?;
191 for line in content.lines() {
192 let line = line.trim();
193 if line.is_empty() || line.starts_with('#') {
194 continue;
195 }
196 let parts: Vec<&str> = line.split_whitespace().collect();
197 if parts.len() >= 2 && parts[0] == bin_name {
198 return Ok(Some(parts[1].to_string()));
199 }
200 }
201 }
202 if !current_dir.pop() {
203 break;
204 }
205 }
206 Ok(None)
207}
208
209fn get_desired_version(
210 bin_name: &str,
211 plugin_manager: Option<&PluginManager>,
212) -> Result<Option<String>> {
213 let env_var_name = format!("ZOI_{}_VERSION", bin_name.to_uppercase().replace('-', "_"));
214 if let Ok(v) = env::var(&env_var_name) {
215 return Ok(Some(v));
216 }
217
218 if let Some(pm) = plugin_manager
219 && let Ok(Some(v)) = pm.trigger_resolve_shim_version(bin_name)
220 {
221 return Ok(Some(v));
222 }
223
224 if let Ok(project_cfg) = project::config::load() {
225 for pkg_spec in project_cfg.pkgs {
226 if let Ok(req) = resolve::parse_source_string(&pkg_spec) {
227 let is_match = req.name == bin_name || {
228 if let Ok(providers) = db::find_provides("local", bin_name) {
229 providers.iter().any(|(p, _)| p.name == req.name)
230 } else {
231 false
232 }
233 };
234
235 if is_match && let Some(v) = req.version_spec {
236 return Ok(Some(v));
237 }
238 }
239 }
240 }
241
242 if let Ok(Some(v)) = find_tool_versions_version(bin_name) {
243 return Ok(Some(v));
244 }
245
246 let cfg = config::read_config()?;
247 if let Some(v) = cfg.versions.get(bin_name) {
248 return Ok(Some(v.clone()));
249 }
250
251 Ok(None)
252}
253
254fn search_store_for_version(
255 pkg_name: &str,
256 version: &str,
257 bin_name: &str,
258) -> Result<Option<PathBuf>> {
259 for scope in [Scope::Project, Scope::User, Scope::System] {
260 let store_root = local::get_store_base_dir(scope)?;
261 if !store_root.exists() {
262 continue;
263 }
264
265 for entry in fs::read_dir(store_root)? {
266 let entry = entry?;
267 let path = entry.path();
268 if !path.is_dir() {
269 continue;
270 }
271
272 if let Some(dir_name) = path.file_name().and_then(|s| s.to_str())
273 && dir_name.ends_with(&format!("-{}", pkg_name))
274 {
275 let latest_dir = path.join("latest");
276 if latest_dir.exists()
277 && (version == "latest" || version.is_empty())
278 && let Some(p) = find_bin_in_dir(&latest_dir, bin_name)
279 {
280 return Ok(Some(p));
281 }
282
283 if version != "latest" && !version.is_empty() {
284 let version_dir = path.join(version);
285 if version_dir.exists()
286 && let Some(p) = find_bin_in_dir(&version_dir, bin_name)
287 {
288 return Ok(Some(p));
289 }
290
291 for v_entry in fs::read_dir(&path)? {
292 let v_entry = v_entry?;
293 let v_name = v_entry.file_name().to_string_lossy().to_string();
294 if v_name.starts_with(version)
295 && v_name != "latest"
296 && v_name != "dependents"
297 {
298 let v_dir = path.join(v_name);
299 if let Some(p) = find_bin_in_dir(&v_dir, bin_name) {
300 return Ok(Some(p));
301 }
302 }
303 }
304 }
305 }
306 }
307 }
308 Ok(None)
309}
310
311fn find_bin_in_dir(dir: &std::path::Path, bin_name: &str) -> Option<PathBuf> {
312 let bin_path = dir.join("bin").join(bin_name);
313 if bin_path.exists() {
314 return Some(bin_path);
315 }
316
317 for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
318 if entry.file_type().is_file() && entry.file_name().to_string_lossy() == bin_name {
319 return Some(entry.path().to_path_buf());
320 }
321 }
322 None
323}
324
325pub fn create_shim(link_path: &std::path::Path) -> Result<()> {
326 let zoi_exe = env::current_exe()?;
327 symlink_file(&zoi_exe, link_path).map_err(|e| anyhow!("Failed to create shim: {}", e))
328}