はじめに
macOSでGUIアプリの環境変数を設定したいこと、ありますよね?(私はないです)
ここではそんなニッチな需要に向けた設定方法を説明します。
この記事で説明すること
- macOSで起動するすべてのGUIアプリの環境変数を設定する方法
- FinderやSpotlightなどから起動するアプリが対象です
この記事で説明しないこと
- macOSで起動する特定のGUIアプリの環境変数を設定する方法
- 環境変数設定した上でそのアプリをopenするシェルスクリプト作るとかすると良いと思います
- それかアプリの
Info.plist
のLSEnvironment
を無理やり書き換えるとか
- macOSのシェルの環境変数を設定する方法
- すべてのユーザに適用したいなら「/etc/paths.d」、特定のユーザに適用したいなら「環境変数 .bash_profile」「環境変数 .zprofile」などで検索するとよいと思います
環境
検証はmacOS
の10.14(Mojave)
と10.15(Catalina)
で行っています。
また各説明箇所でも個別に注記をしていますが、ここで説明する方法は基本的にOS X 10.10(Yosemite)
以降でしか動作しません。
また当然ながら、今後リリースされる新たなOSではここで書かれている方法が使えなくなる可能性があります。
tl;dr
- GUIアプリ用の環境変数は
PATH
とそれ以外とで設定方法が異なる-
PATH
:launchctl config user path <value>
- それ以外:
launchctl setenv <key> <value>
- ただし再起動でリセットされてしまうので、ログインの度にこのコマンドを実行する
launchd
サービスを作成すればOK
- ただし再起動でリセットされてしまうので、ログインの度にこのコマンドを実行する
-
- macOSの「再ログイン時にウィンドウを再度開く」の機能で開いたアプリには上記方法で設定した環境変数は適用されない
- => ただしDaemonから
gui
ドメインの作成を待機してごにょごにょやったりするとできるよ
- => ただしDaemonから
GUIアプリの環境変数設定方法
PATH
とそれ以外とで設定方法が異なります。
PATHを設定する方法
方法
PATH
環境変数はターミナルから下記コマンドを実行すると設定できます。このサブコマンドはOS X 10.10(Yosemite)
以降でのみ有効です。
sudo launchctl config user path <設定したいPATH情報>
反映にはOSの再起動が必要となります。
またここで設定したPATH
は全ユーザに適用されます。
補足説明
-
launchctl
はlaunchd
とやり取りするためのコマンドで、config
はその名の通り各種の設定をするサブコマンドです- 引数の
user
の代わりにsystem
とするとsystem
ドメイン1向けの設定になります- GUIアプリの場合は
user
ドメインを指定しておけばOKです。
- GUIアプリの場合は
-
path
の他にumask
の設定なども行えます -
launchctl config
の詳しい使用方法についてはman launchctl
を参照してください
- 引数の
- ここで設定した
PATH
は実体としては/var/db/com.apple.xpc.launchd/config/user.plist
に保存されているようです - 検索すると
environment.plist
やlaunchd.config
使った方法が出てきますがこれらはOS X 10.9(Mavericks)
以前でしか動作しません
PATH以外を設定する方法
方法
PATH
以外の環境変数はターミナルから下記コマンドを実行すると設定できますが、このコマンドで設定した環境変数はOSを再起動するとリセットされてしまいます。
launchctl setenv <設定したい環境変数のキー> <設定したい環境変数の値>
そこでlaunchd
サービスを作成してこのコマンドをユーザログインの度に実行させましょう。下記のような内容のファイルを作成・保存すればOKです2。
<?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>
<!-- ↓Labelは適当に変えてください↓ -->
<string>homu-konamilk.SetEnv</string>
<key>ProgramArguments</key>
<array>
<string>/bin/launchctl</string>
<string>setenv</string>
<!-- ↓設定したい環境変数のキー↓ -->
<string>HOGE</string>
<!-- ↓設定したい環境変数の値↓ -->
<string>fuga</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
- 保存先:
-
現ユーザのみに環境変数を適用する場合:
~/Library/LaunchAgents/
-
全ユーザに環境変数を適用する場合:
/Library/LaunchAgents/
または/System/Library/LaunchAgents/
- ただし
/System/
ディレクトリ下はSIPの制限がかかってるので基本的に今回使う必要ないかと思います
- ただし
-
現ユーザのみに環境変数を適用する場合:
- ファイル名:
<指定したLabel名>.plist
3- 上の例の場合は
homu-konamilk.SetEnv.plist
- 上の例の場合は
あとはOSの再起動かユーザの再ログインをすれば、次からログイン時にlaunchctl setenv
が実行され、指定した環境変数の設定が適用されます。
もしすぐに反映させたい場合はターミナルから下記のコマンドを実行しましょう。(/path/to/your.plist
の部分は適当に書き換えてください)
# 初回は下記でOK
launchctl bootstrap gui/`id -u` /path/to/your.plist
# 一度bootstrapした後やOS再起動した後でプロパティリストの変更を反映したい場合は、
# 下記のように一度`bootout`してから`bootstrap`し直す必要がある
launchctl bootout gui/`id -u` /path/to/your.plist
launchctl bootstrap gui/`id -u` /path/to/your.plist
補足説明
-
setenv
サブコマンドはOS X 10.10(Yosemite)
以降、LEGACY SUBCOMMANDS
とされています4がとりあえず動作します- これに代わる
LEGACY
ではないサブコマンドは現状存在しないようです
- これに代わる
- 例のプロパティリストの各設定項目はそれぞれ次のような意味を持っています
-
Label
: ラベル名。launchctl
の各種コマンドでサービスを操作するときに使います- とりあえずは他のものと被らなければ適当でOKですが、
reverse domain name notation
が使われることが多いようです(com.klab.hogehoge
とかそういうやつ)
- とりあえずは他のものと被らなければ適当でOKですが、
-
ProgramArguments
: 実行するプログラムとその引数 -
RunAtLoad
: サービスのロードと同時に実行を行うように指定します-
launchd
においてはサービスのロードとそのプロセスの起動はイコールではなく、基本的にすべてのサービスはon demandで動作し、指定のポートにアクセスがあるなど必要があった場合にのみプロセスが起動します - ただし
RunAtLoad
を指定することでサービスのロードと同時にプロセスを起動させることができます
-
-
-
bootstrap
/bootout
は任意のサービスをロード/アンロードするサブコマンドです-
launchd
サービスは所定の場所にプロパティファイルを置いておきさえすればOS再起動、ユーザ再ログイン時にロードされるので、OS再起動、ユーザ再ログインをする場合はこのコマンドの実行は必要ありません -
bootstrap
/bootout
の引数に指定しているgui/`id -u`
はサービスをロードする先のドメイン1です- 他に指定できるドメインとして
system
やuser/`id -u`
などがあります - GUIアプリに環境変数を適用する場合は
launchctl setenv
をgui
ドメインで実行する必要があります
- 他に指定できるドメインとして
-
bootstrap
/bootout
の代わりにload
/unload
でもOKです-
load
/unload
では問答無用で呼び出し元のドメインにサービスがロードされます -
load
/unload
はLEGACY SUBCOMMANDS
です。
-
-
-
launchctl setenv
をユーザログイン時に実行すればよいので、たとえばAutomator
アプリを作成して、「システム環境設定」->「ユーザとグループ」->「ログイン項目」にそのアプリを指定するなどの方法でもOKです
注意点
- macOSには「再ログイン時にウィンドウを再度開く」機能がありますが、この機能で再ログイン後に再度開かれたアプリには、ここで設定した環境変数が適用されません
ちょっと長い余談: 「再ログイン時にウィンドウを再度開く」で起動したアプリにも環境変数の設定を適用するには?
先に書いた通り、紹介した方法を使っても「再ログイン時にウィンドウを再度開く」で起動したアプリにはその環境変数の設定は適用されません。(launchctl config
で設定したPATH
環境変数については問題ありません。PATH
以外の環境変数が問題)
これを実現する方法がないかいろいろ調べて試行錯誤してみました。(そこまでする必要あるかなと思いつつ……Advent Calendarに書くネタとして前半部分だけだと物足りない気がしたから……)
なお先に書いておきますが、最終的にちょっと無理やりな方法しか見つからなかったので、あくまで余談としてお楽しみください。基本的には先に書いた方法だけで十分だと思います。
そもそもなぜ「再度開く」アプリには環境変数設定が適用されないのか?
「再度開く」アプリはいつ起動する?
「再度開く」アプリに環境変数の設定が適用されない理由について、検索してみるとフォーラムなどで「『再度開く』アプリの起動が、RunAtLoad
を指定したLaunchAgentの起動よりも早いから」といったことが書かれているのが見つかります。
おそらくそう推測されるということだと思うのですがソースが見つからなかったので、とりあえずまずは「再度開く」アプリとRunAtLoad
を指定したLaunchAgentの起動順序を検証してみました。
長時間sleepするだけのLaunchAgentを作成して、そのpidを「再度開く」アプリのpidと比べることで簡単に起動順序を確認してみます。
<?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>homu-konamilk.Sleep</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-c</string>
<string>sleep 9999999</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
このファイルを~/Library/LaunchAgents/
に保存してOSを再起動した上で、そのpidを「再度開く」アプリのpidと比べてみます。
上のスクリーンショットで「Sublime Text」「iTerm2」が「再度開く」で起動したアプリで、pidがそれぞれ282
、286
です。一方で今回実行したsleep
はpidが609
でした。
起動直後なのでpidの再利用などは特にないものとすると、とりあえず今回のケースでは「再度開く」アプリの方がRunAtLoad
を指定したLaunchAgentのプログラムよりも早く起動したようです。なおプログラムの起動時刻を記録してみたところ、「再度開く」アプリのほうが5秒以上早く起動していました。
ということで、必ずかはわかりませんが、「再度開く」アプリはRunAtLoad
を指定したLaunchAgentよりも早く起動しているようです。
※ ちなみにアクティビティモニタを見ると「Sublime Text」の前にもいくつかのサービスが起動していますが、launchctl blame
でサービスの起動理由を調べてみた限りだと単に他のプロセスから使用要求があって起動したものと思われます5
どうすれば「再度開く」アプリに環境変数の設定を適用できるか?
gui
ドメインの初期環境変数を設定できるような方法が何かあればそれで解決なんですが、いくら検索してもドキュメントを漁ってもそんな方法は見つかりません。launchd
のソースコードを見れたりすると何かわかるかもしれませんが、現在は残念ながら非公開……6。
なのでとりあえずは「再度開く」アプリの起動よりも前にlaunchctl setenv
を実行する方法を探してみることにします。
ただしlaunchctl setenv
の実行があまりにも早すぎると今度はまだgui
ドメインが作成されていなくて環境変数の設定ができなかったりするので、gui
ドメインの作成後、かつ「再度開く」アプリの起動前にlaunchctl setenv
を実行することを目指していきます。
駄目だった方法
以下、試してみて駄目だった方法です。
- 作成するLaunchAgentのplistファイル名の先頭を「!」にしてみる(ファイル名ソートで先頭の方に来るように)
- => ワンチャンいけるのではと思ったが失敗。ファイル名をいじったLaunchAgentを複数作成して検証してみたがファイル名は特に起動順序には影響しない模様。
- そもそももしなんらかの方法で「
RunAtLoad
のLaunchAgentの中で一番最初に起動させる」ことができたとしても、それをしたところで「再度開く」アプリより起動が遅いままの可能性は高い
-
launchctl setenv
を実行するAutomatorアプリを作成して「システム環境設定」->「ユーザとグループ」->「ログイン項目」に指定してみる- => ワンチャン行けるのではと思ったが失敗2。
RunAtLoad
のLaunchAgentよりは早いタイミングで起動したが、「再度開く」アプリよりは起動が遅かった
- => ワンチャン行けるのではと思ったが失敗2。
成功した方法
方針
AgentではなくDaemonを使います。
AgentとかDaemonとかいうのはlaunchd
における用語で、launchd
によって起動するサービスの種類です。Agentはユーザのコンテキスト、Daemonはシステムの(rootの)コンテキストで実行されるものを指します7。
あるlaunchd
サービスがAgentとDaemonのどちらとしてロードされるかは単にそのplistファイルの保存先ディレクトリによって決定されます。
- Agent用ディレクトリ:
~/Library/LaunchAgents/
/Library/LaunchAgents/
/System/Library/LaunchAgents/
- Daemon用ディレクトリ:
/Library/LaunchDaemons/
/System/Library/LaunchDaemons/
Daemon用ディレクトリに保存したplistにRunAtLoad
の指定がされていた場合、そのサービスはログイン画面でユーザ選択してパスワード入力してEnterを押した後、Agentよりも前に起動します。
ただしこのRunAtLoad
のDaemon実行時点ではまだgui
ドメインが作成されていません。そこでDaemonからスクリプトを開始し、その中でgui
ドメインの作成完了を待機してからlaunchctl setenv
を実行することにします。
gui
ドメインが作成済みかどうかは、ドメインの情報を出力するlaunchctl print
コマンドが成功するかどうかで判定して、このコマンドが成功するまでsleep
しつつループすることにします。
またDaemonはシステムのコンテキストで実行されるので、ここでlaunchctl setenv
を実行してもそのままだと環境変数はsystem
ドメインに適用されてしまいます。(launchctl setenv
では呼び出し元のコンテキストを元に環境変数の設定適用先のドメインが決まる)
launchctl
にはこれを回避してコンテキストを切り替えるためのasuser
というサブコマンドが用意されているので、これを使用してコンテキストを切り替えた上でsetenv
を実行することにしましょう。
まとめると下記の通りです。
- 「再度開く」アプリよりも前に処理を実行するためにAgentではなくDaemonとして
launchd
サービスを作成する -
launchd
サービスからスクリプトを開始してgui
ドメインの作成完了を待機する-
gui
ドメインが作成済みかどうかはlaunchctl print
コマンドの成否で判断する
-
-
gui
ドメインの作成を確認できたらasuser
サブコマンドを介した上でlaunchctl setenv
を実行する8
方法
まずはgui
ドメインの作成を待機してからsetenv
を実行するスクリプトを作成します。下記のファイルを作成して適当な場所に保存してください。ファイル中で書き換えが指示されている箇所は適当に書き換えてください。
#!/bin/bash
# TARGET_UIDは適当に書き換えてください
TARGET_UID=501
# `launchctl print`を繰り返す間隔
# 手元の環境だと1秒で問題なさそうでしたが必要であれば調整してください
# ただし1秒未満の値を指定する場合はsleepコマンドだと対応できないので何か他の方法が必要(pythonやperlを起動してsleepするなり)
DOMAIN_CHECK_RETRY_INTERVAL=1
DOMAIN_CHECK_MAX_RETRY=50
# guiドメインが作成されるまで(=guiドメインのprintが成功するまで)待機
DOMAIN_CREATED=0
for i in `seq 1 $DOMAIN_CHECK_MAX_RETRY`
do
echo "check if gui domain created: $i"
if launchctl print "gui/$TARGET_UID"; then
DOMAIN_CREATED=1
break
else
# guiドメインがまだ作成されてなければ一定時間sleepしてからリトライ
sleep $DOMAIN_CHECK_RETRY_INTERVAL
fi
done
if [ "$DOMAIN_CREATED" = "1" ]; then
# guiドメインが作成されてたらasuserした上でsetenv
# 環境変数のキーと値は適当に書き換えてください
launchctl asuser $TARGET_UID launchctl setenv HOGE fuga
fi
次にこのスクリプトを起動するlaunchd
サービスの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>
<!-- ↓Labelは適当に変えてください↓ -->
<string>homu-konamilk.SetEnv2</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<!-- ↓setenvgui.shへのパス。適当に書き換えてください↓ -->
<string>/path/to/setenvgui.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
- 保存先:
/Library/LaunchDaemons/
または/System/Library/LaunchDaemons/
- ただし
/System/
ディレクトリ下はSIPの制限がかかってるので基本的に今回使う必要ないかと思います - 先に説明したAgentを使った方法とは保存先のディレクトリ名が異なるので注意してください
- ただし
- ファイル名:
<指定したLabel名>.plist
3- 上の例の場合は
homu-konamilk.SetEnv2.plist
- 上の例の場合は
あとはOSを再起動すれば、「再ログイン時に再度開く」機能で起動したアプリにも環境変数が適用されていることが確認できます。
なおこの方法の問題点として、OSの再起動なしにユーザの再ログインをしたときには動作しません。この問題を解決するためにはさらにユーザのログインを監視して処理を実行するような実装が必要そうです。(私は力尽きたので誰か気力のある奇特な人がいたらやってください……)
余談の余談
余談の余談です。
上には書きませんでしたが、DaemonではなくLoginHook9を使う方法でもうまくいきました。実行タイミング的には、
-
RunAtLoad
を指定したDaemon - LoginHook
- 「再度開く」アプリの起動
-
RunAtLoad
を指定したAgent
の順になっているようです。
このLoginHookを使う方法だと、OSの再起動なしにユーザの再ログインをしたときでも正しく動作します。
ちなみにLoginHookはdeprecatedです。
また今回、システムコンテキストで実行するスクリプトからgui
ドメインに環境変数を適用する方法としてasuser
コマンドを使いましたが、これは代わりに、Agentをgui
ドメインにbootstrap
することでも実現できます。(Daemonとは別に、launchctl setenv
を実行するAgentを作って、gui
ドメインの作成完了を待機してからそのAgentをgui
ドメインにbootstrap
する)
最初はasuser
コマンドの存在を知らなくてこの方法で記事書いてたんですが、後からasuser
コマンドの存在を知って記事書き換えました。
おわりに
余談部分、いろいろ試して最終的にうまくいくまでなんだかんだ3,4日かかりました。
その過程でMach(≠Mac)まわりの概念やlaunchdのことについて少しだけ詳しくなれたのはよかったですが……。この余談部分が役に立つ人はいるのだろうか……。
あれこれ推測を重ねてやってるため、Macの内部動作とかをマジで研究してる方々から見るといろいろとツッコミどころがあるかと思いますのでご指摘いただけると嬉しいです。
参考にしたページ
-
launchd
サービスについての公式ドキュメント: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/Introduction.html#//apple_ref/doc/uid/10000172i-SW1-SW1- ただしメンテナンスされてない
- MachのBootstrap Contextについての公式ドキュメント: https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/contexts/contexts.html#//apple_ref/doc/uid/TP30000905-CH212-BEHJDFCA
- これもメンテナンスされてない
- http://docs.macsysadmin.se/2014/pdf/Launchd_-_At_your_service.pdf
- https://babodee.wordpress.com/2016/04/09/launchctl-2-0-syntax/
- http://newosxbook.com/articles/jlaunchctl.html
man launchd
man launchd.plist
man launchctl
-
そもそもここでドメインってなんぞやという説明は
man launchctl
にあります:A domain manages the execution policy for a collection of services. ...(略)... Domains advertise these endpoints in a shared namespace and may be thought of as synonymous with Mach bootstrap subsets.
この最後で言ってるMach bootstrap subsets
についてはこちらが詳しいです ↩ ↩2 -
launchd
サービスについてはman launchd
、man launchd.plist
、man launchctl
等を参照してください ↩ -
man launchd.plist
より:please note that it is the expected convention for launchd property list files to be named <Label>.plist.
↩ ↩2 -
man launchctl
を参照 ↩ -
先述した通り
launchd
サービスは基本的にon demandで起動します: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/Lifecycle.html#//apple_ref/doc/uid/10000172i-SW3-BAJJBJEG ↩ -
OS X 10.9(Mavericks)
までは公開されてました: https://opensource.apple.com/tarballs/launchd/ ↩ -
man launchd
より:In the launchd lexicon, a daemon is, by definition, a system-wide service of which there is one instance for all clients. An agent is a service that runs on a per-user basis.
↩ -
ちなみにこの試行錯誤の過程で、
gui
ドメイン作成前にlaunchctl asuser <uid> launchctl setenv
を実行するとgui
ドメインではなくuser
ドメインに環境変数が設定されることがわかりました。なんでこういう挙動になるのかわからないので誰かご存じの方いたら教えてほしい……。 ↩ -
LoginHook
についてはAppleの公式ドキュメント等を参照(古いですが): https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CustomLogin.html ↩