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 )?;
285
286 install::util::check_policy_compliance(&graph)?;
287 install::util::check_scope_compliance(&graph)?;
288 for node in graph.nodes.values() {
289 if !install::util::display_updates(&node.pkg, yes)? {
290 return Err(anyhow!("Update aborted by user."));
291 }
292 }
293 install::util::check_for_vulnerabilities(&graph, yes)?;
294
295 let install_plan = install::plan::create_install_plan(&graph.nodes, None, false)?;
296
297 let stages = graph.toposort()?;
298 let mut new_manifest_option: Option<types::InstallManifest> = None;
299
300 for stage in stages {
301 for pkg_id in stage {
302 let node = graph.nodes.get(&pkg_id).ok_or_else(|| {
303 anyhow!("Package node '{}' missing from graph during update", pkg_id)
304 })?;
305 if let Some(action) = install_plan.get(&pkg_id) {
306 match install::installer::install_node(
307 node, action, None, None, yes, true, true, false,
308 ) {
309 Ok(m) => {
310 if m.name == new_pkg.name {
311 new_manifest_option = Some(m);
312 }
313 }
314 Err(e) => {
315 eprintln!("\nError: Update failed during installation. Rolling back...");
316 transaction::rollback(&transaction.id)?;
317 ux::print_transaction_summary(&ux::TransactionSummary {
318 command: "update".to_string(),
319 success: 0,
320 failed: 1,
321 skipped: 0,
322 });
323 return Err(anyhow!("Update failed: {}", e));
324 }
325 }
326 }
327 }
328 }
329
330 if let Some(new_manifest) = new_manifest_option {
331 if let Err(e) = transaction::record_operation(
332 &mut transaction,
333 types::TransactionOperation::Upgrade {
334 old_manifest: Box::new(old_manifest.clone()),
335 new_manifest: Box::new(new_manifest.clone()),
336 },
337 ) {
338 eprintln!("Warning: Failed to record transaction for update: {}", e);
339 transaction::delete_log(&transaction.id)?;
340 } else {
341 if let Ok(modified_files) = transaction::get_modified_files(&transaction.id) {
342 let modified_packages =
343 transaction::get_modified_packages(&transaction.id).unwrap_or_default();
344 let _ = crate::pkg::hooks::global::run_global_hooks(
345 crate::pkg::hooks::global::HookWhen::PostTransaction,
346 &modified_files,
347 &modified_packages,
348 "upgrade",
349 old_manifest.scope,
350 );
351 }
352 transaction::commit(&transaction.id)?;
353 }
354
355 if let Some(backup_files) = &old_manifest.backup {
356 println!("Restoring configuration files...");
357 let old_version_dir = local::get_package_version_dir(
358 old_manifest.scope,
359 &old_manifest.registry_handle,
360 &old_manifest.repo,
361 &old_manifest.name,
362 &old_manifest.version,
363 )?;
364 let new_version_dir = local::get_package_version_dir(
365 new_manifest.scope,
366 &new_manifest.registry_handle,
367 &new_manifest.repo,
368 &new_manifest.name,
369 &new_manifest.version,
370 )?;
371
372 handle_backup_files(
373 &old_version_dir,
374 &new_version_dir,
375 backup_files,
376 old_manifest.scope,
377 )?;
378 }
379
380 cleanup_old_versions(
381 &new_pkg.name,
382 old_manifest.scope,
383 &new_pkg.repo,
384 registry_handle.as_deref().unwrap_or("local"),
385 )?;
386
387 let handle = registry_handle.as_deref().unwrap_or("local");
388 if let Ok(conn) = db::open_connection("local") {
389 let _ = db::update_package(
390 &conn,
391 &new_pkg,
392 handle,
393 Some(new_pkg.scope),
394 new_pkg.sub_package.as_deref(),
395 Some(&types::InstallReason::Direct),
396 );
397 }
398
399 if let Some(hooks) = &new_pkg.hooks {
400 hooks::run_hooks(hooks, hooks::HookType::PostUpgrade, new_pkg.scope)?;
401 }
402
403 println!("\n{}", "Success:".green());
404 ux::print_transaction_summary(&ux::TransactionSummary {
405 command: "update".to_string(),
406 success: 1,
407 failed: 0,
408 skipped: 0,
409 });
410 Ok(())
411 } else {
412 eprintln!("\nError: Update failed to produce a new manifest. Rolling back...");
413 transaction::rollback(&transaction.id)?;
414 ux::print_transaction_summary(&ux::TransactionSummary {
415 command: "update".to_string(),
416 success: 0,
417 failed: 1,
418 skipped: 0,
419 });
420 Err(anyhow!("Update failed: could not get new manifest"))
421 }
422}
423
424fn run_update_all_logic(
425 yes: bool,
426 dry_run: bool,
427 explain: bool,
428 plan_json: bool,
429 interactive: bool,
430) -> Result<()> {
431 #[derive(Clone)]
432 struct UpdateCandidate {
433 source: String,
434 new_pkg: types::Package,
435 new_version: String,
436 old_manifest: types::InstallManifest,
437 old_advisories: usize,
438 new_advisories: usize,
439 download_size: u64,
440 new_installed_size: u64,
441 }
442
443 let installed_packages = local::get_installed_packages()?;
444 let mut pinned_sources = Vec::new();
445 let mut skipped_sources = Vec::new();
446 let mut up_to_date_sources = Vec::new();
447 let mut packages_to_upgrade: Vec<UpdateCandidate> = Vec::new();
448
449 println!("{} Checking for upgrades...", "::".bold().blue());
452 let pb = ProgressBar::new(installed_packages.len() as u64);
453 pb.set_style(
454 ProgressStyle::default_bar()
455 .template(
456 "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({msg})",
457 )?
458 .progress_chars("#>-"),
459 );
460 pb.set_message("Checking packages...");
461
462 for manifest in installed_packages {
463 let source = if let Some(sub) = &manifest.sub_package {
464 format!(
465 "#{}@{}/{}:{}",
466 manifest.registry_handle, manifest.repo, manifest.name, sub
467 )
468 } else {
469 format!(
470 "#{}@{}/{}",
471 manifest.registry_handle, manifest.repo, manifest.name
472 )
473 };
474
475 if pin::is_pinned(&source).unwrap_or(false)
476 || pin::is_pinned(&manifest.name).unwrap_or(false)
477 {
478 pinned_sources.push(source.clone());
479 pb.inc(1);
480 continue;
481 }
482
483 let (new_pkg, new_version, _, _, _registry_handle, _, _) =
484 match resolve::resolve_package_and_version(&source, Some(manifest.scope), true, false) {
485 Ok(result) => result,
486 Err(e) => {
487 eprintln!(
488 "{}: Failed to resolve '{}': {}",
489 "Warning".yellow(),
490 source,
491 e
492 );
493 skipped_sources.push(format!("{} ({})", source, e));
494 pb.inc(1);
495 continue;
496 }
497 };
498
499 if manifest.version == new_version && manifest.revision == new_pkg.revision {
500 up_to_date_sources.push(source.clone());
501 pb.inc(1);
502 continue;
503 }
504
505 let (old_adv, new_adv) = advisory_counts(
506 &manifest.registry_handle,
507 &manifest.name,
508 manifest.sub_package.as_deref(),
509 &manifest.version,
510 &new_version,
511 )
512 .unwrap_or((0, 0));
513
514 let (download_size, new_installed_size) =
515 install::util::get_package_sizes(&new_pkg, &manifest.registry_handle, &new_version);
516
517 packages_to_upgrade.push(UpdateCandidate {
518 source,
519 new_pkg,
520 new_version,
521 old_manifest: manifest,
522 old_advisories: old_adv,
523 new_advisories: new_adv,
524 download_size,
525 new_installed_size,
526 });
527 pb.inc(1);
528 }
529 pb.finish_and_clear();
530
531 if !pinned_sources.is_empty() {
532 println!("\n{} Pinned (skipped)", "::".bold().blue());
533 for source in &pinned_sources {
534 println!(" - {}", source.yellow());
535 }
536 }
537 if !skipped_sources.is_empty() {
538 println!("\n{} Skipped (resolve failures)", "::".bold().blue());
539 for source in &skipped_sources {
540 println!(" - {}", source.red());
541 }
542 }
543 if !packages_to_upgrade.is_empty() {
544 println!("\n{} Upgrade candidates", "::".bold().blue());
545 for candidate in &packages_to_upgrade {
546 let delta = candidate.new_advisories as i64 - candidate.old_advisories as i64;
547 let advisory_suffix = if delta > 0 {
548 format!(" (advisories +{})", delta).red().to_string()
549 } else if delta < 0 {
550 format!(" (advisories {})", delta).green().to_string()
551 } else {
552 String::new()
553 };
554
555 let old_display = if candidate.old_manifest.revision != "1" {
556 format!(
557 "{}-{}",
558 candidate.old_manifest.version, candidate.old_manifest.revision
559 )
560 } else {
561 candidate.old_manifest.version.clone()
562 };
563
564 let new_display = if candidate.new_pkg.revision != "1" {
565 format!("{}-{}", candidate.new_version, candidate.new_pkg.revision)
566 } else {
567 candidate.new_version.clone()
568 };
569
570 println!(
571 " - {}: {} -> {}{}",
572 candidate.source.cyan(),
573 old_display.yellow(),
574 new_display.green(),
575 advisory_suffix
576 );
577
578 let _ = install::util::display_updates(&candidate.new_pkg, true);
579 }
580 }
581
582 if packages_to_upgrade.is_empty() {
583 println!("\nAll packages are up to date.");
584 ux::print_transaction_summary(&ux::TransactionSummary {
585 command: "update".to_string(),
586 success: 0,
587 failed: 0,
588 skipped: pinned_sources.len() + skipped_sources.len() + up_to_date_sources.len(),
589 });
590 return Ok(());
591 }
592
593 if explain {
594 let mut report = ux::ExplainReport::new("Update explanation");
595 for candidate in &packages_to_upgrade {
596 report = report.item(
597 candidate.source.clone(),
598 format!(
599 "selected because {} -> {}",
600 candidate.old_manifest.version, candidate.new_version
601 ),
602 Vec::new(),
603 );
604 }
605 ux::print_explain(&report);
606 }
607
608 if interactive && !dry_run {
609 let items: Vec<String> = packages_to_upgrade
610 .iter()
611 .map(|c| {
612 format!(
613 "{} {} -> {}",
614 c.source, c.old_manifest.version, c.new_version
615 )
616 })
617 .collect();
618 let selected = MultiSelect::with_theme(&ColorfulTheme::default())
619 .with_prompt("Select packages to update")
620 .items(&items)
621 .interact()
622 .map_err(|e| anyhow!("Interactive selection failed: {}", e))?;
623
624 if selected.is_empty() {
625 println!("No packages selected.");
626 ux::print_transaction_summary(&ux::TransactionSummary {
627 command: "update".to_string(),
628 success: 0,
629 failed: 0,
630 skipped: packages_to_upgrade.len()
631 + pinned_sources.len()
632 + skipped_sources.len()
633 + up_to_date_sources.len(),
634 });
635 return Ok(());
636 }
637
638 let selected_set: std::collections::HashSet<usize> = selected.into_iter().collect();
639 let mut filtered = Vec::new();
640 for (idx, candidate) in packages_to_upgrade.into_iter().enumerate() {
641 if selected_set.contains(&idx) {
642 filtered.push(candidate);
643 }
644 }
645 packages_to_upgrade = filtered;
646 }
647
648 let total_download_size: u64 = packages_to_upgrade.iter().map(|c| c.download_size).sum();
649 let total_installed_size_diff: i64 = packages_to_upgrade
650 .iter()
651 .map(|c| c.new_installed_size as i64 - c.old_manifest.installed_size.unwrap_or(0) as i64)
652 .sum();
653
654 let preflight = ux::PreflightSummary::new("Update preflight")
655 .row("Candidates", packages_to_upgrade.len().to_string())
656 .row("Pinned skipped", pinned_sources.len().to_string())
657 .row("Other skipped", skipped_sources.len().to_string())
658 .row("Up-to-date", up_to_date_sources.len().to_string())
659 .row(
660 "Download size",
661 crate::pkg::utils::format_bytes(total_download_size),
662 )
663 .row(
664 "Net size",
665 crate::pkg::utils::format_size_diff(total_installed_size_diff),
666 );
667 ux::print_preflight(&preflight);
668
669 if plan_json {
670 let packages: Vec<_> = packages_to_upgrade
671 .iter()
672 .map(|c| {
673 json!({
674 "source": c.source,
675 "name": c.new_pkg.name,
676 "sub_package": c.old_manifest.sub_package,
677 "from_version": c.old_manifest.version,
678 "to_version": c.new_version,
679 "download_bytes": c.download_size,
680 "net_size_bytes": c.new_installed_size as i64 - c.old_manifest.installed_size.unwrap_or(0) as i64,
681 "advisories_old": c.old_advisories,
682 "advisories_new": c.new_advisories,
683 })
684 })
685 .collect();
686
687 let plan = json!({
688 "dry_run": dry_run,
689 "interactive": interactive,
690 "totals": {
691 "candidates": packages_to_upgrade.len(),
692 "pinned_skipped": pinned_sources.len(),
693 "other_skipped": skipped_sources.len(),
694 "up_to_date": up_to_date_sources.len(),
695 "download_bytes": total_download_size,
696 "net_size_bytes": total_installed_size_diff,
697 },
698 "pinned": pinned_sources,
699 "skipped": skipped_sources,
700 "packages": packages,
701 });
702 ux::emit_plan_json_v1("update", plan)?;
703 }
704
705 if dry_run {
706 println!(
707 "\n{} Dry-run: upgrade plan above would be executed.",
708 "::".bold().yellow()
709 );
710 ux::print_transaction_summary(&ux::TransactionSummary {
711 command: "update".to_string(),
712 success: 0,
713 failed: 0,
714 skipped: packages_to_upgrade.len()
715 + pinned_sources.len()
716 + skipped_sources.len()
717 + up_to_date_sources.len(),
718 });
719 return Ok(());
720 }
721
722 println!();
723 if !crate::utils::ask_for_confirmation("Do you want to upgrade these packages?", yes) {
724 ux::print_transaction_summary(&ux::TransactionSummary {
725 command: "update".to_string(),
726 success: 0,
727 failed: 0,
728 skipped: packages_to_upgrade.len()
729 + pinned_sources.len()
730 + skipped_sources.len()
731 + up_to_date_sources.len(),
732 });
733 return Ok(());
734 }
735
736 let transaction = Mutex::new(transaction::begin()?);
740 let transaction_id = transaction.lock().unwrap().id.clone();
741 let failed_updates = Mutex::new(Vec::new());
742 let successful_upgrades = Mutex::new(Vec::new());
743
744 let m = MultiProgress::new();
745
746 packages_to_upgrade
747 .par_iter()
748 .try_for_each(|candidate| -> Result<()> {
749 println!(
750 "\n{} Upgrading {} to {}...",
751 "::".bold().blue(),
752 candidate.source.cyan(),
753 candidate.new_version.green()
754 );
755
756 if let Some(hooks) = &candidate.new_pkg.hooks {
757 hooks::run_hooks(
758 hooks,
759 hooks::HookType::PreUpgrade,
760 candidate.old_manifest.scope,
761 )?;
762 }
763
764 let (graph, _) = match install::resolver::resolve_dependency_graph(
765 std::slice::from_ref(&candidate.source),
766 Some(candidate.old_manifest.scope),
767 true,
768 yes,
769 false,
770 None,
771 false,
772 ) {
773 Ok(res) => res,
774 Err(e) => {
775 eprintln!(
776 "{}: Failed to resolve dependencies for '{}': {}",
777 "Error".red().bold(),
778 candidate.source,
779 e
780 );
781 failed_updates
782 .lock()
783 .map_err(|e| anyhow!("mutex poisoned: {}", e))?
784 .push(candidate.source.clone());
785 return Ok(());
786 }
787 };
788
789 if let Err(e) = install::util::check_policy_compliance(&graph) {
790 eprintln!(
791 "{}: Policy check failed for '{}': {}",
792 "Error".red().bold(),
793 candidate.source,
794 e
795 );
796 failed_updates
797 .lock()
798 .map_err(|e| anyhow!("mutex poisoned: {}", e))?
799 .push(candidate.source.clone());
800 return Ok(());
801 }
802
803 if let Err(e) = install::util::check_scope_compliance(&graph) {
804 eprintln!(
805 "{}: Scope check failed for '{}': {}",
806 "Error".red().bold(),
807 candidate.source,
808 e
809 );
810 failed_updates
811 .lock()
812 .map_err(|e| anyhow!("mutex poisoned: {}", e))?
813 .push(candidate.source.clone());
814 return Ok(());
815 }
816
817 if let Err(e) = install::util::check_for_vulnerabilities(&graph, yes) {
818 eprintln!(
819 "{}: Security check failed for '{}': {}",
820 "Error".red().bold(),
821 candidate.source,
822 e
823 );
824 failed_updates
825 .lock()
826 .map_err(|e| anyhow!("mutex poisoned: {}", e))?
827 .push(candidate.source.clone());
828 return Ok(());
829 }
830
831 let install_plan = match install::plan::create_install_plan(&graph.nodes, None, false) {
832 Ok(plan) => plan,
833 Err(e) => {
834 eprintln!(
835 "{}: Failed to create install plan for '{}': {}",
836 "Error".red().bold(),
837 candidate.source,
838 e
839 );
840 failed_updates
841 .lock()
842 .map_err(|e| anyhow!("mutex poisoned: {}", e))?
843 .push(candidate.source.clone());
844 return Ok(());
845 }
846 };
847
848 let stages = match graph.toposort() {
849 Ok(s) => s,
850 Err(e) => {
851 eprintln!(
852 "{}: Failed to sort dependency graph for '{}': {}",
853 "Error".red().bold(),
854 candidate.source,
855 e
856 );
857 failed_updates
858 .lock()
859 .map_err(|e| anyhow!("mutex poisoned: {}", e))?
860 .push(candidate.source.clone());
861 return Ok(());
862 }
863 };
864
865 let mut new_manifest_option: Option<types::InstallManifest> = None;
866 for stage in stages {
867 for pkg_id in stage {
868 let node = graph.nodes.get(&pkg_id).ok_or_else(|| {
869 anyhow!("Package node '{}' missing from graph during update", pkg_id)
870 })?;
871 if let Some(action) = install_plan.get(&pkg_id) {
872 match install::installer::install_node(
873 node,
874 action,
875 Some(&m),
876 None,
877 yes,
878 true,
879 true,
880 false,
881 ) {
882 Ok(m) => {
883 if m.name == candidate.new_pkg.name
884 && m.sub_package == candidate.old_manifest.sub_package
885 {
886 new_manifest_option = Some(m);
887 }
888 }
889 Err(e) => {
890 eprintln!(
891 "{}: Failed to upgrade '{}': {}",
892 "Error".red().bold(),
893 candidate.source,
894 e
895 );
896 failed_updates
897 .lock()
898 .map_err(|e| anyhow!("mutex poisoned: {}", e))?
899 .push(candidate.source.clone());
900 return Ok(());
901 }
902 }
903 }
904 }
905 }
906
907 if let Some(new_manifest) = new_manifest_option {
908 let mut tx_lock = transaction
909 .lock()
910 .map_err(|e| anyhow!("mutex poisoned: {}", e))?;
911 if let Err(e) = transaction::record_operation(
912 &mut tx_lock,
913 types::TransactionOperation::Upgrade {
914 old_manifest: Box::new(candidate.old_manifest.clone()),
915 new_manifest: Box::new(new_manifest.clone()),
916 },
917 ) {
918 eprintln!(
919 "Error: Failed to record transaction for {}: {}",
920 candidate.source, e
921 );
922 failed_updates
923 .lock()
924 .map_err(|e| anyhow!("mutex poisoned: {}", e))?
925 .push(candidate.source.clone());
926 } else {
927 successful_upgrades
928 .lock()
929 .map_err(|e| anyhow!("mutex poisoned: {}", e))?
930 .push((
931 candidate.old_manifest.clone(),
932 new_manifest.clone(),
933 candidate.new_pkg.clone(),
934 ));
935 }
936 } else {
937 eprintln!("Failed to get new manifest for {}", candidate.source);
938 failed_updates
939 .lock()
940 .map_err(|e| anyhow!("mutex poisoned: {}", e))?
941 .push(candidate.source.clone());
942 }
943 Ok(())
944 })?;
945
946 let failed = failed_updates
947 .into_inner()
948 .map_err(|e| anyhow!("mutex poisoned: {}", e))?;
949 if !failed.is_empty() {
950 eprintln!("\nError: Some packages failed to upgrade. Rolling back all changes...");
951 for pkg in &failed {
952 eprintln!(" - {}", pkg);
953 }
954 transaction::rollback(&transaction_id)?;
955 ux::print_transaction_summary(&ux::TransactionSummary {
956 command: "update".to_string(),
957 success: 0,
958 failed: failed.len(),
959 skipped: pinned_sources.len() + skipped_sources.len() + up_to_date_sources.len(),
960 });
961 return Err(anyhow!("Update failed for some packages."));
962 }
963
964 if let Ok(modified_files) = transaction::get_modified_files(&transaction_id) {
965 let modified_packages =
966 transaction::get_modified_packages(&transaction_id).unwrap_or_default();
967 let upgrades_lock = successful_upgrades
968 .lock()
969 .map_err(|e| anyhow!("mutex poisoned: {}", e))?;
970 let first_scope = upgrades_lock
971 .first()
972 .map(|(old, _, _)| old.scope)
973 .unwrap_or(types::Scope::User);
974 let _ = crate::pkg::hooks::global::run_global_hooks(
975 crate::pkg::hooks::global::HookWhen::PostTransaction,
976 &modified_files,
977 &modified_packages,
978 "upgrade",
979 first_scope,
980 );
981 }
982 transaction::commit(&transaction_id)?;
983
984 println!("\n{}", "Success:".green());
985 let successful_upgrades = successful_upgrades
986 .into_inner()
987 .map_err(|e| anyhow!("mutex poisoned: {}", e))?;
988 for (old_manifest, new_manifest, new_pkg) in &successful_upgrades {
989 if let Some(backup_files) = &old_manifest.backup {
990 println!(
991 "Restoring configuration for {}...",
992 old_manifest.name.cyan()
993 );
994 let old_version_dir = local::get_package_version_dir(
995 old_manifest.scope,
996 &old_manifest.registry_handle,
997 &old_manifest.repo,
998 &old_manifest.name,
999 &old_manifest.version,
1000 )?;
1001 let new_version_dir = local::get_package_version_dir(
1002 new_manifest.scope,
1003 &new_manifest.registry_handle,
1004 &new_manifest.repo,
1005 &new_manifest.name,
1006 &new_manifest.version,
1007 )?;
1008 handle_backup_files(
1009 &old_version_dir,
1010 &new_version_dir,
1011 backup_files,
1012 old_manifest.scope,
1013 )?;
1014 }
1015
1016 if let Err(e) = cleanup_old_versions(
1017 &new_manifest.name,
1018 new_manifest.scope,
1019 &new_manifest.repo,
1020 &new_manifest.registry_handle,
1021 ) {
1022 eprintln!(
1023 "Failed to clean up old versions for {}: {}",
1024 new_manifest.name, e
1025 );
1026 }
1027
1028 if let Ok(conn) = db::open_connection("local") {
1029 let _ = db::update_package(
1030 &conn,
1031 new_pkg,
1032 &new_manifest.registry_handle,
1033 Some(new_manifest.scope),
1034 new_manifest.sub_package.as_deref(),
1035 Some(&old_manifest.reason),
1036 );
1037 }
1038
1039 if let Some(hooks) = &new_pkg.hooks
1040 && let Err(e) =
1041 hooks::run_hooks(hooks, hooks::HookType::PostUpgrade, new_manifest.scope)
1042 {
1043 eprintln!(
1044 "{}: Post-upgrade hook failed for '{}': {}",
1045 "Error".red().bold(),
1046 new_manifest.name,
1047 e
1048 );
1049 }
1050 }
1051
1052 ux::print_transaction_summary(&ux::TransactionSummary {
1053 command: "update".to_string(),
1054 success: successful_upgrades.len(),
1055 failed: 0,
1056 skipped: pinned_sources.len() + skipped_sources.len() + up_to_date_sources.len(),
1057 });
1058 println!("\n{}", "Success:".green());
1059 Ok(())
1060}
1061
1062fn advisory_counts(
1063 registry_handle: &str,
1064 package: &str,
1065 sub_package: Option<&str>,
1066 old_version: &str,
1067 new_version: &str,
1068) -> Result<(usize, usize)> {
1069 let advisories = db::get_advisories_for_package(registry_handle, package, sub_package)?;
1070 let old_ver = Version::parse(old_version)
1071 .map_err(|e| anyhow!("failed to parse old version '{}': {}", old_version, e))?;
1072 let new_ver = Version::parse(new_version)
1073 .map_err(|e| anyhow!("failed to parse new version '{}': {}", new_version, e))?;
1074
1075 let mut old_count = 0usize;
1076 let mut new_count = 0usize;
1077 for adv in advisories {
1078 if let Ok(req) = semver::VersionReq::parse(&adv.affected_range) {
1079 if req.matches(&old_ver) {
1080 old_count += 1;
1081 }
1082 if req.matches(&new_ver) {
1083 new_count += 1;
1084 }
1085 }
1086 }
1087 Ok((old_count, new_count))
1088}
1089
1090fn cleanup_old_versions(
1091 package_name: &str,
1092 scope: types::Scope,
1093 repo: &str,
1094 registry_handle: &str,
1095) -> Result<()> {
1096 let config = config::read_config()?;
1097 let rollback_enabled = config.rollback_enabled;
1098
1099 let package_dir = local::get_package_dir(scope, registry_handle, repo, package_name)?;
1100
1101 let mut versions = Vec::new();
1102 if let Ok(entries) = fs::read_dir(&package_dir) {
1103 for entry in entries.flatten() {
1104 let path = entry.path();
1105 if path.is_dir()
1106 && let Some(version_str) = path.file_name().and_then(|s| s.to_str())
1107 && version_str != "latest"
1108 && let Ok(version) = Version::parse(version_str)
1109 {
1110 versions.push(version);
1111 }
1112 }
1113 }
1114
1115 if versions.is_empty() {
1116 return Ok(());
1117 }
1118
1119 versions.sort();
1120
1121 let versions_to_keep = if rollback_enabled { 3 } else { 1 };
1122
1123 if versions.len() > versions_to_keep {
1124 let num_to_delete = versions.len() - versions_to_keep;
1125 let versions_to_delete = &versions[..num_to_delete];
1126
1127 println!("Cleaning up old versions...");
1128 for version in versions_to_delete {
1129 let version_dir_to_delete = package_dir.join(version.to_string());
1130 println!(" - Removing {}", version_dir_to_delete.display());
1131 if version_dir_to_delete.exists() {
1132 fs::remove_dir_all(version_dir_to_delete)?;
1133 }
1134 }
1135 }
1136
1137 Ok(())
1138}