1use crate::pkg::{cache, local, resolve, types};
2use crate::utils;
3use anyhow::{Result, anyhow};
4use colored::*;
5use std::fs;
6use std::io::Cursor;
7use std::path::PathBuf;
8use std::process::Command;
9use tar::Archive;
10use zstd::stream::read::Decoder as ZstdDecoder;
11
12fn ensure_binary_is_cached(pkg: &types::Package, upstream: bool) -> Result<PathBuf> {
13 let cache_dir = cache::get_cache_root()?;
14 let binary_filename = if cfg!(target_os = "windows") {
15 format!("{}.exe", pkg.name)
16 } else {
17 pkg.name.clone()
18 };
19 let bin_path = cache_dir.join(&binary_filename);
20
21 if upstream && bin_path.exists() {
22 fs::remove_file(&bin_path)?;
23 }
24
25 if bin_path.exists() {
26 println!("Using cached binary for '{}'.", pkg.name.cyan());
27 return Ok(bin_path);
28 }
29
30 if !pkg.types.contains(&"pre-compiled".to_string()) {
31 return Err(anyhow!(
32 "zoi exec only works with 'pre-compiled' package types."
33 ));
34 }
35
36 println!(
37 "No cached binary found for '{}'. Downloading pre-built package...",
38 pkg.name.cyan()
39 );
40 fs::create_dir_all(&cache_dir)?;
41
42 let db_path = resolve::get_db_root()?;
43 let config = crate::pkg::config::read_config()?;
44 let repo_config = if let Some(handle) = config.default_registry.as_ref().map(|r| &r.handle) {
45 crate::pkg::config::read_repo_config(&db_path.join(handle)).ok()
46 } else {
47 None
48 };
49
50 if let Some(repo_config) = repo_config {
51 let mut pkg_links_to_try = Vec::new();
52 if let Some(main_pkg) = repo_config.pkg.iter().find(|p| p.link_type == "main") {
53 pkg_links_to_try.push(main_pkg.clone());
54 }
55 pkg_links_to_try.extend(
56 repo_config
57 .pkg
58 .iter()
59 .filter(|p| p.link_type == "mirror")
60 .cloned(),
61 );
62
63 for pkg_link in pkg_links_to_try {
64 let platform = utils::get_platform()?;
65 let (os, arch) = (
66 platform.split('-').next().unwrap_or_default(),
67 platform.split('-').nth(1).unwrap_or_default(),
68 );
69 let url_dir = pkg_link
70 .url
71 .replace("{os}", os)
72 .replace("{arch}", arch)
73 .replace("{version}", pkg.version.as_deref().unwrap_or_default())
74 .replace("{repo}", &pkg.repo);
75
76 let archive_filename = format!("{}.pkg.tar.zst", pkg.name);
77 let final_url = format!("{}/{}", url_dir.trim_end_matches('/'), archive_filename);
78
79 println!(
80 "Attempting to download pre-built package from: {}",
81 final_url.cyan()
82 );
83
84 let temp_dir = tempfile::Builder::new().prefix("zoi-exec-dl-").tempdir()?;
85 let temp_archive_path = temp_dir.path().join(&archive_filename);
86
87 if crate::pkg::install::util::download_file_with_progress(
88 &final_url,
89 &temp_archive_path,
90 None,
91 None,
92 )
93 .is_ok()
94 {
95 let downloaded_data = fs::read(&temp_archive_path)?;
96 let temp_ext_dir = tempfile::Builder::new().prefix("zoi-exec-ext").tempdir()?;
97 let mut archive = Archive::new(ZstdDecoder::new(Cursor::new(downloaded_data))?);
98 archive.unpack(temp_ext_dir.path())?;
99
100 let bin_dir_in_archive = temp_ext_dir.path().join("data/pkgstore/bin");
101 if bin_dir_in_archive.exists()
102 && let Some(bin_name) = &pkg.bins.as_ref().and_then(|b| b.first())
103 {
104 let bin_in_archive = bin_dir_in_archive.join(bin_name);
105 if bin_in_archive.exists() {
106 let final_bin_path = cache_dir.join(bin_name);
107 fs::copy(&bin_in_archive, &final_bin_path)?;
108 #[cfg(unix)]
109 {
110 use std::os::unix::fs::PermissionsExt;
111 fs::set_permissions(
112 &final_bin_path,
113 fs::Permissions::from_mode(0o755),
114 )?;
115 }
116 println!("Binary cached successfully.");
117 return Ok(final_bin_path);
118 }
119 }
120 }
121 }
122 }
123
124 Err(anyhow!("Could not download pre-built package for exec."))
125}
126
127fn find_executable(
128 pkg: &types::Package,
129 upstream: bool,
130 cache_only: bool,
131 local_only: bool,
132 registry_handle: Option<&str>,
133) -> Result<PathBuf> {
134 let handle = registry_handle.unwrap_or("local");
135
136 if upstream {
137 return ensure_binary_is_cached(pkg, true);
138 }
139
140 let scopes_to_check = if local_only {
141 vec![types::Scope::Project]
142 } else {
143 vec![
144 types::Scope::Project,
145 types::Scope::User,
146 types::Scope::System,
147 ]
148 };
149
150 for scope in scopes_to_check {
151 if let Ok(package_dir) = local::get_package_dir(scope, handle, &pkg.repo, &pkg.name) {
152 let latest_path = package_dir.join("latest");
153 if latest_path.exists() {
154 let binary_filename = if cfg!(target_os = "windows") {
155 format!("{}.exe", pkg.name)
156 } else {
157 pkg.name.clone()
158 };
159 let bin_path = latest_path.join("bin").join(binary_filename);
160 if bin_path.exists() {
161 let scope_str = match scope {
162 types::Scope::Project => "project-local",
163 types::Scope::User => "user",
164 types::Scope::System => "system",
165 };
166 println!("Using {} binary for '{}'.", scope_str, pkg.name.cyan());
167 return Ok(bin_path);
168 }
169 }
170 }
171 }
172
173 if local_only {
174 return Err(anyhow!("No local project binary found."));
175 }
176
177 if cache_only {
178 let cache_dir = cache::get_cache_root()?;
179 let binary_filename = if cfg!(target_os = "windows") {
180 format!("{}.exe", pkg.name)
181 } else {
182 pkg.name.clone()
183 };
184 let bin_path = cache_dir.join(&binary_filename);
185 if bin_path.exists() {
186 println!("Using cached binary for '{}'.", pkg.name.cyan());
187 return Ok(bin_path);
188 }
189 return Err(anyhow!("No cached binary found."));
190 }
191
192 ensure_binary_is_cached(pkg, false)
193}
194
195pub fn run(
196 source: &str,
197 args: Vec<String>,
198 upstream: bool,
199 cache_only: bool,
200 local_only: bool,
201) -> Result<i32> {
202 let resolved_source = resolve::resolve_source(source, false)?;
203
204 if let Some(repo_name) = &resolved_source.repo_name {
205 utils::print_repo_warning(repo_name);
206 }
207
208 let mut pkg: types::Package = crate::pkg::lua::parser::parse_lua_package(
209 resolved_source.path.to_str().unwrap(),
210 None,
211 false,
212 )?;
213
214 if let Some(repo_name) = resolved_source.repo_name.clone() {
215 pkg.repo = repo_name;
216 }
217
218 if pkg.package_type == types::PackageType::App {
219 return Err(anyhow!(
220 "This package is an 'app' template. Use 'zoi create <pkg> <appName>' to create an app from it."
221 ));
222 }
223
224 let bin_path = find_executable(
225 &pkg,
226 upstream,
227 cache_only,
228 local_only,
229 resolved_source.registry_handle.as_deref(),
230 )?;
231
232 match crate::pkg::telemetry::posthog_capture_event(
233 "exec",
234 &pkg,
235 env!("CARGO_PKG_VERSION"),
236 resolved_source
237 .registry_handle
238 .as_deref()
239 .unwrap_or("local"),
240 ) {
241 Ok(true) => println!("{} telemetry sent", "Info:".green()),
242 Ok(false) => (),
243 Err(e) => eprintln!("{} telemetry failed: {}", "Warning:".yellow(), e),
244 }
245
246 println!("\n--- Executing '{}' ---\n", pkg.name.bold());
247
248 let mut command_str = format!("\"{}\"", bin_path.display());
249 if !args.is_empty() {
250 command_str.push(' ');
251 command_str.push_str(&args.join(" "));
252 }
253
254 println!("> {}", command_str.cyan());
255
256 let status = if cfg!(target_os = "windows") {
257 Command::new("pwsh")
258 .arg("-Command")
259 .arg(&command_str)
260 .status()?
261 } else {
262 Command::new("bash").arg("-c").arg(&command_str).status()?
263 };
264
265 Ok(status.code().unwrap_or(1))
266}