Fork of daniellemaywood.uk/gleam — Wasm codegen work
18 kB
464 lines
1use std::collections::HashMap;
2
3use crate::Result;
4use crate::io::{make_relative, ordered_map};
5use crate::requirement::Requirement;
6use camino::{Utf8Path, Utf8PathBuf};
7use ecow::EcoString;
8use hexpm::version::Version;
9use itertools::Itertools;
10
11#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
12pub struct Manifest {
13 #[serde(serialize_with = "ordered_map")]
14 pub requirements: HashMap<EcoString, Requirement>,
15 #[serde(serialize_with = "sorted_vec")]
16 pub packages: Vec<ManifestPackage>,
17}
18
19impl Manifest {
20 // Rather than using the toml library to do serialization we implement it
21 // manually so that we can control the formatting.
22 // We want to keep entries on a single line each so that they are more
23 // resistant to merge conflicts and are easier to fix when it does happen.
24 pub fn to_toml(&self, root_path: &Utf8Path) -> String {
25 let mut buffer = String::new();
26 let Self {
27 requirements,
28 packages,
29 } = self;
30
31 buffer.push_str(
32 "# This file was generated by Gleam
33# You typically do not need to edit this file
34
35",
36 );
37
38 // Packages
39 buffer.push_str("packages = [\n");
40 for ManifestPackage {
41 name,
42 source,
43 version,
44 otp_app,
45 build_tools,
46 requirements,
47 } in packages.iter().sorted_by(|a, b| a.name.cmp(&b.name))
48 {
49 buffer.push_str(r#" {"#);
50 buffer.push_str(r#" name = ""#);
51 buffer.push_str(name);
52 buffer.push_str(r#"", version = ""#);
53 buffer.push_str(&version.to_string());
54 buffer.push_str(r#"", build_tools = ["#);
55 for (i, tool) in build_tools.iter().enumerate() {
56 if i != 0 {
57 buffer.push_str(", ");
58 }
59 buffer.push('"');
60 buffer.push_str(tool);
61 buffer.push('"');
62 }
63
64 buffer.push_str("], requirements = [");
65 for (i, package) in requirements.iter().sorted_by(|a, b| a.cmp(b)).enumerate() {
66 if i != 0 {
67 buffer.push_str(", ");
68 }
69 buffer.push('"');
70 buffer.push_str(package);
71 buffer.push('"');
72 }
73 buffer.push(']');
74
75 if let Some(app) = otp_app {
76 buffer.push_str(", otp_app = \"");
77 buffer.push_str(app);
78 buffer.push('"');
79 }
80
81 match source {
82 ManifestPackageSource::Hex { outer_checksum } => {
83 buffer.push_str(r#", source = "hex", outer_checksum = ""#);
84 buffer.push_str(&outer_checksum.to_string());
85 buffer.push('"');
86 }
87 ManifestPackageSource::Git { repo, commit } => {
88 buffer.push_str(r#", source = "git", repo = ""#);
89 buffer.push_str(repo);
90 buffer.push_str(r#"", commit = ""#);
91 buffer.push_str(commit);
92 buffer.push('"');
93 }
94 ManifestPackageSource::Local { path } => {
95 buffer.push_str(r#", source = "local", path = ""#);
96 buffer.push_str(&make_relative(root_path, path).as_str().replace('\\', "/"));
97 buffer.push('"');
98 }
99 };
100
101 buffer.push_str(" },\n");
102 }
103 buffer.push_str("]\n\n");
104
105 // Requirements
106 buffer.push_str("[requirements]\n");
107 for (name, requirement) in requirements.iter().sorted_by(|a, b| a.0.cmp(b.0)) {
108 buffer.push_str(name);
109 buffer.push_str(" = ");
110 buffer.push_str(&requirement.to_toml(root_path));
111 buffer.push('\n');
112 }
113
114 buffer
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
119pub struct Base16Checksum(pub Vec<u8>);
120
121impl ToString for Base16Checksum {
122 fn to_string(&self) -> String {
123 base16::encode_upper(&self.0)
124 }
125}
126
127impl serde::Serialize for Base16Checksum {
128 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
129 where
130 S: serde::Serializer,
131 {
132 serializer.serialize_str(&base16::encode_upper(&self.0))
133 }
134}
135
136impl<'de> serde::Deserialize<'de> for Base16Checksum {
137 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
138 where
139 D: serde::Deserializer<'de>,
140 {
141 let s: &str = serde::de::Deserialize::deserialize(deserializer)?;
142 base16::decode(s)
143 .map(Base16Checksum)
144 .map_err(serde::de::Error::custom)
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
149pub struct ManifestPackage {
150 pub name: EcoString,
151 pub version: Version,
152 pub build_tools: Vec<EcoString>,
153 #[serde(default)]
154 pub otp_app: Option<EcoString>,
155 #[serde(serialize_with = "sorted_vec")]
156 pub requirements: Vec<EcoString>,
157 #[serde(flatten)]
158 pub source: ManifestPackageSource,
159}
160
161impl ManifestPackage {
162 pub fn with_build_tools(mut self, build_tools: &'static [&'static str]) -> Self {
163 self.build_tools = build_tools.iter().map(|s| (*s).into()).collect();
164 self
165 }
166
167 pub fn application_name(&self) -> &EcoString {
168 match self.otp_app {
169 Some(ref app) => app,
170 None => &self.name,
171 }
172 }
173
174 #[inline]
175 pub fn is_hex(&self) -> bool {
176 matches!(self.source, ManifestPackageSource::Hex { .. })
177 }
178
179 #[inline]
180 pub fn is_local(&self) -> bool {
181 matches!(self.source, ManifestPackageSource::Local { .. })
182 }
183
184 #[inline]
185 pub fn is_git(&self) -> bool {
186 matches!(self.source, ManifestPackageSource::Git { .. })
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
191#[serde(tag = "source")]
192pub enum ManifestPackageSource {
193 #[serde(rename = "hex")]
194 Hex { outer_checksum: Base16Checksum },
195 #[serde(rename = "git")]
196 Git { repo: EcoString, commit: EcoString },
197 #[serde(rename = "local")]
198 Local { path: Utf8PathBuf }, // should be the canonical path
199}
200
201fn sorted_vec<S, T>(value: &[T], serializer: S) -> Result<S::Ok, S::Error>
202where
203 S: serde::Serializer,
204 T: serde::Serialize + Ord,
205{
206 use serde::Serialize;
207 let mut value: Vec<&T> = value.iter().collect();
208 value.sort();
209 value.serialize(serializer)
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[cfg(windows)]
217 const HOME: &'static str = "C:\\home\\louis\\packages\\some_folder";
218
219 #[cfg(windows)]
220 const PACKAGE: &'static str = "C:\\home\\louis\\packages\\path\\to\\package";
221
222 #[cfg(windows)]
223 const PACKAGE_WITH_UNC: &'static str = "\\\\?\\C:\\home\\louis\\packages\\path\\to\\package";
224
225 #[cfg(not(windows))]
226 const HOME: &str = "/home/louis/packages/some_folder";
227
228 #[cfg(not(windows))]
229 const PACKAGE: &str = "/home/louis/packages/path/to/package";
230
231 #[test]
232 fn manifest_toml_format() {
233 let manifest = Manifest {
234 requirements: [
235 ("zzz".into(), Requirement::hex("> 0.0.0").unwrap()),
236 ("aaa".into(), Requirement::hex("> 0.0.0").unwrap()),
237 (
238 "awsome_local2".into(),
239 Requirement::git("https://github.com/gleam-lang/gleam.git", "bd9fe02f"),
240 ),
241 (
242 "awsome_local1".into(),
243 Requirement::path("../path/to/package"),
244 ),
245 ("gleam_stdlib".into(), Requirement::hex("~> 0.17").unwrap()),
246 ("gleeunit".into(), Requirement::hex("~> 0.1").unwrap()),
247 ]
248 .into(),
249 packages: vec![
250 ManifestPackage {
251 name: "gleam_stdlib".into(),
252 version: Version::new(0, 17, 1),
253 build_tools: ["gleam".into()].into(),
254 otp_app: None,
255 requirements: vec![],
256 source: ManifestPackageSource::Hex {
257 outer_checksum: Base16Checksum(vec![1, 22]),
258 },
259 },
260 ManifestPackage {
261 name: "aaa".into(),
262 version: Version::new(0, 4, 0),
263 build_tools: ["rebar3".into(), "make".into()].into(),
264 otp_app: Some("aaa_app".into()),
265 requirements: vec!["zzz".into(), "gleam_stdlib".into()],
266 source: ManifestPackageSource::Hex {
267 outer_checksum: Base16Checksum(vec![3, 22]),
268 },
269 },
270 ManifestPackage {
271 name: "zzz".into(),
272 version: Version::new(0, 4, 0),
273 build_tools: ["mix".into()].into(),
274 otp_app: None,
275 requirements: vec![],
276 source: ManifestPackageSource::Hex {
277 outer_checksum: Base16Checksum(vec![3, 22]),
278 },
279 },
280 ManifestPackage {
281 name: "awsome_local2".into(),
282 version: Version::new(1, 2, 3),
283 build_tools: ["gleam".into()].into(),
284 otp_app: None,
285 requirements: vec![],
286 source: ManifestPackageSource::Git {
287 repo: "https://github.com/gleam-lang/gleam.git".into(),
288 commit: "bd9fe02f72250e6a136967917bcb1bdccaffa3c8".into(),
289 },
290 },
291 ManifestPackage {
292 name: "awsome_local1".into(),
293 version: Version::new(1, 2, 3),
294 build_tools: ["gleam".into()].into(),
295 otp_app: None,
296 requirements: vec![],
297 source: ManifestPackageSource::Local {
298 path: PACKAGE.into(),
299 },
300 },
301 ManifestPackage {
302 name: "gleeunit".into(),
303 version: Version::new(0, 4, 0),
304 build_tools: ["gleam".into()].into(),
305 otp_app: None,
306 requirements: vec!["gleam_stdlib".into()],
307 source: ManifestPackageSource::Hex {
308 outer_checksum: Base16Checksum(vec![3, 46]),
309 },
310 },
311 ],
312 };
313
314 let buffer = manifest.to_toml(HOME.into());
315 assert_eq!(
316 buffer,
317 r#"# This file was generated by Gleam
318# You typically do not need to edit this file
319
320packages = [
321 { name = "aaa", version = "0.4.0", build_tools = ["rebar3", "make"], requirements = ["gleam_stdlib", "zzz"], otp_app = "aaa_app", source = "hex", outer_checksum = "0316" },
322 { name = "awsome_local1", version = "1.2.3", build_tools = ["gleam"], requirements = [], source = "local", path = "../path/to/package" },
323 { name = "awsome_local2", version = "1.2.3", build_tools = ["gleam"], requirements = [], source = "git", repo = "https://github.com/gleam-lang/gleam.git", commit = "bd9fe02f72250e6a136967917bcb1bdccaffa3c8" },
324 { name = "gleam_stdlib", version = "0.17.1", build_tools = ["gleam"], requirements = [], source = "hex", outer_checksum = "0116" },
325 { name = "gleeunit", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "hex", outer_checksum = "032E" },
326 { name = "zzz", version = "0.4.0", build_tools = ["mix"], requirements = [], source = "hex", outer_checksum = "0316" },
327]
328
329[requirements]
330aaa = { version = "> 0.0.0" }
331awsome_local1 = { path = "../path/to/package" }
332awsome_local2 = { git = "https://github.com/gleam-lang/gleam.git", ref = "bd9fe02f" }
333gleam_stdlib = { version = "~> 0.17" }
334gleeunit = { version = "~> 0.1" }
335zzz = { version = "> 0.0.0" }
336"#
337 );
338 }
339
340 #[cfg(windows)]
341 #[test]
342 fn manifest_toml_format_with_unc() {
343 let manifest = Manifest {
344 requirements: [
345 ("zzz".into(), Requirement::hex("> 0.0.0").unwrap()),
346 ("aaa".into(), Requirement::hex("> 0.0.0").unwrap()),
347 (
348 "awsome_local2".into(),
349 Requirement::git("https://github.com/gleam-lang/gleam.git", "main"),
350 ),
351 (
352 "awsome_local1".into(),
353 Requirement::path("../path/to/package"),
354 ),
355 ("gleam_stdlib".into(), Requirement::hex("~> 0.17").unwrap()),
356 ("gleeunit".into(), Requirement::hex("~> 0.1").unwrap()),
357 ]
358 .into(),
359 packages: vec![
360 ManifestPackage {
361 name: "gleam_stdlib".into(),
362 version: Version::new(0, 17, 1),
363 build_tools: ["gleam".into()].into(),
364 otp_app: None,
365 requirements: vec![],
366 source: ManifestPackageSource::Hex {
367 outer_checksum: Base16Checksum(vec![1, 22]),
368 },
369 },
370 ManifestPackage {
371 name: "aaa".into(),
372 version: Version::new(0, 4, 0),
373 build_tools: ["rebar3".into(), "make".into()].into(),
374 otp_app: Some("aaa_app".into()),
375 requirements: vec!["zzz".into(), "gleam_stdlib".into()],
376 source: ManifestPackageSource::Hex {
377 outer_checksum: Base16Checksum(vec![3, 22]),
378 },
379 },
380 ManifestPackage {
381 name: "zzz".into(),
382 version: Version::new(0, 4, 0),
383 build_tools: ["mix".into()].into(),
384 otp_app: None,
385 requirements: vec![],
386 source: ManifestPackageSource::Hex {
387 outer_checksum: Base16Checksum(vec![3, 22]),
388 },
389 },
390 ManifestPackage {
391 name: "awsome_local2".into(),
392 version: Version::new(1, 2, 3),
393 build_tools: ["gleam".into()].into(),
394 otp_app: None,
395 requirements: vec![],
396 source: ManifestPackageSource::Git {
397 repo: "https://github.com/gleam-lang/gleam.git".into(),
398 commit: "bd9fe02f72250e6a136967917bcb1bdccaffa3c8".into(),
399 },
400 },
401 ManifestPackage {
402 name: "awsome_local1".into(),
403 version: Version::new(1, 2, 3),
404 build_tools: ["gleam".into()].into(),
405 otp_app: None,
406 requirements: vec![],
407 source: ManifestPackageSource::Local {
408 path: PACKAGE_WITH_UNC.into(),
409 },
410 },
411 ManifestPackage {
412 name: "gleeunit".into(),
413 version: Version::new(0, 4, 0),
414 build_tools: ["gleam".into()].into(),
415 otp_app: None,
416 requirements: vec!["gleam_stdlib".into()],
417 source: ManifestPackageSource::Hex {
418 outer_checksum: Base16Checksum(vec![3, 46]),
419 },
420 },
421 ],
422 };
423
424 let buffer = manifest.to_toml(HOME.into());
425 assert_eq!(
426 buffer,
427 r#"# This file was generated by Gleam
428# You typically do not need to edit this file
429
430packages = [
431 { name = "aaa", version = "0.4.0", build_tools = ["rebar3", "make"], requirements = ["gleam_stdlib", "zzz"], otp_app = "aaa_app", source = "hex", outer_checksum = "0316" },
432 { name = "awsome_local1", version = "1.2.3", build_tools = ["gleam"], requirements = [], source = "local", path = "../path/to/package" },
433 { name = "awsome_local2", version = "1.2.3", build_tools = ["gleam"], requirements = [], source = "git", repo = "https://github.com/gleam-lang/gleam.git", commit = "bd9fe02f72250e6a136967917bcb1bdccaffa3c8" },
434 { name = "gleam_stdlib", version = "0.17.1", build_tools = ["gleam"], requirements = [], source = "hex", outer_checksum = "0116" },
435 { name = "gleeunit", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "hex", outer_checksum = "032E" },
436 { name = "zzz", version = "0.4.0", build_tools = ["mix"], requirements = [], source = "hex", outer_checksum = "0316" },
437]
438
439[requirements]
440aaa = { version = "> 0.0.0" }
441awsome_local1 = { path = "../path/to/package" }
442awsome_local2 = { git = "https://github.com/gleam-lang/gleam.git", ref = "main" }
443gleam_stdlib = { version = "~> 0.17" }
444gleeunit = { version = "~> 0.1" }
445zzz = { version = "> 0.0.0" }
446"#
447 );
448 }
449
450 impl Default for ManifestPackage {
451 fn default() -> Self {
452 Self {
453 name: Default::default(),
454 build_tools: Default::default(),
455 otp_app: Default::default(),
456 requirements: Default::default(),
457 version: Version::new(1, 0, 0),
458 source: ManifestPackageSource::Hex {
459 outer_checksum: Base16Checksum(vec![]),
460 },
461 }
462 }
463 }
464}