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