@@ -8,7 +8,7 @@ use std::{
88 ops:: { Deref , DerefMut } ,
99} ;
1010
11- use regex:: RegexBuilder ;
11+ use regex:: { Regex , RegexBuilder } ;
1212use serde:: Serialize ;
1313use sha2:: { Digest , Sha256 } ;
1414use thiserror:: Error ;
@@ -17,6 +17,69 @@ pub mod platform;
1717
1818const DEFAULT_ENV_VARS : & [ & str ] = [ "VERCEL_ANALYTICS_ID" , "VERCEL_TARGET_ENV" ] . as_slice ( ) ;
1919
20+ pub const BUILTIN_PASS_THROUGH_ENV : & [ & str ] = & [
21+ "HOME" ,
22+ "USER" ,
23+ "TZ" ,
24+ "LANG" ,
25+ "SHELL" ,
26+ "PWD" ,
27+ "XDG_RUNTIME_DIR" ,
28+ "XAUTHORITY" ,
29+ "DBUS_SESSION_BUS_ADDRESS" ,
30+ "CI" ,
31+ "NODE_OPTIONS" ,
32+ "COREPACK_HOME" ,
33+ "LD_LIBRARY_PATH" ,
34+ "DYLD_FALLBACK_LIBRARY_PATH" ,
35+ "LIBPATH" ,
36+ "LD_PRELOAD" ,
37+ "DYLD_INSERT_LIBRARIES" ,
38+ "COLORTERM" ,
39+ "TERM" ,
40+ "TERM_PROGRAM" ,
41+ "DISPLAY" ,
42+ "TMP" ,
43+ "TEMP" ,
44+ // Windows
45+ "WINDIR" ,
46+ "ProgramFiles" ,
47+ "ProgramFiles(x86)" ,
48+ // VSCode IDE
49+ "VSCODE_*" ,
50+ "ELECTRON_RUN_AS_NODE" ,
51+ // Docker
52+ "DOCKER_*" ,
53+ "BUILDKIT_*" ,
54+ // Docker compose
55+ "COMPOSE_*" ,
56+ // Jetbrains IDE
57+ "JB_IDE_*" ,
58+ "JB_INTERPRETER" ,
59+ "_JETBRAINS_TEST_RUNNER_RUN_SCOPE_TYPE" ,
60+ // Vercel specific
61+ "VERCEL" ,
62+ "VERCEL_*" ,
63+ "NEXT_*" ,
64+ "USE_OUTPUT_FOR_EDGE_FUNCTIONS" ,
65+ "NOW_BUILDER" ,
66+ "VC_MICROFRONTENDS_CONFIG_FILE_NAME" ,
67+ // GitHub Actions
68+ "GITHUB_*" ,
69+ "RUNNER_*" ,
70+ // Command Prompt casing of env variables
71+ "APPDATA" ,
72+ "PATH" ,
73+ "PROGRAMDATA" ,
74+ "SYSTEMROOT" ,
75+ "SYSTEMDRIVE" ,
76+ "USERPROFILE" ,
77+ "HOMEDRIVE" ,
78+ "HOMEPATH" ,
79+ "PNPM_HOME" ,
80+ "NPM_CONFIG_STORE_DIR" ,
81+ ] ;
82+
2083#[ derive( Clone , Debug , Error ) ]
2184pub enum Error {
2285 #[ error( "Failed to parse regex: {0}" ) ]
@@ -107,6 +170,60 @@ impl WildcardMaps {
107170 }
108171}
109172
173+ /// Pre-compiled include/exclude wildcard regexes. Compile once and reuse
174+ /// across tasks that share the same wildcard patterns.
175+ pub struct CompiledWildcards {
176+ include_regex : Option < Regex > ,
177+ exclude_regex : Option < Regex > ,
178+ }
179+
180+ impl CompiledWildcards {
181+ pub fn compile ( wildcard_patterns : & [ impl AsRef < str > ] ) -> Result < Self , Error > {
182+ let mut include_patterns = Vec :: new ( ) ;
183+ let mut exclude_patterns = Vec :: new ( ) ;
184+
185+ for wildcard_pattern in wildcard_patterns {
186+ let wildcard_pattern = wildcard_pattern. as_ref ( ) ;
187+ if let Some ( rest) = wildcard_pattern. strip_prefix ( '!' ) {
188+ exclude_patterns. push ( wildcard_to_regex_pattern ( rest) ) ;
189+ } else if wildcard_pattern. starts_with ( "\\ !" ) {
190+ include_patterns. push ( wildcard_to_regex_pattern ( & wildcard_pattern[ 1 ..] ) ) ;
191+ } else {
192+ include_patterns. push ( wildcard_to_regex_pattern ( wildcard_pattern) ) ;
193+ }
194+ }
195+
196+ let case_insensitive = cfg ! ( windows) ;
197+
198+ let include_regex = if include_patterns. is_empty ( ) {
199+ None
200+ } else {
201+ let pattern = format ! ( "^({})$" , include_patterns. join( "|" ) ) ;
202+ Some (
203+ RegexBuilder :: new ( & pattern)
204+ . case_insensitive ( case_insensitive)
205+ . build ( ) ?,
206+ )
207+ } ;
208+
209+ let exclude_regex = if exclude_patterns. is_empty ( ) {
210+ None
211+ } else {
212+ let pattern = format ! ( "^({})$" , exclude_patterns. join( "|" ) ) ;
213+ Some (
214+ RegexBuilder :: new ( & pattern)
215+ . case_insensitive ( case_insensitive)
216+ . build ( ) ?,
217+ )
218+ } ;
219+
220+ Ok ( CompiledWildcards {
221+ include_regex,
222+ exclude_regex,
223+ } )
224+ }
225+ }
226+
110227impl From < HashMap < String , String > > for EnvironmentVariableMap {
111228 fn from ( map : HashMap < String , String > ) -> Self {
112229 EnvironmentVariableMap ( map)
@@ -285,6 +402,45 @@ impl EnvironmentVariableMap {
285402
286403 Ok ( pass_through_env)
287404 }
405+
406+ /// Like `from_wildcards` but uses pre-compiled regexes.
407+ pub fn from_compiled_wildcards ( & self , compiled : & CompiledWildcards ) -> EnvironmentVariableMap {
408+ let mut output = EnvironmentVariableMap :: default ( ) ;
409+ for ( env_var, env_value) in & self . 0 {
410+ let included = compiled
411+ . include_regex
412+ . as_ref ( )
413+ . is_some_and ( |re| re. is_match ( env_var) ) ;
414+ let excluded = compiled
415+ . exclude_regex
416+ . as_ref ( )
417+ . is_some_and ( |re| re. is_match ( env_var) ) ;
418+ if included && !excluded {
419+ output. insert ( env_var. clone ( ) , env_value. clone ( ) ) ;
420+ }
421+ }
422+ output
423+ }
424+
425+ /// Like `pass_through_env` but uses pre-compiled builtin wildcards.
426+ pub fn pass_through_env_compiled (
427+ & self ,
428+ compiled_builtins : & CompiledWildcards ,
429+ global_env : & Self ,
430+ task_pass_through : & [ impl AsRef < str > ] ,
431+ ) -> Result < Self , Error > {
432+ let default_env_var_pass_through_map = self . from_compiled_wildcards ( compiled_builtins) ;
433+ let task_pass_through_env =
434+ self . wildcard_map_from_wildcards_unresolved ( task_pass_through) ?;
435+
436+ let mut pass_through_env = EnvironmentVariableMap :: default ( ) ;
437+ pass_through_env. union ( & default_env_var_pass_through_map) ;
438+ pass_through_env. union ( global_env) ;
439+ pass_through_env. union ( & task_pass_through_env. inclusions ) ;
440+ pass_through_env. difference ( & task_pass_through_env. exclusions ) ;
441+
442+ Ok ( pass_through_env)
443+ }
288444}
289445
290446const WILDCARD : char = '*' ;
@@ -469,4 +625,124 @@ mod tests {
469625 actual. sort ( ) ;
470626 assert_eq ! ( actual, expected) ;
471627 }
628+
629+ #[ test_case( & [ "FOO*" ] , & [ "FOO" , "FOOBAR" , "FOOD" , "PATH" ] ; "folds 3 sources" ) ]
630+ #[ test_case( & [ "!FOO" ] , & [ "PATH" ] ; "remove global" ) ]
631+ #[ test_case( & [ "!PATH" ] , & [ "FOO" ] ; "remove builtin" ) ]
632+ #[ test_case( & [ "FOO*" , "!FOOD" ] , & [ "FOO" , "FOOBAR" , "PATH" ] ; "mixing negations" ) ]
633+ fn test_pass_through_env_compiled_matches_original ( task : & [ & str ] , expected : & [ & str ] ) {
634+ let env_at_start = EnvironmentVariableMap (
635+ vec ! [
636+ ( "PATH" , "of" ) ,
637+ ( "FOO" , "bar" ) ,
638+ ( "FOOBAR" , "baz" ) ,
639+ ( "FOOD" , "cheese" ) ,
640+ ( "BAR" , "nuts" ) ,
641+ ]
642+ . into_iter ( )
643+ . map ( |( k, v) | ( k. to_owned ( ) , v. to_owned ( ) ) )
644+ . collect ( ) ,
645+ ) ;
646+ let global_env = EnvironmentVariableMap (
647+ vec ! [ ( "FOO" , "bar" ) ]
648+ . into_iter ( )
649+ . map ( |( k, v) | ( k. to_owned ( ) , v. to_owned ( ) ) )
650+ . collect ( ) ,
651+ ) ;
652+ let builtins: & [ & str ] = & [ "PATH" ] ;
653+ let compiled = CompiledWildcards :: compile ( builtins) . unwrap ( ) ;
654+ let output = env_at_start
655+ . pass_through_env_compiled ( & compiled, & global_env, task)
656+ . unwrap ( ) ;
657+ let mut actual: Vec < _ > = output. keys ( ) . map ( |s| s. as_str ( ) ) . collect ( ) ;
658+ actual. sort ( ) ;
659+ assert_eq ! ( actual, expected) ;
660+ }
661+
662+ #[ test]
663+ fn test_compiled_wildcards_matches_from_wildcards ( ) {
664+ let env = EnvironmentVariableMap (
665+ vec ! [
666+ ( "HOME" , "/home/user" ) ,
667+ ( "PATH" , "/usr/bin" ) ,
668+ ( "VSCODE_PID" , "12345" ) ,
669+ ( "DOCKER_HOST" , "tcp://localhost" ) ,
670+ ( "GITHUB_TOKEN" , "ghp_xxx" ) ,
671+ ( "NEXT_PUBLIC_API" , "https://api" ) ,
672+ ( "RANDOM_VAR" , "value" ) ,
673+ ( "CI" , "true" ) ,
674+ ( "VERCEL" , "1" ) ,
675+ ( "VERCEL_URL" , "example.vercel.app" ) ,
676+ ]
677+ . into_iter ( )
678+ . map ( |( k, v) | ( k. to_owned ( ) , v. to_owned ( ) ) )
679+ . collect ( ) ,
680+ ) ;
681+
682+ let original = env. from_wildcards ( BUILTIN_PASS_THROUGH_ENV ) . unwrap ( ) ;
683+ let compiled = CompiledWildcards :: compile ( BUILTIN_PASS_THROUGH_ENV ) . unwrap ( ) ;
684+ let from_compiled = env. from_compiled_wildcards ( & compiled) ;
685+
686+ let mut orig_keys: Vec < _ > = original. keys ( ) . cloned ( ) . collect ( ) ;
687+ let mut comp_keys: Vec < _ > = from_compiled. keys ( ) . cloned ( ) . collect ( ) ;
688+ orig_keys. sort ( ) ;
689+ comp_keys. sort ( ) ;
690+
691+ assert_eq ! (
692+ orig_keys, comp_keys,
693+ "compiled and original wildcard matching must produce identical keys"
694+ ) ;
695+
696+ for key in & orig_keys {
697+ assert_eq ! (
698+ original. get( key) ,
699+ from_compiled. get( key) ,
700+ "values differ for key {key}"
701+ ) ;
702+ }
703+ }
704+
705+ #[ test]
706+ fn test_builtin_pass_through_env_compiles ( ) {
707+ CompiledWildcards :: compile ( BUILTIN_PASS_THROUGH_ENV )
708+ . expect ( "BUILTIN_PASS_THROUGH_ENV should compile without error" ) ;
709+ }
710+
711+ #[ test]
712+ fn test_compiled_wildcards_with_excludes ( ) {
713+ let env = EnvironmentVariableMap (
714+ vec ! [ ( "FOO" , "1" ) , ( "FOOBAR" , "2" ) , ( "FOOD" , "3" ) , ( "BAR" , "4" ) ]
715+ . into_iter ( )
716+ . map ( |( k, v) | ( k. to_owned ( ) , v. to_owned ( ) ) )
717+ . collect ( ) ,
718+ ) ;
719+
720+ let patterns: & [ & str ] = & [ "FOO*" , "!FOOD" ] ;
721+ let original = env. from_wildcards ( patterns) . unwrap ( ) ;
722+ let compiled = CompiledWildcards :: compile ( patterns) . unwrap ( ) ;
723+ let from_compiled = env. from_compiled_wildcards ( & compiled) ;
724+
725+ let mut orig_keys: Vec < _ > = original. keys ( ) . cloned ( ) . collect ( ) ;
726+ let mut comp_keys: Vec < _ > = from_compiled. keys ( ) . cloned ( ) . collect ( ) ;
727+ orig_keys. sort ( ) ;
728+ comp_keys. sort ( ) ;
729+
730+ assert_eq ! ( orig_keys, comp_keys) ;
731+ assert_eq ! ( orig_keys, vec![ "FOO" , "FOOBAR" ] ) ;
732+ }
733+
734+ #[ test]
735+ fn test_compiled_wildcards_empty_patterns ( ) {
736+ let env = EnvironmentVariableMap (
737+ vec ! [ ( "FOO" , "bar" ) ]
738+ . into_iter ( )
739+ . map ( |( k, v) | ( k. to_owned ( ) , v. to_owned ( ) ) )
740+ . collect ( ) ,
741+ ) ;
742+
743+ let empty: & [ & str ] = & [ ] ;
744+ let compiled = CompiledWildcards :: compile ( empty) . unwrap ( ) ;
745+ let result = env. from_compiled_wildcards ( & compiled) ;
746+ assert ! ( result. is_empty( ) , "empty patterns should match nothing" ) ;
747+ }
472748}
0 commit comments