Setting environment variables in OS X Yosemite (and Mavericks)

UPDATE for Yosemite.

Apple and Tim Cook have shown their commitment to the wider developer community, once again, by starting to pull the plug on the launchctl facilities for setting and modifying environment variables.  Here's the rub: with the last lot of changes, Apple got the system working very, very well.

You can get a feel for the impact of the changes by reading the launchctl man page.  See, in particular, the section LEGACY SUBCOMMANDS.  It's not entirely accurate, but that's not unusual.  The critical subcommands are getenv, setenv, and unsetenv. The man page indicates that the export subcommand is available; it is not. (See update in the text, below.)

As far as I know, the procedure outlined below is still valid for Yosemite, but the question is still open.  I would not be surprised to learn that the behaviour of launchctl has changed in ways which invalidate some of this discussion.  If you find any problems, please let me know in comments.

In a previous post I explained how to integrate your .profile settings with the desktop's background environment on Lion, using ~/.MacOSX/environment.plist and code in .profile.

Then, of course, Apple changed all that when they released Mountain Lion. To achieve the same objective now required launchctl, and environment.plist was orphaned. Not wanting to abandon the integration path from .profile settings to the desktop environment, I built a method for using launchctl on top of my existing environment.plist code. That got pretty arcane, but it did the job for me.

There was always the problem with the first method, that in order for environment.plist to "take," you had to go through a login cycle. launchctl overcame that problem. In fact, launchctl was a great advance, because it enabled immediate update of the desktop environment. I've now rewritten the .profile code and the launchctl setup to  completely remove all reference to the plist. In the process, launchctl provides immediate updates of the desktop environment variables, and they persist across login and reboot cycles.

As explained in a previous post, the now-recommended method of system variable setting is to use the setenv subcommand of launchctl, like so:
launchctl setenv M2 /usr/share/maven/bin
The strings defined in setenv statements like the above, must be string literals. There is no means of resolving variable content based on other variables. For example, you cannot define
setenv TMPDIR $HOME/tmp

You can check the state of the launchd environment with the subcommand export:
launchctl export
This will output a series of shell commands of the form
M2=" /usr/share/maven/bin"; export M2;
Nope, not any more.  Tim Cook doesn't think you need this.  Thanks Tim.  I'm looking forward to finding out what else I don't need.

Here's the modified code in .profile to set both launchctl and the bash environment variables.
Create the following functions in .profile.
export LAUNCHCTL_ENV_VARS="$HOME/.launchctl_env_vars"
if [ -f $LAUNCHCTL_ENV_VARS ] ; then rm $LAUNCHCTL_ENV_VARS; fi

set_env_var () {
    eval export $1=\""$2"\"
    set_launchctl_env "$1" "$2"
}
unset_env_var () {
    unset $1
    unset_launchctl_env "$1"
}


set_launchctl_env () {
    eval launchctl setenv "$1" \""$2"\"
    echo launchctl setenv "$1" \""$2"\" >>$LAUNCHCTL_ENV_VARS
} 
unset_launchctl_env () {
    eval launchctl unsetenv "$1"
    echo launchctl unsetenv "$1" >>$LAUNCHCTL_ENV_VARS
}

 You may then use the function set_env_var to set both bash and launchctl entries. For example,
set_env_var M2_HOME "/usr/share/maven"
set_env_var M2 "$M2_HOME/bin"
set_env_var HTML_TIDY "$HOME/.tidy"
Note that the environment variables will immediately be available to any shell scripts or Terminal invocations, and to any applications launched from the desktop.

To get the environment re-established across a login, the launchctl startup features have to be invoked. This is where the file ~/.launchctl_env_vars comes in.  Notice that in the .profile code above, we have been executing a launchctl setenv command, and immediately echoing that same command to ~/.launchctl_env_vars.

When the system starts, the launchd process finds system daemon processes to launch from /System/Library/LaunchDaemons & /Library/LaunchDaemons. When a user logs in, launchd looks for user agents to start up in /System/Library/LaunchAgents, /Library/LaunchAgents & ~user/Library/LaunchAgents. While the first two are system-owned directories, the third is owned by the individual user.
We need two files: 1) a shell script which will actually issue the launchctl setenv commands, and 2) a LaunchAgent file that will tell launchd where to find the executable, and how to run it. The shell script is available here; the LaunchAgent file is available here.

The executable: profile2launchctl

#!/bin/sh

cmd_list="$HOME/.launchctl_env_vars"
SLEEP_TIME=10
# Uncomment following to echo launchctl commands to stdout
# Key StandardOutPath will have to be set in the plist file
# (au.id.pbw.plist2launchctl) for output to be captured.
#ECHO_TO_STDOUT=true

one_cmd () {
    eval "$@"
}

[[ -n "$ECHO_TO_STDOUT" ]] && cat "$cmd_list"

cat "$cmd_list" | while read line; do one_cmd $line; done

[[ -n "$ECHO_TO_STDOUT" ]] && echo Sleeping in profile2launchctl

# Sleep for a while so launchd doesn't get upset
[[ -n "$SLEEP_TIME" ]] && sleep "$SLEEP_TIME"

Notes:
The required sequence of launchctl setenv is in the cmd_list file. The commands are simply read one line at a time,  and each line is handed to eval for execution.
Permissions & Location:
To stay on the safe side, give the file -rwxr-xr-x permissions. It should be placed in /usr/libexec, which is root owned. You will have to use sudo to copy it.

The LaunchAgent file: au.id.pbw.profile2launchctl

<? 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>au.id.pbw.profile2launchctl</string>
        <key>KeepAlive</key>
        <false/>
        <key>Program</key>
        <string>/usr/libexec/profile2launchctl</string>
        <key>RunAtLoad</key>
        <true/>
        <key>UserName<key>
        <string>pbw</string>
    </dict>
</plist>

Notes:
The UserName field will, of course, be set to your own login name.  Likewise, the au.id.pbw can be replaced with any suitable identifier, and the log file directed to a suitable location.
If you need to debug the plist file, add the following lines.
        <key>StandardOutPath</key>
        <string>/Users/pbw/plist.log</string>
        <key>Debug</key>
        <true/>
These lines are used in conjunction with the ECHO_TO_STDOUT variable in the executable file. If used, the StandardOutPath will point to a writable file on your system. In this case, it points to a file in my home directory; you will have to change that.
 
Permissions & Location:
Again, give the file -rwxr-xr-x permissions. Place it in your $HOME/Library/LaunchAgents directory.