Fork of daniellemaywood.uk/gleam — Wasm codegen work
4.4 kB
127 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2025 The Gleam contributors
3
4use crate::manifest::Manifest;
5use crate::requirement::Requirement;
6use ecow::EcoString;
7use hexpm::version::Version;
8use std::collections::{HashMap, HashSet};
9
10#[derive(Debug)]
11pub struct StalePackageRemover<'a> {
12 // These are the packages for which the requirement or their parents
13 // requirement has not changed.
14 fresh: HashSet<&'a str>,
15 locked: HashMap<EcoString, &'a Vec<EcoString>>,
16}
17
18impl<'a> StalePackageRemover<'a> {
19 pub fn fresh_and_locked(
20 requirements: &'a HashMap<EcoString, Requirement>,
21 manifest: &'a Manifest,
22 ) -> HashMap<EcoString, Version> {
23 let locked = manifest
24 .packages
25 .iter()
26 .map(|p| (p.name.clone(), &p.requirements))
27 .collect();
28 Self {
29 fresh: HashSet::new(),
30 locked,
31 }
32 .run(requirements, manifest)
33 }
34
35 fn run(
36 &mut self,
37 requirements: &'a HashMap<EcoString, Requirement>,
38 manifest: &'a Manifest,
39 ) -> HashMap<EcoString, Version> {
40 // Record all the requirements that have not changed
41 for (name, requirement) in requirements {
42 if manifest.requirements.get(name) != Some(requirement) {
43 continue; // This package has changed, don't record it
44 }
45
46 // Recursively record the package and its deps as being fresh
47 self.record_tree_fresh(name);
48 }
49
50 // Return all the previously resolved packages that have not been
51 // recorded as fresh
52 manifest
53 .packages
54 .iter()
55 .filter(|package| {
56 let new = requirements.contains_key(package.name.as_str())
57 && !manifest.requirements.contains_key(package.name.as_str());
58 let fresh = self.fresh.contains(package.name.as_str());
59 let locked = !new && fresh;
60 if !locked {
61 tracing::info!(name = package.name.as_str(), "unlocking_stale_package");
62 }
63 locked
64 })
65 .map(|package| (package.name.clone(), package.version.clone()))
66 .collect()
67 }
68
69 fn record_tree_fresh(&mut self, name: &'a str) {
70 // Record the top level package
71 let _ = self.fresh.insert(name);
72
73 let Some(deps) = self.locked.get(name) else {
74 // If the package is not in the manifest then it means that the package is an optional
75 // dependency that has not been included. That or someone has been editing the manifest
76 // and broken it, but let's hope that's not the case.
77 return;
78 };
79
80 // Record each of its deps recursively
81 for package in *deps {
82 self.record_tree_fresh(package);
83 }
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::manifest::{Base16Checksum, Manifest, ManifestPackage, ManifestPackageSource};
91 use crate::requirement::Requirement;
92 use hexpm::version::{Range, Version};
93 use std::collections::HashMap;
94
95 // https://github.com/gleam-lang/gleam/issues/4152
96 #[test]
97 fn optional_package_not_in_manifest() {
98 let requirements = HashMap::from_iter([(
99 "required_package".into(),
100 Requirement::Hex {
101 version: Range::new("1.0.0".into()).unwrap(),
102 },
103 )]);
104 let manifest = Manifest {
105 requirements: requirements.clone(),
106 packages: vec![ManifestPackage {
107 name: "required_package".into(),
108 version: Version::new(1, 0, 0),
109 build_tools: vec!["gleam".into()],
110 otp_app: None,
111 requirements: vec![
112 // NOTE: this package isn't in the manifest. This will have been because it is
113 // an optional dep of `required_package`.
114 "optional_package".into(),
115 ],
116 source: ManifestPackageSource::Hex {
117 outer_checksum: Base16Checksum(vec![]),
118 },
119 }],
120 };
121
122 assert_eq!(
123 StalePackageRemover::fresh_and_locked(&requirements, &manifest),
124 HashMap::from_iter([("required_package".into(), Version::new(1, 0, 0))])
125 );
126 }
127}