1use crate::pkg::{self, config, install, local, lock, transaction, types};
2use crate::project;
3use anyhow::{Result, anyhow};
4use colored::Colorize;
5use indicatif::MultiProgress;
6use rayon::prelude::*;
7use std::collections::{HashMap, HashSet};
8use std::sync::Mutex;
9
10pub fn run(
11 sources: &[String],
12 repo: Option<String>,
13 force: bool,
14 all_optional: bool,
15 yes: bool,
16 scope: Option<crate::cli::InstallScope>,
17 local: bool,
18 global: bool,
19 save: bool,
20 build_type: Option<String>,
21) -> Result<()> {
22 let mut scope_override = scope.map(|s| match s {
23 crate::cli::InstallScope::User => types::Scope::User,
24 crate::cli::InstallScope::System => types::Scope::System,
25 crate::cli::InstallScope::Project => types::Scope::Project,
26 });
27
28 if local {
29 scope_override = Some(types::Scope::Project);
30 } else if global {
31 scope_override = Some(types::Scope::User);
32 }
33
34 let lockfile_exists = sources.is_empty()
35 && repo.is_none()
36 && std::path::Path::new("zoi.lock").exists()
37 && std::path::Path::new("zoi.yaml").exists();
38
39 let mut sources_to_process: Vec<String> = sources.to_vec();
40 let mut is_project_install = false;
41 if sources.is_empty()
42 && repo.is_none()
43 && let Ok(config) = project::config::load()
44 && config.config.local
45 {
46 if lockfile_exists {
47 println!("zoi.lock found. Installing from zoi.yaml then verifying...");
48 } else {
49 println!("Installing project packages from zoi.yaml...");
50 }
51 sources_to_process = config.pkgs.clone();
52 scope_override = Some(types::Scope::Project);
53 is_project_install = true;
54 }
55
56 if sources_to_process.is_empty() {
57 return Ok(());
58 }
59
60 if let Some(repo_spec) = repo {
61 if scope_override == Some(types::Scope::Project) {
62 return Err(anyhow!(
63 "Installing from a repository to a project scope is not supported."
64 ));
65 }
66 let repo_install_scope = scope_override.map(|s| match s {
67 types::Scope::User => crate::cli::SetupScope::User,
68 types::Scope::System => crate::cli::SetupScope::System,
69 types::Scope::Project => unreachable!(),
70 });
71
72 crate::pkg::repo_install::run(&repo_spec, force, all_optional, yes, repo_install_scope)?;
73 return Ok(());
74 }
75
76 let config = config::read_config().unwrap_or_default();
77 let parallel_jobs = config.parallel_jobs.unwrap_or(3);
78 if parallel_jobs > 0 {
79 rayon::ThreadPoolBuilder::new()
80 .num_threads(parallel_jobs)
81 .build_global()
82 .unwrap();
83 }
84
85 let failed_packages = Mutex::new(Vec::new());
86 let mut temp_files = Vec::new();
87 let mut final_sources = Vec::new();
88
89 for source in &sources_to_process {
90 if source.ends_with("zoi.pkgs.json") {
91 install::lockfile::process_lockfile(source, &mut final_sources, &mut temp_files)?;
92 } else {
93 final_sources.push(source.to_string());
94 }
95 }
96
97 let successfully_installed_sources = Mutex::new(Vec::new());
98 let installed_manifests = Mutex::new(Vec::new());
99
100 println!("{}", "Resolving dependencies...".bold());
101
102 let (graph, non_zoi_deps) = install::resolver::resolve_dependency_graph(
103 &final_sources,
104 scope_override,
105 force,
106 yes,
107 all_optional,
108 build_type.as_deref(),
109 true,
110 )?;
111
112 let packages_to_install: Vec<&types::Package> = graph.nodes.values().map(|n| &n.pkg).collect();
113
114 let mut packages_to_replace = std::collections::HashSet::new();
115 if let Ok(installed_packages) = local::get_installed_packages() {
116 for pkg in &packages_to_install {
117 if let Some(replaces) = &pkg.replaces {
118 for replaced_pkg_name in replaces {
119 if installed_packages
120 .iter()
121 .any(|p| &p.name == replaced_pkg_name)
122 {
123 packages_to_replace.insert(replaced_pkg_name.clone());
124 }
125 }
126 }
127 }
128 }
129
130 if !packages_to_replace.is_empty() {
131 println!("\nThe following packages will be replaced:");
132 for pkg_name in &packages_to_replace {
133 println!("- {}", pkg_name);
134 }
135 if !crate::utils::ask_for_confirmation(
136 "\nDo you want to continue with the replacement?",
137 yes,
138 ) {
139 return Ok(());
140 }
141 }
142
143 println!("{}", "Looking for conflicts...".bold());
144 install::util::check_for_conflicts(&packages_to_install, yes)?;
145
146 let m_for_conflict_check = MultiProgress::new();
147 install::util::check_file_conflicts(&graph, yes, &m_for_conflict_check)?;
148 let _ = m_for_conflict_check.clear();
149
150 let install_plan = install::plan::create_install_plan(&graph.nodes)?;
151
152 let mut to_download = HashMap::new();
153 let mut to_build = HashMap::new();
154
155 for (id, node) in &graph.nodes {
156 match install_plan.get(id) {
157 Some(install::plan::InstallAction::DownloadAndInstall(details)) => {
158 to_download.insert(id.clone(), (node, details.clone()));
159 }
160 Some(install::plan::InstallAction::BuildAndInstall) => {
161 to_build.insert(id.clone(), node);
162 }
163 _ => {}
164 }
165 }
166
167 let total_download_size: u64 = to_download.values().map(|(_, d)| d.download_size).sum();
168 let total_installed_size: u64 = to_download
169 .values()
170 .map(|(n, _)| n.pkg.installed_size.unwrap_or(0))
171 .sum();
172
173 println!("\n--- Summary ---");
174
175 if !to_download.is_empty() {
176 println!("\nPackages to download:");
177 let pkg_list: Vec<_> = to_download
178 .values()
179 .map(|(n, _)| {
180 if let Some(sub) = &n.sub_package {
181 format!("{}:{}@{}", n.pkg.name, sub, n.version)
182 } else {
183 format!("{}@{}", n.pkg.name, n.version)
184 }
185 })
186 .collect();
187 println!("{}", pkg_list.join(" "));
188 println!(
189 "Total Download Size: {}",
190 crate::utils::format_bytes(total_download_size)
191 );
192 println!(
193 "Total Installed Size: {}",
194 crate::utils::format_bytes(total_installed_size)
195 );
196 }
197
198 if !to_build.is_empty() {
199 println!("Packages to build from source:");
200 let pkg_list: Vec<_> = to_build
201 .values()
202 .map(|n| {
203 if let Some(sub) = &n.sub_package {
204 format!("{}:{}@{}", n.pkg.name, sub, n.version)
205 } else {
206 format!("{}@{}", n.pkg.name, n.version)
207 }
208 })
209 .collect();
210 println!("{}", pkg_list.join(" "));
211 }
212
213 if !non_zoi_deps.is_empty() {
214 println!("\nExternal dependencies:");
215 let pkg_list: Vec<_> = non_zoi_deps.iter().map(|d| d.cyan().to_string()).collect();
216 println!("{}", pkg_list.join(" "));
217 }
218
219 if !to_build.is_empty() {
220 } else {
221 println!("\n{}", "Checking available disk space...".bold());
222 let install_path =
223 crate::pkg::local::get_store_base_dir(scope_override.unwrap_or_default())?;
224
225 std::fs::create_dir_all(&install_path)?;
226
227 let available_space = match fs2::available_space(&install_path) {
228 Ok(space) => space,
229 Err(e) => {
230 eprintln!(
231 "{}: Could not check available disk space: {}",
232 "Warning".yellow().bold(),
233 e
234 );
235 u64::MAX
236 }
237 };
238
239 if total_installed_size > available_space {
240 return Err(anyhow!(
241 "Not enough disk space. Required: {}, Available: {}",
242 crate::utils::format_bytes(total_installed_size),
243 crate::utils::format_bytes(available_space)
244 ));
245 }
246 }
247
248 if !crate::utils::ask_for_confirmation("\n:: Proceed with installation?", yes) {
249 let _ = lock::release_lock();
250 return Ok(());
251 }
252
253 let mut final_install_plan = install_plan.clone();
254
255 if !to_download.is_empty() {
256 println!("\n:: Downloading packages...");
257 let mut download_groups: HashMap<String, (&install::plan::PrebuiltDetails, Vec<&str>)> =
258 HashMap::new();
259
260 for (node, details) in to_download.values() {
261 let entry = download_groups
262 .entry(details.info.final_url.clone())
263 .or_insert((details, Vec::new()));
264 if let Some(sub) = &node.sub_package {
265 entry.1.push(sub);
266 }
267 }
268
269 let downloaded_archives: Mutex<HashMap<String, std::path::PathBuf>> =
270 Mutex::new(HashMap::new());
271 let m_for_dl = MultiProgress::new();
272
273 download_groups.par_iter().for_each(|(url, (details, _))| {
274 let first_node = to_download
275 .values()
276 .find(|(_, d)| d.info.final_url == *url)
277 .unwrap()
278 .0;
279
280 match install::installer::download_and_cache_archive(
281 first_node,
282 details,
283 Some(&m_for_dl),
284 ) {
285 Ok(path) => {
286 downloaded_archives
287 .lock()
288 .unwrap()
289 .insert(url.clone(), path);
290 }
291 Err(e) => {
292 eprintln!("Failed to download {}: {}", url, e);
293 failed_packages.lock().unwrap().push(url.clone());
294 }
295 }
296 });
297
298 let downloaded_archives_map = downloaded_archives.lock().unwrap();
299 for (id, (_, details)) in &to_download {
300 if let Some(downloaded_path) = downloaded_archives_map.get(&details.info.final_url) {
301 final_install_plan.insert(
302 id.clone(),
303 install::plan::InstallAction::InstallFromArchive(downloaded_path.clone()),
304 );
305 }
306 }
307 }
308
309 let stages = graph.toposort()?;
310
311 let transaction = transaction::begin()?;
312
313 for pkg_name in packages_to_replace {
314 println!("Replacing package: {}", pkg_name);
315 match pkg::uninstall::run(&pkg_name, None) {
316 Ok(uninstalled_manifest) => {
317 if let Err(e) = transaction::record_operation(
318 &transaction.id,
319 types::TransactionOperation::Uninstall {
320 manifest: Box::new(uninstalled_manifest),
321 },
322 ) {
323 eprintln!("Failed to record uninstall of replaced package: {}", e);
324 }
325 }
326 Err(e) => {
327 eprintln!("Failed to uninstall replaced package '{}': {}", pkg_name, e);
328 }
329 }
330 }
331
332 println!("\n:: Starting installation...");
333 let mut overall_success = true;
334 let m = MultiProgress::new();
335
336 for (i, stage) in stages.iter().enumerate() {
337 println!(
338 ":: Installing Stage {}/{} ({} packages)",
339 i + 1,
340 stages.len(),
341 stage.len()
342 );
343
344 stage.par_iter().for_each(|pkg_id| {
345 let node = graph.nodes.get(pkg_id).unwrap();
346 let action = final_install_plan.get(pkg_id).unwrap();
347
348 match install::installer::install_node(
349 node,
350 action,
351 Some(&m),
352 build_type.as_deref(),
353 yes,
354 ) {
355 Ok(manifest) => {
356 println!("Successfully installed {}", node.pkg.name.green());
357 installed_manifests.lock().unwrap().push(manifest.clone());
358
359 if let Err(e) = transaction::record_operation(
360 &transaction.id,
361 types::TransactionOperation::Install {
362 manifest: Box::new(manifest),
363 },
364 ) {
365 eprintln!(
366 "Error: Failed to record transaction operation for {}: {}",
367 node.pkg.name, e
368 );
369 failed_packages.lock().unwrap().push(node.pkg.name.clone());
370 }
371
372 if matches!(node.reason, types::InstallReason::Direct) {
373 successfully_installed_sources
374 .lock()
375 .unwrap()
376 .push(node.source.clone());
377 }
378 }
379 Err(e) => {
380 eprintln!(
381 "{}: Failed to install {}: {}",
382 "Error".red().bold(),
383 node.pkg.name,
384 e
385 );
386 failed_packages.lock().unwrap().push(node.pkg.name.clone());
387 }
388 }
389 });
390
391 let failed = failed_packages.lock().unwrap();
392 if !failed.is_empty() {
393 eprintln!(
394 "\n{}: Installation failed at stage {}.",
395 "Error".red().bold(),
396 i + 1
397 );
398 overall_success = false;
399 break;
400 }
401 }
402
403 if !overall_success {
404 let failed_list = failed_packages.into_inner().unwrap();
405 eprintln!(
406 "\n{}: The following packages failed to install:",
407 "Error".red().bold()
408 );
409 for pkg in &failed_list {
410 eprintln!(" - {}", pkg);
411 }
412
413 eprintln!("\n{} Rolling back changes...", "---".yellow().bold());
414 if let Err(e) = transaction::rollback(&transaction.id) {
415 eprintln!("\nCRITICAL: Rollback failed: {}", e);
416 eprintln!(
417 "The system may be in an inconsistent state. The transaction log is at ~/.zoi/transactions/{}.json",
418 transaction.id
419 );
420 } else {
421 println!("\n{} Rollback successful.", "Success:".green().bold());
422 }
423
424 return Err(anyhow!(
425 "Installation failed for: {}",
426 failed_list.join(", ")
427 ));
428 }
429
430 if let Err(e) = transaction::commit(&transaction.id) {
431 eprintln!("Warning: Failed to commit transaction: {}", e);
432 }
433
434 if !non_zoi_deps.is_empty() {
435 println!("\n:: Installing external dependencies...");
436 let processed_deps = Mutex::new(HashSet::new());
437 let mut installed_deps_ext = Vec::new();
438 for dep_str in &non_zoi_deps {
439 let dep = match crate::pkg::dependencies::parse_dependency_string(dep_str) {
440 Ok(d) => d,
441 Err(e) => {
442 eprintln!(
443 "{}: Could not parse dependency string '{}': {}",
444 "Error".red().bold(),
445 dep_str,
446 e
447 );
448 continue;
449 }
450 };
451
452 if let Err(e) = crate::pkg::dependencies::install_dependency(
453 &dep,
454 "direct",
455 scope_override.unwrap_or_default(),
456 yes,
457 all_optional,
458 &processed_deps,
459 &mut installed_deps_ext,
460 Some(&m),
461 ) {
462 eprintln!(
463 "{}: Failed to install external dependency {}: {}",
464 "Error".red().bold(),
465 dep_str,
466 e
467 );
468 }
469 }
470 }
471
472 let is_any_project_install = scope_override == Some(types::Scope::Project);
473
474 if is_any_project_install {
475 if is_project_install && lockfile_exists {
476 } else {
477 println!("\nUpdating zoi.lock...");
478 let mut lockfile =
479 project::lockfile::read_zoi_lock().unwrap_or_else(|_| types::ZoiLock {
480 version: "1".to_string(),
481 ..Default::default()
482 });
483
484 lockfile.packages.clear();
485 lockfile.details.clear();
486
487 let all_regs_config = crate::pkg::config::read_config().unwrap_or_default();
488 let mut all_configured_regs = all_regs_config.added_registries;
489 if let Some(default_reg) = all_regs_config.default_registry {
490 all_configured_regs.push(default_reg);
491 }
492
493 let installed_manifests = installed_manifests.into_inner().unwrap();
494 for manifest in &installed_manifests {
495 let name_with_sub = if let Some(sub) = &manifest.sub_package {
496 format!("{}:{}", manifest.name, sub)
497 } else {
498 manifest.name.clone()
499 };
500
501 let full_id = format!(
502 "#{}@{}/{}",
503 manifest.registry_handle, manifest.repo, name_with_sub
504 );
505 lockfile.packages.insert(full_id, manifest.version.clone());
506
507 if let Some(reg) = all_configured_regs
508 .iter()
509 .find(|r| r.handle == manifest.registry_handle)
510 {
511 lockfile
512 .registries
513 .insert(reg.handle.clone(), reg.url.clone());
514 }
515
516 let package_dir = crate::pkg::local::get_package_dir(
517 types::Scope::Project,
518 &manifest.registry_handle,
519 &manifest.repo,
520 &manifest.name,
521 )?;
522 let latest_dir = package_dir.join("latest");
523 let integrity =
524 crate::pkg::hash::calculate_dir_hash(&latest_dir).unwrap_or_else(|e| {
525 eprintln!(
526 "Warning: could not calculate integrity for {}: {}",
527 manifest.name, e
528 );
529 String::new()
530 });
531
532 let pkg_id = if let Some(sub) = &manifest.sub_package {
533 format!("{}@{}:{}", manifest.name, manifest.version, sub)
534 } else {
535 format!("{}@{}", manifest.name, manifest.version)
536 };
537
538 let dependencies: Vec<String> = graph
539 .adj
540 .get(&pkg_id)
541 .map(|deps| {
542 deps.iter()
543 .map(|dep_id| {
544 let node = graph.nodes.get(dep_id).unwrap();
545 if let Some(sub) = &node.pkg.sub_packages {
546 format!(
547 "#{}@{}/{}:{}",
548 node.registry_handle,
549 node.pkg.repo,
550 node.pkg.name,
551 sub.join(",")
552 )
553 } else {
554 format!(
555 "#{}@{}/{}",
556 node.registry_handle, node.pkg.repo, node.pkg.name
557 )
558 }
559 })
560 .collect()
561 })
562 .unwrap_or_default();
563
564 let detail = types::LockPackageDetail {
565 version: manifest.version.clone(),
566 sub_package: manifest.sub_package.clone(),
567 integrity,
568 dependencies,
569 options_dependencies: manifest.chosen_options.clone(),
570 optionals_dependencies: manifest.chosen_optionals.clone(),
571 };
572
573 let registry_key = format!("#{}", manifest.registry_handle);
574 let short_id = format!("@{}/{}", manifest.repo, name_with_sub);
575
576 lockfile
577 .details
578 .entry(registry_key)
579 .or_default()
580 .insert(short_id, detail);
581 }
582
583 if let Err(e) = project::lockfile::write_zoi_lock(&lockfile) {
584 eprintln!("Warning: Failed to write zoi.lock file: {}", e);
585 }
586 }
587 }
588
589 if save && scope_override == Some(types::Scope::Project) {
590 let successfully_installed = successfully_installed_sources.into_inner().unwrap();
591 if !successfully_installed.is_empty()
592 && let Err(e) = project::config::add_packages_to_config(&successfully_installed)
593 {
594 eprintln!(
595 "{}: Failed to save packages to zoi.yaml: {}",
596 "Warning".yellow().bold(),
597 e
598 );
599 }
600 }
601
602 println!("\n{} Installation complete!", "Success:".green().bold());
603
604 if is_project_install && lockfile_exists {
605 println!();
606 project::verify::run()?;
607 }
608 Ok(())
609}