Finding the correct PATH
On a recent project, I got the opportunity to create a desktop application using Electron, React and Go. Electron served as a tool to bundle the React app and the Go server in a neat little installable package. It also took care of starting the Go server as a subprocess when the app launched.
The included Go server performed a few dependency checks at each startup by trying to find some particular executables in the
This worked great during the development period, but once I built an installable package and installed the app, these dependency checks failed every time I launched it.
Curiously, the checks only failed only on MacOS and Linux, not on Windows.
Even more curiously, launching the installed app through a terminal seemingly fixed this problem.
The reason for this behaviour, as I found out after several hours of painful debugging, was unexpected inheritance of the
PATH environment variable.
Inheritance of Environment Variables
A newly spawned child process inherits its parent’s environment variables.
To demonstrate this, let’s build a couple of Go programs:
envReader reads a special environment variable “SpecialEnv” and prints its value, or "<Not Found>” if it is not set.
envReader as a subprocess twice, with the “SpecialEnv” set only in the second invocation.
This produces the following output:
In the first invocation, “SpecialEnv” is not set and hence the produced output contains "<Not Found>”. After this, the parent calls
os.Setenv and sets a value to it. Note that this value is not exported, so it is local to the
envParent processes only1. Yet, the second invocation of the child process finds it successfully.
This proves that child processes inherit their parent’s environment variables.
Inherited PATH outside the Terminal
During development, it is common to launch Electron apps using the
electron command from a terminal. This creates a new process and starts the app. Since this process is spawned from a terminal shell, it inherits all the environment variables of the shell session. This includes any custom variables defined in shell config files such as
.bash_profile, like a custom
Once the app is shipped, consumers don’t use the terminal to launch it. They use GUI icons, which means the app process is not created by a terminal, but by the Desktop Environment.
The desktop environment process does not concern itself with custom configuration files such as
.bash_profile, hence its version of the
PATH environment variable is terse. It does not contain any custom directories the user might have specified in their shell config files.
Here’s what it looks like when launched from the GUI:
And here it is when launched from the terminal:
Clearly, we’re missing out on a lot when our app is launched from the GUI.
(Also, I really need to cleanup my PATH config)
Finding the correct PATH
Fortunately, this problem is easy to solve. With the help of the user’s default shell, we can find the complete PATH with the
- All processes have access to the
SHELLenvironment variable, which contains the location of the default shell executable (eg, "/bin/bash”)
- Most shells support the
envcommand to list all the environment variables they have access to
- Therefore, running the default shell in an interactive, login mode2 with the
envcommand should give us, among other variables, the complete PATH.
The Go implementation of this is not too complicated:
I have created a small Go package which do this, and a little more: pathfix.
To use it, add it to your project with
go get github.com/haroldadmin/pathfix, and then call the
Fix method to fix your process’s PATH:
Here it is in action, using the app shown earlier.
Before the fix:
After the fix:
If you need to do it in Electron using NodeJS, there is a handy package fix-path for it.
After fixing this problem, I wondered why didn’t I encounter this error on Windows. I suspect that Environment Variables on Windows are global by default, and hence every process receives the same
PATHvalue every time. I might be wrong about this, please feel free to correct me.
I also stumbled across PAM Environment Variables which are apparently a more “correct” way of solving this on Linux. You can read more about them in the documentation and the Arch Wiki. The gist of it is that you need to define your environment variables in a
pam_envfile, but the syntax for it is different. I would be grateful if someone took the time out to explain their usage in a more approachable manner.
Another potential solution for Linux users is to define their environment variables in the
~/.profilefile, which is sourced by the OS shell on every login. However, just like with the
pam_envsolution, this puts the burden of configuration on your users instead of you.
Question, comments or feedback? Feel free to reach out to me on Twitter @haroldadmin
Bash environment variables should be declared with
exportto make them available to other processes. eg.,
-iloptions when launching the shell makes sure that it reads common config files, such as
-cat the end just makes sure that the shell reads only the
envcommand, and nothing more. Read more about interactive, non-interactive, login and non-login shells here ↩︎