Setting environment variables on login in OS X

The problem

I require the use of several shell commands in Sublime Text 3. Some used by plugins (requiring for example coffee on the path), others by the build system.

As such, I require a proper PATH so I don’t have to install stuff in the system directories.

For example, I use a custom build system made using Python to handle compilation and propagation of various websites I make. That custom script is placed in a virtualenv.

I install coffeescript through npm, so it gets installed as /usr/local/share/npm/bin/coffee.

And so on.

Common solutions

The most common solution involves editing /etc/launchd.conf and placing a setenv there. This is bad in my case because:

  1. I don’t want root processes to have /usr/local/bin in their environment, because under homebrew, that space is writable by users;
  2. I don’t want root nor other users to load stuff from a specific user’s virtual environment;
  3. I want to be able to have a clean alternate user with which I can sign in in case something is messed up.

Thus I do not want to put that in /etc/launchd.conf.

A better solution

The approach presented in this stackoverflow comment is almost what I needed.

Almost, because while this does work for Finder, Spotlight and the Dock, I actually launch stuff using Alfred, and somehow Alfred seems to be launched before the environment is set.

Or maybe the “Login Items” in the User preferences pane aren’t affected by the env change? I don’t really care why, only that if affects me.

And so launching Sublime Text 3 using Alfred means not having the proper PATH, which is the point of all this.

A working solution

So this solution is a variant from the above stackoverflow post.

Create a launchd file:

cat > ~/Library/LaunchAgents/local.launch-at-login.conf.plist <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>local.launch-at-login.conf</string>
  <key>ProgramArguments</key>
  <array>
    <string>sh</string>
    <string>-c</string>
    <string>~/.launch_at_login.sh</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
</dict>
</plist>
EOF

Create the referenced script:

cat > ~/.launch_at_login.sh <<'EOF'
#!/bin/sh
# This is executed at login through ~/Library/LaunchAgents/local.launchd.conf.plist

launchctl setenv PATH ~/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:~/.virtualenvs/pypy/bin

pid=$(pgrep Alfred)
if [ $? -ne 0 ]; then
    open -a 'Alfred 2.app'
fi
EOF

Then remove Alfred from the user’s login items, in the “Users & Groups” preference pane.

Logout, then log back in, and verify it’s working by starting Sublime Text from Alfred, then opening the console, and typing something like:

>>> import os
>>> os.environ['PATH']
'/Users/serge/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/Users/serge/.virtualenvs/pypy/bin'

Side effects

The same approach could also be used to serialize startup items, which might help attaining faster login times (at least on non-ssd drives!). Simply add a delay between items, or use a slightly more sophisticated script that waits for the previous program to have started before launching the next:

#!/bin/sh
# This is executed at login through ~/Library/LaunchAgents/local.launchd.conf.plist

launchctl setenv PATH ~/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:~/.virtualenvs/pypy/bin

function condlaunch {
    current_pid=$(pgrep -af "$1")
    if [ $? -ne 0 ]; then
        if [ -n "$2" ]; then
            sleep $2
        fi
        open -a "$1"
    fi
}

condlaunch 'Alfred 2.app'
condlaunch 'Mail.app' 1
condlaunch 'Airmail.app' 3
condlaunch 'iTerm.app' 3
condlaunch 'Things.app' 3