Skip to main content

dre_core/
lock.rs

1//! `dre.lock`: the exact plugin versions and package commits a project resolved, committed
2//! alongside it.
3
4use std::collections::BTreeMap;
5use std::path::Path;
6
7use semver::Version;
8use serde::{Deserialize, Serialize};
9
10use dre_protocol::PluginId;
11
12pub const LOCK_FILE: &str = "dre.lock";
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct Locked {
16    pub version: Version,
17    pub sha256: Checksums,
18    /// Where it came from when not the default registry: `github:owner/repo` or
19    /// `registry:<url>`. A different declared source re-resolves the plugin.
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub from: Option<String>,
22    /// The plugins the package provides, so a project can be checked without installing it.
23    #[serde(default, skip_serializing_if = "Vec::is_empty")]
24    pub provides: Vec<PluginId>,
25}
26
27/// A locked plugin's artifact checksums by platform (`macos-aarch64`, `linux-x86_64`, ...), so
28/// one committed `dre.lock` installs the same builds on every machine. Written as a map; a lock
29/// from before this holds one checksum for a platform it didn't name (see [`Checksums::legacy`]).
30#[derive(Debug, Clone, PartialEq, Default)]
31pub struct Checksums(BTreeMap<String, String>);
32
33/// The key of a checksum whose platform isn't known.
34const LEGACY: &str = "";
35
36impl Checksums {
37    pub fn new() -> Checksums {
38        Checksums::default()
39    }
40
41    /// One checksum of unknown platform, as in a lock from before per-platform checksums.
42    pub fn legacy_only(sha: &str) -> Checksums {
43        let mut c = Checksums::new();
44        c.0.insert(LEGACY.into(), sha.to_lowercase());
45        c
46    }
47
48    pub fn get(&self, platform: &str) -> Option<&str> {
49        self.0.get(platform).map(String::as_str)
50    }
51
52    pub fn is_empty(&self) -> bool {
53        self.0.is_empty()
54    }
55
56    /// Whether `sha` is one of the known platforms' checksums.
57    pub fn contains(&self, sha: &str) -> bool {
58        self.0
59            .iter()
60            .any(|(p, s)| p != LEGACY && s.eq_ignore_ascii_case(sha))
61    }
62
63    pub fn legacy(&self) -> Option<&str> {
64        self.get(LEGACY)
65    }
66
67    pub fn insert(&mut self, platform: &str, sha: &str) {
68        self.0.remove(LEGACY);
69        self.0.insert(platform.into(), sha.to_lowercase());
70    }
71
72    /// Add the platforms `other` knows and this one doesn't.
73    pub fn merge(&mut self, other: &Checksums) {
74        for (p, s) in &other.0 {
75            if p != LEGACY {
76                self.0.entry(p.clone()).or_insert_with(|| s.clone());
77            }
78        }
79    }
80}
81
82impl Serialize for Checksums {
83    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
84        match self.legacy() {
85            Some(sha) if self.0.len() == 1 => s.serialize_str(sha),
86            _ => self.0.serialize(s),
87        }
88    }
89}
90
91impl<'de> Deserialize<'de> for Checksums {
92    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
93        #[derive(Deserialize)]
94        #[serde(untagged)]
95        enum Raw {
96            One(String),
97            Many(BTreeMap<String, String>),
98        }
99        Ok(match Raw::deserialize(d)? {
100            Raw::One(sha) => Checksums::legacy_only(&sha),
101            Raw::Many(m) => Checksums(m.into_iter().map(|(k, v)| (k, v.to_lowercase())).collect()),
102        })
103    }
104}
105
106/// A git package pinned to the commit its declared revision resolved to.
107#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
108pub struct LockedPackage {
109    pub git: String,
110    pub revision: String,
111    pub commit: String,
112}
113
114#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
115pub struct Lock {
116    /// Plugin packages by name.
117    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
118    pub plugins: BTreeMap<String, Locked>,
119    /// Macro packages by name.
120    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
121    pub packages: BTreeMap<String, LockedPackage>,
122    /// `local:` plugin packages by name: the path they're used from. Nothing to pin.
123    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
124    pub local: BTreeMap<String, String>,
125}
126
127impl Lock {
128    /// The project's lock, or an empty one when there's no `dre.lock`.
129    pub fn load(root: &Path) -> Result<Lock, String> {
130        let p = root.join(LOCK_FILE);
131        match std::fs::read_to_string(&p) {
132            Ok(t) => serde_yaml_ng::from_str(&t).map_err(|e| format!("{LOCK_FILE}: {e}")),
133            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Lock::default()),
134            Err(e) => Err(format!("{LOCK_FILE}: {e}")),
135        }
136    }
137
138    pub fn save(&self, root: &Path) -> Result<(), String> {
139        let body = serde_yaml_ng::to_string(self).map_err(|e| e.to_string())?;
140        let text = format!(
141            "# Generated by DRE: the exact plugin versions and package commits this project uses. Commit this file.\n{body}"
142        );
143        std::fs::write(root.join(LOCK_FILE), text).map_err(|e| format!("{LOCK_FILE}: {e}"))
144    }
145
146    /// A plugin package's pin.
147    pub fn get(&self, package: &str) -> Option<&Locked> {
148        self.plugins.get(package)
149    }
150
151    pub fn version(&self, package: &str) -> Option<Version> {
152        self.get(package).map(|l| l.version.clone())
153    }
154}