You're writing a GUI app using Tkinter or PySide or your favorite GUI library, and testing it in-place, everything works.
Then you build a .app bundle out of it, double-click it in Finder, and it can't read your environment variables. (In fact, the "open" command, AppleScript, LaunchAgents… anything but directly running the program in the shell has the same problem.)
You check to make sure you've added them in your ~/.bash_profile. You open a new shell in Terminal, and there they are. So why can't your app see them?
Quick fix
If you're familiar with linux or other Unix systems, and are just looking for a fix, and don't care about how and why it works, do this:
- Instead of typing "export FOO=bar", do "launchctl setenv FOO bar".
- Instead of adding "export FOO=bar" to ~/.bash_profile, do "/usr/libexec/PlistBuddy -c 'add FOO string bar' -x ~/.MacOSX/environment.plist".
- Instead of adding "FOO=bar; export FOO" to /etc/profile, add "setenv FOO bar" to /etc/launchd.conf.
What's going on?
On most other Unix desktops, like a typical linux GNOME setup, your file manager is a child of your X11 session, which is a child of your login shell, so it's inherited all the settings from that login shell.
When you double-click an app, it either uses the shell to launch the app, or spawns it directly as a child process. So, your app is going to inherit the environment of either your original login shell, or a new shell; either way, it's going to get all the settings in your ~/.bash_profile, /etc/profile, etc. (assuming your shell is bash).
On OS X, Finder.app is a child of your launchd session, which is a child of the root launchd session, which is process 0. There's no shell anywhere. And when you double-click an app, it asks LaunchServices to run the app. So, your app doesn't inherit anything from Finder, and even if it did, Finder hasn't inherited anything from your login shell.
So, instead of configuring your login shell, you need to configure launchd.
The way to configure launchd is with the launchctl command.
As the manpage explains, launchctl talks to the current launchd process, so any changes it makes aren't going to be persistent to new sessions, but "These commands can be stored in $HOME/.launchd.conf or /etc/launchd.conf to be read at the time launchd starts."
Except, as the launchd.conf manpage mentions, $HOME/.launchd.conf is "currently unsupported". So, how do you add user environment variables?
There's an older mechanism for specifying environment variables for a user session in an environment.plist file, and for backward compatibility reasons, launchd reads that file at startup. So, until user launchd.conf files are supported, you have to use that mechanism instead. You probably don't have a .MacOSX directory, so you'll need to create that, and create the plist file. And, if you do have them, they may be in binary plist form rather than XML. So you probably want to use PlistBuddy.
Except, as the launchd.conf manpage mentions, $HOME/.launchd.conf is "currently unsupported". So, how do you add user environment variables?
There's an older mechanism for specifying environment variables for a user session in an environment.plist file, and for backward compatibility reasons, launchd reads that file at startup. So, until user launchd.conf files are supported, you have to use that mechanism instead. You probably don't have a .MacOSX directory, so you'll need to create that, and create the plist file. And, if you do have them, they may be in binary plist form rather than XML. So you probably want to use PlistBuddy.
How do I do app-specific environment variables?
On most Unix systems, if you want a program to run with app-specific environment variables, you rename the program to "_foo" (or, more likely, hide it somewhere like /usr/lib), then create a wrapper shell script "foo" that just does a few exports and runs the real program.
This won't work on OS X. If you want to actually launch an app as an app (so it does the right thing with the Dock, menubar, etc.), you have to launch if via the "open" tool, or some other LaunchServices-based tool, which means it won't inherit your shell environment.
Fortunately, you don't have to do this on OS X in the first place. The Info.plist file inside every .app bundle has a key named LSEnvironment. When LaunchServices launches the app, it first sets all of the environment variables from the dict under that key.
What about plain Unix executables?
Plain Unix executables, like "ls" and "grep", aren't normally launched by LaunchServices; you just run them from the shell, so of course they inherit your shell's environment.
And if you do something like double-click /bin/ls in Finder, LaunchServices doesn't launch ls, it launches Terminal.app (by default; you can actually change it to iTerm.app or anything else you want, the same way you can change what app handles text files or HTML files), which will open a new window and fire up a new shell running "/bin/ls". So, it will inherit your profile environment.
Why so crazy?
Given the distinction between app bundles and executable files, this design makes perfect sense. Of course not every Unix fan likes the idea of app bundles, or bundles in general. But once you accept that idea, you can't run a bundle from the shell, so why should LaunchServices fake a shell when running a bundle? If you want something you can run from a shell, write a bare executable rather than an app bundle. Or just run the bare executable out of your app, e.g., "/Applications/Foo.app/Contents/MacOS/foo" instead of "open /Applications/Foo.app".
Of course there are limitations to what bare executables can do with the GUI and other parts of the Cocoa environment. But those limitations are directly caused by the fact that they're not launched by LaunchServices and don't have a bundle.
Anyway, this design allows Apple to unify almost everything into launchd: init scripts, rc.d scripts, cronjobs, inetd, the NeXT-style SystemStarter, watchdogs, loginwindow, all the stuff buried in LaunchServices that you couldn't access directly… it's all done by one pretty simple program.
Historical note
Before launchd, OS X used a variety of different methods to launch apps, inherited from NeXTStep, Classic Mac OS, and *BSD. Most ways you'd launch a program inherited their settings from the loginwindow process. When loginwindow logged in a user, it read environment settings out of a file called ~/.MacOSX/environment.plist. Earlier versions of launchd would also read that file for backward compatibility reasons, but that's no longer true in 10.8 and later.
Add a comment