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}