Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

gleam / compiler-cli / src / dependencies / dependency_manager.rs
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}