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    operations::Remove::new(&config)
92        .packages(entrypoints)
93        .remove()
94        .await?;
95
96    if !dependencies.is_empty() {
97        let package_names = dependencies
98            .iter()
99            .map(|pkg_id| unsafe { lockfile.get_unchecked(pkg_id) }.name().to_string())
100            .collect_vec();
101        let prompt = if package_names.len() == 1 {
102            format!(
103                "
104            Package {} can be removed from the entrypoints, but it is also a dependency, so it will have to be reinstalled.
105Reinstall?
106            ",
107                package_names[0]
108            )
109        } else {
110            format!(
111                "
112            The following packages can be removed from the entrypoints, but are also dependencies:
113{:#?}
114
115They will have to be reinstalled.
116Reinstall?
117            ",
118                package_names
119            )
120        };
121        if Confirm::new(&prompt)
122            .with_default(false)
123            .prompt()
124            .expect("Error prompting for reinstall")
125        {
126            let reinstall_specs = dependencies
127                .iter()
128                .map(|pkg_id| {
129                    let package = unsafe { lockfile.get_unchecked(pkg_id) };
130                    PackageInstallSpec::new(
131                        package.clone().into_package_req(),
132                        tree::EntryType::DependencyOnly,
133                    )
134                    .build_behaviour(BuildBehaviour::Force)
135                    .pin(package.pinned())
136                    .opt(package.opt())
137                    .constraint(package.constraint())
138                    .build()
139                })
140                .collect_vec();
141            let progress = MultiProgress::new_arc();
142            operations::Remove::new(&config)
143                .packages(dependencies)
144                .progress(progress.clone())
145                .remove()
146                .await?;
147            operations::Install::new(&config)
148                .packages(reinstall_specs)
149                .tree(tree)
150                .progress(progress)
151                .install()
152                .await?;
153        } else {
154            return Err(eyre!("Operation cancelled."));
155        }
156    };
157
158    Ok(())
159}