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