Aug 23

Unix/GNU Linux 上的 X11 图形程序会从系统的 /etc/profile 以及用户目录的 $HOME/.bashrc、$HOME/.bash_profile、$HOME/.xprofile 等处读取环境变量。例如 GNU Emacs、Code::Blocks、CodeLite、Eclipse 等均可从系统的环境变量中获取需要的环境变量。尽管 Mac OS X 是类 Unix 系统,但毕竟它的图形界面不是由 X11 提供的,因此,它上面的图形程序获取环境变量的方式也是不同的。由于 Mac OS X 中图形程序有多种运行方式,例如从终端调用命令行运行、在 Finder 、Launchpad 以及 Dock 上点击图标运行或者用 SpotLight 调用,这几种运行方式下图形程序的环境变量读取以及生效方式也不尽一致。

首先,谈谈终端环境以及由终端调用的程序(在 Bash Shell 中)如何读取环境变量。根据 /etc/profile 中的设置可以看到,Mac OS X 系统会借助 /usr/libexec/path_helper 程序设置路径变量 PATH。path_helper 会分别读取 /etc/paths、/etc/paths.d/ 与 /etc/manpaths、/etc/manpaths.d/ 中的设置来获取 PATH 与 MANPATH 的环境变量。之后 /etc/profile 还会综合 /etc/bashrc 给出 Shell 中所有系统级环境变量的设置。因此,系统级的环境变量 PATH 的设置可通过修改 /etc/profile、/etc/bashrc 与 /etc/paths.d/ 等相关文件来实现,除此而外的系统级环境变量则可在 /etc/profile、/etc/bashrc 中设置。有一点值得指出,那就是 MANPATH 变量,通常它是不需要设置的,这是因为手册页的搜索除去用 MANPATH 变量配置之外,还可以在 PATH 的相对路径中搜索。至于 Shell 中用户级环境变量的设置,与 Linux 中一样,首先继承来自 /etc/profile 中的系统级环境变量,再根据 Shell 是不是登录 Shell 决定是否读取用户家目录中的 $HOME/.bashrc 或 $HOME/.bash_profile 中的环境变量。一般为了避免配置重复出现,无论登录还是非登录 Shell,均做如下配置

$ cat > ~/.bash_profile << EOF
if [ -f ~/.bashrc ]; then 
    source ~/.bashrc
fi
EOF

相关环境变量的设置都在 $HOME/.bashrc 中进行。这就给出了 Shell 中所有用户级环境变量的配置方法。对于从终端调用的(无论是否是图形界面的)用户级程序而言,环境变量直接继承自 Shell 中所有用户级环境变量。例如从终端打开 Code::Blocks 程序

$ open -a CodeBlocks.app

随后在 Settings -> Compiler...,Global Compiler settings -> Compiler settings -> Other options 中填入 `echo $PATH`,接着随便写一段简单的 C/C++ 程序并点击 Build,在 Code::Blocks 日志框中便会看到环境变量 PATH 的输出。很容易验证 Code::Blocks 确实是从 Shell 中读取了环境变量。先退出 Code::Blocks,记得退出时选择保存设置,因为还要用它进行同样的测试。请记得在测试完了之后,删掉 Code::Blocks 中全局的编译选项设置。为了做一个比较,可以尝试从 Launchpad 中点击 Code::Blocks 图标启动,做同样的尝试,会发现此时 echo $PATH 没有在日志窗口中输出,这表明从 Launchpad 中启动程序不会继承终端程序的环境变量。

那么除了 Terminal.app 之外,对于那些从 Finder、Launchpad 以及 Dock 处启动或者用 SpotLight 调用的程序,是如何读取环境变量的呢?在 Mountain Lion 之前,从那些位置启动的图形界面程序会从 $HOME/.MacOSX/enviroment.plist 文件继承环境变量;但是自 Mountain Lion 之后,~/.MacOSX/enviroment.plist 失效,只有通过 launchtl setenv 命令设置的环境变量才有可能被它们继承,而且设置完必须重启 Finder、Dock 以及 Spotlight 才能生效,具体取决于程序从哪儿被调用?

先来谈谈 ~/.MacOSX/enviroment.plist 的配置,尽管 Mountain Lion 版本已经过时,但难保不会有一些老的机器会停留在那些版本上。通常,~/.MacOSX/environment.plist 文件并不存在,需要先创建再编辑。例如

$ mkdir ~/.MacOSX && touch  ~/.MacOSX/enviroment.plist

根据 ~/.bashrc 中设置的环境变量情况,利用 defaults 命令操作该文件,例如

$ defaults write $HOME/.MacOSX/environment.plist PATH "$PATH”
$ defaults write $HOME/.MacOSX/environment.plist JAVA_HOME "$JAVA_HOME”
$ defaults write $HOME/.MacOSX/environment.plist ANDROID_HOME "$ANDROID_HOME"

注意上述 defaults 命令是将当前终端中的 PATH、JAVA_HOME 与 ANDROID_HOME 写入了 ~/.MacOSX/environment.plist。至于编辑 ~/.MacOSX/environment.plist,最好用 Xcode.app,例如

$ open -a Xcode.app ~/.MacOSX/enviroment.plist

设置调整完成,可以再试试从 Launchpad 中点击 Code::Blocks 图标启动,做前面一样的测试,会发现此时 echo $PATH 在日志窗口中输出正常。

