lux_lib/operations/
remove.rs

1use std::io;
2use std::sync::Arc;
3
4use crate::config::{LuaVersion, LuaVersionUnset};
5use crate::lockfile::{LocalPackage, LocalPackageId};
6use crate::progress::{MultiProgress, Progress, ProgressBar};
7use crate::tree::TreeError;
8use crate::{config::Config, tree::Tree};
9use futures::future::join_all;
10use itertools::Itertools;
11use thiserror::Error;
12
13#[derive(Error, Debug)]
14#[error(transparent)]
15pub enum RemoveError {
16    LuaVersionUnset(#[from] LuaVersionUnset),
17    Io(#[from] io::Error),
18    #[error(transparent)]
19    Tree(#[from] TreeError),
20}
21
22pub struct Remove<'a> {
23    config: &'a Config,
24    packages: Vec<LocalPackageId>,
25    progress: Option<Arc<Progress<MultiProgress>>>,
26}
27
28/// A rocks package remover.
29/// Can remove multiple packages in parallel.
30impl<'a> Remove<'a> {
31    /// Construct a new rocks package remover.
32    pub fn new(config: &'a Config) -> Self {
33        Self {
34            config,
35            packages: Vec::new(),
36            progress: None,
37        }
38    }
39
40    /// Add packages to remove.
41    pub fn packages<I>(self, packages: I) -> Self
42    where
43        I: IntoIterator<Item = LocalPackageId>,
44    {
45        Self {
46            packages: self.packages.into_iter().chain(packages).collect_vec(),
47            ..self
48        }
49    }
50
51    /// Add a package to the set of packages to remove.
52    pub fn package(self, package: LocalPackageId) -> Self {
53        self.packages(std::iter::once(package))
54    }
55
56    /// Pass a `MultiProgress` to this installer.
57    /// By default, a new one will be created.
58    pub fn progress(self, progress: Arc<Progress<MultiProgress>>) -> Self {
59        Self {
60            progress: Some(progress),
61            ..self
62        }
63    }
64
65    /// Remove the packages.
66    pub async fn remove(self) -> Result<(), RemoveError> {
67        let progress = match self.progress {
68            Some(p) => p,
69            None => MultiProgress::new_arc(),
70        };
71        let tree = self
72            .config
73            .user_tree(LuaVersion::from(self.config)?.clone())?;
74        remove(self.packages, tree, &Arc::clone(&progress)).await
75    }
76}
77
78// TODO: Remove dependencies recursively too!
79async fn remove(
80    package_ids: Vec<LocalPackageId>,
81    tree: Tree,
82    progress: &Progress<MultiProgress>,
83) -> Result<(), RemoveError> {
84    let lockfile = tree.lockfile()?;
85
86    let packages = package_ids
87        .iter()
88        .filter_map(|id| lockfile.get(id))
89        .cloned()
90        .collect_vec();
91
92    join_all(packages.into_iter().map(|package| {
93        let bar = progress.map(|p| p.new_bar());
94
95        let tree = tree.clone();
96        tokio::spawn(remove_package(package, tree, bar))
97    }))
98    .await;
99
100    lockfile.map_then_flush(|lockfile| {
101        package_ids
102            .iter()
103            .for_each(|package| lockfile.remove_by_id(package));
104
105        Ok::<_, io::Error>(())
106    })?;
107
108    Ok(())
109}
110
111async fn remove_package(
112    package: LocalPackage,
113    tree: Tree,
114    bar: Progress<ProgressBar>,
115) -> Result<(), RemoveError> {
116    bar.map(|p| {
117        p.set_message(format!(
118            "🗑️ Removing {}@{}",
119            package.name(),
120            package.version()
121        ))
122    });
123
124    let rock_layout = tree.installed_rock_layout(&package)?;
125    tokio::fs::remove_dir_all(&rock_layout.etc).await?;
126    tokio::fs::remove_dir_all(&rock_layout.rock_path).await?;
127
128    // Delete the corresponding binaries attached to the current package (located under `{LUX_TREE}/bin/`)
129    for relative_binary_path in package.spec.binaries() {
130        let binary_file_name = relative_binary_path
131            .file_name()
132            .expect("malformed lockfile");
133
134        let binary_path = tree.bin().join(binary_file_name);
135        if binary_path.is_file() {
136            tokio::fs::remove_file(binary_path).await?;
137        }
138
139        let unwrapped_binary_path = tree.unwrapped_bin().join(binary_file_name);
140        if unwrapped_binary_path.is_file() {
141            tokio::fs::remove_file(unwrapped_binary_path).await?;
142        }
143    }
144
145    bar.map(|p| p.finish_and_clear());
146    Ok(())
147}