Fork of daniellemaywood.uk/gleam — Wasm codegen work
12 kB
327 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2025 The Gleam contributors
3
4use ecow::EcoString;
5use futures::future;
6use gleam_core::{
7 Error, Result,
8 build::{Mode, Telemetry},
9 config::PackageConfig,
10 dependency,
11 manifest::{Manifest, ManifestPackageSource, PackageChanges, Resolved},
12 paths::ProjectPaths,
13 requirement::Requirement,
14};
15use std::collections::HashMap;
16
17use crate::{
18 build_lock::BuildLock,
19 dependencies::{pretty_print_major_versions_available, write_manifest_to_disc},
20 fs::ProjectIO,
21};
22
23use super::{
24 CheckMajorVersions, LocalPackages, UseManifest, add_missing_packages, is_same_requirements,
25 lookup_package, path_dependency_configs_unchanged, provide_git_package, provide_local_package,
26 read_manifest_from_disc, remove_extra_packages, unlock_packages,
27};
28
29/// Verifies that all specified packages exist in the manifest.
30pub fn ensure_packages_exist_locally(manifest: &Manifest, packages: &[EcoString]) -> Result<()> {
31 let missing_packages: Vec<EcoString> = packages
32 .iter()
33 .filter(|package_name| !manifest.packages.iter().any(|p| &p.name == *package_name))
34 .cloned()
35 .collect();
36
37 if !missing_packages.is_empty() {
38 return Err(Error::PackagesToUpdateNotExist {
39 packages: missing_packages,
40 });
41 }
42 Ok(())
43}
44
45pub struct DependencyManagerConfig {
46 // If `Yes` we read the manifest from disc. If not set then we ignore any
47 // manifest which will result in the latest versions of the dependency
48 // packages being resolved (not the locked ones).
49 pub use_manifest: UseManifest,
50 /// When set to `Yes`, the cli will check for major version updates of direct dependencies and
51 /// print them to the console if the major versions are not upgradeable due to constraints.
52 pub check_major_versions: CheckMajorVersions,
53}
54
55impl DependencyManagerConfig {
56 pub fn into_dependency_manager<Telem: Telemetry, P: dependency::PackageFetcher>(
57 self,
58 runtime: tokio::runtime::Handle,
59 package_fetcher: P,
60 telemetry: Telem,
61 mode: Mode,
62 ) -> DependencyManager<Telem, P> {
63 DependencyManager {
64 runtime,
65 package_fetcher,
66 telemetry,
67
68 mode,
69 use_manifest: self.use_manifest,
70 check_major_versions: self.check_major_versions,
71 }
72 }
73}
74
75pub struct DependencyManager<Telem, P> {
76 runtime: tokio::runtime::Handle,
77 package_fetcher: P,
78 mode: Mode,
79 use_manifest: UseManifest,
80 telemetry: Telem,
81 check_major_versions: CheckMajorVersions,
82}
83
84impl<Telem, P> DependencyManager<Telem, P>
85where
86 P: dependency::PackageFetcher,
87 Telem: Telemetry,
88{
89 /// Resolve the dependency versions used by a package.
90 ///
91 /// If the `use_manifest` configuration was set to `false` then it'll always resolve all the
92 /// versions, even if there are already versions locked in the manifest.
93 pub fn resolve_versions(
94 &self,
95 paths: &ProjectPaths,
96 config: &PackageConfig,
97 packages_to_update: Vec<EcoString>,
98 ) -> Result<Resolved> {
99 // If there's no manifest then the only thing we can do is attempt to update the versions.
100 if !paths.manifest().exists() {
101 tracing::debug!("manifest_not_present");
102 let manifest = self.perform_version_resolution(paths, config, None, Vec::new())?;
103 ensure_packages_exist_locally(&manifest, &packages_to_update)?;
104 return Ok(Resolved::all_added(manifest));
105 }
106
107 let existing_manifest = read_manifest_from_disc(paths)?;
108 ensure_packages_exist_locally(&existing_manifest, &packages_to_update)?;
109
110 // If we have been asked not to use the manifest then
111 let (requirements_changed, manifest_for_resolver) = match self.use_manifest {
112 UseManifest::No => (true, None),
113 UseManifest::Yes => {
114 let config_dependencies = config.all_direct_dependencies()?;
115 let same_requirements = is_same_requirements(
116 &existing_manifest.requirements,
117 &config_dependencies,
118 paths.root(),
119 )?;
120
121 // If the manifest is to be used and the requirements have not changed then there's
122 // no point in performing resolution, it'll always result in the same versions
123 // already specified in the manifest.
124 if packages_to_update.is_empty()
125 && same_requirements
126 && path_dependency_configs_unchanged(&config_dependencies, paths)?
127 {
128 return Ok(Resolved::no_change(existing_manifest));
129 }
130
131 // Otherwise, use the manifest to inform resolution.
132 (!same_requirements, Some(&existing_manifest))
133 }
134 };
135
136 tracing::debug!("manifest_outdated");
137 let new_manifest = self.perform_version_resolution(
138 paths,
139 config,
140 manifest_for_resolver,
141 packages_to_update,
142 )?;
143 let resolved = Resolved {
144 package_changes: PackageChanges::between_manifests(&existing_manifest, &new_manifest),
145 manifest: new_manifest,
146 requirements_changed,
147 };
148 Ok(resolved)
149 }
150
151 pub fn resolve_and_download_versions(
152 &self,
153 paths: &ProjectPaths,
154 new_package: Option<(Vec<(EcoString, Requirement)>, bool)>,
155 packages_to_update: Vec<EcoString>,
156 ) -> Result<Manifest> {
157 let span = tracing::info_span!("download_deps");
158 let _enter = span.enter();
159
160 // We do this before acquiring the build lock so that we don't create the
161 // build directory if there is no gleam.toml
162 crate::config::ensure_config_exists(paths)?;
163
164 let lock = BuildLock::new_packages(paths)?;
165 let _guard = lock.lock(&self.telemetry);
166
167 let fs = ProjectIO::boxed();
168
169 // Read the project config
170 let mut config = crate::config::read(paths.root_config())?;
171 let project_name = config.name.clone();
172
173 // Insert the new packages to add, if it exists
174 if let Some((packages, dev)) = new_package {
175 for (package, requirement) in packages {
176 if dev {
177 _ = config.dev_dependencies.insert(package, requirement);
178 } else {
179 _ = config.dependencies.insert(package, requirement);
180 }
181 }
182 }
183
184 // Determine what versions we need
185 let resolved = self.resolve_versions(paths, &config, packages_to_update)?;
186 let local = LocalPackages::read_from_disc(paths)?;
187
188 // Remove any packages that are no longer required due to gleam.toml changes
189 remove_extra_packages(paths, &local, &resolved.manifest, &self.telemetry)?;
190
191 // Download them from Hex to the local cache
192 self.runtime.block_on(add_missing_packages(
193 paths,
194 fs,
195 &resolved.manifest,
196 &local,
197 project_name,
198 &self.telemetry,
199 ))?;
200
201 if resolved.any_changes() {
202 // Record new state of the packages directory
203 // TODO: test
204 tracing::debug!("writing_manifest_toml");
205 write_manifest_to_disc(paths, &resolved.manifest)?;
206 }
207 LocalPackages::from_manifest(&resolved.manifest).write_to_disc(paths)?;
208
209 // Display the changes in versions to the user.
210 self.telemetry
211 .resolved_package_versions(&resolved.package_changes);
212
213 // If requested to do so, check if there are major upgrades that could
214 // be performed with more relaxed version requirements, and inform the
215 // user if so.
216 if let CheckMajorVersions::Yes = self.check_major_versions {
217 let major_versions_available = dependency::check_for_major_version_updates(
218 &resolved.manifest,
219 &self.package_fetcher,
220 );
221 if !major_versions_available.is_empty() {
222 eprintln!(
223 "{}",
224 pretty_print_major_versions_available(major_versions_available)
225 );
226 }
227 }
228 Ok(resolved.manifest)
229 }
230
231 fn perform_version_resolution(
232 &self,
233 project_paths: &ProjectPaths,
234 config: &PackageConfig,
235 manifest: Option<&Manifest>,
236 packages_to_update: Vec<EcoString>,
237 ) -> Result<Manifest, Error> {
238 self.telemetry.resolving_package_versions();
239 let dependencies = config.dependencies_for(self.mode)?;
240 let mut locked = config.locked(manifest)?;
241
242 if !packages_to_update.is_empty() {
243 unlock_packages(&mut locked, &packages_to_update, manifest)?;
244 }
245
246 // Packages which are provided directly instead of downloaded from hex
247 let mut provided_packages = HashMap::new();
248 // The version requires of the current project
249 let mut root_requirements = HashMap::new();
250
251 // Populate the provided_packages and root_requirements maps
252 for (name, requirement) in dependencies.into_iter() {
253 let version = match requirement {
254 Requirement::Hex { version } => version,
255 Requirement::Path { path } => provide_local_package(
256 name.clone(),
257 &path,
258 project_paths.root(),
259 project_paths,
260 &mut provided_packages,
261 &mut vec![],
262 )?,
263 Requirement::Git { git, ref_, path } => {
264 // If this package is locked and we already resolved a commit
265 // hash for it, we want to use that hash rather than pulling
266 // the latest commit.
267 let ref_to_use = if locked.contains_key(&name)
268 && let Some(manifest) = manifest
269 && let Some(package) = manifest
270 .packages
271 .iter()
272 .find(|package| package.name == name)
273 && let ManifestPackageSource::Git { commit, .. } = &package.source
274 {
275 commit
276 } else {
277 // If the package is unlocked or we haven't resolved a version yet, we use
278 // the ref specified in `gleam.toml`.
279 &ref_
280 };
281
282 provide_git_package(
283 name.clone(),
284 &git,
285 ref_to_use,
286 path,
287 project_paths,
288 &mut provided_packages,
289 &mut Vec::new(),
290 )?
291 }
292 };
293 let _ = root_requirements.insert(name, version);
294 }
295
296 // Convert provided packages into hex packages for pub-grub resolve
297 let provided_hex_packages = provided_packages
298 .iter()
299 .map(|(name, package)| (name.clone(), package.to_hex_package(name)))
300 .collect();
301
302 let resolved = dependency::resolve_versions(
303 &self.package_fetcher,
304 provided_hex_packages,
305 config.name.clone(),
306 root_requirements.into_iter(),
307 &locked,
308 )?;
309
310 // Convert the hex packages and local packages into manifest packages
311 let credentials = crate::hex::read_env_readonly_api_key();
312 let manifest_packages =
313 self.runtime
314 .block_on(future::try_join_all(resolved.into_iter().map(
315 |(name, version)| {
316 lookup_package(name, version, &provided_packages, credentials.as_ref())
317 },
318 )))?;
319
320 let manifest = Manifest {
321 packages: manifest_packages,
322 requirements: config.all_direct_dependencies()?,
323 };
324
325 Ok(manifest)
326 }
327}