@@ -265,8 +265,7 @@ impl TaskEnvs {
265265 GlobPatternSet :: new ( task. config . envs . iter ( ) . filter ( |s| !s. starts_with ( '!' ) ) ) ?;
266266 let sensitive_patterns = GlobPatternSet :: new ( SENSITIVE_PATTERNS ) ?;
267267 for ( name, value) in & all_envs {
268- let upper_name = name. to_uppercase ( ) ;
269- if !envs_without_pass_through_patterns. is_match ( & upper_name) {
268+ if !envs_without_pass_through_patterns. is_match ( name) {
270269 continue ;
271270 }
272271 let Some ( value) = value. to_str ( ) else {
@@ -275,7 +274,7 @@ impl TaskEnvs {
275274 value : value. to_os_string ( ) ,
276275 } ) ;
277276 } ;
278- let value: Str = if sensitive_patterns. is_match ( & upper_name ) {
277+ let value: Str = if sensitive_patterns. is_match ( name ) {
279278 let mut hasher = Sha256 :: new ( ) ;
280279 hasher. update ( value. as_bytes ( ) ) ;
281280 format ! ( "sha256:{:x}" , hasher. finalize( ) ) . into ( )
@@ -311,13 +310,31 @@ impl TaskEnvs {
311310 all_envs. insert ( "VITE_TASK_EXECUTION_ENV" . into ( ) , Arc :: < OsStr > :: from ( OsStr :: new ( "1" ) ) ) ;
312311
313312 // Add node_modules/.bin to PATH
314- let env_path =
315- all_envs. entry ( "PATH" . into ( ) ) . or_insert_with ( || Arc :: < OsStr > :: from ( OsStr :: new ( "" ) ) ) ;
316- let paths = split_paths ( env_path) ;
313+ // On Windows, environment variable names are case-insensitive (e.g., "PATH", "Path", "path" are all the same)
314+ // However, Rust's HashMap keys are case-sensitive, so we need to find the existing PATH variable
315+ // regardless of its casing to avoid creating duplicate PATH entries with different casings.
316+ // For example, if the system has "Path", we should use that instead of creating a new "PATH" entry.
317+ let env_path = {
318+ if cfg ! ( windows)
319+ && let Some ( existing_path) = all_envs. iter_mut ( ) . find_map ( |( name, value) | {
320+ if name. eq_ignore_ascii_case ( "path" ) { Some ( value) } else { None }
321+ } )
322+ {
323+ // Found existing PATH variable (with any casing), use it
324+ existing_path
325+ } else {
326+ // On Unix or no existing PATH on Windows, create/get "PATH" entry
327+ all_envs. entry ( "PATH" . into ( ) ) . or_insert_with ( || Arc :: < OsStr > :: from ( OsStr :: new ( "" ) ) )
328+ }
329+ } ;
330+ let paths = split_paths ( env_path) . filter ( |path| !path. as_os_str ( ) . is_empty ( ) ) ;
331+
332+ const NODE_MODULES_DOT_BIN : & str =
333+ if cfg ! ( windows) { "node_modules\\ .bin" } else { "node_modules/.bin" } ;
317334
318335 let node_modules_bin_paths = [
319- base_dir. join ( & task. config . cwd ) . join ( "node_modules/.bin" ) . into_path_buf ( ) ,
320- base_dir. join ( & task. config_dir ) . join ( "node_modules/.bin" ) . into_path_buf ( ) ,
336+ base_dir. join ( & task. config . cwd ) . join ( NODE_MODULES_DOT_BIN ) . into_path_buf ( ) ,
337+ base_dir. join ( & task. config_dir ) . join ( NODE_MODULES_DOT_BIN ) . into_path_buf ( ) ,
321338 ] ;
322339 * env_path = join_paths ( node_modules_bin_paths. into_iter ( ) . chain ( paths) ) ?. into ( ) ;
323340
@@ -887,4 +904,183 @@ mod tests {
887904 assert ! ( all_envs. contains_key( "app1_name" ) ) ;
888905 assert ! ( all_envs. contains_key( "app2_name" ) ) ;
889906 }
907+
908+ #[ test]
909+ #[ cfg( windows) ]
910+ fn test_windows_path_case_insensitive_mixed_case ( ) {
911+ use crate :: {
912+ collections:: HashSet ,
913+ config:: { ResolvedTaskConfig , TaskCommand , TaskConfig } ,
914+ } ;
915+
916+ let task_config = TaskConfig {
917+ command : TaskCommand :: ShellScript ( "echo test" . into ( ) ) ,
918+ cwd : RelativePathBuf :: default ( ) ,
919+ cacheable : true ,
920+ inputs : HashSet :: new ( ) ,
921+ envs : HashSet :: new ( ) ,
922+ pass_through_envs : HashSet :: new ( ) ,
923+ fingerprint_ignores : None ,
924+ } ;
925+ let resolved =
926+ ResolvedTaskConfig { config_dir : RelativePathBuf :: default ( ) , config : task_config } ;
927+
928+ // Mock environment with mixed case "Path" (common on Windows)
929+ let mock_envs = vec ! [
930+ ( OsString :: from( "Path" ) , OsString :: from( "C:\\ existing\\ path" ) ) ,
931+ ( OsString :: from( "OTHER_VAR" ) , OsString :: from( "value" ) ) ,
932+ ] ;
933+
934+ let base_dir = AbsolutePath :: new ( "C:\\ workspace\\ packages\\ app" ) . unwrap ( ) ;
935+
936+ let result = TaskEnvs :: resolve ( mock_envs. into_iter ( ) , & base_dir, & resolved) . unwrap ( ) ;
937+
938+ let all_envs = result. all_envs ;
939+
940+ // Verify that the original "Path" casing is preserved, not "PATH"
941+ assert ! ( all_envs. contains_key( "Path" ) ) ;
942+ assert ! ( !all_envs. contains_key( "PATH" ) ) ;
943+
944+ // Verify the complete PATH value matches expected
945+ let path_value = all_envs. get ( "Path" ) . unwrap ( ) ;
946+ assert_eq ! (
947+ path_value. as_ref( ) ,
948+ OsStr :: new(
949+ "C:\\ workspace\\ packages\\ app\\ node_modules\\ .bin;C:\\ workspace\\ packages\\ app\\ node_modules\\ .bin;C:\\ existing\\ path"
950+ )
951+ ) ;
952+
953+ // Verify no duplicate PATH entry was created
954+ let path_like_keys: Vec < _ > =
955+ all_envs. keys ( ) . filter ( |k| k. eq_ignore_ascii_case ( "path" ) ) . collect ( ) ;
956+ assert_eq ! ( path_like_keys. len( ) , 1 ) ;
957+ }
958+
959+ #[ test]
960+ #[ cfg( windows) ]
961+ fn test_windows_path_case_insensitive_uppercase ( ) {
962+ use crate :: {
963+ collections:: HashSet ,
964+ config:: { ResolvedTaskConfig , TaskCommand , TaskConfig } ,
965+ } ;
966+
967+ let task_config = TaskConfig {
968+ command : TaskCommand :: ShellScript ( "echo test" . into ( ) ) ,
969+ cwd : RelativePathBuf :: default ( ) ,
970+ cacheable : true ,
971+ inputs : HashSet :: new ( ) ,
972+ envs : HashSet :: new ( ) ,
973+ pass_through_envs : HashSet :: new ( ) ,
974+ fingerprint_ignores : None ,
975+ } ;
976+ let resolved =
977+ ResolvedTaskConfig { config_dir : RelativePathBuf :: default ( ) , config : task_config } ;
978+
979+ // Mock environment with uppercase "PATH"
980+ let mock_envs = vec ! [
981+ ( OsString :: from( "PATH" ) , OsString :: from( "C:\\ existing\\ path" ) ) ,
982+ ( OsString :: from( "OTHER_VAR" ) , OsString :: from( "value" ) ) ,
983+ ] ;
984+
985+ let base_dir = AbsolutePath :: new ( "C:\\ workspace\\ packages\\ app" ) . unwrap ( ) ;
986+
987+ let result = TaskEnvs :: resolve ( mock_envs. into_iter ( ) , & base_dir, & resolved) . unwrap ( ) ;
988+
989+ let all_envs = result. all_envs ;
990+
991+ // Verify the complete PATH value matches expected
992+ let path_value = all_envs. get ( "PATH" ) . unwrap ( ) ;
993+ assert_eq ! (
994+ path_value. as_ref( ) ,
995+ OsStr :: new(
996+ "C:\\ workspace\\ packages\\ app\\ node_modules\\ .bin;C:\\ workspace\\ packages\\ app\\ node_modules\\ .bin;C:\\ existing\\ path"
997+ )
998+ ) ;
999+ }
1000+
1001+ #[ test]
1002+ #[ cfg( windows) ]
1003+ fn test_windows_path_created_when_missing ( ) {
1004+ use crate :: {
1005+ collections:: HashSet ,
1006+ config:: { ResolvedTaskConfig , TaskCommand , TaskConfig } ,
1007+ } ;
1008+
1009+ let task_config = TaskConfig {
1010+ command : TaskCommand :: ShellScript ( "echo test" . into ( ) ) ,
1011+ cwd : RelativePathBuf :: default ( ) ,
1012+ cacheable : true ,
1013+ inputs : HashSet :: new ( ) ,
1014+ envs : HashSet :: new ( ) ,
1015+ pass_through_envs : HashSet :: new ( ) ,
1016+ fingerprint_ignores : None ,
1017+ } ;
1018+ let resolved =
1019+ ResolvedTaskConfig { config_dir : RelativePathBuf :: default ( ) , config : task_config } ;
1020+
1021+ // Mock environment without any PATH variable
1022+ let mock_envs = vec ! [ ( OsString :: from( "OTHER_VAR" ) , OsString :: from( "value" ) ) ] ;
1023+
1024+ let base_dir = AbsolutePath :: new ( "C:\\ workspace\\ packages\\ app" ) . unwrap ( ) ;
1025+
1026+ let result = TaskEnvs :: resolve ( mock_envs. into_iter ( ) , & base_dir, & resolved) . unwrap ( ) ;
1027+
1028+ let all_envs = result. all_envs ;
1029+
1030+ // Verify the complete PATH value matches expected (only node_modules/.bin paths, no existing path)
1031+ let path_value = all_envs. get ( "PATH" ) . unwrap ( ) ;
1032+ assert_eq ! (
1033+ path_value. as_ref( ) ,
1034+ OsStr :: new(
1035+ "C:\\ workspace\\ packages\\ app\\ node_modules\\ .bin;C:\\ workspace\\ packages\\ app\\ node_modules\\ .bin"
1036+ )
1037+ ) ;
1038+ }
1039+
1040+ #[ test]
1041+ #[ cfg( unix) ]
1042+ fn test_unix_path_case_sensitive ( ) {
1043+ use crate :: {
1044+ collections:: HashSet ,
1045+ config:: { ResolvedTaskConfig , TaskCommand , TaskConfig } ,
1046+ } ;
1047+
1048+ let task_config = TaskConfig {
1049+ command : TaskCommand :: ShellScript ( "echo test" . into ( ) ) ,
1050+ cwd : RelativePathBuf :: default ( ) ,
1051+ cacheable : true ,
1052+ inputs : HashSet :: new ( ) ,
1053+ envs : HashSet :: new ( ) ,
1054+ pass_through_envs : HashSet :: new ( ) ,
1055+ fingerprint_ignores : None ,
1056+ } ;
1057+ let resolved =
1058+ ResolvedTaskConfig { config_dir : RelativePathBuf :: default ( ) , config : task_config } ;
1059+
1060+ // Mock environment with "PATH" in uppercase (standard on Unix)
1061+ let mock_envs = vec ! [
1062+ ( OsString :: from( "PATH" ) , OsString :: from( "/existing/path" ) ) ,
1063+ ( OsString :: from( "OTHER_VAR" ) , OsString :: from( "value" ) ) ,
1064+ ] ;
1065+
1066+ let base_dir = AbsolutePath :: new ( "/workspace/packages/app" ) . unwrap ( ) ;
1067+
1068+ let result = TaskEnvs :: resolve ( mock_envs. into_iter ( ) , & base_dir, & resolved) . unwrap ( ) ;
1069+
1070+ let all_envs = result. all_envs ;
1071+
1072+ // Verify "PATH" exists and the complete value matches expected
1073+ let path_value = all_envs. get ( "PATH" ) . unwrap ( ) ;
1074+ assert_eq ! (
1075+ path_value. as_ref( ) ,
1076+ OsStr :: new(
1077+ "/workspace/packages/app/node_modules/.bin:/workspace/packages/app/node_modules/.bin:/existing/path"
1078+ )
1079+ ) ;
1080+
1081+ // Verify that on Unix, the code uses exact "PATH" match (case-sensitive)
1082+ // This is a regression test to ensure Windows case-insensitive logic doesn't affect Unix
1083+ assert ! ( !all_envs. contains_key( "Path" ) ) ;
1084+ assert ! ( !all_envs. contains_key( "path" ) ) ;
1085+ }
8901086}
0 commit comments