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.sha256.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
45        LockExtension {
46            name: ext.name.clone(),
47            version: ext.version.clone(),
48            url: plan.url.clone(),
49            sha256,
50        }
51    }
52}
53
54impl LockLib {
55    pub fn new(lib: &Lib, plan: &FetchPlan) -> Self {
56        LockLib {
57            name: lib.name.clone(),
58            version: lib.version.clone(),
59            url: plan.url.clone(),
60            sha256: lib.sha256.clone(),
61        }
62    }
63}
64
65impl Lockfile {
66    /// Render the lockfile as a Lua return-table source string.
67    pub fn to_lua(&self) -> String {
68        let mut out = String::with_capacity(256);
69        out.push_str("-- Generated by `assay install`. Do not edit by hand.\n");
70        out.push_str("return {\n");
71
72        if let Some(v) = &self.assay {
73            writeln!(out, "  assay = {},", lua_string(v)).unwrap();
74        }
75
76        out.push_str("  extensions = {\n");
77        for e in &self.extensions {
78            writeln!(out, "    {{").unwrap();
79            writeln!(out, "      name = {},", lua_string(&e.name)).unwrap();
80            writeln!(out, "      version = {},", lua_string(&e.version)).unwrap();
81            writeln!(out, "      url = {},", lua_string(&e.url)).unwrap();
82            out.push_str("      sha256 = {");
83            let mut first = true;
84            for (arch, hash) in &e.sha256 {
85                if !first {
86                    out.push(',');
87                }
88                write!(out, " {arch} = {}", lua_string(hash)).unwrap();
89                first = false;
90            }
91            out.push_str(" },\n");
92            writeln!(out, "    }},").unwrap();
93        }
94        out.push_str("  },\n");
95
96        out.push_str("  libs = {\n");
97        for l in &self.libs {
98            writeln!(out, "    {{").unwrap();
99            writeln!(out, "      name = {},", lua_string(&l.name)).unwrap();
100            writeln!(out, "      version = {},", lua_string(&l.version)).unwrap();
101            writeln!(out, "      url = {},", lua_string(&l.url)).unwrap();
102            writeln!(out, "      sha256 = {},", lua_string(&l.sha256)).unwrap();
103            writeln!(out, "    }},").unwrap();
104        }
105        out.push_str("  },\n");
106
107        out.push_str("}\n");
108        out
109    }
110}
111
112fn lua_string(s: &str) -> String {
113    let mut out = String::with_capacity(s.len() + 2);
114    out.push('"');
115    for c in s.chars() {
116        match c {
117            '\\' => out.push_str("\\\\"),
118            '"' => out.push_str("\\\""),
119            '\n' => out.push_str("\\n"),
120            '\r' => out.push_str("\\r"),
121            '\t' => out.push_str("\\t"),
122            c if (c as u32) < 0x20 => write!(out, "\\{}", c as u32).unwrap(),
123            c => out.push(c),
124        }
125    }
126    out.push('"');
127    out
128}