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 packages: Vec<PackageReq>,
19}
20
21pub 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 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}