Skip to main content

assay/install/
lock.rs

1//! `Manifest.lock` writer.
2//!
3//! The lockfile is itself a Lua return-table (matches the plan's
4//! "default to Lua return-table for consistency" decision in the open
5//! questions section). Sha256 maps are emitted with sorted keys so
6//! lockfile diffs are stable across re-installs.
7//!
8//! Timestamps are intentionally omitted: a lockfile that changes every
9//! re-install is noisy in version control and complicates "did anything
10//! change?" diffs.
11
12use std::collections::BTreeMap;
13use std::fmt::Write;
14
15use super::fetch::FetchPlan;
16use super::manifest::{Extension, Lib};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct Lockfile {
20    pub assay: Option<String>,
21    pub extensions: Vec<LockExtension>,
22    pub libs: Vec<LockLib>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct LockExtension {
27    pub name: String,
28    pub version: String,
29    pub url: String,
30    /// Sorted by arch token for stable diffs.
31    pub sha256: BTreeMap<String, String>,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct LockLib {
36    pub name: String,
37    pub version: String,
38    pub url: String,
39    pub sha256: String,
40}
41
42impl LockExtension {
43    pub fn new(ext: &Extension, plan: &FetchPlan) -> Self {
44        let sha256 = ext
45            .sha256
46            .iter()
47            .map(|(k, v)| (k.clone(), v.clone()))
48            .collect();
49        LockExtension {
50            name: ext.name.clone(),
51            version: ext.version.clone(),
52            url: plan.url.clone(),
53            sha256,
54        }
55    }
56}
57
58impl LockLib {
59    pub fn new(lib: &Lib, plan: &FetchPlan) -> Self {
60        LockLib {
61            name: lib.name.clone(),
62            version: lib.version.clone(),
63            url: plan.url.clone(),
64            sha256: lib.sha256.clone(),
65        }
66    }
67}
68
69impl Lockfile {
70    /// Render the lockfile as a Lua return-table source string.
71    pub fn to_lua(&self) -> String {
72        let mut out = String::with_capacity(256);
73        out.push_str("-- Generated by `assay install`. Do not edit by hand.\n");
74        out.push_str("return {\n");
75
76        if let Some(v) = &self.assay {
77            writeln!(out, "  assay = {},", lua_string(v)).unwrap();
78        }
79
80        out.push_str("  extensions = {\n");
81        for e in &self.extensions {
82            writeln!(out, "    {{").unwrap();
83            writeln!(out, "      name = {},", lua_string(&e.name)).unwrap();
84            writeln!(out, "      version = {},", lua_string(&e.version)).unwrap();
85            writeln!(out, "      url = {},", lua_string(&e.url)).unwrap();
86            out.push_str("      sha256 = {");
87            let mut first = true;
88            for (arch, hash) in &e.sha256 {
89                if !first {
90                    out.push(',');
91                }
92                write!(out, " {arch} = {}", lua_string(hash)).unwrap();
93                first = false;
94            }
95            out.push_str(" },\n");
96            writeln!(out, "    }},").unwrap();
97        }
98        out.push_str("  },\n");
99
100        out.push_str("  libs = {\n");
101        for l in &self.libs {
102            writeln!(out, "    {{").unwrap();
103            writeln!(out, "      name = {},", lua_string(&l.name)).unwrap();
104            writeln!(out, "      version = {},", lua_string(&l.version)).unwrap();
105            writeln!(out, "      url = {},", lua_string(&l.url)).unwrap();
106            writeln!(out, "      sha256 = {},", lua_string(&l.sha256)).unwrap();
107            writeln!(out, "    }},").unwrap();
108        }
109        out.push_str("  },\n");
110
111        out.push_str("}\n");
112        out
113    }
114}
115
116fn lua_string(s: &str) -> String {
117    let mut out = String::with_capacity(s.len() + 2);
118    out.push('"');
119    for c in s.chars() {
120        match c {
121            '\\' => out.push_str("\\\\"),
122            '"' => out.push_str("\\\""),
123            '\n' => out.push_str("\\n"),
124            '\r' => out.push_str("\\r"),
125            '\t' => out.push_str("\\t"),
126            c if (c as u32) < 0x20 => write!(out, "\\{}", c as u32).unwrap(),
127            c => out.push(c),
128        }
129    }
130    out.push('"');
131    out
132}