1use crate::{
2 package::{RemoteName, RemotePackage},
3 Package, PackageError, PackageName, RepoPublicKeyFile,
4};
5use serde_derive::{Deserialize, Serialize};
6use std::{
7 cmp::Ordering,
8 collections::{BTreeMap, BTreeSet},
9 path::Path,
10};
11
12#[derive(Serialize, Deserialize, Debug, Clone)]
14#[serde(default)]
15pub struct PackageState {
16 pub protected: BTreeSet<PackageName>,
18 pub pubkeys: BTreeMap<RemoteName, RepoPublicKeyFile>,
21 pub installed: BTreeMap<PackageName, InstallState>,
23}
24
25#[derive(Serialize, Deserialize, Default, Debug, Clone)]
26#[serde(default)]
27pub struct InstallState {
28 pub remote: RemoteName,
29 pub blake3: String,
30 pub manual: bool,
31 #[serde(skip_serializing)]
33 pub network_size: u64,
34 pub storage_size: u64,
35 pub dependencies: BTreeSet<PackageName>,
36 pub dependents: BTreeSet<PackageName>,
37}
38
39impl InstallState {
40 pub fn from_package(
41 pkg: &Package,
42 remote: RemoteName,
43 manual: bool,
44 dependents: BTreeSet<PackageName>,
45 ) -> Self {
46 Self {
47 remote,
48 blake3: pkg.blake3.clone(),
49 manual,
50 network_size: pkg.network_size,
51 storage_size: pkg.storage_size,
52 dependencies: pkg.depends.iter().cloned().collect(),
53 dependents,
54 }
55 }
56}
57
58#[derive(Default, Debug, Clone)]
59pub struct PackageList {
60 pub install: Vec<PackageName>,
61 pub uninstall: Vec<PackageName>,
62 pub update: Vec<PackageName>,
63 pub install_size: u64,
64 pub network_size: u64,
65 pub uninstall_size: u64,
66}
67
68impl PackageState {
69 pub fn from_sysroot<P: AsRef<Path>>(install_path: P) -> Result<Self, PackageError> {
70 let packages_path = install_path.as_ref().join(crate::PACKAGES_TOML_PATH);
71
72 match std::fs::read_to_string(&packages_path) {
73 Ok(toml) => {
74 toml::from_str(&toml).map_err(|e| PackageError::Parse(e, Some(packages_path)))
75 }
76 Err(_) => Ok(PackageState::default()),
77 }
78 }
79
80 pub fn from_toml(text: &str) -> Result<Self, PackageError> {
81 toml::from_str(text).map_err(|err| PackageError::Parse(err, None))
82 }
83
84 pub fn to_toml(&self) -> String {
85 toml::to_string(self).unwrap()
87 }
88
89 pub fn to_sysroot<P: AsRef<Path>>(&self, install_path: P) -> Result<(), std::io::Error> {
90 let packages_path = install_path.as_ref().join(crate::PACKAGES_TOML_PATH);
91 let packages_dir = packages_path.parent().unwrap();
92 if !packages_dir.is_dir() {
93 std::fs::create_dir_all(packages_dir)?;
94 }
95 std::fs::write(&packages_path, self.to_toml())
96 }
97
98 pub fn install(&mut self, packages: &[RemotePackage]) -> Vec<PackageName> {
103 let mut missing_set = BTreeSet::new();
104 let mut missing_deps = Vec::new();
105 let package_names: BTreeSet<&PackageName> =
106 packages.iter().map(|p| &p.package.name).collect();
107
108 let mut recursion = 100;
109 loop {
110 let mut has_new_missing_deps = false;
111
112 for pkg in packages {
113 if missing_set.contains(&pkg.package.name) {
114 continue;
115 }
116
117 let mut has_missing_deps = false;
118 for dep_name in &pkg.package.depends {
119 if self.installed.contains_key(dep_name) {
120 } else if !package_names.contains(dep_name) {
121 if missing_set.insert(dep_name.clone()) {
122 missing_deps.push(dep_name.clone());
123 }
124 has_missing_deps = true;
125 } else if missing_set.contains(dep_name) {
126 has_missing_deps = true;
127 } else {
128 }
129 }
130
131 if has_missing_deps {
132 if missing_set.insert(pkg.package.name.clone()) {
133 missing_deps.push(pkg.package.name.clone());
134 }
135 has_new_missing_deps = true;
137 }
138 }
139
140 if !has_new_missing_deps {
141 break;
142 }
143
144 if recursion == 0 {
145 panic!("Dependencies recursion exhausted");
146 }
147 recursion -= 1;
148 }
149
150 let mut unsatisfied_deps: BTreeMap<PackageName, BTreeSet<PackageName>> = BTreeMap::new();
152 for rpkg in packages {
153 let pkg = &rpkg.package;
154 if missing_set.contains(&pkg.name) {
155 continue;
156 }
157
158 let (manual, dependents, remote) = if let Some(existing) = self.installed.get(&pkg.name)
159 {
160 (
161 existing.manual,
162 existing.dependents.clone(),
163 existing.remote.clone(),
164 )
165 } else {
166 (
167 false,
168 unsatisfied_deps.remove(&pkg.name).unwrap_or_default(),
169 rpkg.remote.to_string(),
170 )
171 };
172
173 let new_state = InstallState::from_package(pkg, remote, manual, dependents);
174
175 self.installed.insert(pkg.name.clone(), new_state);
176
177 for dep_name in &pkg.depends {
178 if let Some(dep_state) = self.installed.get_mut(dep_name) {
179 dep_state.dependents.insert(pkg.name.clone());
180 } else {
181 if let Some(dep_state) = unsatisfied_deps.get_mut(dep_name) {
182 dep_state.insert(pkg.name.clone());
183 } else {
184 let mut dep_state = BTreeSet::new();
185 dep_state.insert(pkg.name.clone());
186 unsatisfied_deps.insert(dep_name.clone(), dep_state);
187 }
188 }
189 }
190 }
191
192 if !unsatisfied_deps.is_empty() {
193 panic!("Some unsatisfied deps are remained: {:?}", unsatisfied_deps);
194 }
195
196 missing_deps
197 }
198
199 pub fn uninstall(&mut self, packages: &[PackageName]) -> Vec<PackageName> {
204 let mut pending_resolution = Vec::new();
205 let mut packages_to_remove = packages.to_vec();
206
207 packages_to_remove.retain(|name| !self.protected.contains(name));
209
210 let remove_set: BTreeSet<&PackageName> = packages_to_remove.iter().collect();
211 let mut safe_to_remove = Vec::new();
212
213 for name in &packages_to_remove {
214 let Some(state) = self.installed.get(name) else {
215 continue;
216 };
217 let missing_dependents: Vec<_> = state
218 .dependents
219 .iter()
220 .cloned()
221 .filter(|dep| !remove_set.contains(dep))
222 .collect();
223 let missing_dependencies: Vec<_> = state
224 .dependencies
225 .iter()
226 .cloned()
227 .filter(|dep| {
228 !remove_set.contains(dep) && self.installed.get(dep).is_some_and(|p| !p.manual)
229 })
230 .collect();
231
232 if missing_dependents.is_empty() && missing_dependencies.is_empty() {
233 safe_to_remove.push(name.clone());
234 } else {
235 pending_resolution.extend(missing_dependents);
236 pending_resolution.push(name.clone());
237 pending_resolution.extend(missing_dependencies);
238 }
239 }
240
241 for name in safe_to_remove {
242 if let Some(state) = self.installed.remove(&name) {
243 for dep_name in &state.dependencies {
244 if let Some(dep_state) = self.installed.get_mut(dep_name) {
245 dep_state.dependents.remove(&name);
246 }
247 }
248 }
249 }
250
251 pending_resolution
252 }
253
254 pub fn diff(&self, newer: &Self) -> PackageList {
256 let mut diff = PackageList::default();
257
258 let mut old = self.installed.iter();
259 let mut new = newer.installed.iter();
260 let mut old_item = old.next();
261 let mut new_item = new.next();
262
263 loop {
264 match (old_item, new_item) {
265 (Some((k1, v1)), Some((k2, v2))) => match k1.cmp(k2) {
266 Ordering::Less => {
267 diff.uninstall.push(k1.clone());
268 diff.uninstall_size += v1.storage_size;
269 old_item = old.next();
270 }
271 Ordering::Greater => {
272 diff.install.push(k2.clone());
273 diff.install_size += v2.storage_size;
274 diff.network_size += v2.network_size;
275 new_item = new.next();
276 }
277 Ordering::Equal => {
278 if v1.blake3 != v2.blake3 {
279 diff.update.push(k1.clone());
280 diff.install_size += v2.storage_size;
281 diff.uninstall_size += v1.storage_size;
282 diff.network_size += v2.network_size;
283 }
284 old_item = old.next();
285 new_item = new.next();
286 }
287 },
288 (Some((k1, v1)), None) => {
289 diff.uninstall.push(k1.clone());
290 diff.uninstall_size += v1.storage_size;
291 old_item = old.next();
292 }
293 (None, Some((k2, v2))) => {
294 diff.install.push(k2.clone());
295 diff.install_size += v2.storage_size;
296 diff.network_size += v2.network_size;
297 new_item = new.next();
298 }
299 (None, None) => break,
300 }
301 }
302
303 diff
304 }
305
306 pub fn get_installed_list(&self) -> Vec<PackageName> {
307 self.installed.keys().cloned().collect()
308 }
309
310 pub fn mark_as_manual(&mut self, manual: bool, packages: &[PackageName]) -> Vec<PackageName> {
313 let mut marked = Vec::new();
314
315 for package in packages {
316 if let Some(pkg) = self.installed.get_mut(package) {
317 if pkg.manual == manual {
318 continue;
319 }
320 pkg.manual = manual;
321 marked.push(package.clone());
322 }
323 }
324 marked
325 }
326}
327
328impl Default for PackageState {
329 fn default() -> Self {
330 Self {
331 protected: vec![
333 PackageName::new("kernel").unwrap(),
334 PackageName::new("base-initfs").unwrap(),
335 PackageName::new("base").unwrap(),
336 PackageName::new("ion").unwrap(),
337 PackageName::new("pkg").unwrap(),
338 PackageName::new("relibc").unwrap(),
339 PackageName::new("libgcc").unwrap(),
340 PackageName::new("libstdcxx").unwrap(),
341 ]
342 .into_iter()
343 .collect(),
344 pubkeys: Default::default(),
345 installed: Default::default(),
346 }
347 }
348}
349
350impl PackageList {
351 pub fn is_empty(&self) -> bool {
352 self.install.is_empty() && self.uninstall.is_empty() && self.update.is_empty()
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use crate::Package;
359
360 use super::*;
361
362 fn cpkg(name: &str) -> PackageName {
365 PackageName::new(name).unwrap()
366 }
367
368 fn mock_package(name: &str, depends: Vec<&str>) -> RemotePackage {
369 RemotePackage {
370 package: Package {
371 name: cpkg(name),
372 version: "1.0.0".to_string(),
373 target: "x86_64-unknown-redox".to_string(),
374 blake3: "hash".to_string(),
375 source_identifier: "src".to_string(),
376 commit_identifier: "commit".to_string(),
377 time_identifier: "time".to_string(),
378 storage_size: 1000,
379 network_size: 500,
380 depends: depends.into_iter().map(|s| cpkg(s)).collect(),
381 },
382 remote: "origin".into(),
383 }
384 }
385
386 fn mock_empty_db() -> PackageState {
387 PackageState {
388 protected: BTreeSet::new(),
389 pubkeys: BTreeMap::new(),
390 installed: BTreeMap::new(),
391 }
392 }
393
394 #[test]
395 fn test_install_simple_success() {
396 let mut db = mock_empty_db();
397 let nano = mock_package("nano", vec![]);
398 let packages = vec![nano];
399 let names = vec![cpkg("nano")];
400
401 let missing = db.install(&packages);
402
403 assert_eq!(missing, vec![]);
404 assert_eq!(db.get_installed_list(), names);
405 assert_eq!(db.installed[&cpkg("nano")].manual, false);
406 assert_eq!(db.installed[&cpkg("nano")].remote, "origin");
407
408 assert_eq!(db.mark_as_manual(true, &names), vec![cpkg("nano")]);
409 assert_eq!(db.installed[&cpkg("nano")].manual, true);
410 }
411
412 #[test]
413 fn test_install_missing_dependency() {
414 let mut db = mock_empty_db();
415 let bash = mock_package("bash", vec!["readline", "terminfo"]);
416 let readline = mock_package("readline", vec!["ncurses"]);
417 let ncurses = mock_package("ncurses", vec![]);
418 let terminfo = mock_package("terminfo", vec![]);
419 let packages = vec![bash, readline, terminfo, ncurses];
420 let missing = db.install(&packages[..1]);
422 assert_eq!(
423 missing,
424 vec![cpkg("readline"), cpkg("terminfo"), cpkg("bash")]
425 );
426 assert_eq!(db.get_installed_list(), vec![]);
427 let missing = db.install(&packages[..3]);
429 assert_eq!(
430 missing,
431 vec![cpkg("ncurses"), cpkg("readline"), cpkg("bash")]
432 );
433 assert_eq!(db.get_installed_list(), vec![cpkg("terminfo")]);
434 let missing = db.install(&packages[..]);
436 assert_eq!(missing, vec![]);
437 assert_eq!(
438 db.get_installed_list(),
439 vec![
440 cpkg("bash"),
441 cpkg("ncurses"),
442 cpkg("readline"),
443 cpkg("terminfo"),
444 ]
445 );
446
447 assert_eq!(
448 db.installed[&cpkg("bash")].dependents,
449 vec![].iter().cloned().collect()
450 );
451 assert_eq!(
452 db.installed[&cpkg("readline")].dependents,
453 vec![cpkg("bash")].iter().cloned().collect()
454 );
455 assert_eq!(
456 db.installed[&cpkg("ncurses")].dependents,
457 vec![cpkg("readline")].iter().cloned().collect()
458 );
459 }
460
461 #[test]
462 fn test_uninstall_dependent() {
463 let mut db = mock_empty_db();
464 let base = mock_package("base", vec![]);
465 let init = mock_package("base-initfs", vec!["redoxfs"]);
466 let redoxfs = mock_package("redoxfs", vec![]);
467 db.install(&[base, init, redoxfs]);
468 let result = db.uninstall(&[cpkg("redoxfs")]);
469 assert_eq!(
470 db.get_installed_list(),
471 vec![cpkg("base"), cpkg("base-initfs"), cpkg("redoxfs")]
472 );
473 assert_eq!(result, vec![cpkg("base-initfs"), cpkg("redoxfs")]);
474 let result = db.uninstall(&result);
475 assert_eq!(result, vec![]);
476 assert_eq!(db.get_installed_list(), vec![cpkg("base")]);
477 }
478
479 #[test]
480 fn test_uninstall_with_dependencies_unmarked() {
481 let mut db = mock_empty_db();
482
483 let gettext = mock_package("gettext", vec!["libiconv"]);
484 let libiconv = mock_package("libiconv", vec![]);
485 db.install(&[gettext, libiconv]);
486 let result = db.uninstall(&[cpkg("gettext")]);
487 assert_eq!(result, vec![cpkg("gettext"), cpkg("libiconv")]);
488 assert_eq!(
489 db.get_installed_list(),
490 vec![cpkg("gettext"), cpkg("libiconv")]
491 );
492 let result = db.uninstall(&result);
493 assert_eq!(result, vec![]);
494 assert_eq!(db.get_installed_list(), vec![]);
495 }
496
497 #[test]
498 fn test_uninstall_with_dependencies_marked() {
499 let mut db = mock_empty_db();
500
501 let gettext = mock_package("gettext", vec!["libiconv"]);
502 let libiconv = mock_package("libiconv", vec![]);
503 db.install(&[gettext, libiconv]);
504 let result = db.mark_as_manual(true, &vec![cpkg("gettext"), cpkg("libiconv")]);
505 assert_eq!(result.len(), 2usize);
506 let result = db.uninstall(&[cpkg("gettext")]);
507 assert_eq!(result, vec![]);
508 assert_eq!(db.get_installed_list(), vec![cpkg("libiconv")]);
509 }
510
511 #[test]
512 fn test_toml_integration() -> Result<(), PackageError> {
513 const TOML_DATA: &str = r#"
514 [installed.bash]
515 remote = "origin"
516 blake3 = "abc"
517 manual = true
518 storage_size = 3000
519 network_size = 2000
520 dependencies = ["ncurses"]
521 dependents = []
522
523 [installed.ncurses]
524 remote = "origin"
525 blake3 = "def"
526 manual = false
527 storage_size = 2000
528 network_size = 1000
529 dependencies = []
530 dependents = ["bash"]
531 "#;
532
533 let db: PackageState = PackageState::from_toml(TOML_DATA)?;
534
535 assert_eq!(db.get_installed_list(), vec![cpkg("bash"), cpkg("ncurses")]);
536
537 Ok(())
538 }
539}