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-core / src / manifest.rs
36 kB 999 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2021 The Gleam contributors 3 4use std::collections::HashMap; 5use std::fmt; 6 7use crate::Result; 8use crate::io::{make_relative, ordered_map}; 9use crate::requirement::Requirement; 10use camino::{Utf8Path, Utf8PathBuf}; 11use ecow::EcoString; 12use hexpm::version::Version; 13use itertools::Itertools; 14 15#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] 16pub struct Manifest { 17 #[serde( 18 serialize_with = "ordered_map", 19 deserialize_with = "super::config::map_with_package_name_keys::deserialize" 20 )] 21 pub requirements: HashMap<EcoString, Requirement>, 22 #[serde(serialize_with = "sorted_vec")] 23 pub packages: Vec<ManifestPackage>, 24} 25 26impl Manifest { 27 // Rather than using the toml library to do serialization we implement it 28 // manually so that we can control the formatting. 29 // We want to keep entries on a single line each so that they are more 30 // resistant to merge conflicts and are easier to fix when it does happen. 31 pub fn to_toml(&self, root_path: &Utf8Path) -> String { 32 let mut buffer = String::new(); 33 let Self { 34 requirements, 35 packages, 36 } = self; 37 38 buffer.push_str( 39 "# Do not manually edit this file, it is managed by Gleam. 40# 41# This file locks the dependency versions used, to make your build 42# deterministic and to prevent unexpected versions from being included 43# in your application. 44# 45# You should check this file into your source control repository. 46 47", 48 ); 49 50 // Packages 51 buffer.push_str("packages = [\n"); 52 for ManifestPackage { 53 name, 54 source, 55 version, 56 otp_app, 57 build_tools, 58 requirements, 59 } in packages.iter().sorted_by(|a, b| a.name.cmp(&b.name)) 60 { 61 buffer.push_str(r#" {"#); 62 buffer.push_str(r#" name = ""#); 63 buffer.push_str(name); 64 buffer.push_str(r#"", version = ""#); 65 buffer.push_str(&version.to_string()); 66 buffer.push_str(r#"", build_tools = ["#); 67 for (i, tool) in build_tools.iter().enumerate() { 68 if i != 0 { 69 buffer.push_str(", "); 70 } 71 buffer.push('"'); 72 buffer.push_str(tool); 73 buffer.push('"'); 74 } 75 76 buffer.push_str("], requirements = ["); 77 for (i, package) in requirements.iter().sorted_by(|a, b| a.cmp(b)).enumerate() { 78 if i != 0 { 79 buffer.push_str(", "); 80 } 81 buffer.push('"'); 82 buffer.push_str(package); 83 buffer.push('"'); 84 } 85 buffer.push(']'); 86 87 if let Some(app) = otp_app { 88 buffer.push_str(", otp_app = \""); 89 buffer.push_str(app); 90 buffer.push('"'); 91 } 92 93 match source { 94 ManifestPackageSource::Hex { outer_checksum } => { 95 buffer.push_str(r#", source = "hex", outer_checksum = ""#); 96 buffer.push_str(&outer_checksum.base_16_encoded_string()); 97 buffer.push('"'); 98 } 99 ManifestPackageSource::Git { repo, commit, path } => { 100 buffer.push_str(r#", source = "git", repo = ""#); 101 buffer.push_str(repo); 102 buffer.push_str(r#"", commit = ""#); 103 buffer.push_str(commit); 104 buffer.push('"'); 105 if let Some(path) = path { 106 buffer.push_str(r#", path = ""#); 107 buffer.push_str(&path.as_str().replace('\\', "/")); 108 buffer.push('"'); 109 } 110 } 111 ManifestPackageSource::Local { path } => { 112 buffer.push_str(r#", source = "local", path = ""#); 113 buffer.push_str(&make_relative(root_path, path).as_str().replace('\\', "/")); 114 buffer.push('"'); 115 } 116 } 117 118 buffer.push_str(" },\n"); 119 } 120 buffer.push_str("]\n\n"); 121 122 // Requirements 123 buffer.push_str("[requirements]\n"); 124 for (name, requirement) in requirements.iter().sorted_by(|a, b| a.0.cmp(b.0)) { 125 buffer.push_str(name); 126 buffer.push_str(" = "); 127 buffer.push_str(&requirement.to_toml(root_path)); 128 buffer.push('\n'); 129 } 130 131 buffer 132 } 133} 134 135#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 136pub struct Base16Checksum(pub Vec<u8>); 137 138impl Base16Checksum { 139 pub fn base_16_encoded_string(&self) -> String { 140 base16::encode_upper(&self.0) 141 } 142} 143 144impl serde::Serialize for Base16Checksum { 145 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 146 where 147 S: serde::Serializer, 148 { 149 serializer.serialize_str(&base16::encode_upper(&self.0)) 150 } 151} 152 153impl<'de> serde::Deserialize<'de> for Base16Checksum { 154 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 155 where 156 D: serde::Deserializer<'de>, 157 { 158 deserializer.deserialize_str(Base16ChecksumVisitor) 159 } 160} 161 162struct Base16ChecksumVisitor; 163 164impl<'de> serde::de::Visitor<'de> for Base16ChecksumVisitor { 165 type Value = Base16Checksum; 166 167 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 168 formatter.write_str("a base 16 checksum") 169 } 170 171 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> 172 where 173 E: serde::de::Error, 174 { 175 base16::decode(value) 176 .map(Base16Checksum) 177 .map_err(serde::de::Error::custom) 178 } 179} 180 181#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] 182pub struct ManifestPackage { 183 #[serde(deserialize_with = "super::config::package_name::deserialize")] 184 pub name: EcoString, 185 pub version: Version, 186 pub build_tools: Vec<EcoString>, 187 #[serde( 188 default, 189 deserialize_with = "super::config::package_name::optional_deserialize" 190 )] 191 pub otp_app: Option<EcoString>, 192 #[serde(serialize_with = "sorted_vec")] 193 pub requirements: Vec<EcoString>, 194 #[serde(flatten)] 195 pub source: ManifestPackageSource, 196} 197 198impl ManifestPackage { 199 pub fn with_build_tools(mut self, build_tools: &'static [&'static str]) -> Self { 200 self.build_tools = build_tools.iter().map(|s| (*s).into()).collect(); 201 self 202 } 203 204 pub fn application_name(&self) -> &EcoString { 205 match self.otp_app { 206 Some(ref app) => app, 207 None => &self.name, 208 } 209 } 210 211 #[inline] 212 pub fn is_hex(&self) -> bool { 213 matches!(self.source, ManifestPackageSource::Hex { .. }) 214 } 215 216 #[inline] 217 pub fn is_local(&self) -> bool { 218 matches!(self.source, ManifestPackageSource::Local { .. }) 219 } 220 221 #[inline] 222 pub fn is_git(&self) -> bool { 223 matches!(self.source, ManifestPackageSource::Git { .. }) 224 } 225} 226 227#[cfg(test)] 228impl Default for ManifestPackage { 229 fn default() -> Self { 230 Self { 231 name: EcoString::new(), 232 build_tools: vec![], 233 otp_app: None, 234 requirements: vec![], 235 version: Version::new(1, 0, 0), 236 source: ManifestPackageSource::Hex { 237 outer_checksum: Base16Checksum(vec![]), 238 }, 239 } 240 } 241} 242 243#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] 244#[serde(tag = "source")] 245pub enum ManifestPackageSource { 246 #[serde(rename = "hex")] 247 Hex { outer_checksum: Base16Checksum }, 248 #[serde(rename = "git")] 249 Git { 250 repo: EcoString, 251 commit: EcoString, 252 #[serde( 253 default, 254 skip_serializing_if = "Option::is_none", 255 deserialize_with = "super::config::package_scoped_path::optional_deserialize" 256 )] 257 path: Option<Utf8PathBuf>, 258 }, 259 #[serde(rename = "local")] 260 Local { path: Utf8PathBuf }, // should be the canonical path 261} 262 263impl ManifestPackageSource { 264 pub fn kind(&self) -> ManifestPackageSourceKind { 265 match self { 266 ManifestPackageSource::Hex { .. } => ManifestPackageSourceKind::Hex, 267 ManifestPackageSource::Git { .. } => ManifestPackageSourceKind::Git, 268 ManifestPackageSource::Local { .. } => ManifestPackageSourceKind::Local, 269 } 270 } 271} 272 273#[derive(Debug, PartialEq, Eq, Clone, Copy)] 274pub enum ManifestPackageSourceKind { 275 Hex, 276 Git, 277 Local, 278} 279 280fn sorted_vec<S, T>(value: &[T], serializer: S) -> Result<S::Ok, S::Error> 281where 282 S: serde::Serializer, 283 T: serde::Serialize + Ord, 284{ 285 use serde::Serialize; 286 let mut value: Vec<&T> = value.iter().collect(); 287 value.sort(); 288 value.serialize(serializer) 289} 290 291#[cfg(test)] 292mod tests { 293 use super::*; 294 295 #[cfg(windows)] 296 const HOME: &'static str = "C:\\home\\louis\\packages\\some_folder"; 297 298 #[cfg(not(windows))] 299 const HOME: &str = "/home/louis/packages/some_folder"; 300 301 #[cfg(windows)] 302 const PACKAGE: &'static str = "C:\\home\\louis\\packages\\path\\to\\package"; 303 304 #[cfg(not(windows))] 305 const PACKAGE: &str = "/home/louis/packages/path/to/package"; 306 307 #[cfg(windows)] 308 const PACKAGE_WITH_UNC: &'static str = "\\\\?\\C:\\home\\louis\\packages\\path\\to\\package"; 309 310 #[test] 311 fn manifest_toml_format() { 312 let manifest = Manifest { 313 requirements: [ 314 ("zzz".into(), Requirement::hex("> 0.0.0").unwrap()), 315 ("aaa".into(), Requirement::hex("> 0.0.0").unwrap()), 316 ( 317 "awsome_local2".into(), 318 Requirement::git("https://github.com/gleam-lang/gleam.git", "bd9fe02f"), 319 ), 320 ( 321 "awsome_local3".into(), 322 Requirement::git_with_path( 323 "https://github.com/gleam-lang/gleam.git", 324 "bd9fe02f", 325 "packages/sub", 326 ), 327 ), 328 ( 329 "awsome_local1".into(), 330 Requirement::path("../path/to/package"), 331 ), 332 ("gleam_stdlib".into(), Requirement::hex("~> 0.17").unwrap()), 333 ("gleeunit".into(), Requirement::hex("~> 0.1").unwrap()), 334 ] 335 .into(), 336 packages: vec![ 337 ManifestPackage { 338 name: "gleam_stdlib".into(), 339 version: Version::new(0, 17, 1), 340 build_tools: ["gleam".into()].into(), 341 otp_app: None, 342 requirements: vec![], 343 source: ManifestPackageSource::Hex { 344 outer_checksum: Base16Checksum(vec![1, 22]), 345 }, 346 }, 347 ManifestPackage { 348 name: "aaa".into(), 349 version: Version::new(0, 4, 0), 350 build_tools: ["rebar3".into(), "make".into()].into(), 351 otp_app: Some("aaa_app".into()), 352 requirements: vec!["zzz".into(), "gleam_stdlib".into()], 353 source: ManifestPackageSource::Hex { 354 outer_checksum: Base16Checksum(vec![3, 22]), 355 }, 356 }, 357 ManifestPackage { 358 name: "zzz".into(), 359 version: Version::new(0, 4, 0), 360 build_tools: ["mix".into()].into(), 361 otp_app: None, 362 requirements: vec![], 363 source: ManifestPackageSource::Hex { 364 outer_checksum: Base16Checksum(vec![3, 22]), 365 }, 366 }, 367 ManifestPackage { 368 name: "awsome_local2".into(), 369 version: Version::new(1, 2, 3), 370 build_tools: ["gleam".into()].into(), 371 otp_app: None, 372 requirements: vec![], 373 source: ManifestPackageSource::Git { 374 repo: "https://github.com/gleam-lang/gleam.git".into(), 375 commit: "bd9fe02f72250e6a136967917bcb1bdccaffa3c8".into(), 376 path: None, 377 }, 378 }, 379 ManifestPackage { 380 name: "awsome_local3".into(), 381 version: Version::new(1, 2, 3), 382 build_tools: ["gleam".into()].into(), 383 otp_app: None, 384 requirements: vec![], 385 source: ManifestPackageSource::Git { 386 repo: "https://github.com/gleam-lang/gleam.git".into(), 387 commit: "bd9fe02f72250e6a136967917bcb1bdccaffa3c8".into(), 388 path: Some("packages/sub".into()), 389 }, 390 }, 391 ManifestPackage { 392 name: "awsome_local1".into(), 393 version: Version::new(1, 2, 3), 394 build_tools: ["gleam".into()].into(), 395 otp_app: None, 396 requirements: vec![], 397 source: ManifestPackageSource::Local { 398 path: PACKAGE.into(), 399 }, 400 }, 401 ManifestPackage { 402 name: "gleeunit".into(), 403 version: Version::new(0, 4, 0), 404 build_tools: ["gleam".into()].into(), 405 otp_app: None, 406 requirements: vec!["gleam_stdlib".into()], 407 source: ManifestPackageSource::Hex { 408 outer_checksum: Base16Checksum(vec![3, 46]), 409 }, 410 }, 411 ], 412 }; 413 414 let buffer = manifest.to_toml(HOME.into()); 415 assert_eq!( 416 buffer, 417 r#"# Do not manually edit this file, it is managed by Gleam. 418# 419# This file locks the dependency versions used, to make your build 420# deterministic and to prevent unexpected versions from being included 421# in your application. 422# 423# You should check this file into your source control repository. 424 425packages = [ 426 { name = "aaa", version = "0.4.0", build_tools = ["rebar3", "make"], requirements = ["gleam_stdlib", "zzz"], otp_app = "aaa_app", source = "hex", outer_checksum = "0316" }, 427 { name = "awsome_local1", version = "1.2.3", build_tools = ["gleam"], requirements = [], source = "local", path = "../path/to/package" }, 428 { name = "awsome_local2", version = "1.2.3", build_tools = ["gleam"], requirements = [], source = "git", repo = "https://github.com/gleam-lang/gleam.git", commit = "bd9fe02f72250e6a136967917bcb1bdccaffa3c8" }, 429 { name = "awsome_local3", version = "1.2.3", build_tools = ["gleam"], requirements = [], source = "git", repo = "https://github.com/gleam-lang/gleam.git", commit = "bd9fe02f72250e6a136967917bcb1bdccaffa3c8", path = "packages/sub" }, 430 { name = "gleam_stdlib", version = "0.17.1", build_tools = ["gleam"], requirements = [], source = "hex", outer_checksum = "0116" }, 431 { name = "gleeunit", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "hex", outer_checksum = "032E" }, 432 { name = "zzz", version = "0.4.0", build_tools = ["mix"], requirements = [], source = "hex", outer_checksum = "0316" }, 433] 434 435[requirements] 436aaa = { version = "> 0.0.0" } 437awsome_local1 = { path = "../path/to/package" } 438awsome_local2 = { git = "https://github.com/gleam-lang/gleam.git", ref = "bd9fe02f" } 439awsome_local3 = { git = "https://github.com/gleam-lang/gleam.git", ref = "bd9fe02f", path = "packages/sub" } 440gleam_stdlib = { version = "~> 0.17" } 441gleeunit = { version = "~> 0.1" } 442zzz = { version = "> 0.0.0" } 443"# 444 ); 445 } 446 447 #[test] 448 fn git_package_path_with_backslashes_is_normalised() { 449 let manifest = Manifest { 450 requirements: HashMap::new(), 451 packages: vec![ManifestPackage { 452 name: "wibble".into(), 453 version: Version::new(1, 0, 0), 454 build_tools: ["gleam".into()].into(), 455 otp_app: None, 456 requirements: vec![], 457 source: ManifestPackageSource::Git { 458 repo: "https://github.com/gleam-lang/gleam.git".into(), 459 commit: "bd9fe02f72250e6a136967917bcb1bdccaffa3c8".into(), 460 path: Some("packages\\wibble".into()), 461 }, 462 }], 463 }; 464 465 let buffer = manifest.to_toml(HOME.into()); 466 assert_eq!( 467 buffer, 468 r#"# Do not manually edit this file, it is managed by Gleam. 469# 470# This file locks the dependency versions used, to make your build 471# deterministic and to prevent unexpected versions from being included 472# in your application. 473# 474# You should check this file into your source control repository. 475 476packages = [ 477 { name = "wibble", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "git", repo = "https://github.com/gleam-lang/gleam.git", commit = "bd9fe02f72250e6a136967917bcb1bdccaffa3c8", path = "packages/wibble" }, 478] 479 480[requirements] 481"# 482 ); 483 } 484 485 #[cfg(windows)] 486 #[test] 487 fn manifest_toml_format_with_unc() { 488 let manifest = Manifest { 489 requirements: [ 490 ("zzz".into(), Requirement::hex("> 0.0.0").unwrap()), 491 ("aaa".into(), Requirement::hex("> 0.0.0").unwrap()), 492 ( 493 "awsome_local2".into(), 494 Requirement::git("https://github.com/gleam-lang/gleam.git", "main"), 495 ), 496 ( 497 "awsome_local1".into(), 498 Requirement::path("../path/to/package"), 499 ), 500 ("gleam_stdlib".into(), Requirement::hex("~> 0.17").unwrap()), 501 ("gleeunit".into(), Requirement::hex("~> 0.1").unwrap()), 502 ] 503 .into(), 504 packages: vec![ 505 ManifestPackage { 506 name: "gleam_stdlib".into(), 507 version: Version::new(0, 17, 1), 508 build_tools: ["gleam".into()].into(), 509 otp_app: None, 510 requirements: vec![], 511 source: ManifestPackageSource::Hex { 512 outer_checksum: Base16Checksum(vec![1, 22]), 513 }, 514 }, 515 ManifestPackage { 516 name: "aaa".into(), 517 version: Version::new(0, 4, 0), 518 build_tools: ["rebar3".into(), "make".into()].into(), 519 otp_app: Some("aaa_app".into()), 520 requirements: vec!["zzz".into(), "gleam_stdlib".into()], 521 source: ManifestPackageSource::Hex { 522 outer_checksum: Base16Checksum(vec![3, 22]), 523 }, 524 }, 525 ManifestPackage { 526 name: "zzz".into(), 527 version: Version::new(0, 4, 0), 528 build_tools: ["mix".into()].into(), 529 otp_app: None, 530 requirements: vec![], 531 source: ManifestPackageSource::Hex { 532 outer_checksum: Base16Checksum(vec![3, 22]), 533 }, 534 }, 535 ManifestPackage { 536 name: "awsome_local2".into(), 537 version: Version::new(1, 2, 3), 538 build_tools: ["gleam".into()].into(), 539 otp_app: None, 540 requirements: vec![], 541 source: ManifestPackageSource::Git { 542 repo: "https://github.com/gleam-lang/gleam.git".into(), 543 commit: "bd9fe02f72250e6a136967917bcb1bdccaffa3c8".into(), 544 path: None, 545 }, 546 }, 547 ManifestPackage { 548 name: "awsome_local1".into(), 549 version: Version::new(1, 2, 3), 550 build_tools: ["gleam".into()].into(), 551 otp_app: None, 552 requirements: vec![], 553 source: ManifestPackageSource::Local { 554 path: PACKAGE_WITH_UNC.into(), 555 }, 556 }, 557 ManifestPackage { 558 name: "gleeunit".into(), 559 version: Version::new(0, 4, 0), 560 build_tools: ["gleam".into()].into(), 561 otp_app: None, 562 requirements: vec!["gleam_stdlib".into()], 563 source: ManifestPackageSource::Hex { 564 outer_checksum: Base16Checksum(vec![3, 46]), 565 }, 566 }, 567 ], 568 }; 569 570 let buffer = manifest.to_toml(HOME.into()); 571 assert_eq!( 572 buffer, 573 r#"# Do not manually edit this file, it is managed by Gleam. 574# 575# This file locks the dependency versions used, to make your build 576# deterministic and to prevent unexpected versions from being included 577# in your application. 578# 579# You should check this file into your source control repository. 580 581packages = [ 582 { name = "aaa", version = "0.4.0", build_tools = ["rebar3", "make"], requirements = ["gleam_stdlib", "zzz"], otp_app = "aaa_app", source = "hex", outer_checksum = "0316" }, 583 { name = "awsome_local1", version = "1.2.3", build_tools = ["gleam"], requirements = [], source = "local", path = "../path/to/package" }, 584 { name = "awsome_local2", version = "1.2.3", build_tools = ["gleam"], requirements = [], source = "git", repo = "https://github.com/gleam-lang/gleam.git", commit = "bd9fe02f72250e6a136967917bcb1bdccaffa3c8" }, 585 { name = "gleam_stdlib", version = "0.17.1", build_tools = ["gleam"], requirements = [], source = "hex", outer_checksum = "0116" }, 586 { name = "gleeunit", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "hex", outer_checksum = "032E" }, 587 { name = "zzz", version = "0.4.0", build_tools = ["mix"], requirements = [], source = "hex", outer_checksum = "0316" }, 588] 589 590[requirements] 591aaa = { version = "> 0.0.0" } 592awsome_local1 = { path = "../path/to/package" } 593awsome_local2 = { git = "https://github.com/gleam-lang/gleam.git", ref = "main" } 594gleam_stdlib = { version = "~> 0.17" } 595gleeunit = { version = "~> 0.1" } 596zzz = { version = "> 0.0.0" } 597"# 598 ); 599 } 600} 601 602#[derive(Debug)] 603pub struct Resolved { 604 pub manifest: Manifest, 605 pub package_changes: PackageChanges, 606 pub requirements_changed: bool, 607} 608 609#[derive(Debug)] 610pub struct PackageChanges { 611 pub added: Vec<(EcoString, Version)>, 612 pub changed: Vec<Changed>, 613 /// When updating git dependencies, it is possible to update to a newer commit 614 /// without updating the version of the package (which is specified in 615 /// `gleam.toml`). In this case, we still want to record the change, but it 616 /// must be stored differently. 617 pub changed_git: Vec<ChangedGit>, 618 pub removed: Vec<EcoString>, 619} 620 621#[derive(Debug, PartialEq)] 622pub struct Changed { 623 pub name: EcoString, 624 pub old: Version, 625 pub new: Version, 626} 627 628#[derive(Debug, PartialEq)] 629pub struct ChangedGit { 630 pub name: EcoString, 631 pub old_hash: EcoString, 632 pub new_hash: EcoString, 633} 634 635impl Resolved { 636 pub fn any_changes(&self) -> bool { 637 self.requirements_changed || self.package_changes.any_changes() 638 } 639 640 pub fn all_added(manifest: Manifest) -> Resolved { 641 let added = manifest 642 .packages 643 .iter() 644 .map(|package| (package.name.clone(), package.version.clone())) 645 .collect(); 646 Self { 647 manifest, 648 requirements_changed: true, 649 package_changes: PackageChanges { 650 added, 651 changed: vec![], 652 changed_git: vec![], 653 removed: vec![], 654 }, 655 } 656 } 657 658 pub fn no_change(manifest: Manifest) -> Self { 659 Self { 660 manifest, 661 requirements_changed: false, 662 package_changes: PackageChanges { 663 added: vec![], 664 changed: vec![], 665 changed_git: vec![], 666 removed: vec![], 667 }, 668 } 669 } 670} 671 672impl PackageChanges { 673 pub fn any_changes(&self) -> bool { 674 !self.added.is_empty() 675 || !self.changed.is_empty() 676 || !self.changed_git.is_empty() 677 || !self.removed.is_empty() 678 } 679 680 /// Compare the old and new versions of the manifest and determine the package changes 681 pub fn between_manifests(old: &Manifest, new: &Manifest) -> Self { 682 let mut added = vec![]; 683 let mut changed = vec![]; 684 let mut changed_git = vec![]; 685 let mut removed = vec![]; 686 let mut old: HashMap<_, _> = old 687 .packages 688 .iter() 689 .map(|package| (&package.name, package)) 690 .collect(); 691 692 for new in &new.packages { 693 match old.remove(&new.name) { 694 // If the kind of source changed, the packages bear essentially no connection 695 Some(old) if new.source.kind() != old.source.kind() => { 696 removed.push(old.name.clone()); 697 added.push((new.name.clone(), new.version.clone())); 698 } 699 Some(old) if old.version == new.version => match (&old.source, &new.source) { 700 ( 701 ManifestPackageSource::Git { 702 commit: old_hash, 703 path: old_path, 704 .. 705 }, 706 ManifestPackageSource::Git { 707 commit: new_hash, 708 path: new_path, 709 .. 710 }, 711 ) if old_hash != new_hash || old_path != new_path => { 712 changed_git.push(ChangedGit { 713 name: new.name.clone(), 714 old_hash: old_hash.clone(), 715 new_hash: new_hash.clone(), 716 }); 717 } 718 ( 719 ManifestPackageSource::Hex { .. } 720 | ManifestPackageSource::Local { .. } 721 | ManifestPackageSource::Git { .. }, 722 _, 723 ) => {} 724 }, 725 Some(old) => { 726 changed.push(Changed { 727 name: new.name.clone(), 728 old: old.version.clone(), 729 new: new.version.clone(), 730 }); 731 } 732 None => { 733 added.push((new.name.clone(), new.version.clone())); 734 } 735 } 736 } 737 738 removed.extend(old.into_keys().cloned()); 739 740 Self { 741 added, 742 changed, 743 changed_git, 744 removed, 745 } 746 } 747} 748 749#[cfg(test)] 750mod manifest_update_tests { 751 use std::collections::HashMap; 752 753 use ecow::EcoString; 754 use hexpm::version::Version; 755 756 use crate::manifest::{ 757 Base16Checksum, Changed, ChangedGit, Manifest, ManifestPackage, ManifestPackageSource, 758 PackageChanges, 759 }; 760 761 #[test] 762 fn resolved_with_updated() { 763 let package = |name: &str, version| ManifestPackage { 764 name: EcoString::from(name), 765 version, 766 build_tools: vec![], 767 otp_app: None, 768 requirements: vec![], 769 source: ManifestPackageSource::Hex { 770 outer_checksum: Base16Checksum(vec![]), 771 }, 772 }; 773 774 let old = Manifest { 775 requirements: HashMap::new(), 776 packages: vec![ 777 package("unchanged1", Version::new(3, 0, 0)), 778 package("unchanged2", Version::new(0, 1, 0)), 779 package("changed1", Version::new(3, 0, 0)), 780 package("changed2", Version::new(0, 1, 0)), 781 package("removed1", Version::new(10, 0, 0)), 782 package("removed2", Version::new(20, 1, 0)), 783 ], 784 }; 785 786 let new = Manifest { 787 requirements: HashMap::new(), 788 packages: vec![ 789 package("new1", Version::new(1, 0, 0)), 790 package("new2", Version::new(2, 1, 0)), 791 package("unchanged1", Version::new(3, 0, 0)), 792 package("unchanged2", Version::new(0, 1, 0)), 793 package("changed1", Version::new(5, 0, 0)), 794 package("changed2", Version::new(3, 0, 0)), 795 ], 796 }; 797 798 let mut changes = PackageChanges::between_manifests(&old, &new); 799 changes.added.sort(); 800 changes.changed.sort_by(|a, b| a.name.cmp(&b.name)); 801 changes.removed.sort(); 802 803 assert_eq!( 804 changes.added, 805 vec![ 806 ("new1".into(), Version::new(1, 0, 0)), 807 ("new2".into(), Version::new(2, 1, 0)), 808 ] 809 ); 810 811 assert_eq!( 812 changes.changed, 813 vec![ 814 Changed { 815 name: "changed1".into(), 816 old: Version::new(3, 0, 0), 817 new: Version::new(5, 0, 0) 818 }, 819 Changed { 820 name: "changed2".into(), 821 old: Version::new(0, 1, 0), 822 new: Version::new(3, 0, 0) 823 }, 824 ] 825 ); 826 827 assert_eq!( 828 changes.removed, 829 vec![EcoString::from("removed1"), EcoString::from("removed2")] 830 ); 831 } 832 833 #[test] 834 fn resolved_with_source_type_change() { 835 let name = EcoString::from("wibble"); 836 let version = Version::new(1, 0, 0); 837 let package = |source| ManifestPackage { 838 name: name.clone(), 839 version: version.clone(), 840 build_tools: vec![], 841 otp_app: None, 842 requirements: vec![], 843 source, 844 }; 845 846 let old = Manifest { 847 requirements: HashMap::new(), 848 packages: vec![package(ManifestPackageSource::Local { 849 path: "wibble".into(), 850 })], 851 }; 852 853 let new = Manifest { 854 requirements: HashMap::new(), 855 packages: vec![package(ManifestPackageSource::Hex { 856 outer_checksum: Base16Checksum(vec![]), 857 })], 858 }; 859 860 let changes = PackageChanges::between_manifests(&old, &new); 861 assert!(changes.changed.is_empty()); 862 assert_eq!(changes.removed, vec![name.clone()]); 863 assert_eq!(changes.added, vec![(name.clone(), version.clone())]); 864 } 865 866 #[test] 867 fn resolved_with_git_path_change_same_commit() { 868 let name = EcoString::from("wibble"); 869 let commit = EcoString::from("bd9fe02f72250e6a136967917bcb1bdccaffa3c8"); 870 let package = |path: &str| ManifestPackage { 871 name: name.clone(), 872 version: Version::new(1, 0, 0), 873 build_tools: vec![], 874 otp_app: None, 875 requirements: vec![], 876 source: ManifestPackageSource::Git { 877 repo: "https://github.com/gleam-lang/gleam.git".into(), 878 commit: commit.clone(), 879 path: Some(path.into()), 880 }, 881 }; 882 883 let old = Manifest { 884 requirements: HashMap::new(), 885 packages: vec![package("packages/a")], 886 }; 887 let new = Manifest { 888 requirements: HashMap::new(), 889 packages: vec![package("packages/b")], 890 }; 891 892 let changes = PackageChanges::between_manifests(&old, &new); 893 assert_eq!( 894 changes.changed_git, 895 vec![ChangedGit { 896 name: name.clone(), 897 old_hash: commit.clone(), 898 new_hash: commit.clone(), 899 }] 900 ); 901 assert!(changes.added.is_empty()); 902 assert!(changes.changed.is_empty()); 903 assert!(changes.removed.is_empty()); 904 } 905} 906 907#[test] 908fn invalid_requirements_dependency_name() { 909 // This is valid toml but wibble/wobble is not a valid package name 910 let toml = r#"# This file was generated by Gleam 911# You typically do not need to edit this file 912 913packages = [ 914 { name = "gleam_stdlib", version = "0.58.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "091F2D2C4A3A4E2047986C47E2C2C9D728A4E068ABB31FDA17B0D347E6248467" }, 915] 916 917[requirements] 918"wibble/wobble" = { version = ">= 0.58.0 and < 2.0.0" } 919"#; 920 921 let manifest: Result<Manifest, _> = toml::from_str(toml); 922 let error = manifest.expect_err("should fail to deserialise because invalid name"); 923 insta::assert_snapshot!(insta::internals::AutoName, error.to_string()); 924} 925 926#[test] 927fn invalid_packages_dependency_name() { 928 // This is valid toml but wibble/wobble is not a valid package name 929 let toml = r#"# This file was generated by Gleam 930# You typically do not need to edit this file 931 932packages = [ 933 { name = "wibble/wobble", version = "0.58.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "091F2D2C4A3A4E2047986C47E2C2C9D728A4E068ABB31FDA17B0D347E6248467" }, 934] 935 936[requirements] 937gleam_stdlib = { version = ">= 0.58.0 and < 2.0.0" } 938"#; 939 940 let manifest: Result<Manifest, _> = toml::from_str(toml); 941 let error = manifest.expect_err("should fail to deserialise because invalid name"); 942 insta::assert_snapshot!(insta::internals::AutoName, error.to_string()); 943} 944 945#[test] 946fn invalid_packages_otp_app_name() { 947 // This is valid toml but wibble/wobble is not a valid package name 948 let toml = r#"# This file was generated by Gleam 949# You typically do not need to edit this file 950 951packages = [ 952 { name = "gleam_stdlib", version = "0.58.0", build_tools = ["gleam"], requirements = [], otp_app = "wibble/wobble", source = "hex", outer_checksum = "091F2D2C4A3A4E2047986C47E2C2C9D728A4E068ABB31FDA17B0D347E6248467" }, 953] 954 955[requirements] 956gleam_stdlib = { version = ">= 0.58.0 and < 2.0.0" } 957"#; 958 959 let manifest: Result<Manifest, _> = toml::from_str(toml); 960 let error = manifest.expect_err("should fail to deserialise because invalid name"); 961 insta::assert_snapshot!(insta::internals::AutoName, error.to_string()); 962} 963 964#[test] 965fn git_package_with_escaping_path() { 966 let toml = r#"# This file was generated by Gleam 967# You typically do not need to edit this file 968 969packages = [ 970 { name = "wibble", version = "0.1.0", build_tools = ["gleam"], requirements = [], source = "git", repo = "https://github.com/gleam-lang/gleam.git", commit = "bd9fe02f72250e6a136967917bcb1bdccaffa3c8", path = "../escape" }, 971] 972 973[requirements] 974wibble = { git = "https://github.com/gleam-lang/gleam.git", ref = "main", path = "wibble" } 975"#; 976 977 let manifest: Result<Manifest, _> = toml::from_str(toml); 978 let error = 979 manifest.expect_err("should fail to deserialise because path escapes the repository"); 980 insta::assert_snapshot!(insta::internals::AutoName, error.to_string()); 981} 982 983#[test] 984fn no_otp_app() { 985 // This is valid toml but wibble/wobble is not a valid package name 986 let toml = r#"# This file was generated by Gleam 987# You typically do not need to edit this file 988 989packages = [ 990 { name = "gleam_stdlib", version = "0.58.0", build_tools = ["gleam"], requirements = [], source = "hex", outer_checksum = "091F2D2C4A3A4E2047986C47E2C2C9D728A4E068ABB31FDA17B0D347E6248467" }, 991] 992 993[requirements] 994gleam_stdlib = { version = ">= 0.58.0 and < 2.0.0" } 995"#; 996 997 let manifest: Result<Manifest, _> = toml::from_str(toml); 998 assert!(manifest.is_ok()); 999}