@@ -244,13 +244,14 @@ func (d *Devbox) Shell() error {
244
244
return err
245
245
}
246
246
247
- env , err := plugin .Env (d .packages (), d .projectDir )
248
- if err != nil {
249
- return err
250
- }
251
-
247
+ var env map [string ]string
252
248
if featureflag .UnifiedEnv .Enabled () {
253
- env , err = d .computeNixEnv (false )
249
+ env , err = d .computeNixEnv ()
250
+ if err != nil {
251
+ return err
252
+ }
253
+ } else {
254
+ env , err = plugin .Env (d .packages (), d .projectDir )
254
255
if err != nil {
255
256
return err
256
257
}
@@ -293,7 +294,7 @@ func (d *Devbox) RunScript(cmdName string, cmdArgs []string) error {
293
294
return err
294
295
}
295
296
296
- env , err := d .computeNixEnv (true )
297
+ env , err := d .computeNixEnv ()
297
298
if err != nil {
298
299
return err
299
300
}
@@ -443,7 +444,7 @@ func (d *Devbox) Exec(cmds ...string) error {
443
444
}
444
445
}
445
446
446
- func (d * Devbox ) PrintEnv (setFullPath bool ) (string , error ) {
447
+ func (d * Devbox ) PrintEnv () (string , error ) {
447
448
script := ""
448
449
if featureflag .UnifiedEnv .Disabled () {
449
450
envs , err := plugin .Env (d .packages (), d .projectDir )
@@ -455,7 +456,7 @@ func (d *Devbox) PrintEnv(setFullPath bool) (string, error) {
455
456
}
456
457
return script , nil
457
458
}
458
- envs , err := d .computeNixEnv (setFullPath )
459
+ envs , err := d .computeNixEnv ()
459
460
if err != nil {
460
461
return "" , err
461
462
}
@@ -748,66 +749,85 @@ func (d *Devbox) printPackageUpdateMessage(
748
749
return nil
749
750
}
750
751
751
- // computeNixEnv computes the environment (i.e. set of env variables) to be used on
752
- // devbox execution commands (i.e. devbox run, shell). In short, the environment is
753
- // calculated as follows:
754
- // 1. Start with the output of nix print-dev-env
755
- // 2. Allow a limited set of variables (e.g. leakedVars) in the host machine to "leak" in (e.g. HOME).
756
- // 3. Include any plugin env vars.
757
- // 4. Include any user-defined env vars from devbox.json.
752
+ // computeNixEnv computes the set of environment variables that define a Devbox
753
+ // environment. The "devbox run" and "devbox shell" commands source these
754
+ // variables into a shell before executing a command or showing an interactive
755
+ // prompt.
756
+ //
757
+ // The process for building the environment involves layering sets of
758
+ // environment variables on top of each other, with each layer overwriting any
759
+ // duplicate keys from the previous:
758
760
//
759
- // The PATH variable has some special handling. In short:
760
- // 1. Start with the PATH as defined by nix (through nix print-dev-env).
761
- // 2. Clean the host PATH of any nix paths.
762
- // 3. Append the cleaned host PATH (tradeoff between reproducibility and ease of use).
763
- // 4. Prepend the paths of any plugins (tbd whether it's actually needed).
764
- func (d * Devbox ) computeNixEnv (setFullPath bool ) (map [string ]string , error ) {
761
+ // 1. Copy variables from the current environment except for those in
762
+ // ignoreCurrentEnvVar, such as PWD and SHELL.
763
+ // 2. Copy variables from "nix print-dev-env" except for those in
764
+ // ignoreDevEnvVar, such as TMPDIR and HOME.
765
+ // 3. Copy variables from Devbox plugins.
766
+ // 4. Set PATH to the concatenation of the PATHs from step 3, step 2, and
767
+ // step 1 (in that order).
768
+ //
769
+ // The final result is a set of environment variables where Devbox plugins have
770
+ // the highest priority, then Nix environment variables, and then variables
771
+ // from the current environment. Similarly, the PATH gives Devbox plugin
772
+ // binaries the highest priority, then Nix packages, and then non-Nix
773
+ // programs.
774
+ //
775
+ // Note that the shellrc.tmpl template (which sources this environment) does
776
+ // some additional processing. The computeNixEnv environment won't necessarily
777
+ // represent the final "devbox run" or "devbox shell" environments.
778
+ func (d * Devbox ) computeNixEnv () (map [string ]string , error ) {
779
+ currentEnv := os .Environ ()
780
+ env := make (map [string ]string , len (currentEnv ))
781
+ for _ , kv := range currentEnv {
782
+ key , val , found := strings .Cut (kv , "=" )
783
+ if ! found {
784
+ return nil , errors .Errorf ("expected \" =\" in keyval: %s" , kv )
785
+ }
786
+ if ignoreCurrentEnvVar [key ] {
787
+ continue
788
+ }
789
+ env [key ] = val
790
+ }
791
+ currentEnvPath := env ["PATH" ]
792
+ debug .Log ("current environment PATH is: %s" , currentEnvPath )
765
793
766
794
vaf , err := nix .PrintDevEnv (d .nixShellFilePath (), d .nixFlakesFilePath ())
767
795
if err != nil {
768
796
return nil , err
769
797
}
770
798
771
- env := map [string ]string {}
772
- for k , v := range vaf .Variables {
799
+ // Add environment variables from "nix print-dev-env" except for a few
800
+ // special ones we need to ignore.
801
+ for key , val := range vaf .Variables {
773
802
// We only care about "exported" because the var and array types seem to only be used by nix-defined
774
803
// functions that we don't need (like genericBuild). For reference, each type translates to bash as follows:
775
804
// var: export VAR=VAL
776
805
// exported: export VAR=VAL
777
806
// array: declare -a VAR=('VAL1' 'VAL2' )
778
- if v .Type == "exported" {
779
- env [k ] = v .Value .(string )
780
- }
781
- }
782
-
783
- // Hack to quickly fix TMPDIR being set to the temp directory Nix used
784
- // in the build environment. When there's more time to test, we should
785
- // probably include all of the variables that Nix ignores:
786
- // https://github.com/NixOS/nix/blob/92611e6e4c1c5c712ca7d5f9a258640662d006df/src/nix/develop.cc#L291-L357
787
- delete (env , "TEMP" )
788
- delete (env , "TEMPDIR" )
789
- delete (env , "TMP" )
790
- delete (env , "TMPDIR" )
791
-
792
- // Copy over (and overwrite) vars that we explicitly "leak", as well as DEVBOX_ vars.
793
- for _ , kv := range os .Environ () {
794
- key , val , found := strings .Cut (kv , "=" )
795
- if ! found {
796
- return nil , errors .Errorf ("expected \" =\" in keyval: %s" , kv )
807
+ if val .Type != "exported" {
808
+ continue
797
809
}
798
810
799
- if strings .HasPrefix (key , "DEVBOX_" ) {
800
- env [key ] = val
811
+ // SSL_CERT_FILE is a special-case. We only ignore it if it's
812
+ // set to a specific value. This emulates the behavior of
813
+ // "nix develop".
814
+ if key == "SSL_CERT_FILE" && val .Value .(string ) == "/no-cert-file.crt" {
815
+ continue
801
816
}
802
817
803
- if _ , ok := leakedVars [key ]; ok {
804
- env [key ] = val
818
+ // Certain variables get set to invalid values after Nix builds
819
+ // the shell environment. For example, HOME=/homeless-shelter
820
+ // and TMPDIR points to a missing directory. We want to ignore
821
+ // those values and just use the values from the current
822
+ // environment instead.
823
+ if ignoreDevEnvVar [key ] {
824
+ continue
805
825
}
806
826
807
- if _ , ok := leakedVarsForShell [key ]; ok {
808
- env [key ] = val
809
- }
827
+ env [key ] = val .Value .(string )
810
828
}
829
+ nixEnvPath := env ["PATH" ]
830
+ debug .Log ("nix environment PATH is: %s" , nixEnvPath )
811
831
812
832
// These variables are only needed for shell, but we include them here in the computed env
813
833
// for both shell and run in order to be as identical as possible.
@@ -831,22 +851,12 @@ func (d *Devbox) computeNixEnv(setFullPath bool) (map[string]string, error) {
831
851
}
832
852
}
833
853
834
- // PATH handling.
835
- pluginVirtenvPath := d .pluginVirtenvPath () // TODO: consider removing this; not being used?
836
- nixPath := env ["PATH" ]
837
- hostPath := nix .CleanEnvPath (os .Getenv ("PATH" ), os .Getenv ("NIX_PROFILES" ))
854
+ // TODO: consider removing this; not being used?
855
+ pluginVirtenvPath := d .pluginVirtenvPath ()
856
+ debug .Log ("plugin virtual environment PATH is: %s" , pluginVirtenvPath )
838
857
839
- // NOTE: for devbox shell, we need to defer the PATH setting, because a user's init file may prepend
840
- // stuff to PATH, which will then take precedence over the devbox-set PATH. Instead, we do the path
841
- // prepending in shellrc.tmpl. I chose to use the `setFullPath` variable instead of something like
842
- // `isShell` to discourage the addition of more logic that makes shell/run differ more.
843
- pathPrepend := fmt .Sprintf ("%s:%s" , pluginVirtenvPath , nixPath )
844
- if setFullPath {
845
- env ["PATH" ] = fmt .Sprintf ("%s:%s" , pathPrepend , hostPath )
846
- } else {
847
- env ["PATH" ] = hostPath
848
- env ["DEVBOX_PATH_PREPEND" ] = pathPrepend
849
- }
858
+ env ["PATH" ] = nix .JoinPathLists (pluginVirtenvPath , nixEnvPath , currentEnvPath )
859
+ debug .Log ("computed unified environment PATH is: %s" , env ["PATH" ])
850
860
851
861
return env , nil
852
862
}
@@ -1025,63 +1035,43 @@ func commandExists(command string) bool {
1025
1035
return err == nil
1026
1036
}
1027
1037
1028
- // leakedVars contains a list of variables that, if set in the host, will be copied
1029
- // to the environment of devbox run/shell. If they're NOT set in the host, they will be set
1030
- // to an empty value .
1031
- // NOTE: we want to keep this list AS SMALL AS POSSIBLE. The longer this list, the less "pure"
1032
- // (and therefore, reproducible) devbox becomes.
1033
- // TODO: allow user to specify more vars to leak, in order to make development easier .
1034
- var leakedVars = map [ string ] bool {
1035
- "HOME " : true , // Without this, HOME is set to /homeless-shelter and most programs fail.
1036
-
1037
- // Where to write temporary files. nix print-dev-env sets these to an unwriteable path,
1038
- // so we override that here with whatever the host has set .
1039
- "TMP" : true ,
1040
- "TEMP" : true ,
1041
- "TMPDIR" : true ,
1042
- "TEMPDIR " : true ,
1038
+ // ignoreCurrentEnvVar contains environment variables that Devbox should remove
1039
+ // from the slice of [os.Environ] variables before sourcing them. These are
1040
+ // variables that are set automatically by a new shell .
1041
+ var ignoreCurrentEnvVar = map [ string ] bool {
1042
+ // Devbox may change the working directory of the shell, so using the
1043
+ // original PWD and OLDPWD would be wrong .
1044
+ "PWD" : true ,
1045
+ "OLDPWD " : true ,
1046
+
1047
+ // SHLVL is the number of nested shells. Copying it would give the
1048
+ // Devbox shell the same level as the parent shell .
1049
+ "SHLVL" : true ,
1050
+
1051
+ // The parent shell isn't guaranteed to be the same as the Devbox shell.
1052
+ "SHELL " : true ,
1043
1053
}
1044
1054
1045
- var leakedVarsForShell = map [string ]bool {
1046
- // POSIX
1047
- //
1048
- // Variables that are part of the POSIX standard.
1049
- "OLDPWD" : true ,
1050
- "PWD" : true ,
1051
- "TERM" : true ,
1052
- "TZ" : true ,
1053
- "USER" : true ,
1054
-
1055
- // POSIX Locale
1056
- //
1057
- // Variables that are part of the POSIX standard which define
1058
- // the shell's locale.
1059
- "LC_ALL" : true , // Sets and overrides all of the variables below.
1060
- "LANG" : true , // Default to use for any of the variables below that are unset or null.
1061
- "LC_COLLATE" : true , // Collation order.
1062
- "LC_CTYPE" : true , // Character classification and case conversion.
1063
- "LC_MESSAGES" : true , // Formats of informative and diagnostic messages and interactive responses.
1064
- "LC_MONETARY" : true , // Monetary formatting.
1065
- "LC_NUMERIC" : true , // Numeric, non-monetary formatting.
1066
- "LC_TIME" : true , // Date and time formats.
1067
-
1068
- // Common
1069
- //
1070
- // Variables that most programs agree on, but aren't strictly
1071
- // part of POSIX.
1072
- "TERM_PROGRAM" : true , // Name of the terminal the shell is running in.
1073
- "TERM_PROGRAM_VERSION" : true , // The version of TERM_PROGRAM.
1074
- "SHLVL" : true , // The number of nested shells.
1075
-
1076
- // Apple Terminal
1077
- //
1078
- // Special-cased variables that macOS's Terminal.app sets before
1079
- // launching the shell. It's not clear what exactly all of these do,
1080
- // but it seems like omitting them can cause problems.
1081
- "TERM_SESSION_ID" : true ,
1082
- "SHELL_SESSIONS_DISABLE" : true , // Respect session save/resume setting (see /etc/zshrc_Apple_Terminal).
1083
- "SECURITYSESSIONID" : true ,
1084
-
1085
- // SSH variables
1086
- "SSH_TTY" : true , // Used by devbox telemetry logging
1055
+ // ignoreDevEnvVar contains environment variables that Devbox should remove from
1056
+ // the slice of [Devbox.PrintDevEnv] variables before sourcing them.
1057
+ //
1058
+ // This list comes directly from the "nix develop" source:
1059
+ // https://github.com/NixOS/nix/blob/f08ad5bdbac02167f7d9f5e7f9bab57cf1c5f8c4/src/nix/develop.cc#L257-L275
1060
+ var ignoreDevEnvVar = map [string ]bool {
1061
+ "BASHOPTS" : true ,
1062
+ "HOME" : true ,
1063
+ "NIX_BUILD_TOP" : true ,
1064
+ "NIX_ENFORCE_PURITY" : true ,
1065
+ "NIX_LOG_FD" : true ,
1066
+ "NIX_REMOTE" : true ,
1067
+ "PPID" : true ,
1068
+ "SHELL" : true ,
1069
+ "SHELLOPTS" : true ,
1070
+ "TEMP" : true ,
1071
+ "TEMPDIR" : true ,
1072
+ "TERM" : true ,
1073
+ "TMP" : true ,
1074
+ "TMPDIR" : true ,
1075
+ "TZ" : true ,
1076
+ "UID" : true ,
1087
1077
}
0 commit comments