1use crate::error::Result;
4use crate::paths::Paths;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct InstalledPackage {
11 pub version: String,
12 #[serde(default)]
13 pub revision: u32,
14 pub installed_at: String,
15 #[serde(default = "default_installed_by")]
16 pub installed_by: String,
17 #[serde(default)]
18 pub requested: bool,
19 #[serde(default)]
20 pub pinned: bool,
21 #[serde(default)]
22 pub dependencies: Vec<String>,
23 #[serde(default)]
25 pub head_sha: Option<String>,
26 #[serde(default)]
28 pub is_head: bool,
29 #[serde(default)]
31 pub bottle_sha256: Option<String>,
32}
33
34fn default_installed_by() -> String {
35 "stout".to_string()
36}
37
38impl InstalledPackage {
39 pub fn is_head_install(&self) -> bool {
41 self.is_head || self.version.starts_with("HEAD")
42 }
43
44 pub fn short_sha(&self) -> Option<&str> {
46 if self.version.starts_with("HEAD-") {
47 Some(&self.version[5..])
48 } else {
49 None
50 }
51 }
52}
53
54#[derive(Debug, Default, Clone, Serialize, Deserialize)]
56pub struct InstalledPackages {
57 #[serde(default)]
58 pub packages: HashMap<String, InstalledPackage>,
59}
60
61impl InstalledPackages {
62 pub fn load(paths: &Paths) -> Result<Self> {
64 let file_path = paths.installed_file();
65
66 if file_path.exists() {
67 let contents = std::fs::read_to_string(&file_path)?;
68 let packages: InstalledPackages = toml::from_str(&contents)?;
69 Ok(packages)
70 } else {
71 Ok(Self::default())
72 }
73 }
74
75 pub fn save(&self, paths: &Paths) -> Result<()> {
77 let file_path = paths.installed_file();
78
79 if let Some(parent) = file_path.parent() {
80 std::fs::create_dir_all(parent)?;
81 }
82
83 let contents = toml::to_string_pretty(self)?;
84 std::fs::write(&file_path, contents)?;
85 Ok(())
86 }
87
88 pub fn add(&mut self, name: &str, version: &str, revision: u32, requested: bool) {
90 self.add_with_deps(name, version, revision, requested, Vec::new());
91 }
92
93 pub fn add_with_deps(
95 &mut self,
96 name: &str,
97 version: &str,
98 revision: u32,
99 requested: bool,
100 dependencies: Vec<String>,
101 ) {
102 let now = chrono_lite_now();
103 let pinned = self.packages.get(name).map(|p| p.pinned).unwrap_or(false);
105 self.packages.insert(
106 name.to_string(),
107 InstalledPackage {
108 version: version.to_string(),
109 revision,
110 installed_at: now,
111 installed_by: "stout".to_string(),
112 requested,
113 pinned,
114 dependencies,
115 head_sha: None,
116 is_head: version.starts_with("HEAD"),
117 bottle_sha256: None,
118 },
119 );
120 }
121
122 #[allow(clippy::too_many_arguments)]
124 pub fn add_imported(
125 &mut self,
126 name: &str,
127 version: &str,
128 revision: u32,
129 requested: bool,
130 installed_by: &str,
131 installed_at: &str,
132 dependencies: Vec<String>,
133 ) {
134 let pinned = self.packages.get(name).map(|p| p.pinned).unwrap_or(false);
136 self.packages.insert(
137 name.to_string(),
138 InstalledPackage {
139 version: version.to_string(),
140 revision,
141 installed_at: installed_at.to_string(),
142 installed_by: installed_by.to_string(),
143 requested,
144 pinned,
145 dependencies,
146 head_sha: None,
147 is_head: version.starts_with("HEAD"),
148 bottle_sha256: None,
149 },
150 );
151 }
152
153 pub fn add_head(
155 &mut self,
156 name: &str,
157 short_sha: &str,
158 full_sha: &str,
159 requested: bool,
160 dependencies: Vec<String>,
161 ) {
162 let now = chrono_lite_now();
163 let pinned = self.packages.get(name).map(|p| p.pinned).unwrap_or(false);
165 self.packages.insert(
166 name.to_string(),
167 InstalledPackage {
168 version: format!("HEAD-{}", short_sha),
169 revision: 0,
170 installed_at: now,
171 installed_by: "stout".to_string(),
172 requested,
173 pinned,
174 dependencies,
175 head_sha: Some(full_sha.to_string()),
176 is_head: true,
177 bottle_sha256: None,
178 },
179 );
180 }
181
182 pub fn pin(&mut self, name: &str) -> bool {
184 if let Some(pkg) = self.packages.get_mut(name) {
185 pkg.pinned = true;
186 true
187 } else {
188 false
189 }
190 }
191
192 pub fn unpin(&mut self, name: &str) -> bool {
194 if let Some(pkg) = self.packages.get_mut(name) {
195 pkg.pinned = false;
196 true
197 } else {
198 false
199 }
200 }
201
202 pub fn is_pinned(&self, name: &str) -> bool {
204 self.packages.get(name).map(|p| p.pinned).unwrap_or(false)
205 }
206
207 pub fn pinned(&self) -> impl Iterator<Item = (&String, &InstalledPackage)> {
209 self.packages.iter().filter(|(_, p)| p.pinned)
210 }
211
212 pub fn remove(&mut self, name: &str) -> Option<InstalledPackage> {
214 self.packages.remove(name)
215 }
216
217 pub fn get(&self, name: &str) -> Option<&InstalledPackage> {
219 self.packages.get(name)
220 }
221
222 pub fn is_installed(&self, name: &str) -> bool {
224 self.packages.contains_key(name)
225 }
226
227 pub fn is_version_installed(&self, name: &str, version: &str) -> bool {
229 self.packages
230 .get(name)
231 .map(|p| p.version == version)
232 .unwrap_or(false)
233 }
234
235 pub fn names(&self) -> impl Iterator<Item = &String> {
237 self.packages.keys()
238 }
239
240 pub fn count(&self) -> usize {
242 self.packages.len()
243 }
244
245 pub fn requested(&self) -> impl Iterator<Item = (&String, &InstalledPackage)> {
247 self.packages.iter().filter(|(_, p)| p.requested)
248 }
249
250 pub fn dependencies(&self) -> impl Iterator<Item = (&String, &InstalledPackage)> {
252 self.packages.iter().filter(|(_, p)| !p.requested)
253 }
254
255 pub fn iter(&self) -> impl Iterator<Item = (&String, &InstalledPackage)> {
257 self.packages.iter()
258 }
259}
260
261fn chrono_lite_now() -> String {
263 jiff::Timestamp::now()
264 .strftime("%Y-%m-%dT%H:%M:%SZ")
265 .to_string()
266}