@@ -21,6 +21,15 @@ use vt_path::{AbsolutePath, AbsolutePathBuf, RelativePathBuf};
2121
2222mod ps1_shim;
2323
24+ fn normalize_path_env (
25+ path_env : & OsStr ,
26+ cwd : & AbsolutePath ,
27+ ) -> Result < OsString , std:: env:: JoinPathsError > {
28+ std:: env:: join_paths ( std:: env:: split_paths ( path_env) . map ( |path| {
29+ if path. is_absolute ( ) || path. starts_with ( "~" ) { path } else { cwd. as_path ( ) . join ( path) }
30+ } ) )
31+ }
32+
2433/// Result of running a command with fspy tracking.
2534#[ derive( Debug ) ]
2635pub struct FspyCommandResult {
@@ -39,15 +48,22 @@ pub fn resolve_bin(
3948 path_env : Option < & OsStr > ,
4049 cwd : impl AsRef < AbsolutePath > ,
4150) -> Result < AbsolutePathBuf , Error > {
51+ let cwd = cwd. as_ref ( ) ;
4252 let current_path;
4353 let path_env = if let Some ( p) = path_env {
4454 p
4555 } else {
4656 current_path = std:: env:: var_os ( "PATH" ) . unwrap_or_default ( ) ;
4757 & current_path
4858 } ;
49- let path = which:: which_in ( bin_name, Some ( path_env) , cwd. as_ref ( ) )
59+ // `which` resolves relative PATH entries against the process cwd instead of the supplied
60+ // command cwd. Commands are spawned with `cwd`, so resolve the entries the same way first;
61+ // leave `~` entries for `which` to expand against the user's home directory.
62+ let path_env = normalize_path_env ( path_env, cwd)
5063 . map_err ( |_| Error :: CannotFindBinaryPath ( bin_name. into ( ) ) ) ?;
64+ let path = which:: which_in ( bin_name, Some ( & path_env) , cwd)
65+ . map_err ( |_| Error :: CannotFindBinaryPath ( bin_name. into ( ) ) ) ?;
66+ let path = if path. is_absolute ( ) { path } else { cwd. as_path ( ) . join ( path) } ;
5167 AbsolutePathBuf :: new ( path) . ok_or_else ( || Error :: CannotFindBinaryPath ( bin_name. into ( ) ) )
5268}
5369
@@ -399,6 +415,96 @@ mod tests {
399415 tempdir ( ) . expect ( "Failed to create temp directory" )
400416 }
401417
418+ #[ cfg( unix) ]
419+ fn create_executable ( path : & std:: path:: Path ) {
420+ use std:: { fs, os:: unix:: fs:: PermissionsExt } ;
421+
422+ fs:: create_dir_all ( path. parent ( ) . unwrap ( ) ) . unwrap ( ) ;
423+ fs:: write ( path, "#!/bin/sh\n exit 0\n " ) . unwrap ( ) ;
424+ let mut permissions = fs:: metadata ( path) . unwrap ( ) . permissions ( ) ;
425+ permissions. set_mode ( 0o755 ) ;
426+ fs:: set_permissions ( path, permissions) . unwrap ( ) ;
427+ }
428+
429+ #[ cfg( unix) ]
430+ #[ test]
431+ fn test_resolve_bin_with_relative_path_entry ( ) {
432+ use std:: path:: PathBuf ;
433+
434+ let temp_dir = create_temp_dir ( ) ;
435+ let cwd_path = temp_dir. path ( ) . canonicalize ( ) . unwrap ( ) ;
436+ let cwd = AbsolutePathBuf :: new ( cwd_path. clone ( ) ) . unwrap ( ) ;
437+ let bin_dir = cwd_path. join ( "node_modules/.bin" ) ;
438+ let bin_path = bin_dir. join ( "fake-node" ) ;
439+ let fallback_bin_dir = cwd_path. join ( "fallback-bin" ) ;
440+ let fallback_bin_path = fallback_bin_dir. join ( "fake-node" ) ;
441+
442+ create_executable ( & bin_path) ;
443+ create_executable ( & fallback_bin_path) ;
444+
445+ let path_env =
446+ std:: env:: join_paths ( [ PathBuf :: from ( "./node_modules/.bin" ) , fallback_bin_dir] ) . unwrap ( ) ;
447+ let resolved = resolve_bin ( "fake-node" , Some ( & path_env) , & cwd) . unwrap ( ) ;
448+
449+ assert_eq ! ( resolved. into_path_buf( ) , bin_path) ;
450+ }
451+
452+ #[ cfg( unix) ]
453+ #[ test]
454+ fn test_resolve_bin_continues_after_missing_relative_path_entry ( ) {
455+ use std:: path:: PathBuf ;
456+
457+ let temp_dir = create_temp_dir ( ) ;
458+ let cwd_path = temp_dir. path ( ) . canonicalize ( ) . unwrap ( ) ;
459+ let cwd = AbsolutePathBuf :: new ( cwd_path. clone ( ) ) . unwrap ( ) ;
460+ let fallback_bin_dir = cwd_path. join ( "fallback-bin" ) ;
461+ let fallback_bin_path = fallback_bin_dir. join ( "fake-node" ) ;
462+
463+ create_executable ( & fallback_bin_path) ;
464+
465+ let path_env =
466+ std:: env:: join_paths ( [ PathBuf :: from ( "./missing-bin" ) , fallback_bin_dir] ) . unwrap ( ) ;
467+ let resolved = resolve_bin ( "fake-node" , Some ( & path_env) , & cwd) . unwrap ( ) ;
468+
469+ assert_eq ! ( resolved. into_path_buf( ) , fallback_bin_path) ;
470+ }
471+
472+ #[ cfg( unix) ]
473+ #[ test]
474+ fn test_resolve_bin_with_empty_path_entry ( ) {
475+ use std:: path:: PathBuf ;
476+
477+ let temp_dir = create_temp_dir ( ) ;
478+ let cwd_path = temp_dir. path ( ) . canonicalize ( ) . unwrap ( ) ;
479+ let cwd = AbsolutePathBuf :: new ( cwd_path. clone ( ) ) . unwrap ( ) ;
480+ let bin_path = cwd_path. join ( "fake-node" ) ;
481+
482+ create_executable ( & bin_path) ;
483+
484+ let path_env = std:: env:: join_paths ( [ PathBuf :: new ( ) ] ) . unwrap ( ) ;
485+ let resolved = resolve_bin ( "fake-node" , Some ( & path_env) , & cwd) . unwrap ( ) ;
486+
487+ assert_eq ! ( resolved. into_path_buf( ) , bin_path) ;
488+ }
489+
490+ #[ cfg( unix) ]
491+ #[ test]
492+ fn test_normalize_path_env_preserves_tilde_entry ( ) {
493+ use std:: path:: PathBuf ;
494+
495+ let temp_dir = create_temp_dir ( ) ;
496+ let cwd_path = temp_dir. path ( ) . canonicalize ( ) . unwrap ( ) ;
497+ let cwd = AbsolutePathBuf :: new ( cwd_path) . unwrap ( ) ;
498+ let path_env = std:: env:: join_paths ( [ PathBuf :: from ( "~/bin" ) ] ) . unwrap ( ) ;
499+
500+ let normalized = normalize_path_env ( & path_env, & cwd) . unwrap ( ) ;
501+
502+ assert_eq ! (
503+ std:: env:: split_paths( & normalized) . collect:: <Vec <_>>( ) ,
504+ [ PathBuf :: from( "~/bin" ) ]
505+ ) ;
506+ }
507+
402508 mod run_command_tests {
403509
404510 use super :: * ;
0 commit comments