1use crate::fsutil;
2use crate::{LovelyError, Result};
3use std::collections::BTreeMap;
4use std::path::Path;
5
6pub const LOCK_FILE: &str = "lovely.lock";
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct LockFile {
10 pub schema: u32,
11 pub runtime_channel: String,
12 pub love: LockedComponent,
13 pub emscripten: LockedComponent,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct LockedComponent {
18 pub source: String,
19 pub revision: String,
20 pub sha256: String,
21}
22
23impl LockFile {
24 pub fn preview_default() -> Self {
25 Self {
26 schema: 1,
27 runtime_channel: "love-11-plus".to_string(),
28 love: LockedComponent {
29 source: "https://github.com/love2d/love".to_string(),
30 revision: "main".to_string(),
31 sha256: "unresolved".to_string(),
32 },
33 emscripten: LockedComponent {
34 source: "emsdk".to_string(),
35 revision: "pinned-by-runtime-manifest".to_string(),
36 sha256: "unresolved".to_string(),
37 },
38 }
39 }
40
41 pub fn load_from(path: &Path) -> Result<Self> {
42 let text = fsutil::read_to_string(path)?;
43 Self::parse(&text)
44 }
45
46 pub fn parse(text: &str) -> Result<Self> {
47 let mut values = BTreeMap::<String, String>::new();
48 for (index, line) in text.lines().enumerate() {
49 let line = line.trim();
50 if line.is_empty() || line.starts_with('#') {
51 continue;
52 }
53 let Some((key, value)) = line.split_once('=') else {
54 return Err(LovelyError::Lock(format!(
55 "line {} is not a key/value pair",
56 index + 1
57 )));
58 };
59 values.insert(key.trim().to_string(), unquote(value.trim()));
60 }
61
62 let schema = values
63 .get("schema")
64 .and_then(|value| value.parse::<u32>().ok())
65 .unwrap_or(1);
66 Ok(Self {
67 schema,
68 runtime_channel: take(&values, "runtime_channel")?,
69 love: LockedComponent {
70 source: take(&values, "love.source")?,
71 revision: take(&values, "love.revision")?,
72 sha256: take(&values, "love.sha256")?,
73 },
74 emscripten: LockedComponent {
75 source: take(&values, "emscripten.source")?,
76 revision: take(&values, "emscripten.revision")?,
77 sha256: take(&values, "emscripten.sha256")?,
78 },
79 })
80 }
81
82 pub fn to_text(&self) -> String {
83 format!(
84 r#"# Generated by Lovely. Commit this file for reproducible builds.
85schema = {schema}
86runtime_channel = "{runtime_channel}"
87
88love.source = "{love_source}"
89love.revision = "{love_revision}"
90love.sha256 = "{love_sha}"
91
92emscripten.source = "{emscripten_source}"
93emscripten.revision = "{emscripten_revision}"
94emscripten.sha256 = "{emscripten_sha}"
95"#,
96 schema = self.schema,
97 runtime_channel = escape(&self.runtime_channel),
98 love_source = escape(&self.love.source),
99 love_revision = escape(&self.love.revision),
100 love_sha = escape(&self.love.sha256),
101 emscripten_source = escape(&self.emscripten.source),
102 emscripten_revision = escape(&self.emscripten.revision),
103 emscripten_sha = escape(&self.emscripten.sha256),
104 )
105 }
106
107 pub fn has_unresolved_checksums(&self) -> bool {
108 [&self.love, &self.emscripten]
109 .iter()
110 .any(|component| component.sha256 == "unresolved")
111 }
112}
113
114fn take(values: &BTreeMap<String, String>, key: &str) -> Result<String> {
115 values
116 .get(key)
117 .cloned()
118 .ok_or_else(|| LovelyError::Lock(format!("missing {key}")))
119}
120
121fn unquote(value: &str) -> String {
122 if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 {
123 value[1..value.len() - 1].replace("\\\"", "\"")
124 } else {
125 value.to_string()
126 }
127}
128
129fn escape(input: &str) -> String {
130 input.replace('\\', "\\\\").replace('"', "\\\"")
131}