35
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

KLab EngineerAdvent Calendar 2019

Day 20

macOSでGUIアプリの環境変数を設定する方法探求

Last updated at Posted at 2019-12-19

はじめに

macOSでGUIアプリの環境変数を設定したいこと、ありますよね?(私はないです)
ここではそんなニッチな需要に向けた設定方法を説明します。

この記事で説明すること

  • macOSで起動するすべてのGUIアプリの環境変数を設定する方法
    • FinderやSpotlightなどから起動するアプリが対象です

この記事で説明しないこと

  • macOSで起動する特定のGUIアプリの環境変数を設定する方法
    • 環境変数設定した上でそのアプリをopenするシェルスクリプト作るとかすると良いと思います
    • それかアプリのInfo.plistLSEnvironmentを無理やり書き換えるとか
  • macOSのシェルの環境変数を設定する方法
    • すべてのユーザに適用したいなら「/etc/paths.d」、特定のユーザに適用したいなら「環境変数 .bash_profile」「環境変数 .zprofile」などで検索するとよいと思います

環境

検証はmacOS10.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ドメインの作成を待機してごにょごにょやったりするとできるよ

GUIアプリの環境変数設定方法

PATHとそれ以外とで設定方法が異なります。

PATHを設定する方法

方法

PATH環境変数はターミナルから下記コマンドを実行すると設定できます。このサブコマンドはOS X 10.10(Yosemite)以降でのみ有効です。

sudo launchctl config user path <設定したいPATH情報>

反映にはOSの再起動が必要となります。
またここで設定したPATH全ユーザに適用されます。

補足説明

  • launchctllaunchdとやり取りするためのコマンドで、configはその名の通り各種の設定をするサブコマンドです
    • 引数のuserの代わりにsystemとするとsystemドメイン1向けの設定になります
      • GUIアプリの場合はuserドメインを指定しておけばOKです。
    • pathの他にumaskの設定なども行えます
    • launchctl configの詳しい使用方法についてはman launchctlを参照してください
  • ここで設定したPATHは実体としては/var/db/com.apple.xpc.launchd/config/user.plistに保存されているようです
  • 検索するとenvironment.plistlaunchd.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名>.plist3
    • 上の例の場合は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とかそういうやつ)
    • ProgramArguments: 実行するプログラムとその引数
    • RunAtLoad: サービスのロードと同時に実行を行うように指定します
      • launchdにおいてはサービスのロードとそのプロセスの起動はイコールではなく、基本的にすべてのサービスはon demandで動作し、指定のポートにアクセスがあるなど必要があった場合にのみプロセスが起動します
      • ただしRunAtLoadを指定することでサービスのロードと同時にプロセスを起動させることができます
  • bootstrap/bootoutは任意のサービスをロード/アンロードするサブコマンドです
    • launchdサービスは所定の場所にプロパティファイルを置いておきさえすればOS再起動、ユーザ再ログイン時にロードされるので、OS再起動、ユーザ再ログインをする場合はこのコマンドの実行は必要ありません
    • bootstrap/bootoutの引数に指定しているgui/`id -u` はサービスをロードする先のドメイン1です
      • 他に指定できるドメインとしてsystemuser/`id -u`などがあります
      • GUIアプリに環境変数を適用する場合はlaunchctl setenvguiドメインで実行する必要があります
    • bootstrap/bootoutの代わりにload/unloadでもOKです
      • load/unloadでは問答無用で呼び出し元のドメインにサービスがロードされます
      • load/unloadLEGACY SUBCOMMANDSです。
  • launchctl setenvをユーザログイン時に実行すればよいので、たとえばAutomatorアプリを作成して、「システム環境設定」->「ユーザとグループ」->「ログイン項目」にそのアプリを指定するなどの方法でもOKです

注意点

  • macOSには「再ログイン時にウィンドウを再度開く」機能がありますが、この機能で再ログイン後に再度開かれたアプリには、ここで設定した環境変数が適用されません
    • image.png
    • 上記ダイアログで「再ログイン時にウィンドウを再度開く」のチェックを外しておくか、ログイン後に改めてアプリを開き直してください
    • どうしてもこの機能を使いたい場合は後述の余談を参照してください

