Fork of daniellemaywood.uk/gleam — Wasm codegen work
17 kB
459 lines
1use std::collections::HashMap;
2
3use crate::io::{make_relative, ordered_map};
4use crate::requirement::Requirement;
5use crate::Result;
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
185#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
186#[serde(tag = "source")]
187pub enum ManifestPackageSource {
188 #[serde(rename = "hex")]
189 Hex { outer_checksum: Base16Checksum },
190 #[serde(rename = "git")]
191 Git { repo: EcoString, commit: EcoString },
192 #[serde(rename = "local")]
193 Local { path: Utf8PathBuf }, // should be the canonical path
194}
195
196fn sorted_vec<S, T>(value: &[T], serializer: S) -> Result<S::Ok, S::Error>
197where
198 S: serde::Serializer,
199 T: serde::Serialize + Ord,
200{
201 use serde::Serialize;
202 let mut value: Vec<&T> = value.iter().collect();
203 value.sort();
204 value.serialize(serializer)
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[cfg(windows)]
212 const HOME: &'static str = "C:\\home\\louis\\packages\\some_folder";
213
214 #[cfg(windows)]
215 const PACKAGE: &'static str = "C:\\home\\louis\\packages\\path\\to\\package";
216
217 #[cfg(windows)]
218 const PACKAGE_WITH_UNC: &'static str = "\\\\?\\C:\\home\\louis\\packages\\path\\to\\package";
219
220 #[cfg(not(windows))]
221 const HOME: &str = "/home/louis/packages/some_folder";
222
223 #[cfg(not(windows))]
224 const PACKAGE: &str = "/home/louis/packages/path/to/package";
225
226 #[test]
227 fn manifest_toml_format() {
228 let manifest = Manifest {
229 requirements: [
230 ("zzz".into(), Requirement::hex("> 0.0.0")),
231 ("aaa".into(), Requirement::hex("> 0.0.0")),
232 (
233 "awsome_local2".into(),
234 Requirement::git("https://github.com/gleam-lang/gleam.git"),
235 ),
236 (
237 "awsome_local1".into(),
238 Requirement::path("../path/to/package"),
239 ),
240 ("gleam_stdlib".into(), Requirement::hex("~> 0.17")),
241 ("gleeunit".into(), Requirement::hex("~> 0.1")),
242 ]
243 .into(),
244 packages: vec![
245 ManifestPackage {
246 name: "gleam_stdlib".into(),
247 version: Version::new(0, 17, 1),
248 build_tools: ["gleam".into()].into(),
249 otp_app: None,
250 requirements: vec![],
251 source: ManifestPackageSource::Hex {
252 outer_checksum: Base16Checksum(vec![1, 22]),
253 },
254 },
255 ManifestPackage {
256 name: "aaa".into(),
257 version: Version::new(0, 4, 0),
258 build_tools: ["rebar3".into(), "make".into()].into(),
259 otp_app: Some("aaa_app".into()),
260 requirements: vec!["zzz".into(), "gleam_stdlib".into()],
261 source: ManifestPackageSource::Hex {
262 outer_checksum: Base16Checksum(vec![3, 22]),
263 },
264 },
265 ManifestPackage {
266 name: "zzz".into(),
267 version: Version::new(0, 4, 0),
268 build_tools: ["mix".into()].into(),
269 otp_app: None,
270 requirements: vec![],
271 source: ManifestPackageSource::Hex {
272 outer_checksum: Base16Checksum(vec![3, 22]),
273 },
274 },
275 ManifestPackage {
276 name: "awsome_local2".into(),
277 version: Version::new(1, 2, 3),
278 build_tools: ["gleam".into()].into(),
279 otp_app: None,
280 requirements: vec![],
281 source: ManifestPackageSource::Git {
282 repo: "https://github.com/gleam-lang/gleam.git".into(),
283 commit: "bd9fe02f72250e6a136967917bcb1bdccaffa3c8".into(),
284 },
285 },
286 ManifestPackage {
287 name: "awsome_local1".into(),
288 version: Version::new(1, 2, 3),
289 build_tools: ["gleam".into()].into(),
290 otp_app: None,
291 requirements: vec![],
292 source: ManifestPackageSource::Local {
293 path: PACKAGE.into(),
294 },
295 },
296 ManifestPackage {
297 name: "gleeunit".into(),
298 version: Version::new(0, 4, 0),
299 build_tools: ["gleam".into()].into(),
300 otp_app: None,
301 requirements: vec!["gleam_stdlib".into()],
302 source: ManifestPackageSource::Hex {
303 outer_checksum: Base16Checksum(vec![3, 46]),
304 },
305 },
306 ],
307 };
308
309 let buffer = manifest.to_toml(HOME.into());
310 assert_eq!(
311 buffer,
312 r#"# This file was generated by Gleam
313# You typically do not need to edit this file
314
315packages = [
316 { name = "aaa", version = "0.4.0", build_tools = ["rebar3", "make"], requirements = ["gleam_stdlib", "zzz"], otp_app = "aaa_app", source = "hex", outer_checksum = "0316" },
317 { name = "awsome_local1", version = "1.2.3", build_tools = ["gleam"], requirements = [], source = "local", path = "../path/to/package" },
318 { name = "awsome_local2", version = "1.2.3", build_tools = ["gleam"], requirements = [], source = "git", repo = "https://github.com/gleam-lang/gleam.git", commit = "bd9fe02f72250e6a136967917bcb1bdccaffa3c8" },
319 { name = "gleam_stdlib", version = "0.17.1", build_tools = ["gleam"], requirements = [], source = "hex", outer_checksum = "0116" },
320 { name = "gleeunit", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "hex", outer_checksum = "032E" },
321 { name = "zzz", version = "0.4.0", build_tools = ["mix"], requirements = [], source = "hex", outer_checksum = "0316" },
322]
323
324[requirements]
325aaa = { version = "> 0.0.0" }
326awsome_local1 = { path = "../path/to/package" }
327awsome_local2 = { git = "https://github.com/gleam-lang/gleam.git" }
328gleam_stdlib = { version = "~> 0.17" }
329gleeunit = { version = "~> 0.1" }
330zzz = { version = "> 0.0.0" }
331"#
332 );
333 }
334
335 #[cfg(windows)]
336 #[test]
337 fn manifest_toml_format_with_unc() {
338 let manifest = Manifest {
339 requirements: [
340 ("zzz".into(), Requirement::hex("> 0.0.0")),
341 ("aaa".into(), Requirement::hex("> 0.0.0")),
342 (
343 "awsome_local2".into(),
344 Requirement::git("https://github.com/gleam-lang/gleam.git"),
345 ),
346 (
347 "awsome_local1".into(),
348 Requirement::path("../path/to/package"),
349 ),
350 ("gleam_stdlib".into(), Requirement::hex("~> 0.17")),
351 ("gleeunit".into(), Requirement::hex("~> 0.1")),
352 ]
353 .into(),
354 packages: vec![
355 ManifestPackage {
356 name: "gleam_stdlib".into(),
357 version: Version::new(0, 17, 1),
358 build_tools: ["gleam".into()].into(),
359 otp_app: None,
360 requirements: vec![],
361 source: ManifestPackageSource::Hex {
362 outer_checksum: Base16Checksum(vec![1, 22]),
363 },
364 },
365 ManifestPackage {
366 name: "aaa".into(),
367 version: Version::new(0, 4, 0),
368 build_tools: ["rebar3".into(), "make".into()].into(),
369 otp_app: Some("aaa_app".into()),
370 requirements: vec!["zzz".into(), "gleam_stdlib".into()],
371 source: ManifestPackageSource::Hex {
372 outer_checksum: Base16Checksum(vec![3, 22]),
373 },
374 },
375 ManifestPackage {
376 name: "zzz".into(),
377 version: Version::new(0, 4, 0),
378 build_tools: ["mix".into()].into(),
379 otp_app: None,
380 requirements: vec![],
381 source: ManifestPackageSource::Hex {
382 outer_checksum: Base16Checksum(vec![3, 22]),
383 },
384 },
385 ManifestPackage {
386 name: "awsome_local2".into(),
387 version: Version::new(1, 2, 3),
388 build_tools: ["gleam".into()].into(),
389 otp_app: None,
390 requirements: vec![],
391 source: ManifestPackageSource::Git {
392 repo: "https://github.com/gleam-lang/gleam.git".into(),
393 commit: "bd9fe02f72250e6a136967917bcb1bdccaffa3c8".into(),
394 },
395 },
396 ManifestPackage {
397 name: "awsome_local1".into(),
398 version: Version::new(1, 2, 3),
399 build_tools: ["gleam".into()].into(),
400 otp_app: None,
401 requirements: vec![],
402 source: ManifestPackageSource::Local {
403 path: PACKAGE_WITH_UNC.into(),
404 },
405 },
406 ManifestPackage {
407 name: "gleeunit".into(),
408 version: Version::new(0, 4, 0),
409 build_tools: ["gleam".into()].into(),
410 otp_app: None,
411 requirements: vec!["gleam_stdlib".into()],
412 source: ManifestPackageSource::Hex {
413 outer_checksum: Base16Checksum(vec![3, 46]),
414 },
415 },
416 ],
417 };
418
419 let buffer = manifest.to_toml(HOME.into());
420 assert_eq!(
421 buffer,
422 r#"# This file was generated by Gleam
423# You typically do not need to edit this file
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 = "gleam_stdlib", version = "0.17.1", build_tools = ["gleam"], requirements = [], source = "hex", outer_checksum = "0116" },
430 { name = "gleeunit", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "hex", outer_checksum = "032E" },
431 { name = "zzz", version = "0.4.0", build_tools = ["mix"], requirements = [], source = "hex", outer_checksum = "0316" },
432]
433
434[requirements]
435aaa = { version = "> 0.0.0" }
436awsome_local1 = { path = "../path/to/package" }
437awsome_local2 = { git = "https://github.com/gleam-lang/gleam.git" }
438gleam_stdlib = { version = "~> 0.17" }
439gleeunit = { version = "~> 0.1" }
440zzz = { version = "> 0.0.0" }
441"#
442 );
443 }
444
445 impl Default for ManifestPackage {
446 fn default() -> Self {
447 Self {
448 name: Default::default(),
449 build_tools: Default::default(),
450 otp_app: Default::default(),
451 requirements: Default::default(),
452 version: Version::new(1, 0, 0),
453 source: ManifestPackageSource::Hex {
454 outer_checksum: Base16Checksum(vec![]),
455 },
456 }
457 }
458 }
459}