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, 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(&config);
92
93    if dependencies.is_empty() {
94        operations::Uninstall::new()
95            .config(&config)
96            .packages(entrypoints)
97            .remove()
98            .await?;
99    } else {
100        let package_names = dependencies
101            .iter()
102            .map(|pkg_id| unsafe { lockfile.get_unchecked(pkg_id) }.name().to_string())
103            .collect_vec();
104        let prompt = if package_names.len() == 1 {
105            format!(
106                "
107            Package {} can be removed from the entrypoints, but it is also a dependency, so it will have to be reinstalled.
108Reinstall?
109            ",
110                package_names[0]
111            )
112        } else {
113            format!(
114                "
115            The following packages can be removed from the entrypoints, but are also dependencies:
116{package_names:#?}
117
118They will have to be reinstalled.
119Reinstall?
120            ",
121            )
122        };
123        if Confirm::new(&prompt)
124            .with_default(false)
125            .prompt()
126            .wrap_err("Error prompting for reinstall")?
127        {
128            operations::Uninstall::new()
129                .config(&config)
130                .packages(entrypoints)
131                .progress(progress.clone())
132                .remove()
133                .await?;
134
135            let reinstall_specs = dependencies
136                .iter()
137                .map(|pkg_id| {
138                    let package = unsafe { lockfile.get_unchecked(pkg_id) };
139                    PackageInstallSpec::new(
140                        package.clone().into_package_req(),
141                        tree::EntryType::DependencyOnly,
142                    )
143                    .build_behaviour(BuildBehaviour::Force)
144                    .pin(package.pinned())
145                    .opt(package.opt())
146                    .constraint(package.constraint())
147                    .build()
148                })
149                .collect_vec();
150            operations::Uninstall::new()
151                .config(&config)
152                .packages(dependencies)
153                .progress(progress.clone())
154                .remove()
155                .await?;
156            operations::Install::new(&config)
157                .packages(reinstall_specs)
158                .tree(tree)
159                .progress(progress.clone())
160                .install()
161                .await?;
162        } else {
163            return Err(eyre!("Operation cancelled."));
164        }
165    };
166
167    let mut has_dangling_rocks = true;
168    while has_dangling_rocks {
169        let tree = config.user_tree(LuaVersion::from(&config)?.clone())?;
170        let lockfile = tree.lockfile()?;
171        let dangling_rocks = lockfile
172            .rocks()
173            .iter()
174            .filter_map(|(pkg_id, _)| {
175                if lockfile.is_entrypoint(pkg_id) || lockfile.is_dependency(pkg_id) {
176                    None
177                } else {
178                    Some(pkg_id)
179                }
180            })
181            .cloned()
182            .collect_vec();
183        if dangling_rocks.is_empty() {
184            has_dangling_rocks = false
185        } else {
186            operations::Uninstall::new()
187                .config(&config)
188                .packages(dangling_rocks)
189                .progress(progress.clone())
190                .remove()
191                .await?;
192        }
193    }
194
195    Ok(())
196}