• 作成:

Emacsのexec-path-from-shellがNixのPATHだけ引き継いでくれない問題の解決

背景

Nixパッケージマネージャが便利なので、 GentooからNixOSに移行することを考えていますが、まずhome-managerでdotfilesを管理するところからやるべきだと思っていたりするため、なまじNixパッケージマネージャだけでも仕事が出来てしまうので移行が進まない。

前からWSLのUbuntuにはNixパッケージマネージャを入れていますが、ちょっとインストールしたいソフトウェアがあったけどPortageに存在しなかったので、 GentooのLinuxデスクトップの方にもNixパッケージマネージャをインストールしました。

そうしたら以下の問題に直面しました。

問題

私はEmacsでいろいろな環境変数を設定するのに以下のパッケージを使っています。

purcell/exec-path-from-shell: Make Emacs use the $PATH set up by the user's shell

設定というよりもシェルと同期しているという言い方が正しいかもしれません。同じものを2つ管理するのはやりたくありませんからね。

これで同期されるPATHに何故かnixのものが含まれませんでした。

具体的に期待するのは~/.nix-profile/binですね。

zshで直接echo $PATHを実行するとPATHの中にNix管理の実行パスが含まれていることが確認できます。

sttyをシェルじゃないのに実行しているから警告が出ていて、警告を消すために端末でないなら早期リターンとかする必要があるのかと思いましたが、今回の問題には関係ないらしいです。

起動シェルのオプションから-iを消してインタラクティブシェルじゃなくしたら、他のPATH含めて全部が消え去ってしまいます。

公式READMEに書いてある以下の設定は関係ありませんでした。

(dolist (var '("SSH_AUTH_SOCK" "SSH_AGENT_PID" "GPG_AGENT_INFO" "LANG" "LC_CTYPE" "NIX_SSL_CERT_FILE" "NIX_PATH"))
  (add-to-list 'exec-path-from-shell-variables var))

原因

/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.shをまじまじと観察してやっとわかりました。

このスクリプトは以下のコードで環境変数を使って重複した実行を避けています。

# Only execute this file once per shell.
# This file is tested by tests/installer/default.nix.
if [ -n "${__ETC_PROFILE_NIX_SOURCED:-}" ]; then return; fi
export __ETC_PROFILE_NIX_SOURCED=1

普通はプロセスを起動する機能は子のプロセスに自分自身の環境変数を引き継がせます。 Emacsのcall-processも普通に引き継がせます。なのでexec-path-from-shellがシェルを起動した時も環境変数__ETC_PROFILE_NIX_SOURCEDを引き継ぎます。しかし実行されるのはログインシェル相当なので、 PATHなど一部の環境変数はOSの標準のものにリセットされます。そしてゼロから再構築されます。通常はそれで問題ないですが、 Nixの初期化スクリプトだけ重複実行のチェックによって実行されないため、 Nixのパスだけが欠けてしまいます。

解決

その場しのぎの対処をするとしたら__ETC_PROFILE_NIX_SOURCED環境変数に対してのみアプローチをかけるんですが、確認してないけど他にも同じ原理で環境変数で重複実行を避けているソフトウェアがある気がします。

Nixのスクリプトの重複実行チェックを修正して、実際にPATHに入っているかを確かめるのもありですが、シンプルな環境変数一つの数値チェックと違って、ちゃんと判定出来るか難しい気がしますね。

exec-path-from-shellの存在意義を考えると、 zshが起動する時に親の環境変数情報を渡さないという方針が正しいような気がします。そうするとログインシェルが起動するのに似た起動の仕方をするので、他のズレが発生する可能性も減るでしょう。

よってenv --ignore-environmentを使って環境変数を吹き飛ばす方法と、 process-environmentを実行時に消去する方法が考えられます。

:inherit-environmentを使う方法はEmacs v27以降らしいので、 exec-path-from-shellはEmacs v24までの依存関係なのでだめですね。

どちらでも動作するはずですが、 Emacs Lispのピュアの機能で十分無理せずに実装できるものは、わざわざ外部コマンドに依存する必要はないでしょう。シェルがある環境でenvコマンドが存在しないということはないでしょうけど。

なので以下の変更を加えました。

1 file changed, 22 insertions(+), 2 deletions(-)
exec-path-from-shell.el | 24 ++++++++++++++++++++++--

modified   exec-path-from-shell.el
@@ -157,6 +157,26 @@ The limit is given by `exec-path-from-shell-warn-duration-millis'."
                (message "Warning: exec-path-from-shell execution took %dms. See the README for tips on reducing this." ,duration-millis)
              (exec-path-from-shell--debug "Shell execution took %dms" ,duration-millis)))))))

+(defun exec-path-from-shell--call-process-with-clean-env (program &optional infile destination display &rest args)
+  "Call PROGRAM with a completely clean environment.
+
+This function is a thin wrapper around `call-process'.
+It binds `process-environment' to an empty list so that no inherited
+environment variables from Emacs (e.g. PATH, LANG)
+ are passed to the subprocess.
+
+Arguments are the same as for `call-process':
+
+  PROGRAM     — the executable to run (string)
+  INFILE      — nil, t, or a file name for standard input
+  DESTINATION — nil, t, or a buffer/file for standard output
+  DISPLAY     — if non-nil, redisplay buffer as output is inserted
+  ARGS        — additional arguments passed to PROGRAM
+
+Returns the exit code of the called process."
+  (let ((process-environment nil))
+    (apply #'call-process program infile destination display args)))
+
 (defun exec-path-from-shell-printf (str &optional args)
   "Return the result of printing STR in the user's shell.

@@ -183,7 +203,7 @@ shell-escaped, so they may contain $ etc."
     (with-temp-buffer
       (exec-path-from-shell--debug "Invoking shell %s with args %S" shell shell-args)
       (let ((exit-code (exec-path-from-shell--warn-duration
-                        (apply #'call-process shell nil t nil shell-args))))
+                        (apply #'exec-path-from-shell--call-process-with-clean-env shell nil t nil shell-args))))
         (exec-path-from-shell--debug "Shell printed: %S" (buffer-string))
         (unless (zerop exit-code)
           (error "Non-zero exit code from shell %s invoked with args %S.  Output was:\n%S"
@@ -209,7 +229,7 @@ The result is a list of (NAME . VALUE) pairs."
     (with-temp-buffer
       (exec-path-from-shell--debug "Invoking shell %s with args %S" shell shell-args)
       (let ((exit-code (exec-path-from-shell--warn-duration
-                        (apply #'call-process shell nil t nil shell-args))))
+                        (apply #'exec-path-from-shell--call-process-with-clean-env shell nil t nil shell-args))))
         (exec-path-from-shell--debug "Shell printed: %S" (buffer-string))
         (unless (zerop exit-code)
           (error "Non-zero exit code from shell %s invoked with args %S.  Output was:\n%S"

PRにして提出しました。

fix: run shell with clean environment by ncaq · Pull Request #123 · purcell/exec-path-from-shell