uv_install_wheel/
uninstall.rs1use std::collections::BTreeSet;
2use std::path::{Component, Path, PathBuf};
3
4use std::sync::{LazyLock, Mutex};
5use tracing::trace;
6use uv_fs::write_atomic_sync;
7
8use crate::Error;
9use crate::wheel::read_record_file;
10
11pub fn uninstall_wheel(dist_info: &Path) -> Result<Uninstall, Error> {
13 let Some(site_packages) = dist_info.parent() else {
14 return Err(Error::BrokenVenv(
15 "dist-info directory is not in a site-packages directory".to_string(),
16 ));
17 };
18
19 let record = {
21 let record_path = dist_info.join("RECORD");
22 let mut record_file = match fs_err::File::open(&record_path) {
23 Ok(record_file) => record_file,
24 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
25 return Err(Error::MissingRecord(record_path));
26 }
27 Err(err) => return Err(err.into()),
28 };
29 read_record_file(&mut record_file)?
30 };
31
32 let mut file_count = 0usize;
33 let mut dir_count = 0usize;
34
35 #[cfg(windows)]
36 let itself = std::env::current_exe().ok();
37
38 let mut visited = BTreeSet::new();
40 for entry in &record {
41 let path = site_packages.join(&entry.path);
42
43 #[cfg(windows)]
45 if let Some(itself) = itself.as_ref() {
46 if itself
47 .file_name()
48 .is_some_and(|itself| path.file_name().is_some_and(|path| itself == path))
49 {
50 if same_file::is_same_file(itself, &path).unwrap_or(false) {
51 tracing::debug!("Detected self-delete of executable: {}", path.display());
52 match self_replace::self_delete_outside_path(site_packages) {
53 Ok(()) => {
54 trace!("Removed file: {}", path.display());
55 file_count += 1;
56 if let Some(parent) = path.parent() {
57 visited.insert(normalize_path(parent));
58 }
59 }
60 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
61 Err(err) => return Err(err.into()),
62 }
63 continue;
64 }
65 }
66 }
67
68 match fs_err::remove_file(&path) {
69 Ok(()) => {
70 trace!("Removed file: {}", path.display());
71 file_count += 1;
72 if let Some(parent) = path.parent() {
73 visited.insert(normalize_path(parent));
74 }
75 }
76 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
77 Err(err) => match fs_err::remove_dir_all(&path) {
78 Ok(()) => {
79 trace!("Removed directory: {}", path.display());
80 dir_count += 1;
81 }
82 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
83 Err(_) => return Err(err.into()),
84 },
85 }
86 }
87
88 for path in visited.iter().rev() {
91 if !path.starts_with(site_packages) {
93 continue;
94 }
95
96 let mut path = path.as_path();
100 loop {
101 if path == site_packages {
103 break;
104 }
105
106 let pycache = path.join("__pycache__");
110 match fs_err::remove_dir_all(&pycache) {
111 Ok(()) => {
112 trace!("Removed directory: {}", pycache.display());
113 dir_count += 1;
114 }
115 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
116 Err(err) => return Err(err.into()),
117 }
118
119 let mut read_dir = match fs_err::read_dir(path) {
122 Ok(read_dir) => read_dir,
123 Err(err) if err.kind() == std::io::ErrorKind::NotFound => break,
124 Err(err) => return Err(err.into()),
125 };
126
127 if read_dir.next().is_some() {
129 break;
130 }
131
132 fs_err::remove_dir(path)?;
133
134 trace!("Removed directory: {}", path.display());
135 dir_count += 1;
136
137 if let Some(parent) = path.parent() {
138 path = parent;
139 } else {
140 break;
141 }
142 }
143 }
144
145 Ok(Uninstall {
146 file_count,
147 dir_count,
148 })
149}
150
151pub fn uninstall_egg(egg_info: &Path) -> Result<Uninstall, Error> {
155 let mut file_count = 0usize;
156 let mut dir_count = 0usize;
157
158 let dist_location = egg_info
159 .parent()
160 .expect("egg-info directory is not in a site-packages directory");
161
162 let namespace_packages = {
164 let namespace_packages_path = egg_info.join("namespace_packages.txt");
165 match fs_err::read_to_string(namespace_packages_path) {
166 Ok(namespace_packages) => namespace_packages
167 .lines()
168 .map(ToString::to_string)
169 .collect::<Vec<_>>(),
170 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
171 vec![]
172 }
173 Err(err) => return Err(err.into()),
174 }
175 };
176
177 let top_level = {
179 let top_level_path = egg_info.join("top_level.txt");
180 match fs_err::read_to_string(&top_level_path) {
181 Ok(top_level) => top_level
182 .lines()
183 .map(ToString::to_string)
184 .filter(|line| !namespace_packages.contains(line))
185 .collect::<Vec<_>>(),
186 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
187 return Err(Error::MissingTopLevel(top_level_path));
188 }
189 Err(err) => return Err(err.into()),
190 }
191 };
192
193 for entry in top_level {
195 let path = dist_location.join(&entry);
196
197 match fs_err::remove_dir_all(&path) {
199 Ok(()) => {
200 trace!("Removed directory: {}", path.display());
201 dir_count += 1;
202 continue;
203 }
204 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
205 Err(err) => return Err(err.into()),
206 }
207
208 for extension in &["py", "pyc", "pyo"] {
210 let path = path.with_extension(extension);
211 match fs_err::remove_file(&path) {
212 Ok(()) => {
213 trace!("Removed file: {}", path.display());
214 file_count += 1;
215 break;
216 }
217 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
218 Err(err) => return Err(err.into()),
219 }
220 }
221 }
222
223 match fs_err::remove_dir_all(egg_info) {
225 Ok(()) => {
226 trace!("Removed directory: {}", egg_info.display());
227 dir_count += 1;
228 }
229 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
230 Err(err) => {
231 return Err(err.into());
232 }
233 }
234
235 Ok(Uninstall {
236 file_count,
237 dir_count,
238 })
239}
240
241fn normcase(s: &str) -> String {
242 if cfg!(windows) {
243 s.replace('/', "\\").to_lowercase()
244 } else {
245 s.to_owned()
246 }
247}
248
249static EASY_INSTALL_PTH: LazyLock<Mutex<i32>> = LazyLock::new(Mutex::default);
250
251pub fn uninstall_legacy_editable(egg_link: &Path) -> Result<Uninstall, Error> {
255 let mut file_count = 0usize;
256
257 let contents = fs_err::read_to_string(egg_link)?;
259 let target_line = contents
260 .lines()
261 .find_map(|line| {
262 let line = line.trim();
263 if line.is_empty() { None } else { Some(line) }
264 })
265 .ok_or_else(|| Error::InvalidEggLink(egg_link.to_path_buf()))?;
266
267 let target_line = normcase(target_line);
269
270 match fs_err::remove_file(egg_link) {
271 Ok(()) => {
272 trace!("Removed file: {}", egg_link.display());
273 file_count += 1;
274 }
275 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
276 Err(err) => return Err(err.into()),
277 }
278
279 let site_package = egg_link.parent().ok_or(Error::BrokenVenv(
280 "`.egg-link` file is not in a directory".to_string(),
281 ))?;
282 let easy_install = site_package.join("easy-install.pth");
283
284 let _guard = EASY_INSTALL_PTH.lock().unwrap();
288
289 let content = fs_err::read_to_string(&easy_install)?;
290 let mut new_content = String::with_capacity(content.len());
291 let mut removed = false;
292
293 for line in content.lines() {
295 if !removed && line.trim() == target_line {
296 removed = true;
297 } else {
298 new_content.push_str(line);
299 new_content.push('\n');
300 }
301 }
302 if removed {
303 write_atomic_sync(&easy_install, new_content)?;
304 trace!("Removed line from `easy-install.pth`: {target_line}");
305 }
306
307 Ok(Uninstall {
308 file_count,
309 dir_count: 0usize,
310 })
311}
312
313#[derive(Debug, Default)]
314pub struct Uninstall {
315 pub file_count: usize,
317 pub dir_count: usize,
319}
320
321fn normalize_path(path: &Path) -> PathBuf {
325 let mut components = path.components().peekable();
326 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() {
327 components.next();
328 PathBuf::from(c.as_os_str())
329 } else {
330 PathBuf::new()
331 };
332
333 for component in components {
334 match component {
335 Component::Prefix(..) => unreachable!(),
336 Component::RootDir => {
337 ret.push(component.as_os_str());
338 }
339 Component::CurDir => {}
340 Component::ParentDir => {
341 ret.pop();
342 }
343 Component::Normal(c) => {
344 ret.push(c);
345 }
346 }
347 }
348 ret
349}