Skip to main content

pkg/
package_state.rs

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/// Contains current user packages state
13#[derive(Serialize, Deserialize, Debug, Clone)]
14#[serde(default)]
15pub struct PackageState {
16    /// list of can't be accidentally uninstalled packages
17    pub protected: BTreeSet<PackageName>,
18    /// installed public keys per remote name.
19    /// using pkgar_keys as a wrapper of dryoc public key.
20    pub pubkeys: BTreeMap<RemoteName, RepoPublicKeyFile>,
21    /// install state per packages
22    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    // only useful during install
32    #[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        // to_string *should* be safe to unwrap for this struct
86        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    // mutably add valid packages to the graph.
99    /// Returns list of packages that need to be resolved,
100    /// which are not yet added to the package config.
101    /// If zero vector returned, it means all package deps are satisfied
102    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                    // dependents should be marked as missing well
136                    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        // all packages with their dependents should be satisfied
151        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    // mutably remove packages from the graph.
200    /// Returns list of packages that also need to be resolved,
201    /// which are not all of their deps is listed in list of packages.
202    /// If zero vector returned, it means uninstallation can be executed.
203    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        // Filter out protected packages. Caller can wipe out the list beforehand to skip this behaviour.
208        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    // Diff between old and new state, returns list of installed and uninstalled packages
255    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    /// Mark packages manually installed or not. Returns list of changed packages.
311    /// PackageState are not marked automatically in any install mechanism.
312    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            // TODO: Hardcoded
332            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    // --- Helper Functions for Test Data ---
363
364    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        // 1-st
421        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        // 2-nd
428        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        // 3-rd
435        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}