1use clap::Args;
2use inquire::Confirm;
3use itertools::Itertools;
4use lux_lib::{
5 build::BuildBehaviour,
6 config::Config,
7 lockfile::LocalPackageId,
8 lua_version::LuaVersion,
9 operations::{self, PackageInstallSpec},
10 package::PackageReq,
11 progress::MultiProgress,
12 tree::{self, InstallTree, RockMatches, TreeError},
13};
14use miette::{miette, IntoDiagnostic, Result};
15
16#[derive(Args)]
17pub struct Uninstall {
18 packages: Vec<PackageReq>,
20}
21
22pub 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 return Err(miette!(
48 "The following packages were not found: {:#?}",
49 nonexistent_packages
50 ));
51 }
52
53 if !duplicate_packages.is_empty() {
54 return Err(miette!(
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(miette!(
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 !config.no_prompt()
125 && Confirm::new(&prompt)
126 .with_default(false)
127 .prompt()
128 .into_diagnostic()
129 .map_err(|_| miette!("Error prompting for reinstall"))?
130 {
131 operations::Uninstall::new()
132 .config(&config)
133 .packages(entrypoints)
134 .progress(progress.clone())
135 .remove()
136 .await?;
137
138 let reinstall_specs = dependencies
139 .iter()
140 .map(|pkg_id| {
141 let package = unsafe { lockfile.get_unchecked(pkg_id) };
142 PackageInstallSpec::new(
143 package.clone().into_package_req(),
144 tree::EntryType::DependencyOnly,
145 )
146 .build_behaviour(BuildBehaviour::Force)
147 .pin(package.pinned())
148 .opt(package.opt())
149 .constraint(package.constraint())
150 .build()
151 })
152 .collect_vec();
153 operations::Uninstall::new()
154 .config(&config)
155 .packages(dependencies)
156 .progress(progress.clone())
157 .remove()
158 .await?;
159 operations::Install::new(&config)
160 .packages(reinstall_specs)
161 .tree(tree)
162 .progress(progress.clone())
163 .install()
164 .await?;
165 } else {
166 return Err(miette!("Operation cancelled."));
167 }
168 };
169
170 let mut has_dangling_rocks = true;
171 while has_dangling_rocks {
172 let tree = config.user_tree(LuaVersion::from(&config)?.clone())?;
173 let lockfile = tree.lockfile()?;
174 let dangling_rocks = lockfile
175 .rocks()
176 .keys()
177 .filter(|pkg_id| !lockfile.is_entrypoint(pkg_id) && !lockfile.is_dependency(pkg_id))
178 .cloned()
179 .collect_vec();
180 if dangling_rocks.is_empty() {
181 has_dangling_rocks = false
182 } else {
183 operations::Uninstall::new()
184 .config(&config)
185 .packages(dangling_rocks)
186 .progress(progress.clone())
187 .remove()
188 .await?;
189 }
190 }
191
192 Ok(())
193}