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}