Skip to main content

lux_cli/
uninstall.rs

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