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{:#?}
116
117They will have to be reinstalled.
118Reinstall?
119            ",
120                package_names
121            )
122        };
123        if Confirm::new(&prompt)
124            .with_default(false)
125            .prompt()
126            .expect("Error prompting for reinstall")
127        {
128            operations::Remove::new(&config)
129                .packages(entrypoints)
130                .progress(progress.clone())
131                .remove()
132                .await?;
133
134            let reinstall_specs = dependencies
135                .iter()
136                .map(|pkg_id| {
137                    let package = unsafe { lockfile.get_unchecked(pkg_id) };
138                    PackageInstallSpec::new(
139                        package.clone().into_package_req(),
140                        tree::EntryType::DependencyOnly,
141                    )
142                    .build_behaviour(BuildBehaviour::Force)
143                    .pin(package.pinned())
144                    .opt(package.opt())
145                    .constraint(package.constraint())
146                    .build()
147                })
148                .collect_vec();
149            operations::Remove::new(&config)
150                .packages(dependencies)
151                .progress(progress.clone())
152                .remove()
153                .await?;
154            operations::Install::new(&config)
155                .packages(reinstall_specs)
156                .tree(tree)
157                .progress(progress.clone())
158                .install()
159                .await?;
160        } else {
161            return Err(eyre!("Operation cancelled."));
162        }
163    };
164
165    let mut has_dangling_rocks = true;
166    while has_dangling_rocks {
167        let tree = config.user_tree(LuaVersion::from(&config)?.clone())?;
168        let lockfile = tree.lockfile()?;
169        let dangling_rocks = lockfile
170            .rocks()
171            .iter()
172            .filter_map(|(pkg_id, _)| {
173                if lockfile.is_entrypoint(pkg_id) || lockfile.is_dependency(pkg_id) {
174                    None
175                } else {
176                    Some(pkg_id)
177                }
178            })
179            .cloned()
180            .collect_vec();
181        if dangling_rocks.is_empty() {
182            has_dangling_rocks = false
183        } else {
184            operations::Remove::new(&config)
185                .packages(dangling_rocks)
186                .progress(progress.clone())
187                .remove()
188                .await?;
189        }
190    }
191
192    Ok(())
193}