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