Skip to main content

zoi_cli/cmd/
exec.rs

1use crate::pkg::{install, local};
2use anyhow::{Result, anyhow};
3use colored::*;
4use indicatif::MultiProgress;
5use rayon::prelude::*;
6use std::collections::{HashMap, HashSet};
7use std::fs;
8use std::path::PathBuf;
9use std::process::Command;
10use std::sync::Mutex;
11
12pub fn run(source: String, bin: Option<String>, args: Vec<String>, verbose: bool) -> Result<()> {
13    if verbose {
14        println!("{} Resolving package...", "::".bold().blue());
15    }
16
17    let installed_before: HashSet<String> = local::get_installed_packages()?
18        .into_iter()
19        .map(|m| local::installed_manifest_source(&m))
20        .collect();
21
22    let (graph, _non_zoi_deps) = install::resolver::resolve_dependency_graph(
23        &[source],
24        None,
25        false,
26        true,
27        true,
28        None,
29        !verbose,
30    )?;
31
32    let install_plan = install::plan::create_install_plan(&graph.nodes, None, false)?;
33    let stages = graph.toposort()?;
34
35    let mut session_installed = Vec::new();
36    if !install_plan.is_empty() {
37        if verbose {
38            println!("\n{} Preparing packages...", "::".bold().blue());
39        }
40        let m_prep = MultiProgress::new();
41        if !verbose {
42            m_prep.set_draw_target(indicatif::ProgressDrawTarget::hidden());
43        }
44        let prepared_nodes = Mutex::new(HashMap::new());
45
46        stages
47            .par_iter()
48            .flatten()
49            .try_for_each(|pkg_id| -> Result<()> {
50                let node = graph.nodes.get(pkg_id).ok_or_else(|| {
51                    anyhow!(
52                        "Package node missing from graph for '{}' during preparation",
53                        pkg_id
54                    )
55                })?;
56                let action = install_plan.get(pkg_id).ok_or_else(|| {
57                    anyhow!(
58                        "Install action missing for package '{}' during preparation",
59                        pkg_id
60                    )
61                })?;
62
63                let prepared =
64                    install::installer::prepare_node(node, action, Some(&m_prep), None, verbose)?;
65
66                let mut lock = prepared_nodes.lock().map_err(|e| {
67                    anyhow!("Prepared nodes mutex poisoned during preparation: {}", e)
68                })?;
69                lock.insert(pkg_id.clone(), prepared);
70                Ok(())
71            })?;
72
73        if verbose {
74            println!(
75                "{} Installing {} packages...",
76                "::".bold().blue(),
77                install_plan.len()
78            );
79        }
80        let m = indicatif::MultiProgress::new();
81        if !verbose {
82            m.set_draw_target(indicatif::ProgressDrawTarget::hidden());
83        }
84        let session_installed_mutex = Mutex::new(Vec::new());
85
86        for stage in stages {
87            stage.into_par_iter().try_for_each(|pkg_id| -> Result<()> {
88                let node = graph
89                    .nodes
90                    .get(&pkg_id)
91                    .ok_or_else(|| anyhow!("Package node missing from graph for '{}'", pkg_id))?;
92
93                let prepared = {
94                    let lock = prepared_nodes.lock().map_err(|e| {
95                        anyhow!("Prepared nodes mutex poisoned during install: {}", e)
96                    })?;
97                    lock.get(&pkg_id)
98                        .cloned()
99                        .ok_or_else(|| anyhow!("Prepared node missing for: {}", pkg_id))?
100                };
101
102                let manifest = install::installer::install_prepared_node(
103                    node,
104                    &prepared,
105                    Some(&m),
106                    true,
107                    false,
108                    false,
109                    verbose,
110                )?;
111
112                let mut session_lock = session_installed_mutex.lock().unwrap();
113                session_lock.push(manifest);
114                Ok(())
115            })?;
116        }
117        session_installed = session_installed_mutex.into_inner().unwrap();
118    }
119
120    let root_ids: Vec<&String> = graph
121        .adj
122        .get("$root")
123        .map(|s| s.iter().collect())
124        .unwrap_or_default();
125    let root_id = root_ids
126        .first()
127        .ok_or_else(|| anyhow!("Could not find root package in dependency graph"))?;
128    let node = graph
129        .nodes
130        .get(*root_id)
131        .ok_or_else(|| anyhow!("Root package node not found"))?;
132
133    let bin_name = match bin {
134        Some(name) => name,
135        None => {
136            let bins = node
137                .pkg
138                .bins
139                .as_ref()
140                .ok_or_else(|| anyhow!("Package '{}' provides no binaries", node.pkg.name))?;
141            if bins.len() == 1 {
142                bins[0].clone()
143            } else {
144                return Err(anyhow!(
145                    "Package '{}' provides multiple binaries ({}). Use --bin to specify which to run.",
146                    node.pkg.name,
147                    bins.join(", ")
148                ));
149            }
150        }
151    };
152
153    let temp_dir = tempfile::Builder::new().prefix("zoi-exec-").tempdir()?;
154    let temp_bin_dir = temp_dir.path().join("bin");
155    fs::create_dir_all(&temp_bin_dir)?;
156
157    for gnode in graph.nodes.values() {
158        let Ok(pkg_dir) = local::get_package_dir(
159            gnode.pkg.scope,
160            &gnode.registry_handle,
161            &gnode.pkg.repo,
162            &gnode.pkg.name,
163        ) else {
164            continue;
165        };
166        let version_dir = pkg_dir.join(&gnode.version);
167        let bin_dir = version_dir.join("bin");
168        if bin_dir.exists() {
169            for entry in fs::read_dir(bin_dir)? {
170                let entry = entry?;
171                let path = entry.path();
172                if (path.is_file() || path.is_symlink())
173                    && let Some(file_name) = path.file_name()
174                {
175                    let dest = temp_bin_dir.join(file_name);
176                    let _ = fs::remove_file(&dest);
177                    let _ = crate::utils::symlink_file(&path, &dest);
178                }
179            }
180        }
181    }
182
183    let package_dir = local::get_package_dir(
184        node.pkg.scope,
185        &node.registry_handle,
186        &node.pkg.repo,
187        &node.pkg.name,
188    )?;
189    let version_dir = package_dir.join(&node.version);
190    let actual_bin_path = version_dir.join("bin").join(&bin_name);
191
192    if !actual_bin_path.exists() {
193        return Err(anyhow!(
194            "Binary '{}' not found in package '{}'",
195            bin_name,
196            node.pkg.name
197        ));
198    }
199
200    let mut new_path = temp_bin_dir.to_string_lossy().to_string();
201    if let Ok(old_path) = std::env::var("PATH") {
202        new_path = format!(
203            "{}{}{}",
204            new_path,
205            if cfg!(windows) { ";" } else { ":" },
206            old_path
207        );
208    }
209
210    if verbose {
211        println!(
212            "{} Running '{}' from '{}'...",
213            "::".bold().blue(),
214            bin_name.cyan(),
215            node.pkg.name.cyan()
216        );
217    }
218
219    let mut envs = HashMap::new();
220    envs.insert("PATH".to_string(), new_path);
221
222    #[cfg(target_os = "linux")]
223    let mut cmd = {
224        let sysroot = zoi_core::sysroot::get_sysroot();
225        if let Some(root) = sysroot {
226            if verbose {
227                println!(
228                    "{} Executing within sysroot: {}",
229                    "::".bold().yellow(),
230                    root.display()
231                );
232            }
233
234            let extra_binds = vec![(temp_dir.path().to_path_buf(), temp_dir.path().to_path_buf())];
235
236            let exe_inside_root = actual_bin_path
237                .strip_prefix(&root)
238                .map(PathBuf::from)
239                .unwrap_or(actual_bin_path.clone());
240
241            crate::sandbox::wrap_command_in_root(
242                &root,
243                &exe_inside_root,
244                &args,
245                &envs,
246                &extra_binds,
247                false,
248            )?
249        } else if let Some(sandbox_config) = &node.pkg.sandbox
250            && sandbox_config.enabled
251        {
252            if verbose {
253                println!("{} Sandboxing with bwrap.", "::".bold().yellow());
254            }
255            crate::sandbox::wrap_command(&actual_bin_path, &args, sandbox_config, &version_dir)?
256        } else {
257            let mut c = Command::new(&actual_bin_path);
258            c.args(&args);
259            c.envs(&envs);
260            c
261        }
262    };
263
264    #[cfg(not(target_os = "linux"))]
265    let mut cmd = {
266        let mut c = Command::new(&actual_bin_path);
267        c.args(&args);
268        c.envs(&envs);
269        c
270    };
271
272    let status = cmd.status()?;
273
274    if !session_installed.is_empty() {
275        if verbose {
276            println!("{} Cleaning up ephemeral packages...", "::".bold().blue());
277        }
278        for manifest in session_installed {
279            let ident = crate::pkg::local::installed_manifest_source(&manifest);
280            if installed_before.contains(&ident) {
281                continue;
282            }
283            let version_dir = match get_version_dir_from_manifest(&manifest) {
284                Ok(d) => d,
285                Err(e) => {
286                    eprintln!("Warning: failed to resolve path for {}: {}", ident, e);
287                    continue;
288                }
289            };
290            if version_dir.exists()
291                && let Err(e) = fs::remove_dir_all(&version_dir)
292            {
293                eprintln!(
294                    "Warning: failed to cleanup ephemeral package {}: {}",
295                    ident, e
296                );
297            }
298            let package_dir = version_dir.parent().unwrap().to_path_buf();
299            if let Ok(mut entries) = fs::read_dir(&package_dir) {
300                let has_other_entries = entries.any(|e| {
301                    e.as_ref()
302                        .is_ok_and(|e| e.file_name() != "latest" && e.file_name() != "dependents")
303                });
304                if !has_other_entries {
305                    let _ = fs::remove_dir_all(&package_dir);
306                }
307            }
308        }
309    }
310
311    if !status.success() {
312        std::process::exit(status.code().unwrap_or(1));
313    }
314
315    Ok(())
316}
317
318fn get_version_dir_from_manifest(manifest: &zoi_core::types::InstallManifest) -> Result<PathBuf> {
319    crate::pkg::local::get_package_version_dir(
320        manifest.scope,
321        &manifest.registry_handle,
322        &manifest.repo,
323        &manifest.name,
324        &manifest.version,
325    )
326}