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