1+ use std:: { env, path:: PathBuf } ;
2+
13use directories:: BaseDirs ;
24use vite_path:: { AbsolutePathBuf , current_dir} ;
3- use which:: which;
45
56use crate :: EnvConfig ;
67
@@ -10,7 +11,7 @@ const VITE_PLUS_HOME_DIR: &str = ".vite-plus";
1011/// Get the vite-plus home directory.
1112///
1213/// Uses `EnvConfig::get().vite_plus_home` if set,
13- /// or the `vp` executable's grandparent directory ,
14+ /// or the `VP_HOME/bin` directory on `PATH` ,
1415/// otherwise defaults to `~/.vite-plus`.
1516/// Falls back to `$CWD/.vite-plus` if the home directory cannot be determined.
1617pub fn get_vp_home ( ) -> std:: io:: Result < AbsolutePathBuf > {
@@ -21,14 +22,9 @@ pub fn get_vp_home() -> std::io::Result<AbsolutePathBuf> {
2122 return Ok ( path) ;
2223 }
2324
24- // Get from `vp` executable file's grandparent directory (~/.vite-plus/bin/vp)
25- // For the case where `VP_HOME` is missing
26- if let Ok ( path) = which ( "vp" )
27- && let Some ( parent) = path. parent ( )
28- && parent. file_name ( ) . is_some_and ( |name| name == "bin" )
29- && let Some ( grandparent) = parent. parent ( )
30- {
31- return Ok ( AbsolutePathBuf :: new ( grandparent. to_path_buf ( ) ) . unwrap ( ) ) ;
25+ // Project-local .bin wrappers can shadow Vite+ shims; only trust a full install layout.
26+ if let Some ( home) = infer_vp_home_from_path ( ) ? {
27+ return Ok ( home) ;
3228 }
3329
3430 // Default to ~/.vite-plus
@@ -44,10 +40,120 @@ pub fn get_vp_home() -> std::io::Result<AbsolutePathBuf> {
4440 }
4541}
4642
43+ fn infer_vp_home_from_path ( ) -> std:: io:: Result < Option < AbsolutePathBuf > > {
44+ let Some ( path_env) = env:: var_os ( "PATH" ) else {
45+ return Ok ( None ) ;
46+ } ;
47+
48+ for path_entry in env:: split_paths ( & path_env) {
49+ if path_entry. as_os_str ( ) . is_empty ( ) {
50+ continue ;
51+ }
52+
53+ let bin_dir = absolute_path_entry ( path_entry) ?;
54+ if bin_dir. as_path ( ) . file_name ( ) . is_none_or ( |name| name != "bin" ) {
55+ continue ;
56+ }
57+ let Some ( home) = bin_dir. parent ( ) else {
58+ continue ;
59+ } ;
60+ if is_vp_home_layout ( & bin_dir, home) {
61+ return Ok ( Some ( home. to_absolute_path_buf ( ) ) ) ;
62+ }
63+ }
64+
65+ Ok ( None )
66+ }
67+
68+ fn absolute_path_entry ( path : PathBuf ) -> std:: io:: Result < AbsolutePathBuf > {
69+ if let Some ( path) = AbsolutePathBuf :: new ( path. clone ( ) ) {
70+ return Ok ( path) ;
71+ }
72+
73+ Ok ( current_dir ( ) ?. join ( path) )
74+ }
75+
76+ fn is_vp_home_layout ( bin_dir : & vite_path:: AbsolutePath , home : & vite_path:: AbsolutePath ) -> bool {
77+ let vp_bin = if cfg ! ( windows) { bin_dir. join ( "vp.exe" ) } else { bin_dir. join ( "vp" ) } ;
78+ let current_vp = if cfg ! ( windows) {
79+ home. join ( "current" ) . join ( "bin" ) . join ( "vp.exe" )
80+ } else {
81+ home. join ( "current" ) . join ( "bin" ) . join ( "vp" )
82+ } ;
83+
84+ vp_bin. as_path ( ) . is_file ( ) && current_vp. as_path ( ) . is_file ( )
85+ }
86+
4787#[ cfg( test) ]
4888mod tests {
89+ use std:: ffi:: { OsStr , OsString } ;
90+
4991 use super :: * ;
5092
93+ struct EnvVarGuard {
94+ name : & ' static str ,
95+ original : Option < OsString > ,
96+ }
97+
98+ impl EnvVarGuard {
99+ fn set ( name : & ' static str , value : impl AsRef < OsStr > ) -> Self {
100+ let guard = Self { name, original : std:: env:: var_os ( name) } ;
101+ // SAFETY: these serial tests own process environment mutations and restore them on drop.
102+ unsafe { std:: env:: set_var ( name, value) } ;
103+ guard
104+ }
105+
106+ fn remove ( name : & ' static str ) -> Self {
107+ let guard = Self { name, original : std:: env:: var_os ( name) } ;
108+ // SAFETY: these serial tests own process environment mutations and restore them on drop.
109+ unsafe { std:: env:: remove_var ( name) } ;
110+ guard
111+ }
112+ }
113+
114+ impl Drop for EnvVarGuard {
115+ fn drop ( & mut self ) {
116+ // SAFETY: restore the environment snapshot captured by this serial test.
117+ unsafe {
118+ match & self . original {
119+ Some ( value) => std:: env:: set_var ( self . name , value) ,
120+ None => std:: env:: remove_var ( self . name ) ,
121+ }
122+ }
123+ }
124+ }
125+
126+ struct CurrentDirGuard {
127+ original : AbsolutePathBuf ,
128+ }
129+
130+ impl CurrentDirGuard {
131+ fn set ( path : impl AsRef < std:: path:: Path > ) -> Self {
132+ let guard = Self { original : current_dir ( ) . unwrap ( ) } ;
133+ std:: env:: set_current_dir ( path) . unwrap ( ) ;
134+ guard
135+ }
136+ }
137+
138+ impl Drop for CurrentDirGuard {
139+ fn drop ( & mut self ) {
140+ std:: env:: set_current_dir ( & self . original ) . unwrap ( ) ;
141+ }
142+ }
143+
144+ fn write_executable ( path : & std:: path:: Path ) {
145+ #[ cfg( windows) ]
146+ std:: fs:: write ( path, b"MZ" ) . unwrap ( ) ;
147+ #[ cfg( not( windows) ) ]
148+ {
149+ std:: fs:: write ( path, "#!/bin/sh\n echo 'fake vp'" ) . unwrap ( ) ;
150+ use std:: os:: unix:: fs:: PermissionsExt ;
151+ let mut perms = std:: fs:: metadata ( path) . unwrap ( ) . permissions ( ) ;
152+ perms. set_mode ( 0o755 ) ;
153+ std:: fs:: set_permissions ( path, perms) . unwrap ( ) ;
154+ }
155+ }
156+
51157 #[ test]
52158 fn test_get_vp_home ( ) {
53159 let home = get_vp_home ( ) . unwrap ( ) ;
@@ -66,74 +172,63 @@ mod tests {
66172 #[ test]
67173 #[ serial_test:: serial]
68174 fn test_get_vp_home_without_vp_home_infers_from_vp_on_path ( ) {
69- use std:: {
70- ffi:: { OsStr , OsString } ,
71- path:: PathBuf ,
72- } ;
73-
74175 let temp_dir = PathBuf :: from (
75176 std:: env:: temp_dir ( ) . join ( format ! ( "vp-test-vp-path-{}" , std:: process:: id( ) ) ) ,
76177 ) ;
77178 let vite_plus_home = temp_dir. join ( ".vite-plus" ) ;
78179 let bin_dir = vite_plus_home. join ( "bin" ) ;
180+ let current_bin_dir = vite_plus_home. join ( "current" ) . join ( "bin" ) ;
79181 std:: fs:: create_dir_all ( & bin_dir) . unwrap ( ) ;
182+ std:: fs:: create_dir_all ( & current_bin_dir) . unwrap ( ) ;
80183
81184 #[ cfg( windows) ]
82185 let vp_path = bin_dir. join ( "vp.exe" ) ;
83186 #[ cfg( not( windows) ) ]
84187 let vp_path = bin_dir. join ( "vp" ) ;
188+ write_executable ( & vp_path) ;
85189
86190 #[ cfg( windows) ]
87- std :: fs :: write ( & vp_path , b"MZ" ) . unwrap ( ) ;
191+ let current_vp_path = current_bin_dir . join ( "vp.exe" ) ;
88192 #[ cfg( not( windows) ) ]
89- {
90- std:: fs:: write ( & vp_path, "#!/bin/sh\n echo 'fake vp'" ) . unwrap ( ) ;
91- use std:: os:: unix:: fs:: PermissionsExt ;
92- let mut perms = std:: fs:: metadata ( & vp_path) . unwrap ( ) . permissions ( ) ;
93- perms. set_mode ( 0o755 ) ;
94- std:: fs:: set_permissions ( & vp_path, perms) . unwrap ( ) ;
95- }
193+ let current_vp_path = current_bin_dir. join ( "vp" ) ;
194+ write_executable ( & current_vp_path) ;
96195
97- struct EnvVarGuard {
98- name : & ' static str ,
99- original : Option < OsString > ,
100- }
196+ let path = std:: env:: join_paths ( [ bin_dir. as_os_str ( ) ] ) . unwrap ( ) ;
197+ let _path_guard = EnvVarGuard :: set ( "PATH" , path) ;
198+ let _vp_home_guard = EnvVarGuard :: remove ( crate :: env_vars:: VP_HOME ) ;
101199
102- impl EnvVarGuard {
103- fn set ( name : & ' static str , value : impl AsRef < OsStr > ) -> Self {
104- let guard = Self { name, original : std:: env:: var_os ( name) } ;
105- // SAFETY: this serial test owns process environment mutations and restores them on drop.
106- unsafe { std:: env:: set_var ( name, value) } ;
107- guard
108- }
200+ EnvConfig :: test_scope ( EnvConfig :: for_test ( ) , || {
201+ let home = get_vp_home ( ) . unwrap ( ) ;
202+ assert_eq ! ( home. as_path( ) , vite_plus_home. as_path( ) ) ;
203+ } ) ;
109204
110- fn remove ( name : & ' static str ) -> Self {
111- let guard = Self { name, original : std:: env:: var_os ( name) } ;
112- // SAFETY: this serial test owns process environment mutations and restores them on drop.
113- unsafe { std:: env:: remove_var ( name) } ;
114- guard
115- }
116- }
205+ let _ = std:: fs:: remove_dir_all ( & temp_dir) ;
206+ }
117207
118- impl Drop for EnvVarGuard {
119- fn drop ( & mut self ) {
120- // SAFETY: restore the environment snapshot captured by this serial test.
121- unsafe {
122- match & self . original {
123- Some ( value) => std:: env:: set_var ( self . name , value) ,
124- None => std:: env:: remove_var ( self . name ) ,
125- }
126- }
127- }
128- }
208+ #[ test]
209+ #[ serial_test:: serial]
210+ fn test_get_vp_home_without_vp_home_ignores_relative_bin_without_current_vp ( ) {
211+ let temp_dir = PathBuf :: from (
212+ std:: env:: temp_dir ( ) . join ( format ! ( "vp-test-relative-bin-{}" , std:: process:: id( ) ) ) ,
213+ ) ;
214+ let project_dir = temp_dir. join ( "project" ) ;
215+ let bin_dir = project_dir. join ( "tools" ) . join ( "bin" ) ;
216+ std:: fs:: create_dir_all ( & bin_dir) . unwrap ( ) ;
129217
130- let path = std:: env:: join_paths ( [ bin_dir. as_os_str ( ) ] ) . unwrap ( ) ;
218+ #[ cfg( windows) ]
219+ let vp_path = bin_dir. join ( "vp.exe" ) ;
220+ #[ cfg( not( windows) ) ]
221+ let vp_path = bin_dir. join ( "vp" ) ;
222+ write_executable ( & vp_path) ;
223+
224+ let _cwd_guard = CurrentDirGuard :: set ( & project_dir) ;
225+ let path = std:: env:: join_paths ( [ std:: path:: Path :: new ( "tools/bin" ) ] ) . unwrap ( ) ;
131226 let _path_guard = EnvVarGuard :: set ( "PATH" , path) ;
132227 let _vp_home_guard = EnvVarGuard :: remove ( crate :: env_vars:: VP_HOME ) ;
133228
134229 EnvConfig :: test_scope ( EnvConfig :: for_test ( ) , || {
135230 let home = get_vp_home ( ) . unwrap ( ) ;
136- assert_eq ! ( home. as_path( ) , vite_plus_home . as_path( ) ) ;
231+ assert_ne ! ( home. as_path( ) , project_dir . join ( "tools" ) . as_path( ) ) ;
137232 } ) ;
138233
139234 let _ = std:: fs:: remove_dir_all ( & temp_dir) ;
0 commit comments