ちょっと長い余談: 「再ログイン時にウィンドウを再度開く」で起動したアプリにも環境変数の設定を適用するには?

先に書いた通り、紹介した方法を使っても「再ログイン時にウィンドウを再度開く」で起動したアプリにはその環境変数の設定は適用されません。(launchctl configで設定したPATH環境変数については問題ありません。PATH以外の環境変数が問題)

image.png

これを実現する方法がないかいろいろ調べて試行錯誤してみました。(そこまでする必要あるかなと思いつつ……Advent Calendarに書くネタとして前半部分だけだと物足りない気がしたから……)

なお先に書いておきますが、最終的にちょっと無理やりな方法しか見つからなかったので、あくまで余談としてお楽しみください。基本的には先に書いた方法だけで十分だと思います。

そもそもなぜ「再度開く」アプリには環境変数設定が適用されないのか?

「再度開く」アプリはいつ起動する?

「再度開く」アプリに環境変数の設定が適用されない理由について、検索してみるとフォーラムなどで「『再度開く』アプリの起動が、RunAtLoadを指定したLaunchAgentの起動よりも早いから」といったことが書かれているのが見つかります。

おそらくそう推測されるということだと思うのですがソースが見つからなかったので、とりあえずまずは「再度開く」アプリとRunAtLoadを指定したLaunchAgentの起動順序を検証してみました。

長時間sleepするだけのLaunchAgentを作成して、そのpidを「再度開く」アプリのpidと比べることで簡単に起動順序を確認してみます。

~/Library/LaunchAgents/homu-konamilk.Sleep.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>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と比べてみます。

スクリーンショット 2019-12-13 12.05.01.png スクリーンショット 2019-12-13 12.07.08.png

上のスクリーンショットで「Sublime Text」「iTerm2」が「再度開く」で起動したアプリで、pidがそれぞれ282286です。一方で今回実行した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よりは早いタイミングで起動したが、「再度開く」アプリよりは起動が遅かった

成功した方法

方針

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を実行するスクリプトを作成します。下記のファイルを作成して適当な場所に保存してください。ファイル中で書き換えが指示されている箇所は適当に書き換えてください。

setenvgui.sh
#!/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ファイルを作成します。

/Library/LaunchDaemons/homu-konamilk.SetEnv2.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名>.plist3
    • 上の例の場合はhomu-konamilk.SetEnv2.plist

あとはOSを再起動すれば、「再ログイン時に再度開く」機能で起動したアプリにも環境変数が適用されていることが確認できます。

なおこの方法の問題点として、OSの再起動なしにユーザの再ログインをしたときには動作しません。この問題を解決するためにはさらにユーザのログインを監視して処理を実行するような実装が必要そうです。(私は力尽きたので誰か気力のある奇特な人がいたらやってください……)

余談の余談

余談の余談です。

上には書きませんでしたが、DaemonではなくLoginHook9を使う方法でもうまくいきました。実行タイミング的には、

  1. RunAtLoadを指定したDaemon
  2. LoginHook
  3. 「再度開く」アプリの起動
  4. 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の内部動作とかをマジで研究してる方々から見るといろいろとツッコミどころがあるかと思いますのでご指摘いただけると嬉しいです。

参考にしたページ

  1. そもそもここでドメインってなんぞやという説明は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

  2. launchdサービスについてはman launchdman launchd.plistman launchctl等を参照してください

  3. man launchd.plistより: please note that it is the expected convention for launchd property list files to be named <Label>.plist. 2

  4. man launchctlを参照

  5. 先述した通りlaunchdサービスは基本的にon demandで起動します: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/Lifecycle.html#//apple_ref/doc/uid/10000172i-SW3-BAJJBJEG

  6. OS X 10.9(Mavericks)までは公開されてました: https://opensource.apple.com/tarballs/launchd/

  7. 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.

  8. ちなみにこの試行錯誤の過程で、guiドメイン作成前にlaunchctl asuser <uid> launchctl setenvを実行するとguiドメインではなくuserドメインに環境変数が設定されることがわかりました。なんでこういう挙動になるのかわからないので誰かご存じの方いたら教えてほしい……。

  9. LoginHookについてはAppleの公式ドキュメント等を参照(古いですが): https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CustomLogin.html

35
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?