Skip to main content

runmat_package/resolve/
update.rs

1use 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(&current_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}