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 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(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}