1use std::fs;
2use std::sync::Mutex;
3
4use anyhow::{Result, anyhow};
5use colored::Colorize;
6use dialoguer::MultiSelect;
7use dialoguer::theme::ColorfulTheme;
8use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
9use rayon::prelude::*;
10use semver::Version;
11use serde_json::json;
12
13use crate::cmd::{utils as cmd_utils, ux};
14use crate::pkg::merge::handle_backup_files;
15use crate::pkg::{
16 config, db, hooks, install, local, pin, resolve, transaction, types
17};
18
19pub fn run(
33 all: bool,
34 package_names: &[String],
35 yes: bool,
36 dry_run: bool,
37 explain: bool,
38 plan_json: bool,
39 interactive: bool
40) -> Result<()> {
41 if all {
42 return run_update_all_logic(
43 yes,
44 dry_run,
45 explain,
46 plan_json,
47 interactive
48 );
49 }
50
51 let expanded_package_names =
52 cmd_utils::expand_split_packages(package_names, "Updating")?;
53
54 let mut failed_packages = Vec::new();
55
56 for (i, package_name) in expanded_package_names.iter().enumerate() {
57 if i > 0 {
58 println!();
59 }
60 if let Err(e) = run_update_single_logic(
61 package_name,
62 yes,
63 dry_run,
64 explain,
65 plan_json
66 ) {
67 eprintln!(
68 "{}: Failed to update '{}': {}",
69 "Error".red().bold(),
70 package_name,
71 e
72 );
73 failed_packages.push(package_name.clone());
74 }
75 }
76
77 if !failed_packages.is_empty() {
78 return Err(anyhow!(
79 "The following packages failed to update: {}",
80 failed_packages.join(", ")
81 ));
82 } else if !package_names.is_empty() && !dry_run {
83 println!("\n{}", "Success:".green());
84 }
85 Ok(())
86}
87
88fn run_update_single_logic(
90 package_name: &str,
91 yes: bool,
92 dry_run: bool,
93 explain: bool,
94 plan_json: bool
95) -> Result<()> {
96 println!(
97 "{} Updating package '{}'...",
98 "::".bold().blue(),
99 package_name.cyan().bold()
100 );
101
102 let (new_pkg, new_version, _, _, registry_handle, _, _) =
103 resolve::resolve_package_and_version(package_name, None, false, yes)?;
104
105 if pin::is_pinned(package_name)? {
106 println!(
107 "Package '{}' is pinned. Skipping update.",
108 package_name.yellow()
109 );
110 return Ok(());
111 }
112
113 let installed_source = if let Some(sub) = &new_pkg.sub_package {
114 format!(
115 "#{}@{}/{}:{}",
116 registry_handle.as_deref().unwrap_or("local"),
117 new_pkg.repo,
118 new_pkg.name,
119 sub
120 )
121 } else {
122 format!(
123 "#{}@{}/{}",
124 registry_handle.as_deref().unwrap_or("local"),
125 new_pkg.repo,
126 new_pkg.name
127 )
128 };
129 let installed_request = resolve::parse_source_string(&installed_source)?;
130 let mut candidates = Vec::new();
131 candidates.extend(local::find_installed_manifests_matching(
132 &installed_request,
133 types::Scope::User
134 )?);
135 candidates.extend(local::find_installed_manifests_matching(
136 &installed_request,
137 types::Scope::System
138 )?);
139
140 let old_manifest = crate::cmd::installed_select::choose_installed_manifest(
141 package_name,
142 &candidates,
143 yes
144 )
145 .map_err(|e| {
146 if candidates.is_empty() {
147 anyhow!(
148 "Package '{package_name}' is not installed. Use 'zoi install' \
149 instead."
150 )
151 } else {
152 e
153 }
154 })?;
155
156 println!(
157 "Currently installed version: {}",
158 old_manifest.version.yellow()
159 );
160 if old_manifest.revision != "1" || new_pkg.revision != "1" {
161 println!(
162 "Currently installed revision: {}",
163 old_manifest.revision.yellow()
164 );
165 }
166 println!("Available version: {}", new_version.green());
167 if old_manifest.revision != "1" || new_pkg.revision != "1" {
168 println!("Available revision: {}", new_pkg.revision.green());
169 }
170
171 if old_manifest.version == new_version
172 && old_manifest.revision == new_pkg.revision
173 {
174 println!("\nPackage is already up to date.");
175 ux::print_transaction_summary(&ux::TransactionSummary {
176 command: "update".to_string(),
177 success: 0,
178 failed: 0,
179 skipped: 1
180 });
181 return Ok(());
182 }
183
184 let (download_size, new_installed_size) = install::util::get_package_sizes(
185 &new_pkg,
186 registry_handle.as_deref().unwrap_or("local"),
187 &new_version
188 );
189 let old_installed_size = old_manifest.installed_size.unwrap_or(0);
190 let installed_size_diff =
191 new_installed_size as i64 - old_installed_size as i64;
192
193 println!();
194 if download_size > 0 {
195 println!(
196 "Total Download Size: {}",
197 crate::pkg::utils::format_bytes(download_size)
198 );
199 }
200 if installed_size_diff != 0 {
201 println!(
202 "Net Upgrade Size: {}",
203 crate::pkg::utils::format_size_diff(installed_size_diff)
204 );
205 }
206 println!();
207
208 let preflight = ux::PreflightSummary::new("Update preflight")
209 .row("Package", new_pkg.name.clone())
210 .row("From", old_manifest.version.clone())
211 .row("To", new_version.clone())
212 .row("Scope", format!("{:?}", old_manifest.scope))
213 .row(
214 "Download size",
215 crate::pkg::utils::format_bytes(download_size)
216 )
217 .row(
218 "Net size",
219 crate::pkg::utils::format_size_diff(installed_size_diff)
220 );
221 ux::print_preflight(&preflight);
222
223 if explain {
224 let mut report = ux::ExplainReport::new("Update explanation");
225 report = report.item(
226 new_pkg.name.clone(),
227 format!(
228 "selected because newer version {} is available over \
229 installed {}",
230 new_version, old_manifest.version
231 ),
232 Vec::new()
233 );
234 if let Ok((old_adv, new_adv)) = advisory_counts(
235 &old_manifest.registry_handle,
236 &new_pkg.name,
237 old_manifest.sub_package.as_deref(),
238 &old_manifest.version,
239 &new_version
240 ) {
241 report = report.item(
242 "advisories",
243 format!(
244 "old={}, new={}, delta={}",
245 old_adv,
246 new_adv,
247 (new_adv as i64 - old_adv as i64)
248 ),
249 Vec::new()
250 );
251 }
252 ux::print_explain(&report);
253 }
254
255 if plan_json {
256 let plan = json!({
257 "dry_run": dry_run,
258 "package": {
259 "name": new_pkg.name,
260 "sub_package": old_manifest.sub_package,
261 "registry": old_manifest.registry_handle,
262 "repo": old_manifest.repo,
263 "scope": format!("{:?}", old_manifest.scope),
264 "from_version": old_manifest.version,
265 "to_version": new_version,
266 "download_bytes": download_size,
267 "net_size_bytes": installed_size_diff,
268 }
269 });
270 ux::emit_plan_json_v1("update", plan)?;
271 }
272
273 if dry_run {
274 println!(
275 "{} Dry-run: would upgrade {} from {} to {}",
276 "::".bold().yellow(),
277 new_pkg.name,
278 old_manifest.version,
279 new_version
280 );
281 ux::print_transaction_summary(&ux::TransactionSummary {
282 command: "update".to_string(),
283 success: 0,
284 failed: 0,
285 skipped: 1
286 });
287 return Ok(());
288 }
289
290 if !crate::utils::ask_for_confirmation(
291 &format!("Update from {} to {}?", old_manifest.version, new_version),
292 yes
293 ) {
294 ux::print_transaction_summary(&ux::TransactionSummary {
295 command: "update".to_string(),
296 success: 0,
297 failed: 0,
298 skipped: 1
299 });
300 return Ok(());
301 }
302
303 let mut transaction = transaction::begin()?;
304
305 if let Some(hooks) = &new_pkg.hooks {
306 hooks::run_hooks(
307 hooks,
308 hooks::HookType::PreUpgrade,
309 old_manifest.scope
310 )?;
311 }
312
313 let (graph, _) = install::resolver::resolve_dependency_graph(
314 &[package_name.to_string()],
315 Some(old_manifest.scope),
316 true,
317 yes,
318 false,
319 None,
320 false,
321 None
322 )?;
323
324 zoi_install::preflight::check_policy_compliance(&graph)?;
325 zoi_install::preflight::check_scope_compliance(&graph)?;
326 zoi_install::preflight::check_zoios_compliance(&graph)?;
327 for node in graph.nodes.values() {
328 if !install::util::display_updates(&node.pkg, yes)? {
329 return Err(anyhow!("Update aborted by user."));
330 }
331 }
332 zoi_install::preflight::check_for_vulnerabilities(&graph, yes)?;
333
334 let install_plan =
335 install::plan::create_install_plan(&graph.nodes, None, false)?;
336
337 let stages = graph.toposort()?;
338 let mut new_manifest_option: Option<types::InstallManifest> = None;
339
340 for stage in stages {
341 for pkg_id in stage {
342 let node = graph.nodes.get(&pkg_id).ok_or_else(|| {
343 anyhow!(
344 "Package node '{pkg_id}' missing from graph during update"
345 )
346 })?;
347 if let Some(action) = install_plan.get(&pkg_id) {
348 match install::installer::install_node(
349 node, action, None, None, yes, true, true, false
350 ) {
351 Ok(m) => {
352 if m.name == new_pkg.name {
353 new_manifest_option = Some(m);
354 }
355 }
356 Err(e) => {
357 eprintln!(
358 "\nError: Update failed during installation. \
359 Rolling back..."
360 );
361 transaction::rollback(&transaction.id)?;
362 ux::print_transaction_summary(
363 &ux::TransactionSummary {
364 command: "update".to_string(),
365 success: 0,
366 failed: 1,
367 skipped: 0
368 }
369 );
370 return Err(anyhow!("Update failed: {e}"));
371 }
372 }
373 }
374 }
375 }
376
377 if let Some(new_manifest) = new_manifest_option {
378 if let Err(e) = transaction::record_operation(
379 &mut transaction,
380 types::TransactionOperation::Upgrade {
381 old_manifest: Box::new(old_manifest.clone()),
382 new_manifest: Box::new(new_manifest.clone())
383 }
384 ) {
385 eprintln!("Warning: Failed to record transaction for update: {e}");
386 transaction::delete_log(&transaction.id)?;
387 } else {
388 if let Ok(modified_files) =
389 transaction::get_modified_files(&transaction.id)
390 {
391 let modified_packages =
392 transaction::get_modified_packages(&transaction.id)
393 .unwrap_or_default();
394 let _ = crate::pkg::hooks::global::run_global_hooks(
395 crate::pkg::hooks::global::HookWhen::PostTransaction,
396 &modified_files,
397 &modified_packages,
398 "upgrade",
399 old_manifest.scope
400 );
401 }
402 transaction::commit(&transaction.id)?;
403 }
404
405 if let Some(backup_files) = &old_manifest.backup {
406 println!("Restoring configuration files...");
407 let old_version_dir = local::get_package_version_dir(
408 old_manifest.scope,
409 &old_manifest.registry_handle,
410 &old_manifest.repo,
411 &old_manifest.name,
412 &old_manifest.version
413 )?;
414 let new_version_dir = local::get_package_version_dir(
415 new_manifest.scope,
416 &new_manifest.registry_handle,
417 &new_manifest.repo,
418 &new_manifest.name,
419 &new_manifest.version
420 )?;
421
422 handle_backup_files(
423 &old_version_dir,
424 &new_version_dir,
425 backup_files,
426 old_manifest.scope
427 )?;
428 }
429
430 cleanup_old_versions(
431 &new_pkg.name,
432 old_manifest.scope,
433 &new_pkg.repo,
434 registry_handle.as_deref().unwrap_or("local")
435 )?;
436
437 let handle = registry_handle.as_deref().unwrap_or("local");
438 if let Ok(conn) = db::open_connection("local") {
439 let _ = db::update_package(
440 &conn,
441 &new_pkg,
442 handle,
443 Some(new_pkg.scope),
444 new_pkg.sub_package.as_deref(),
445 Some(&types::InstallReason::Direct)
446 );
447 }
448
449 if let Some(hooks) = &new_pkg.hooks {
450 hooks::run_hooks(
451 hooks,
452 hooks::HookType::PostUpgrade,
453 new_pkg.scope
454 )?;
455 }
456
457 println!("\n{}", "Success:".green());
458 ux::print_transaction_summary(&ux::TransactionSummary {
459 command: "update".to_string(),
460 success: 1,
461 failed: 0,
462 skipped: 0
463 });
464 Ok(())
465 } else {
466 eprintln!(
467 "\nError: Update failed to produce a new manifest. Rolling back..."
468 );
469 transaction::rollback(&transaction.id)?;
470 ux::print_transaction_summary(&ux::TransactionSummary {
471 command: "update".to_string(),
472 success: 0,
473 failed: 1,
474 skipped: 0
475 });
476 Err(anyhow!("Update failed: could not get new manifest"))
477 }
478}
479
480fn run_update_all_logic(
482 yes: bool,
483 dry_run: bool,
484 explain: bool,
485 plan_json: bool,
486 interactive: bool
487) -> Result<()> {
488 #[derive(Clone)]
489 struct UpdateCandidate {
490 source: String,
491 new_pkg: types::Package,
492 new_version: String,
493 old_manifest: types::InstallManifest,
494 old_advisories: usize,
495 new_advisories: usize,
496 download_size: u64,
497 new_installed_size: u64
498 }
499
500 let installed_packages = local::get_installed_packages()?;
501 let mut pinned_sources = Vec::new();
502 let mut skipped_sources = Vec::new();
503 let mut up_to_date_sources = Vec::new();
504 let mut packages_to_upgrade: Vec<UpdateCandidate> = Vec::new();
505
506 println!("{} Checking for upgrades...", "::".bold().blue());
510 let pb = ProgressBar::new(installed_packages.len() as u64);
511 pb.set_style(
512 ProgressStyle::default_bar()
513 .template(
514 "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] \
515 {pos}/{len} ({msg})"
516 )?
517 .progress_chars("#>-")
518 );
519 pb.set_message("Checking packages...");
520
521 for manifest in installed_packages {
522 let source = if let Some(sub) = &manifest.sub_package {
523 format!(
524 "#{}@{}/{}:{}",
525 manifest.registry_handle, manifest.repo, manifest.name, sub
526 )
527 } else {
528 format!(
529 "#{}@{}/{}",
530 manifest.registry_handle, manifest.repo, manifest.name
531 )
532 };
533
534 if pin::is_pinned(&source).unwrap_or(false)
535 || pin::is_pinned(&manifest.name).unwrap_or(false)
536 {
537 pinned_sources.push(source.clone());
538 pb.inc(1);
539 continue;
540 }
541
542 let (new_pkg, new_version, _, _, _registry_handle, _, _) =
543 match resolve::resolve_package_and_version(
544 &source,
545 Some(manifest.scope),
546 true,
547 false
548 ) {
549 Ok(result) => result,
550 Err(e) => {
551 eprintln!(
552 "{}: Failed to resolve '{}': {}",
553 "Warning".yellow(),
554 source,
555 e
556 );
557 skipped_sources.push(format!("{source} ({e})"));
558 pb.inc(1);
559 continue;
560 }
561 };
562
563 if manifest.version == new_version
564 && manifest.revision == new_pkg.revision
565 {
566 up_to_date_sources.push(source.clone());
567 pb.inc(1);
568 continue;
569 }
570
571 let (old_adv, new_adv) = advisory_counts(
572 &manifest.registry_handle,
573 &manifest.name,
574 manifest.sub_package.as_deref(),
575 &manifest.version,
576 &new_version
577 )
578 .unwrap_or((0, 0));
579
580 let (download_size, new_installed_size) =
581 install::util::get_package_sizes(
582 &new_pkg,
583 &manifest.registry_handle,
584 &new_version
585 );
586
587 packages_to_upgrade.push(UpdateCandidate {
588 source,
589 new_pkg,
590 new_version,
591 old_manifest: manifest,
592 old_advisories: old_adv,
593 new_advisories: new_adv,
594 download_size,
595 new_installed_size
596 });
597 pb.inc(1);
598 }
599 pb.finish_and_clear();
600
601 if !pinned_sources.is_empty() {
602 println!("\n{} Pinned (skipped)", "::".bold().blue());
603 for source in &pinned_sources {
604 println!(" - {}", source.yellow());
605 }
606 }
607 if !skipped_sources.is_empty() {
608 println!("\n{} Skipped (resolve failures)", "::".bold().blue());
609 for source in &skipped_sources {
610 println!(" - {}", source.red());
611 }
612 }
613 if !packages_to_upgrade.is_empty() {
614 println!("\n{} Upgrade candidates", "::".bold().blue());
615 for candidate in &packages_to_upgrade {
616 let delta = candidate.new_advisories as i64
617 - candidate.old_advisories as i64;
618 let advisory_suffix = match delta.cmp(&0) {
619 std::cmp::Ordering::Greater => {
620 format!(" (advisories +{delta})").red().to_string()
621 }
622 std::cmp::Ordering::Less => {
623 format!(" (advisories {delta})").green().to_string()
624 }
625 std::cmp::Ordering::Equal => String::new()
626 };
627
628 let old_display = if candidate.old_manifest.revision == "1" {
629 candidate.old_manifest.version.clone()
630 } else {
631 format!(
632 "{}-{}",
633 candidate.old_manifest.version,
634 candidate.old_manifest.revision
635 )
636 };
637
638 let new_display = if candidate.new_pkg.revision == "1" {
639 candidate.new_version.clone()
640 } else {
641 format!(
642 "{}-{}",
643 candidate.new_version, candidate.new_pkg.revision
644 )
645 };
646
647 println!(
648 " - {}: {} -> {}{}",
649 candidate.source.cyan(),
650 old_display.yellow(),
651 new_display.green(),
652 advisory_suffix
653 );
654
655 let _ = install::util::display_updates(&candidate.new_pkg, true);
656 }
657 }
658
659 if packages_to_upgrade.is_empty() {
660 println!("\nAll packages are up to date.");
661 ux::print_transaction_summary(&ux::TransactionSummary {
662 command: "update".to_string(),
663 success: 0,
664 failed: 0,
665 skipped: pinned_sources.len()
666 + skipped_sources.len()
667 + up_to_date_sources.len()
668 });
669 return Ok(());
670 }
671
672 if explain {
673 let mut report = ux::ExplainReport::new("Update explanation");
674 for candidate in &packages_to_upgrade {
675 report = report.item(
676 candidate.source.clone(),
677 format!(
678 "selected because {} -> {}",
679 candidate.old_manifest.version, candidate.new_version
680 ),
681 Vec::new()
682 );
683 }
684 ux::print_explain(&report);
685 }
686
687 if interactive && !dry_run {
688 let items: Vec<String> = packages_to_upgrade
689 .iter()
690 .map(|c| {
691 format!(
692 "{} {} -> {}",
693 c.source, c.old_manifest.version, c.new_version
694 )
695 })
696 .collect();
697 let selected = MultiSelect::with_theme(&ColorfulTheme::default())
698 .with_prompt("Select packages to update")
699 .items(&items)
700 .interact()
701 .map_err(|e| anyhow!("Interactive selection failed: {e}"))?;
702
703 if selected.is_empty() {
704 println!("No packages selected.");
705 ux::print_transaction_summary(&ux::TransactionSummary {
706 command: "update".to_string(),
707 success: 0,
708 failed: 0,
709 skipped: packages_to_upgrade.len()
710 + pinned_sources.len()
711 + skipped_sources.len()
712 + up_to_date_sources.len()
713 });
714 return Ok(());
715 }
716
717 let selected_set: std::collections::HashSet<usize> =
718 selected.into_iter().collect();
719 let mut filtered = Vec::new();
720 for (idx, candidate) in packages_to_upgrade.into_iter().enumerate() {
721 if selected_set.contains(&idx) {
722 filtered.push(candidate);
723 }
724 }
725 packages_to_upgrade = filtered;
726 }
727
728 let total_download_size: u64 =
729 packages_to_upgrade.iter().map(|c| c.download_size).sum();
730 let total_installed_size_diff: i64 = packages_to_upgrade
731 .iter()
732 .map(|c| {
733 c.new_installed_size as i64
734 - c.old_manifest.installed_size.unwrap_or(0) as i64
735 })
736 .sum();
737
738 let preflight = ux::PreflightSummary::new("Update preflight")
739 .row("Candidates", packages_to_upgrade.len().to_string())
740 .row("Pinned skipped", pinned_sources.len().to_string())
741 .row("Other skipped", skipped_sources.len().to_string())
742 .row("Up-to-date", up_to_date_sources.len().to_string())
743 .row(
744 "Download size",
745 crate::pkg::utils::format_bytes(total_download_size)
746 )
747 .row(
748 "Net size",
749 crate::pkg::utils::format_size_diff(total_installed_size_diff)
750 );
751 ux::print_preflight(&preflight);
752
753 if plan_json {
754 let packages: Vec<_> = packages_to_upgrade
755 .iter()
756 .map(|c| {
757 json!({
758 "source": c.source,
759 "name": c.new_pkg.name,
760 "sub_package": c.old_manifest.sub_package,
761 "from_version": c.old_manifest.version,
762 "to_version": c.new_version,
763 "download_bytes": c.download_size,
764 "net_size_bytes": c.new_installed_size as i64 - c.old_manifest.installed_size.unwrap_or(0) as i64,
765 "advisories_old": c.old_advisories,
766 "advisories_new": c.new_advisories,
767 })
768 })
769 .collect();
770
771 let plan = json!({
772 "dry_run": dry_run,
773 "interactive": interactive,
774 "totals": {
775 "candidates": packages_to_upgrade.len(),
776 "pinned_skipped": pinned_sources.len(),
777 "other_skipped": skipped_sources.len(),
778 "up_to_date": up_to_date_sources.len(),
779 "download_bytes": total_download_size,
780 "net_size_bytes": total_installed_size_diff,
781 },
782 "pinned": pinned_sources,
783 "skipped": skipped_sources,
784 "packages": packages,
785 });
786 ux::emit_plan_json_v1("update", plan)?;
787 }
788
789 if dry_run {
790 println!(
791 "\n{} Dry-run: upgrade plan above would be executed.",
792 "::".bold().yellow()
793 );
794 ux::print_transaction_summary(&ux::TransactionSummary {
795 command: "update".to_string(),
796 success: 0,
797 failed: 0,
798 skipped: packages_to_upgrade.len()
799 + pinned_sources.len()
800 + skipped_sources.len()
801 + up_to_date_sources.len()
802 });
803 return Ok(());
804 }
805
806 println!();
807 if !crate::utils::ask_for_confirmation(
808 "Do you want to upgrade these packages?",
809 yes
810 ) {
811 ux::print_transaction_summary(&ux::TransactionSummary {
812 command: "update".to_string(),
813 success: 0,
814 failed: 0,
815 skipped: packages_to_upgrade.len()
816 + pinned_sources.len()
817 + skipped_sources.len()
818 + up_to_date_sources.len()
819 });
820 return Ok(());
821 }
822
823 let transaction = Mutex::new(transaction::begin()?);
827 let transaction_id = transaction
828 .lock()
829 .expect("failed to lock transaction")
830 .id
831 .clone();
832 let failed_updates = Mutex::new(Vec::new());
833 let successful_upgrades = Mutex::new(Vec::new());
834
835 let m = MultiProgress::new();
836
837 packages_to_upgrade
838 .par_iter()
839 .try_for_each(|candidate| -> Result<()> {
840 println!(
841 "\n{} Upgrading {} to {}...",
842 "::".bold().blue(),
843 candidate.source.cyan(),
844 candidate.new_version.green()
845 );
846
847 if let Some(hooks) = &candidate.new_pkg.hooks {
848 hooks::run_hooks(
849 hooks,
850 hooks::HookType::PreUpgrade,
851 candidate.old_manifest.scope
852 )?;
853 }
854
855 let (graph, _) = match install::resolver::resolve_dependency_graph(
856 std::slice::from_ref(&candidate.source),
857 Some(candidate.old_manifest.scope),
858 true,
859 yes,
860 false,
861 None,
862 false,
863 None
864 ) {
865 Ok(res) => res,
866 Err(e) => {
867 eprintln!(
868 "{}: Failed to resolve dependencies for '{}': {}",
869 "Error".red().bold(),
870 candidate.source,
871 e
872 );
873 failed_updates
874 .lock()
875 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
876 .push(candidate.source.clone());
877 return Ok(());
878 }
879 };
880
881 if let Err(e) =
882 zoi_install::preflight::check_policy_compliance(&graph)
883 {
884 eprintln!(
885 "{}: Policy check failed for '{}': {}",
886 "Error".red().bold(),
887 candidate.source,
888 e
889 );
890 failed_updates
891 .lock()
892 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
893 .push(candidate.source.clone());
894 return Ok(());
895 }
896
897 if let Err(e) =
898 zoi_install::preflight::check_scope_compliance(&graph)
899 {
900 eprintln!(
901 "{}: Scope check failed for '{}': {}",
902 "Error".red().bold(),
903 candidate.source,
904 e
905 );
906 failed_updates
907 .lock()
908 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
909 .push(candidate.source.clone());
910 return Ok(());
911 }
912
913 if let Err(e) =
914 zoi_install::preflight::check_zoios_compliance(&graph)
915 {
916 eprintln!(
917 "{}: ZoiOS check failed for '{}': {}",
918 "Error".red().bold(),
919 candidate.source,
920 e
921 );
922 failed_updates
923 .lock()
924 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
925 .push(candidate.source.clone());
926 return Ok(());
927 }
928
929 if let Err(e) =
930 zoi_install::preflight::check_for_vulnerabilities(&graph, yes)
931 {
932 eprintln!(
933 "{}: Security check failed for '{}': {}",
934 "Error".red().bold(),
935 candidate.source,
936 e
937 );
938 failed_updates
939 .lock()
940 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
941 .push(candidate.source.clone());
942 return Ok(());
943 }
944
945 let install_plan = match install::plan::create_install_plan(
946 &graph.nodes,
947 None,
948 false
949 ) {
950 Ok(plan) => plan,
951 Err(e) => {
952 eprintln!(
953 "{}: Failed to create install plan for '{}': {}",
954 "Error".red().bold(),
955 candidate.source,
956 e
957 );
958 failed_updates
959 .lock()
960 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
961 .push(candidate.source.clone());
962 return Ok(());
963 }
964 };
965
966 let stages = match graph.toposort() {
967 Ok(s) => s,
968 Err(e) => {
969 eprintln!(
970 "{}: Failed to sort dependency graph for '{}': {}",
971 "Error".red().bold(),
972 candidate.source,
973 e
974 );
975 failed_updates
976 .lock()
977 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
978 .push(candidate.source.clone());
979 return Ok(());
980 }
981 };
982
983 let mut new_manifest_option: Option<types::InstallManifest> = None;
984 for stage in stages {
985 for pkg_id in stage {
986 let node = graph.nodes.get(&pkg_id).ok_or_else(|| {
987 anyhow!(
988 "Package node '{pkg_id}' missing from graph \
989 during update"
990 )
991 })?;
992 if let Some(action) = install_plan.get(&pkg_id) {
993 match install::installer::install_node(
994 node,
995 action,
996 Some(&m),
997 None,
998 yes,
999 true,
1000 true,
1001 false
1002 ) {
1003 Ok(m) => {
1004 if m.name == candidate.new_pkg.name
1005 && m.sub_package
1006 == candidate.old_manifest.sub_package
1007 {
1008 new_manifest_option = Some(m);
1009 }
1010 }
1011 Err(e) => {
1012 eprintln!(
1013 "{}: Failed to upgrade '{}': {}",
1014 "Error".red().bold(),
1015 candidate.source,
1016 e
1017 );
1018 failed_updates
1019 .lock()
1020 .map_err(|e| {
1021 anyhow!("mutex poisoned: {e}")
1022 })?
1023 .push(candidate.source.clone());
1024 return Ok(());
1025 }
1026 }
1027 }
1028 }
1029 }
1030
1031 if let Some(new_manifest) = new_manifest_option {
1032 let mut tx_lock = transaction
1033 .lock()
1034 .map_err(|e| anyhow!("mutex poisoned: {e}"))?;
1035 if let Err(e) = transaction::record_operation(
1036 &mut tx_lock,
1037 types::TransactionOperation::Upgrade {
1038 old_manifest: Box::new(candidate.old_manifest.clone()),
1039 new_manifest: Box::new(new_manifest.clone())
1040 }
1041 ) {
1042 eprintln!(
1043 "Error: Failed to record transaction for {}: {}",
1044 candidate.source, e
1045 );
1046 failed_updates
1047 .lock()
1048 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
1049 .push(candidate.source.clone());
1050 } else {
1051 successful_upgrades
1052 .lock()
1053 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
1054 .push((
1055 candidate.old_manifest.clone(),
1056 new_manifest.clone(),
1057 candidate.new_pkg.clone()
1058 ));
1059 }
1060 } else {
1061 eprintln!(
1062 "Failed to get new manifest for {}",
1063 candidate.source
1064 );
1065 failed_updates
1066 .lock()
1067 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
1068 .push(candidate.source.clone());
1069 }
1070 Ok(())
1071 })?;
1072
1073 let failed = failed_updates
1074 .into_inner()
1075 .map_err(|e| anyhow!("mutex poisoned: {e}"))?;
1076 if !failed.is_empty() {
1077 eprintln!(
1078 "\nError: Some packages failed to upgrade. Rolling back all \
1079 changes..."
1080 );
1081 for pkg in &failed {
1082 eprintln!(" - {pkg}");
1083 }
1084 transaction::rollback(&transaction_id)?;
1085 ux::print_transaction_summary(&ux::TransactionSummary {
1086 command: "update".to_string(),
1087 success: 0,
1088 failed: failed.len(),
1089 skipped: pinned_sources.len()
1090 + skipped_sources.len()
1091 + up_to_date_sources.len()
1092 });
1093 return Err(anyhow!("Update failed for some packages."));
1094 }
1095
1096 if let Ok(modified_files) = transaction::get_modified_files(&transaction_id)
1097 {
1098 let modified_packages =
1099 transaction::get_modified_packages(&transaction_id)
1100 .unwrap_or_default();
1101 let upgrades_lock = successful_upgrades
1102 .lock()
1103 .map_err(|e| anyhow!("mutex poisoned: {e}"))?;
1104 let first_scope = upgrades_lock
1105 .first()
1106 .map_or(types::Scope::User, |(old, _, _)| old.scope);
1107 let _ = crate::pkg::hooks::global::run_global_hooks(
1108 crate::pkg::hooks::global::HookWhen::PostTransaction,
1109 &modified_files,
1110 &modified_packages,
1111 "upgrade",
1112 first_scope
1113 );
1114 }
1115 transaction::commit(&transaction_id)?;
1116
1117 println!("\n{}", "Success:".green());
1118 let successful_upgrades = successful_upgrades
1119 .into_inner()
1120 .map_err(|e| anyhow!("mutex poisoned: {e}"))?;
1121 for (old_manifest, new_manifest, new_pkg) in &successful_upgrades {
1122 if let Some(backup_files) = &old_manifest.backup {
1123 println!(
1124 "Restoring configuration for {}...",
1125 old_manifest.name.cyan()
1126 );
1127 let old_version_dir = local::get_package_version_dir(
1128 old_manifest.scope,
1129 &old_manifest.registry_handle,
1130 &old_manifest.repo,
1131 &old_manifest.name,
1132 &old_manifest.version
1133 )?;
1134 let new_version_dir = local::get_package_version_dir(
1135 new_manifest.scope,
1136 &new_manifest.registry_handle,
1137 &new_manifest.repo,
1138 &new_manifest.name,
1139 &new_manifest.version
1140 )?;
1141 handle_backup_files(
1142 &old_version_dir,
1143 &new_version_dir,
1144 backup_files,
1145 old_manifest.scope
1146 )?;
1147 }
1148
1149 if let Err(e) = cleanup_old_versions(
1150 &new_manifest.name,
1151 new_manifest.scope,
1152 &new_manifest.repo,
1153 &new_manifest.registry_handle
1154 ) {
1155 eprintln!(
1156 "Failed to clean up old versions for {}: {}",
1157 new_manifest.name, e
1158 );
1159 }
1160
1161 if let Ok(conn) = db::open_connection("local") {
1162 let _ = db::update_package(
1163 &conn,
1164 new_pkg,
1165 &new_manifest.registry_handle,
1166 Some(new_manifest.scope),
1167 new_manifest.sub_package.as_deref(),
1168 Some(&old_manifest.reason)
1169 );
1170 }
1171
1172 if let Some(hooks) = &new_pkg.hooks
1173 && let Err(e) = hooks::run_hooks(
1174 hooks,
1175 hooks::HookType::PostUpgrade,
1176 new_manifest.scope
1177 )
1178 {
1179 eprintln!(
1180 "{}: Post-upgrade hook failed for '{}': {}",
1181 "Error".red().bold(),
1182 new_manifest.name,
1183 e
1184 );
1185 }
1186 }
1187
1188 ux::print_transaction_summary(&ux::TransactionSummary {
1189 command: "update".to_string(),
1190 success: successful_upgrades.len(),
1191 failed: 0,
1192 skipped: pinned_sources.len()
1193 + skipped_sources.len()
1194 + up_to_date_sources.len()
1195 });
1196 println!("\n{}", "Success:".green());
1197 Ok(())
1198}
1199
1200fn advisory_counts(
1203 registry_handle: &str,
1204 package: &str,
1205 sub_package: Option<&str>,
1206 old_version: &str,
1207 new_version: &str
1208) -> Result<(usize, usize)> {
1209 let advisories =
1210 db::get_advisories_for_package(registry_handle, package, sub_package)?;
1211 let old_ver = Version::parse(old_version).map_err(|e| {
1212 anyhow!("failed to parse old version '{old_version}': {e}")
1213 })?;
1214 let new_ver = Version::parse(new_version).map_err(|e| {
1215 anyhow!("failed to parse new version '{new_version}': {e}")
1216 })?;
1217
1218 let mut old_count = 0usize;
1219 let mut new_count = 0usize;
1220 for adv in advisories {
1221 if let Ok(req) = semver::VersionReq::parse(&adv.affected_range) {
1222 if req.matches(&old_ver) {
1223 old_count += 1;
1224 }
1225 if req.matches(&new_ver) {
1226 new_count += 1;
1227 }
1228 }
1229 }
1230 Ok((old_count, new_count))
1231}
1232
1233fn cleanup_old_versions(
1236 package_name: &str,
1237 scope: types::Scope,
1238 repo: &str,
1239 registry_handle: &str
1240) -> Result<()> {
1241 let config = config::read_config()?;
1242 let rollback_enabled = config.rollback_enabled;
1243
1244 let package_dir =
1245 local::get_package_dir(scope, registry_handle, repo, package_name)?;
1246
1247 let mut versions = Vec::new();
1248 if let Ok(entries) = fs::read_dir(&package_dir) {
1249 for entry in entries.flatten() {
1250 let path = entry.path();
1251 if path.is_dir()
1252 && let Some(version_str) =
1253 path.file_name().and_then(|s| s.to_str())
1254 && version_str != "latest"
1255 && let Ok(version) = Version::parse(version_str)
1256 {
1257 versions.push(version);
1258 }
1259 }
1260 }
1261
1262 if versions.is_empty() {
1263 return Ok(());
1264 }
1265
1266 versions.sort();
1267
1268 let versions_to_keep = if rollback_enabled { 3 } else { 1 };
1269
1270 if versions.len() > versions_to_keep {
1271 let num_to_delete = versions.len() - versions_to_keep;
1272 let Some(versions_to_delete) = versions.get(..num_to_delete) else {
1273 return Ok(());
1274 };
1275
1276 println!("Cleaning up old versions...");
1277 for version in versions_to_delete {
1278 let version_dir_to_delete = package_dir.join(version.to_string());
1279 println!(" - Removing {}", version_dir_to_delete.display());
1280 if version_dir_to_delete.exists() {
1281 fs::remove_dir_all(version_dir_to_delete)?;
1282 }
1283 }
1284 }
1285
1286 Ok(())
1287}