现在来谈谈 Mountain Lion 版本以后,用 launchctl setenv 设置图形界面程序的环境变量的方法。从当前终端运行

$ launchctl setenv PATH “$PATH"

别急着作测试,因为从 Mac OS Yosemite 开始,光这样设置,从 Finder、SpotLight、Launchpad 以及 Dock 点击图标运行的程序还是不会继承这些变量。对于从 Launchpad 或 Dock 中启动的程序,需重启 Dock 才能让设置生效:

$ killall Dock

对于从 Finder 中启动的程序,需重启 Finder 才能让设置生效:

$ killall Finder

对于从 Spotlight 中启动的程序,需重启才能让设置生效:

$ killall Spotlight
$ killall SystemUIServer

好了,有了这些操作之后,再试试从 Launchpad 中点击 Code::Blocks 图标启动,做前面一样的测试,会发现此时 echo $PATH 在日志窗口中输出正常。

但是可以看到,这样的操作无比麻烦。为了省去这些麻烦,可以建立一个用户级的服务来替代这些操作过程。先创建

$ mkdir -p ~/.local/bin
$ cat > ~/.local/bin/environment.sh <<EOF
#!/bin/sh

set -e

syslog -s -l warn "Set environment variables with ~/.local/bin/environment.sh \$(whoami) - start"

launchctl setenv PATH "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/usr/local/android-sdk-manager/platform-tools:/usr/local/android-sdk-manager/tools:/usr/local/apache-ant/bin:/usr/local/mysql/bin:/usr/local/MacGPG2/bin:/usr/texbin:"
launchctl setenve JAVA_HOME "/Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home"
launchctl setenv ANDROID_HOME "/usr/local/android-sdk-manager/"

osascript -e 'tell app "Dock" to quit'
osascript -e 'tell app “Finder" to quit'
osascript -e 'tell app “Spotlight" to quit'
osascript -e 'tell app "SystemUIServer" to quit'

syslog -s -l warn "Set environment variables with ~/.local/bin/environment.sh \$(whoami) - complete"
EOF
$ chmod +x ~/.local/bin/environment.sh

接着再创建服务配置文件

$ nano -w ~/Library/LaunchAgents/org.easior.environment.plist
<?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>org.easior.environment.plist</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/easior/.local/bin/environment.sh</string>
    </array>
    <key>KeepAlive</key>
    <false/>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

并启动它

$ launchctl load -w ~/Library/LaunchAgents/org.easior.environment.plist

现在来看看以上设置对于环境变量 PATH 带来了怪异现象。首先,第三方终端程序 iTerm.app 作为图形程序,将继承自 launchctl setenv 设定的环境变量 PATH;其次,Mac OS 自带的终端程序 Terminal.app 不会继承来自 launchctl setenv 设置的环境变量,它仍按 Shell 的标准方法设置 PATH 变量;再次,由 XQuartz 提供的 X11.app 中终端程序 xterm 将既会继承由 launchctl setenv 设定环境变量 PATH,还会再次执行登录 Shell,也即会再次继承系统级的 PATH 变量。如此一来,若既在 ~/.bashrc 或 ~/.bash_profile 中设置 PATH,也在 ~/.local/bin/environment.sh 中设置 PATH,且这两个 PATH 设置不一致的话,例如

$ cat >> ~/.bashrc << EOF
~/.local/bin
EOF
$ launchctl setenv PATH "$HOME/bin"
$ killall Dock

那么从 Launchpad 启动的这三个终端的 PATH 变量将会各不相同。为了避免这种情况的发生,建议在 ~/.bashrc 或者 ~/.bash_profile 中设置

if [ ! `launchctl getenv PATH` == "" ]; then
   export PATH=.:`launchctl getenv PATH`
fi

这样一来,每个终端启动的 PATH 均是一致的。还有一点需要指出:launchctl setenv PATH 设置为空与不设置它,这是两码事。例如

$ launchctl getenv PATH ""
$ killall Dock

现在试试从 Launchpad 或 Dock 调用 iTerm.app,会有意想不到的事情发生。

除了 launchctl setenv 的方法之外,修改 Mac OS X 图形程序的 Info.plist 文件中的 LSEnvironment 变量也可以设置需要读取环境变量。例如,在 Finder 的 /Applications 目录鼠标右击 Codeblocks.app 图标 -> Show package contents,接着用 Xcode.app 打开 Contents 目录的 info.plist 文件,在其中增加一个新的 key/dict 对:

<key>LSEnvironment</key>
<dict>
     <key>PATH</key>
<string>/Users/James/.rvm/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:</string>
</dict>

保存完改动并在命令行中执行

$ /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user 
$ killall Finder

再试试从 Launchpad 中点击 Code::Blocks 图标启动,还是做与前面一样的测试,看看 echo $PATH 在日志窗口中输出。为了 Code::Blocks 能正常工作,测试完成后请去掉 Info.list 中的 LSEnvironment 设置。

另外,还有些图形程序,本身提供了设定环境变量的方法。例如对于 GNU Emacs 而言,要继承环境变量可直接在 Emacs 中配置。例如

$ cat >> ~/.emacs <<EOF
(setenv “PATH” (concat (getenv “PATH”) “:” “/opt/local/bin” “:” “/opt/local/sbin”))
EOF

能让 GNU Emacs 的子进程直接读取 PATH 变量。