runmat_package/resolve/
update.rs1use super::Resolution;
2use crate::{CanonicalPackageId, ContentDigest, ResolveError};
3use std::collections::{BTreeMap, BTreeSet};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum UpdatePolicy {
7 Full,
8 Packages {
9 packages: BTreeSet<CanonicalPackageId>,
10 recursive: bool,
11 },
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct UpdatePlan {
16 pub added: BTreeSet<ContentDigest>,
17 pub removed: BTreeSet<ContentDigest>,
18 pub retained: BTreeSet<ContentDigest>,
19 pub changed_packages: BTreeSet<CanonicalPackageId>,
20}
21
22pub fn plan_update(
23 current: &Resolution,
24 proposed: &Resolution,
25 policy: &UpdatePolicy,
26) -> Result<UpdatePlan, ResolveError> {
27 let current_instances = current.packages.keys().cloned().collect::<BTreeSet<_>>();
28 let proposed_instances = proposed.packages.keys().cloned().collect::<BTreeSet<_>>();
29 let current_by_package = instances_by_package(current);
30 let proposed_by_package = instances_by_package(proposed);
31 let package_ids = current_by_package
32 .keys()
33 .chain(proposed_by_package.keys())
34 .cloned()
35 .collect::<BTreeSet<_>>();
36 let changed_packages = package_ids
37 .into_iter()
38 .filter(|package| current_by_package.get(package) != proposed_by_package.get(package))
39 .collect::<BTreeSet<_>>();
40 let allowed = allowed_packages(current, proposed, policy);
41 let prohibited = changed_packages
42 .difference(&allowed)
43 .cloned()
44 .collect::<Vec<_>>();
45 if !prohibited.is_empty() {
46 return Err(ResolveError::Conflict(format!(
47 "constrained update would change frozen packages: {}",
48 prohibited
49 .iter()
50 .map(ToString::to_string)
51 .collect::<Vec<_>>()
52 .join(", ")
53 )));
54 }
55 Ok(UpdatePlan {
56 added: proposed_instances
57 .difference(¤t_instances)
58 .cloned()
59 .collect(),
60 removed: current_instances
61 .difference(&proposed_instances)
62 .cloned()
63 .collect(),
64 retained: current_instances
65 .intersection(&proposed_instances)
66 .cloned()
67 .collect(),
68 changed_packages,
69 })
70}
71
72fn instances_by_package(
73 resolution: &Resolution,
74) -> BTreeMap<CanonicalPackageId, BTreeSet<ContentDigest>> {
75 let mut result = BTreeMap::<CanonicalPackageId, BTreeSet<ContentDigest>>::new();
76 for (identity, package) in &resolution.packages {
77 result
78 .entry(package.candidate.instance.package.clone())
79 .or_default()
80 .insert(identity.clone());
81 }
82 result
83}
84
85fn allowed_packages(
86 current: &Resolution,
87 proposed: &Resolution,
88 policy: &UpdatePolicy,
89) -> BTreeSet<CanonicalPackageId> {
90 match policy {
91 UpdatePolicy::Full => current
92 .packages
93 .values()
94 .chain(proposed.packages.values())
95 .map(|package| package.candidate.instance.package.clone())
96 .collect(),
97 UpdatePolicy::Packages {
98 packages,
99 recursive: false,
100 } => packages.clone(),
101 UpdatePolicy::Packages {
102 packages,
103 recursive: true,
104 } => {
105 let mut allowed = packages.clone();
106 extend_private_descendants(current, &mut allowed);
107 extend_private_descendants(proposed, &mut allowed);
108 allowed
109 }
110 }
111}
112
113fn extend_private_descendants(resolution: &Resolution, allowed: &mut BTreeSet<CanonicalPackageId>) {
114 loop {
115 let mut changed = false;
116 for edge in &resolution.edges {
117 let Some(from) = &edge.from else {
118 continue;
119 };
120 let from_package = &resolution.packages[from].candidate.instance.package;
121 let to_package = &resolution.packages[&edge.to].candidate.instance.package;
122 if !allowed.contains(from_package) || allowed.contains(to_package) {
123 continue;
124 }
125 let externally_shared = resolution.edges.iter().any(|incoming| {
126 incoming.to == edge.to
127 && incoming.from.as_ref().is_none_or(|owner| {
128 !allowed.contains(&resolution.packages[owner].candidate.instance.package)
129 })
130 });
131 if !externally_shared {
132 changed |= allowed.insert(to_package.clone());
133 }
134 }
135 if !changed {
136 break;
137 }
138 }
139}