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 !config.no_prompt()
125 && Confirm::new(&prompt)
126 .with_default(false)
127 .prompt()
128 .wrap_err("Error prompting for reinstall")?
129 {
130 operations::Uninstall::new()
131 .config(&config)
132 .packages(entrypoints)
133 .progress(progress.clone())
134 .remove()
135 .await?;
136
137 let reinstall_specs = dependencies
138 .iter()
139 .map(|pkg_id| {
140 let package = unsafe { lockfile.get_unchecked(pkg_id) };
141 PackageInstallSpec::new(
142 package.clone().into_package_req(),
143 tree::EntryType::DependencyOnly,
144 )
145 .build_behaviour(BuildBehaviour::Force)
146 .pin(package.pinned())
147 .opt(package.opt())
148 .constraint(package.constraint())
149 .build()
150 })
151 .collect_vec();
152 operations::Uninstall::new()
153 .config(&config)
154 .packages(dependencies)
155 .progress(progress.clone())
156 .remove()
157 .await?;
158 operations::Install::new(&config)
159 .packages(reinstall_specs)
160 .tree(tree)
161 .progress(progress.clone())
162 .install()
163 .await?;
164 } else {
165 return Err(eyre!("Operation cancelled."));
166 }
167 };
168
169 let mut has_dangling_rocks = true;
170 while has_dangling_rocks {
171 let tree = config.user_tree(LuaVersion::from(&config)?.clone())?;
172 let lockfile = tree.lockfile()?;
173 let dangling_rocks = lockfile
174 .rocks()
175 .keys()
176 .filter(|pkg_id| !lockfile.is_entrypoint(pkg_id) && !lockfile.is_dependency(pkg_id))
177 .cloned()
178 .collect_vec();
179 if dangling_rocks.is_empty() {
180 has_dangling_rocks = false
181 } else {
182 operations::Uninstall::new()
183 .config(&config)
184 .packages(dangling_rocks)
185 .progress(progress.clone())
186 .remove()
187 .await?;
188 }
189 }
190
191 Ok(())
192}