@@ -7,8 +7,13 @@ use futures::StreamExt;
77use itertools:: Itertools ;
88use owo_colors:: OwoColorize ;
99
10+ use same_file:: is_same_file;
11+ use tracing:: { debug, warn} ;
12+ use uv_fs:: Simplified ;
1013use 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+ } ;
1217use uv_python:: PythonRequest ;
1318
1419use crate :: commands:: python:: { ChangeEvent , ChangeEventKind } ;
@@ -121,6 +126,48 @@ async fn do_uninstall(
121126 return Ok ( ExitStatus :: Failure ) ;
122127 }
123128
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+
124171 let mut tasks = FuturesUnordered :: new ( ) ;
125172 for installation in & matching_installations {
126173 tasks. push ( async {
@@ -180,7 +227,13 @@ async fn do_uninstall(
180227 {
181228 match event. kind {
182229 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+ ) ?;
184237 }
185238 _ => unreachable ! ( ) ,
186239 }
0 commit comments