Fork of daniellemaywood.uk/gleam — Wasm codegen work
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}