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