zoi/pkg/
exec.rs

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}