@@ -8,10 +8,9 @@ use crate::{
8
8
utils:: PythonEnv ,
9
9
} ;
10
10
use regex:: Regex ;
11
- use std:: { collections:: HashSet , fs :: DirEntry , path:: PathBuf } ;
11
+ use std:: { collections:: HashSet , path:: PathBuf } ;
12
12
13
- fn is_symlinked_python_executable ( path : & DirEntry ) -> Option < PathBuf > {
14
- let path = path. path ( ) ;
13
+ fn is_symlinked_python_executable ( path : & PathBuf ) -> Option < PathBuf > {
15
14
let name = path. file_name ( ) ?. to_string_lossy ( ) ;
16
15
if !name. starts_with ( "python" ) || name. ends_with ( "-config" ) || name. ends_with ( "-build" ) {
17
16
return None ;
@@ -23,6 +22,149 @@ fn is_symlinked_python_executable(path: &DirEntry) -> Option<PathBuf> {
23
22
Some ( std:: fs:: canonicalize ( path) . ok ( ) ?)
24
23
}
25
24
25
+ fn get_homebrew_prefix_env_var ( environment : & dyn Environment ) -> Option < PathBuf > {
26
+ if let Some ( homebrew_prefix) = environment. get_env_var ( "HOMEBREW_PREFIX" . to_string ( ) ) {
27
+ let homebrew_prefix_bin = PathBuf :: from ( homebrew_prefix) . join ( "bin" ) ;
28
+ if homebrew_prefix_bin. exists ( ) {
29
+ return Some ( homebrew_prefix_bin) ;
30
+ }
31
+ }
32
+ None
33
+ }
34
+
35
+ fn get_homebrew_prefix_bin ( environment : & dyn Environment ) -> Option < PathBuf > {
36
+ if let Some ( homebrew_prefix) = get_homebrew_prefix_env_var ( environment) {
37
+ return Some ( homebrew_prefix) ;
38
+ }
39
+
40
+ // Homebrew install folders documented here https://docs.brew.sh/Installation
41
+ // /opt/homebrew for Apple Silicon,
42
+ // /usr/local for macOS Intel
43
+ // /home/linuxbrew/.linuxbrew for Linux
44
+ [
45
+ "/home/linuxbrew/.linuxbrew/bin" ,
46
+ "/opt/homebrew/bin" ,
47
+ "/usr/local/bin" ,
48
+ ]
49
+ . iter ( )
50
+ . map ( |p| PathBuf :: from ( p) )
51
+ . find ( |p| p. exists ( ) )
52
+ }
53
+
54
+ fn get_env_path ( python_exe_from_bin_dir : & PathBuf , resolved_file : & PathBuf ) -> Option < PathBuf > {
55
+ // If the fully resolved file path contains the words `/homebrew/` or `/linuxbrew/`
56
+ // Then we know this is definitely a home brew version of python.
57
+ // And in these cases we can compute the sysprefix.
58
+
59
+ let resolved_file = resolved_file. to_str ( ) ?;
60
+ // 1. MacOS Silicon
61
+ if python_exe_from_bin_dir
62
+ . to_string_lossy ( )
63
+ . to_lowercase ( )
64
+ . starts_with ( "/opt/homebrew/bin/python" )
65
+ {
66
+ // Resolved exe is something like `/opt/homebrew/Cellar/[email protected] /3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12`
67
+ let reg_ex = Regex :: new ( "/opt/homebrew/Cellar/python@((\\ d+\\ .?)*)/(\\ d+\\ .?)*/Frameworks/Python.framework/Versions/(\\ d+\\ .?)*/bin/python(\\ d+\\ .?)*" ) . unwrap ( ) ;
68
+ let captures = reg_ex. captures ( & resolved_file) ?;
69
+ let version = captures. get ( 1 ) . map ( |m| m. as_str ( ) ) . unwrap_or_default ( ) ;
70
+ // SysPrefix- /opt/homebrew/opt/[email protected] /Frameworks/Python.framework/Versions/3.12
71
+ let sys_prefix = PathBuf :: from ( format ! (
72
+ "/opt/homebrew/opt/python@{}/Frameworks/Python.framework/Versions/{}" ,
73
+ version, version
74
+ ) ) ;
75
+
76
+ return if sys_prefix. exists ( ) {
77
+ Some ( sys_prefix)
78
+ } else {
79
+ None
80
+ } ;
81
+ }
82
+
83
+ // 2. Linux
84
+ if python_exe_from_bin_dir
85
+ . to_string_lossy ( )
86
+ . to_lowercase ( )
87
+ . starts_with ( "/usr/local/bin/python" )
88
+ {
89
+ // Resolved exe is something like `/home/linuxbrew/.linuxbrew/Cellar/[email protected] /3.12.3/bin/python3.12`
90
+ let reg_ex = Regex :: new ( "/home/linuxbrew/.linuxbrew/Cellar/python@(\\ d+\\ .?\\ d+\\ .?)/(\\ d+\\ .?\\ d+\\ .?\\ d+\\ .?)/bin/python.*" ) . unwrap ( ) ;
91
+ let captures = reg_ex. captures ( & resolved_file) ?;
92
+ let version = captures. get ( 1 ) . map ( |m| m. as_str ( ) ) . unwrap_or_default ( ) ;
93
+ let full_version = captures. get ( 2 ) . map ( |m| m. as_str ( ) ) . unwrap_or_default ( ) ;
94
+ // SysPrefix- /home/linuxbrew/.linuxbrew/Cellar/[email protected] /3.12.3
95
+ let sys_prefix = PathBuf :: from ( format ! (
96
+ "/home/linuxbrew/.linuxbrew/Cellar/python@{}/{}" ,
97
+ version, full_version
98
+ ) ) ;
99
+
100
+ return if sys_prefix. exists ( ) {
101
+ Some ( sys_prefix)
102
+ } else {
103
+ None
104
+ } ;
105
+ }
106
+
107
+ // 3. MacOS Intel
108
+ if python_exe_from_bin_dir
109
+ . to_string_lossy ( )
110
+ . to_lowercase ( )
111
+ . starts_with ( "/usr/local/bin/python" )
112
+ {
113
+ // Resolved exe is something like `/usr/local/Cellar/[email protected] /3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12`
114
+ let reg_ex = Regex :: new ( "/usr/local/Cellar/python@(\\ d+\\ .?\\ d+\\ .?)/(\\ d+\\ .?\\ d+\\ .?\\ d+\\ .?)/Frameworks/Python.framework/Versions/(\\ d+\\ .?\\ d+\\ .?)/bin/python.*" ) . unwrap ( ) ;
115
+ let captures = reg_ex. captures ( & resolved_file) ?;
116
+ let version = captures. get ( 1 ) . map ( |m| m. as_str ( ) ) . unwrap_or_default ( ) ;
117
+ let full_version = captures. get ( 2 ) . map ( |m| m. as_str ( ) ) . unwrap_or_default ( ) ;
118
+ // SysPrefix- /usr/local/Cellar/[email protected] /3.8.19/Frameworks/Python.framework/Versions/3.8
119
+ let sys_prefix = PathBuf :: from ( format ! (
120
+ "/usr/local/Cellar/python@{}/{}/Frameworks/Python.framework/Versions/{}" ,
121
+ version, full_version, version
122
+ ) ) ;
123
+
124
+ return if sys_prefix. exists ( ) {
125
+ Some ( sys_prefix)
126
+ } else {
127
+ None
128
+ } ;
129
+ }
130
+ None
131
+ }
132
+
133
+ fn get_python_info (
134
+ python_exe_from_bin_dir : & PathBuf ,
135
+ reported : & mut HashSet < String > ,
136
+ python_version_regex : & Regex ,
137
+ ) -> Option < PythonEnvironment > {
138
+ // Possible we do not have python3.12 or the like in bin directory
139
+ // & we have only python3, in that case we should add python3 to the list
140
+ if let Some ( resolved_exe) = is_symlinked_python_executable ( python_exe_from_bin_dir) {
141
+ let user_friendly_exe = python_exe_from_bin_dir;
142
+ let python_version = resolved_exe. to_string_lossy ( ) . to_string ( ) ;
143
+ let version = match python_version_regex. captures ( & python_version) {
144
+ Some ( captures) => match captures. get ( 1 ) {
145
+ Some ( version) => Some ( version. as_str ( ) . to_string ( ) ) ,
146
+ None => None ,
147
+ } ,
148
+ None => None ,
149
+ } ;
150
+ if reported. contains ( & resolved_exe. to_string_lossy ( ) . to_string ( ) ) {
151
+ return None ;
152
+ }
153
+ reported. insert ( resolved_exe. to_string_lossy ( ) . to_string ( ) ) ;
154
+ return Some ( PythonEnvironment :: new (
155
+ None ,
156
+ None ,
157
+ Some ( user_friendly_exe. clone ( ) ) ,
158
+ crate :: messaging:: PythonEnvironmentCategory :: Homebrew ,
159
+ version,
160
+ get_env_path ( python_exe_from_bin_dir, & resolved_exe) ,
161
+ None ,
162
+ Some ( vec ! [ user_friendly_exe. to_string_lossy( ) . to_string( ) ] ) ,
163
+ ) ) ;
164
+ }
165
+ None
166
+ }
167
+
26
168
pub struct Homebrew < ' a > {
27
169
pub environment : & ' a dyn Environment ,
28
170
}
@@ -34,64 +176,100 @@ impl Homebrew<'_> {
34
176
}
35
177
36
178
impl Locator for Homebrew < ' _ > {
37
- fn resolve ( & self , _env : & PythonEnv ) -> Option < PythonEnvironment > {
38
- None
179
+ fn resolve ( & self , env : & PythonEnv ) -> Option < PythonEnvironment > {
180
+ let python_regex = Regex :: new ( r"/(\d+\.\d+\.\d+)/" ) . unwrap ( ) ;
181
+ let exe = env. executable . clone ( ) ;
182
+ let exe_file_name = exe. file_name ( ) ?;
183
+ let mut reported: HashSet < String > = HashSet :: new ( ) ;
184
+ if exe. starts_with ( "/opt/homebrew/bin/python" )
185
+ || exe. starts_with ( "/opt/homebrew/Cellar/python@" )
186
+ || exe. starts_with ( "/opt/homebrew/opt/python@" )
187
+ || exe. starts_with ( "/opt/homebrew/opt/python" )
188
+ || exe. starts_with ( "/opt/homebrew/Frameworks/Python.framework/Versions/" )
189
+ {
190
+ // Symlink - /opt/homebrew/bin/python3.12
191
+ // Symlink - /opt/homebrew/opt/python3/bin/python3.12
192
+ // Symlink - /opt/homebrew/Cellar/[email protected] /3.12.3/bin/python3.12
193
+ // Symlink - /opt/homebrew/opt/[email protected] /bin/python3.12
194
+ // Symlink - /opt/homebrew/Cellar/[email protected] /3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12
195
+ // Symlink - /opt/homebrew/Cellar/[email protected] /3.12.3/Frameworks/Python.framework/Versions/Current/bin/python3.12
196
+ // Symlink - /opt/homebrew/Frameworks/Python.framework/Versions/3.12/bin/python3.12
197
+ // Symlink - /opt/homebrew/Frameworks/Python.framework/Versions/Current/bin/python3.12
198
+ // Real exe - /opt/homebrew/Cellar/[email protected] /3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12
199
+ // SysPrefix- /opt/homebrew/opt/[email protected] /Frameworks/Python.framework/Versions/3.12
200
+ get_python_info (
201
+ & PathBuf :: from ( "/opt/homebrew/bin" ) . join ( exe_file_name) ,
202
+ & mut reported,
203
+ & python_regex,
204
+ )
205
+ } else if exe. starts_with ( "/usr/local/bin/python" )
206
+ || exe. starts_with ( "/usr/local/opt/python@" )
207
+ || exe. starts_with ( "/usr/local/Cellar/python@" )
208
+ {
209
+ // Symlink - /usr/local/bin/python3.8
210
+ // Symlink - /usr/local/opt/[email protected] /bin/python3.8
211
+ // Symlink - /usr/local/Cellar/[email protected] /3.8.19/bin/python3.8
212
+ // Real exe - /usr/local/Cellar/[email protected] /3.8.19/Frameworks/Python.framework/Versions/3.8/bin/python3.8
213
+ // SysPrefix- /usr/local/Cellar/[email protected] /3.8.19/Frameworks/Python.framework/Versions/3.8
214
+ get_python_info (
215
+ & PathBuf :: from ( "/usr/local/bin" ) . join ( exe_file_name) ,
216
+ & mut reported,
217
+ & python_regex,
218
+ )
219
+ } else if exe. starts_with ( "/usr/local/bin/python" )
220
+ || exe. starts_with ( "/home/linuxbrew/.linuxbrew/bin/python" )
221
+ || exe. starts_with ( "/home/linuxbrew/.linuxbrew/opt/python@" )
222
+ || exe. starts_with ( "/home/linuxbrew/.linuxbrew/Cellar/python" )
223
+ {
224
+ // Symlink - /usr/local/bin/python3.12
225
+ // Symlink - /home/linuxbrew/.linuxbrew/bin/python3.12
226
+ // Symlink - /home/linuxbrew/.linuxbrew/opt/[email protected] /bin/python3.12
227
+ // Real exe - /home/linuxbrew/.linuxbrew/Cellar/[email protected] /3.12.3/bin/python3.12
228
+ // SysPrefix- /home/linuxbrew/.linuxbrew/Cellar/[email protected] /3.12.3
229
+
230
+ get_python_info (
231
+ & PathBuf :: from ( "/usr/local/bin" ) . join ( exe_file_name) ,
232
+ & mut reported,
233
+ & python_regex,
234
+ )
235
+ } else {
236
+ None
237
+ }
39
238
}
40
239
41
240
fn find ( & mut self ) -> Option < LocatorResult > {
42
- let homebrew_prefix = self
43
- . environment
44
- . get_env_var ( "HOMEBREW_PREFIX" . to_string ( ) ) ?;
45
- let homebrew_prefix_bin = PathBuf :: from ( homebrew_prefix) . join ( "bin" ) ;
241
+ let homebrew_prefix_bin = get_homebrew_prefix_bin ( self . environment ) ?;
46
242
let mut reported: HashSet < String > = HashSet :: new ( ) ;
47
243
let python_regex = Regex :: new ( r"/(\d+\.\d+\.\d+)/" ) . unwrap ( ) ;
48
244
let mut environments: Vec < PythonEnvironment > = vec ! [ ] ;
49
- for file in std:: fs:: read_dir ( homebrew_prefix_bin)
245
+ for file in std:: fs:: read_dir ( & homebrew_prefix_bin)
50
246
. ok ( ) ?
51
247
. filter_map ( Result :: ok)
52
248
{
53
- if let Some ( exe) = is_symlinked_python_executable ( & file) {
54
- let python_version = exe. to_string_lossy ( ) . to_string ( ) ;
55
- let version = match python_regex. captures ( & python_version) {
56
- Some ( captures) => match captures. get ( 1 ) {
57
- Some ( version) => Some ( version. as_str ( ) . to_string ( ) ) ,
58
- None => None ,
59
- } ,
60
- None => None ,
61
- } ;
62
- if reported. contains ( & exe. to_string_lossy ( ) . to_string ( ) ) {
249
+ // If this file name is `python3`, then ignore this for now.
250
+ // We would prefer to use `python3.x` instead of `python3`.
251
+ // That way its more consistent and future proof
252
+ if let Some ( file_name) = file. file_name ( ) . to_str ( ) {
253
+ if file_name. to_lowercase ( ) == "python3" {
63
254
continue ;
64
255
}
65
- let env_path = match exe. parent ( ) {
66
- Some ( path) => {
67
- if let Some ( name) = path. file_name ( ) {
68
- if name. to_ascii_lowercase ( ) == "bin"
69
- || name. to_ascii_lowercase ( ) == "Scripts"
70
- {
71
- Some ( path. parent ( ) ?. to_path_buf ( ) )
72
- } else {
73
- Some ( path. to_path_buf ( ) )
74
- }
75
- } else {
76
- None
77
- }
78
- }
79
- None => continue ,
80
- } ;
81
- reported. insert ( exe. to_string_lossy ( ) . to_string ( ) ) ;
82
- let env = crate :: messaging:: PythonEnvironment :: new (
83
- None ,
84
- None ,
85
- Some ( exe. clone ( ) ) ,
86
- crate :: messaging:: PythonEnvironmentCategory :: Homebrew ,
87
- version,
88
- env_path,
89
- None ,
90
- Some ( vec ! [ exe. to_string_lossy( ) . to_string( ) ] ) ,
91
- ) ;
256
+ }
257
+
258
+ if let Some ( env) = get_python_info ( & file. path ( ) , & mut reported, & python_regex) {
92
259
environments. push ( env) ;
93
260
}
94
261
}
262
+
263
+ // Possible we do not have python3.12 or the like in bin directory
264
+ // & we have only python3, in that case we should add python3 to the list
265
+ if let Some ( env) = get_python_info (
266
+ & homebrew_prefix_bin. join ( "python3" ) ,
267
+ & mut reported,
268
+ & python_regex,
269
+ ) {
270
+ environments. push ( env) ;
271
+ }
272
+
95
273
if environments. is_empty ( ) {
96
274
None
97
275
} else {
0 commit comments