@@ -244,13 +244,14 @@ func (d *Devbox) Shell() error {
244244 return err
245245 }
246246
247- env , err := plugin .Env (d .packages (), d .projectDir )
248- if err != nil {
249- return err
250- }
251-
247+ var env map [string ]string
252248 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 )
254255 if err != nil {
255256 return err
256257 }
@@ -293,7 +294,7 @@ func (d *Devbox) RunScript(cmdName string, cmdArgs []string) error {
293294 return err
294295 }
295296
296- env , err := d .computeNixEnv (true )
297+ env , err := d .computeNixEnv ()
297298 if err != nil {
298299 return err
299300 }
@@ -443,7 +444,7 @@ func (d *Devbox) Exec(cmds ...string) error {
443444 }
444445}
445446
446- func (d * Devbox ) PrintEnv (setFullPath bool ) (string , error ) {
447+ func (d * Devbox ) PrintEnv () (string , error ) {
447448 script := ""
448449 if featureflag .UnifiedEnv .Disabled () {
449450 envs , err := plugin .Env (d .packages (), d .projectDir )
@@ -455,7 +456,7 @@ func (d *Devbox) PrintEnv(setFullPath bool) (string, error) {
455456 }
456457 return script , nil
457458 }
458- envs , err := d .computeNixEnv (setFullPath )
459+ envs , err := d .computeNixEnv ()
459460 if err != nil {
460461 return "" , err
461462 }
@@ -748,66 +749,85 @@ func (d *Devbox) printPackageUpdateMessage(
748749 return nil
749750}
750751
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:
758760//
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 )
765793
766794 vaf , err := nix .PrintDevEnv (d .nixShellFilePath (), d .nixFlakesFilePath ())
767795 if err != nil {
768796 return nil , err
769797 }
770798
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 {
773802 // We only care about "exported" because the var and array types seem to only be used by nix-defined
774803 // functions that we don't need (like genericBuild). For reference, each type translates to bash as follows:
775804 // var: export VAR=VAL
776805 // exported: export VAR=VAL
777806 // 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
797809 }
798810
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
801816 }
802817
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
805825 }
806826
807- if _ , ok := leakedVarsForShell [key ]; ok {
808- env [key ] = val
809- }
827+ env [key ] = val .Value .(string )
810828 }
829+ nixEnvPath := env ["PATH" ]
830+ debug .Log ("nix environment PATH is: %s" , nixEnvPath )
811831
812832 // These variables are only needed for shell, but we include them here in the computed env
813833 // 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) {
831851 }
832852 }
833853
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 )
838857
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" ])
850860
851861 return env , nil
852862}
@@ -1025,63 +1035,43 @@ func commandExists(command string) bool {
10251035 return err == nil
10261036}
10271037
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 ,
10431053}
10441054
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 ,
10871077}
0 commit comments