11// Copyright (c) Microsoft Corporation.
22// Licensed under the MIT License.
33
4+ use lazy_static:: lazy_static;
45use log:: trace;
56use pet_core:: manager:: { EnvManager , EnvManagerType } ;
7+ use pet_fs:: path:: resolve_any_symlink;
8+ use regex:: Regex ;
69use std:: { env, path:: PathBuf } ;
710
811use crate :: env_variables:: EnvVariables ;
912
13+ lazy_static ! {
14+ /// Matches Homebrew Cellar path for poetry: /Cellar/poetry/X.Y.Z or /Cellar/poetry/X.Y.Z_N
15+ static ref HOMEBREW_POETRY_VERSION : Regex =
16+ Regex :: new( r"/Cellar/poetry/(\d+\.\d+\.\d+)" ) . expect( "error parsing Homebrew poetry version regex" ) ;
17+ }
18+
1019#[ derive( Clone , PartialEq , Eq , Debug ) ]
1120pub struct PoetryManager {
1221 pub executable : PathBuf ,
22+ pub version : Option < String > ,
1323}
1424
1525impl PoetryManager {
1626 pub fn find ( executable : Option < PathBuf > , env_variables : & EnvVariables ) -> Option < Self > {
1727 if let Some ( executable) = executable {
1828 if executable. is_file ( ) {
19- return Some ( PoetryManager { executable } ) ;
29+ let version = Self :: extract_version_from_path ( & executable) ;
30+ return Some ( PoetryManager {
31+ executable,
32+ version,
33+ } ) ;
2034 }
2135 }
2236
@@ -107,7 +121,11 @@ impl PoetryManager {
107121 }
108122 for executable in search_paths {
109123 if executable. is_file ( ) {
110- return Some ( PoetryManager { executable } ) ;
124+ let version = Self :: extract_version_from_path ( & executable) ;
125+ return Some ( PoetryManager {
126+ executable,
127+ version,
128+ } ) ;
111129 }
112130 }
113131
@@ -116,12 +134,20 @@ impl PoetryManager {
116134 for each in env:: split_paths ( env_path) {
117135 let executable = each. join ( "poetry" ) ;
118136 if executable. is_file ( ) {
119- return Some ( PoetryManager { executable } ) ;
137+ let version = Self :: extract_version_from_path ( & executable) ;
138+ return Some ( PoetryManager {
139+ executable,
140+ version,
141+ } ) ;
120142 }
121143 if std:: env:: consts:: OS == "windows" {
122144 let executable = each. join ( "poetry.exe" ) ;
123145 if executable. is_file ( ) {
124- return Some ( PoetryManager { executable } ) ;
146+ let version = Self :: extract_version_from_path ( & executable) ;
147+ return Some ( PoetryManager {
148+ executable,
149+ version,
150+ } ) ;
125151 }
126152 }
127153 }
@@ -130,11 +156,133 @@ impl PoetryManager {
130156 trace ! ( "Poetry exe not found" ) ;
131157 None
132158 }
159+
160+ /// Extracts poetry version from Homebrew Cellar path.
161+ ///
162+ /// Homebrew installs poetry to paths like:
163+ /// - macOS ARM: /opt/homebrew/Cellar/poetry/1.8.3_2/bin/poetry
164+ /// - macOS Intel: /usr/local/Cellar/poetry/1.8.3/bin/poetry
165+ /// - Linux: /home/linuxbrew/.linuxbrew/Cellar/poetry/1.8.3/bin/poetry
166+ ///
167+ /// The symlink at /opt/homebrew/bin/poetry points to the Cellar path.
168+ fn extract_version_from_path ( executable : & PathBuf ) -> Option < String > {
169+ // First try to resolve the symlink to get the actual Cellar path
170+ let resolved = resolve_any_symlink ( executable) . unwrap_or_else ( || executable. clone ( ) ) ;
171+ let path_str = resolved. to_string_lossy ( ) ;
172+
173+ // Check if this is a Homebrew Cellar path and extract version
174+ if let Some ( captures) = HOMEBREW_POETRY_VERSION . captures ( & path_str) {
175+ if let Some ( version_match) = captures. get ( 1 ) {
176+ let version = version_match. as_str ( ) . to_string ( ) ;
177+ trace ! (
178+ "Extracted Poetry version {} from Homebrew path: {:?}" ,
179+ version,
180+ resolved
181+ ) ;
182+ return Some ( version) ;
183+ }
184+ }
185+ None
186+ }
187+
133188 pub fn to_manager ( & self ) -> EnvManager {
134189 EnvManager {
135190 executable : self . executable . clone ( ) ,
136- version : None ,
191+ version : self . version . clone ( ) ,
137192 tool : EnvManagerType :: Poetry ,
138193 }
139194 }
195+
196+ /// Extracts version from a path string using the Homebrew Cellar regex.
197+ /// This is exposed for testing purposes.
198+ #[ cfg( test) ]
199+ fn extract_version_from_path_str ( path_str : & str ) -> Option < String > {
200+ if let Some ( captures) = HOMEBREW_POETRY_VERSION . captures ( path_str) {
201+ captures. get ( 1 ) . map ( |m| m. as_str ( ) . to_string ( ) )
202+ } else {
203+ None
204+ }
205+ }
206+ }
207+
208+ #[ cfg( test) ]
209+ mod tests {
210+ use super :: * ;
211+
212+ #[ test]
213+ fn test_extract_version_macos_arm ( ) {
214+ // macOS ARM Homebrew path
215+ let path = "/opt/homebrew/Cellar/poetry/1.8.3/bin/poetry" ;
216+ assert_eq ! (
217+ PoetryManager :: extract_version_from_path_str( path) ,
218+ Some ( "1.8.3" . to_string( ) )
219+ ) ;
220+ }
221+
222+ #[ test]
223+ fn test_extract_version_macos_arm_with_revision ( ) {
224+ // macOS ARM Homebrew path with revision suffix
225+ let path = "/opt/homebrew/Cellar/poetry/1.8.3_2/bin/poetry" ;
226+ assert_eq ! (
227+ PoetryManager :: extract_version_from_path_str( path) ,
228+ Some ( "1.8.3" . to_string( ) )
229+ ) ;
230+ }
231+
232+ #[ test]
233+ fn test_extract_version_macos_intel ( ) {
234+ // macOS Intel Homebrew path
235+ let path = "/usr/local/Cellar/poetry/2.0.1/bin/poetry" ;
236+ assert_eq ! (
237+ PoetryManager :: extract_version_from_path_str( path) ,
238+ Some ( "2.0.1" . to_string( ) )
239+ ) ;
240+ }
241+
242+ #[ test]
243+ fn test_extract_version_linux ( ) {
244+ // Linux Homebrew path
245+ let path = "/home/linuxbrew/.linuxbrew/Cellar/poetry/1.7.0/bin/poetry" ;
246+ assert_eq ! (
247+ PoetryManager :: extract_version_from_path_str( path) ,
248+ Some ( "1.7.0" . to_string( ) )
249+ ) ;
250+ }
251+
252+ #[ test]
253+ fn test_extract_version_non_homebrew_path ( ) {
254+ // Non-Homebrew installation paths should return None
255+ let paths = [
256+ "/usr/local/bin/poetry" ,
257+ "/home/user/.local/bin/poetry" ,
258+ "/home/user/.poetry/bin/poetry" ,
259+ "C:\\ Users\\ user\\ AppData\\ Roaming\\ pypoetry\\ venv\\ Scripts\\ poetry.exe" ,
260+ ] ;
261+ for path in paths {
262+ assert_eq ! (
263+ PoetryManager :: extract_version_from_path_str( path) ,
264+ None ,
265+ "Expected None for path: {}" ,
266+ path
267+ ) ;
268+ }
269+ }
270+
271+ #[ test]
272+ fn test_extract_version_invalid_version_format ( ) {
273+ // Invalid version formats should not match
274+ let paths = [
275+ "/opt/homebrew/Cellar/poetry/invalid/bin/poetry" ,
276+ "/opt/homebrew/Cellar/poetry/1.8/bin/poetry" , // Missing patch version
277+ "/opt/homebrew/Cellar/poetry/v1.8.3/bin/poetry" , // Has 'v' prefix
278+ ] ;
279+ for path in paths {
280+ assert_eq ! (
281+ PoetryManager :: extract_version_from_path_str( path) ,
282+ None ,
283+ "Expected None for path: {}" ,
284+ path
285+ ) ;
286+ }
287+ }
140288}
0 commit comments