lux_cli/
uninstall.rs

1use clap::Args;
2use eyre::{eyre, Result};
3use inquire::Confirm;
4use itertools::Itertools;
5use lux_lib::{
6    build::BuildBehaviour,
7    config::{Config, LuaVersion},
8    lockfile::LocalPackageId,
9    operations::{self, PackageInstallSpec},
10    package::PackageReq,
11    progress::MultiProgress,
12    tree::{self, RockMatches, TreeError},
13};
14
15#[derive(Args)]
16pub struct Uninstall {
17    /// The package or packages to uninstall from the system.
18    packages: Vec<PackageReq>,
19}
20
21/// Uninstall one or multiple rocks from the user tree
22pub async fn uninstall(uninstall_args: Uninstall, config: Config) -> Result<()> {
23    let tree = config.user_tree(LuaVersion::from(&config)?.clone())?;
24
25    let package_matches = uninstall_args
26        .packages
27        .iter()
28        .map(|package_req| tree.match_rocks(package_req))
29        .try_collect::<_, Vec<_>, TreeError>()?;
30
31    let (packages, nonexistent_packages, duplicate_packages) = package_matches.into_iter().fold(
32        (Vec::new(), Vec::new(), Vec::new()),
33        |(mut p, mut n, mut d), rock_match| {
34            match rock_match {
35                RockMatches::NotFound(req) => n.push(req),
36                RockMatches::Single(package) => p.push(package),
37                RockMatches::Many(packages) => d.extend(packages),
38            };
39
40            (p, n, d)
41        },
42    );
43
44    if !nonexistent_packages.is_empty() {
45        // TODO(vhyrro): Render this in the form of a tree.
46        return Err(eyre!(
47            "The following packages were not found: {:#?}",
48            nonexistent_packages
49        ));
50    }
51
52    if !duplicate_packages.is_empty() {
53        return Err(eyre!(
54            "
55Multiple packages satisfying your version requirements were found:
56{:#?}
57
58Please specify the exact package to uninstall:
59> lux uninstall '<name>@<version>'
60",
61            duplicate_packages,
62        ));
63    }
64
65    let lockfile = tree.lockfile()?;
66    let non_entrypoints = packages
67        .iter()
68        .filter_map(|pkg_id| {
69            if lockfile.is_entrypoint(pkg_id) {
70                None
71            } else {
72                Some(unsafe { lockfile.get_unchecked(pkg_id) }.name().to_string())
73            }
74        })
75        .collect_vec();
76    if !non_entrypoints.is_empty() {
77        return Err(eyre!(
78            "
79Cannot uninstall dependencies:
80{:#?}
81",
82            non_entrypoints,
83        ));
84    }
85
86    let (dependencies, entrypoints): (Vec<LocalPackageId>, Vec<LocalPackageId>) = packages
87        .iter()
88        .cloned()
89        .partition(|pkg_id| lockfile.is_dependency(pkg_id));
90
91    let progress = MultiProgress::new_arc();
92
93    if dependencies.is_empty() {
94        operations::Remove::new(&config)
95            .packages(entrypoints)
96            .remove()
97            .await?;
98    } else {
99        let package_names = dependencies
100            .iter()
101            .map(|pkg_id| unsafe { lockfile.get_unchecked(pkg_id) }.name().to_string())
102            .collect_vec();
103        let prompt = if package_names.len() == 1 {
104            format!(
105                "
106            Package {} can be removed from the entrypoints, but it is also a dependency, so it will have to be reinstalled.
107Reinstall?
108            ",
109                package_names[0]
110            )
111        } else {
112            format!(
113                "
114            The following packages can be removed from the entrypoints, but are also dependencies:
115{package_names:#?}
116
117They will have to be reinstalled.
118Reinstall?
119            ",
120            )
121        };
122        if Confirm::new(&prompt)
123            .with_default(false)
124            .prompt()
125            .expect("Error prompting for reinstall")
126        {
127            operations::Remove::new(&config)
128                .packages(entrypoints)
129                .progress(progress.clone())
130                .remove()
131                .await?;
132
133            let reinstall_specs = dependencies
134                .iter()
135                .map(|pkg_id| {
136                    let package = unsafe { lockfile.get_unchecked(pkg_id) };
137                    PackageInstallSpec::new(
138                        package.clone().into_package_req(),
139                        tree::EntryType::DependencyOnly,
140                    )
141                    .build_behaviour(BuildBehaviour::Force)
142                    .pin(package.pinned())
143                    .opt(package.opt())
144                    .constraint(package.constraint())
145                    .build()
146                })
147                .collect_vec();
148            operations::Remove::new(&config)
149                .packages(dependencies)
150                .progress(progress.clone())
151                .remove()
152                .await?;
153            operations::Install::new(&config)
154                .packages(reinstall_specs)
155                .tree(tree)
156                .progress(progress.clone())
157                .install()
158                .await?;
159        } else {
160            return Err(eyre!("Operation cancelled."));
161        }
162    };
163
164    let mut has_dangling_rocks = true;
165    while has_dangling_rocks {
166        let tree = config.user_tree(LuaVersion::from(&config)?.clone())?;
167        let lockfile = tree.lockfile()?;
168        let dangling_rocks = lockfile
169            .rocks()
170            .iter()
171            .filter_map(|(pkg_id, _)| {
172                if lockfile.is_entrypoint(pkg_id) || lockfile.is_dependency(pkg_id) {
173                    None
174                } else {
175                    Some(pkg_id)
176                }
177            })
178            .cloned()
179            .collect_vec();
180        if dangling_rocks.is_empty() {
181            has_dangling_rocks = false
182        } else {
183            operations::Remove::new(&config)
184                .packages(dangling_rocks)
185                .progress(progress.clone())
186                .remove()
187                .await?;
188        }
189    }
190
191    Ok(())
192}