@@ -7,8 +7,13 @@ use futures::StreamExt;
7
7
use itertools:: Itertools ;
8
8
use owo_colors:: OwoColorize ;
9
9
10
+ use same_file:: is_same_file;
11
+ use tracing:: { debug, warn} ;
12
+ use uv_fs:: Simplified ;
10
13
use uv_python:: downloads:: PythonDownloadRequest ;
11
- use uv_python:: managed:: ManagedPythonInstallations ;
14
+ use uv_python:: managed:: {
15
+ is_windows_python_shim, python_executable_dir, ManagedPythonInstallations ,
16
+ } ;
12
17
use uv_python:: PythonRequest ;
13
18
14
19
use crate :: commands:: python:: { ChangeEvent , ChangeEventKind } ;
@@ -121,6 +126,48 @@ async fn do_uninstall(
121
126
return Ok ( ExitStatus :: Failure ) ;
122
127
}
123
128
129
+ // Collect files in a directory
130
+ let executables = python_executable_dir ( ) ?
131
+ . read_dir ( ) ?
132
+ . filter_map ( |entry| match entry {
133
+ Ok ( entry) => Some ( entry) ,
134
+ Err ( err) => {
135
+ warn ! ( "Failed to read executable: {}" , err) ;
136
+ None
137
+ }
138
+ } )
139
+ . filter ( |entry| entry. file_type ( ) . is_ok_and ( |file_type| !file_type. is_dir ( ) ) )
140
+ . map ( |entry| entry. path ( ) )
141
+ // Only include files that match the expected Python executable names
142
+ // TODO(zanieb): This is a minor optimization to avoid opening more files, but we could
143
+ // leave broken links behind, i.e., if the user created them.
144
+ . filter ( |path| {
145
+ matching_installations. iter ( ) . any ( |installation| {
146
+ path. file_name ( ) . and_then ( |name| name. to_str ( ) )
147
+ == Some ( & installation. key ( ) . executable_name_minor ( ) )
148
+ } )
149
+ } )
150
+ // Only include Python executables that match the installations
151
+ . filter ( |path| {
152
+ matching_installations. iter ( ) . any ( |installation| {
153
+ if cfg ! ( unix) {
154
+ is_same_file ( path, installation. executable ( ) ) . unwrap_or_default ( )
155
+ } else if cfg ! ( windows) {
156
+ // TODO(zanieb): We need to check if the target path matches the executable
157
+ // otherwise we will remove the "default" executables
158
+ is_windows_python_shim ( path)
159
+ } else {
160
+ unreachable ! ( "Only Windows and Unix are supported" )
161
+ }
162
+ } )
163
+ } )
164
+ . collect :: < BTreeSet < _ > > ( ) ;
165
+
166
+ for executable in & executables {
167
+ fs_err:: remove_file ( executable) ?;
168
+ debug ! ( "Removed {}" , executable. user_display( ) ) ;
169
+ }
170
+
124
171
let mut tasks = FuturesUnordered :: new ( ) ;
125
172
for installation in & matching_installations {
126
173
tasks. push ( async {
@@ -180,7 +227,13 @@ async fn do_uninstall(
180
227
{
181
228
match event. kind {
182
229
ChangeEventKind :: Removed => {
183
- writeln ! ( printer. stderr( ) , " {} {}" , "-" . red( ) , event. key. bold( ) ) ?;
230
+ writeln ! (
231
+ printer. stderr( ) ,
232
+ " {} {} ({})" ,
233
+ "-" . red( ) ,
234
+ event. key. bold( ) ,
235
+ event. key. executable_name_minor( )
236
+ ) ?;
184
237
}
185
238
_ => unreachable ! ( ) ,
186
239
}
0 commit comments