2022-04-11

Windows上のEmacs 28.1でネイティブコンパイルする方法(まとめ)

(2024-02-19追記: 手っ取り早く使い方を知りたい人はMS-Windows版 Emacs 29.1への移行作業をご覧下さい)

前回の続き。

前提

  • MSYS2のMinGW64環境下でGCCが使える状況になっていること。
  • MSYS2にlibgccjitパッケージが入っていること。
  • 公式で配布しているWindowsバイナリ emacs-28.1.zip を使用すること。

MSYS2のセットアップについては割愛します。Emacsはlibgccjitを使用してネイティブコンパイルを行うため、それに関連したファイル群をMSYS2から入手する必要があります。おそらく base-devel, mingw-w64-x86_64-toolchain, mingw-w64-x86_64-libgccjit あたりのパッケージが入っていれば良いのだと思います。

ただし実際にEmacsを使用するときにはMSYS2全体は必ずしも必要なく、少数のファイルだけコピーして他のPCに移すことも出来ます。

ここでは公式配布の emacs-28.1.zip を前提に説明しますが、自分で --with-native-compilation を指定してビルドしたEmacsを他の環境へ移す場合にも同じ考え方が適用出来ると思います。ちなみに emacs-28.1.zip は --with-native-compilation を指定してビルドされていますが(C-h v system-configuration-optionsで確認できる)ネイティブコンパイルに必要なDLL等が含まれていない状態です。

方法1:Emacs起動前からmingw64/binへPATHを通す

Emacsが起動する前に、環境変数PATHにMSYS2内の mingw64/bin ディレクトリが含まれていればネイティブコンパイルは正常に動作します(もちろんlibgccjitパッケージがインストールされていること)。

Windowsの「システムの詳細設定」で環境変数を変更するか、次のようなbatファイルを経由するといった方法が考えられます。

set PATH=c:/hogehoge/msys/mingw64/bin;%PATH%
c:/hogehoge/emacs-28.1/bin/runemacs.exe

mingw64/bin/には libgccjit-0.dll, as.exe, ld.exe が含まれている必要があります。また、 mingw64/lib/以下には必要なライブラリが含まれている必要があります。

ここで大事なのは「Emacsの起動前から」という点です。

環境変数を変更するには early-init.el や init.el 内でsetenv関数を呼び出す方法もありますが、それだと起動後すぐのネイティブコンパイルには適用されません。一番最初のネイティブコンパイルは early-init.el よりも前に起動する場合があります。なので early-init.el で (setenv "PATH" ~) しても手遅れな場合があります。

この方法は環境変数PATHを常時変更するため、人によっては許容できない場合があります。例えばCygwinやその他GNUツールを含むプロダクトに既にPATHが通っていてEmacs使用中にそのコマンドを使いたい場合です。

方法2:必要なファイルをEmacsにコピーして必要なときだけPATHを通す

alpha.gnu.org has shiny new Emacs 28.0.91 Windows binaries : emacs のコメントによれば、ネイティブコンパイルに必要なファイルは次の17個だそうです。

  • binに入れるもの
    • libgccjit-0.dll
    • (追記: 2022-09-23)libisl-23.dll (←最新のlibgccjitが必要とするdll)
    • (追記: 2023-07-31)Emacs29.1が出たので入れ直しましたが、libmpc-3.dllとlibmpfr-6.dllも増えていました(MS-Windows版 Emacs 29.1へ移行)
  • lib/gccに入れるもの
    • crtbegin.o
    • crtend.o
    • dllcrt2.o
    • libadvapi32.a
    • libgcc.a
    • libgcc_s.a
    • libkernel32.a
    • libmingw32.a
    • libmingwex.a
    • libmoldname.a
    • libmsvcrt.a
    • libpthread.a
    • libshell32.a
    • libuser32.a
    • ld.exe
    • as.exe (ldとasはリンク先では libexec/emacs/28.0.91/x86_64-w64-mingw32/ とありますが、libexec/emacs/28.1/x86_64-w64-mingw32に置いても認識されず、色々試したところlib/gccに置いたら認識されました)

これらをemacs-28.1.zipを展開して出来たディレクトリの適切なディレクトリへコピーするとネイティブコンパイルが条件付きで動作するようになります。libgccjit-0.dll は bin (既にrunemacs.exe等があるディレクトリ)へ、それ以外は lib (既にemacs/やsystemd/がある)の下にgccというディレクトリを作成してその中へ入れてください。

これだけである程度の割合でネイティブコンパイルが成功するようになるのですが、自動で起動する非同期コンパイルがなぜか失敗する場合があります(カレントディレクトリがbinやlib等の階層に無い場合にas.exeが無いと言われます)。

そこで early-init.el (あるいはinit.el)に次のコードを追加します。

(2022-09-23追記: Windows版Emacsを28.1に上げたのでNative Compilationフィーバーに便乗する - Qiitaにあるように native-comp-driver-options 変数に -B オプションを指定した方が良さそうです。私はearly-init.elで (setq native-comp-driver-options (list "-B" (expand-file-name (file-name-concat invocation-directory "../lib/gcc")) )) のようにしました)

(when (and (fboundp #'native-comp-available-p) ;;emacs-28以降
           (native-comp-available-p) ;;libgccjitが使える
           (eq system-type 'windows-nt)) ;;Windowsの場合 (他必要に応じて条件を追加すること)
  (defun my-comp-wrap-process-call (orig-fun &rest args)
    (let* (;; emacs.exeのあるディレクトリの一つ上のlib/gcc
           (lib-dir (expand-file-name
                     (file-name-concat invocation-directory "../lib/gcc")))
           ;; 環境変数PATHとLIBRARY_PATHを一時的に変更
           (process-environment
            (append
             (list (concat "PATH=" lib-dir ";" (getenv "PATH"))
                   (concat "LIBRARY_PATH=" lib-dir))
             process-environment)))
      ;; 元の関数を呼び出す
      (apply orig-fun args)))

  ;; コンパイル用にemacsを起動する関数をラップする
  (advice-add #'comp-final :around #'my-comp-wrap-process-call)
  (advice-add #'comp-run-async-workers :around #'my-comp-wrap-process-call))

このコードはネイティブコンパイルのために別プロセスでemacsを起動するときにだけ環境変数PATHとLIBRARY_PATHを変更します。起動されたemacsは最初からネイティブコンパイルに必要なコマンドにPATHが通った状態になります。

つまり、early-init.elより前に起動するネイティブコンパイルについてはlib/gccに置いたas.exeやld.exeが使われることによって解決し、それ以降なぜかエラーになるケースについては環境変数の一時的な変更で解決します。

ちなみに前者はあらかじめ必要なファイルをネイティブコンパイルしておくことによって回避可能です。エラーが出るファイルを手動でネイティブコンパイルしたり、試してはいませんが NATIVE_FULL_AOT=1 でビルドすると回避できるかもしれません。

なお、既にCygwin等にPATHが通っていると、設定が間違っていてもCygwinのas.exeやld.exeが起動する場合があるので注意が必要です。

なぜか失敗するケースは、調べた限りコンパイル時のカレントディレクトリによるようなので次のようなコードでも良いかもしれません。ただ、ネイティブコンパイルがカレントディレクトリに依存している(または将来するようになる)とまずいかもしれません。

(when (and (fboundp #'native-comp-available-p) ;;emacs-28以降
           (native-comp-available-p) ;;libgccjitが使える
           (eq system-type 'windows-nt)) ;;Windowsの場合 (他必要に応じて条件を追加すること)
  (defun my-comp-wrap-process-call (orig-fun &rest args)
    ;; 一時的にカレントディレクトリを emacs-28.1/bin にする
    ;; でないと emacs-28.1/lib/gcc/as.exe を見つけてくれない
    (let ((default-directory invocation-directory))
      ;; 元の関数を呼び出す
      (apply orig-fun args)))

  ;; コンパイル用にemacsを起動する関数をラップする
  (advice-add #'comp-final :around #'my-comp-wrap-process-call)
  (advice-add #'comp-run-async-workers :around #'my-comp-wrap-process-call))

方法3:comp.elを修正する

最も単純な方法は comp.el を修正することだと思います。

前述の通りネイティブコンパイルはearly-init.elよりも前に起動する場合があり、comp.elもその時にロードされます。

従ってearly-init.elやinit.elで挙動を完全に修正するのは無理なので、comp.elを直接書き替えてしまった方が素直な方法となるでしょう。(Emacsの初期化プロセスについて詳しくないので他に何か方法があったらすみません)

具体的には、前と同じようなことをcomp.elの末尾に書き加えてやれば良いでしょう。例えば:

;; share/emacs/28.1/lisp/emacs-lisp/comp.el の末尾、(provide 'comp)の前に以下を追加
(defconst my-comp-tool-path "c:/hogehoge/msys/mingw64/bin") ;;自分のmingw64/binの場所

(defun my-comp-wrap-process-call (orig-fun &rest args)
  (let* ((process-environment
          (cons
           (concat "PATH=" my-comp-tool-path ";" (getenv "PATH"))
           process-environment)))
    ;; 元の関数を呼び出す
    (apply orig-fun args)))

;; コンパイル用にemacsを起動する関数をラップする
(advice-add #'comp-final :around #'my-comp-wrap-process-call)
(advice-add #'comp-run-async-workers :around #'my-comp-wrap-process-call)

my-comp-tool-pathにはmingw64/binの場所を指定してください。もしくは前にやったようにemacs.exeの位置から相対的に割り出しても良いでしょう(関連ファイルのコピーが必要になりますが)。

ファイル(comp.el)の途中を書き替えるのも分かりづらいかなと思ったのでadviceのままにしてあります。make-processやcall-processの前後を直接書き替えても良いでしょう。

comp.elを書き替えたら対応する.elcや.elnを削除するのもお忘れ無く。

(追記:2022-09-23)Emacs 28.2でNative Compileする

Emacs 28.2がリリースされてWindows版の公式ビルドも公開されました。

Index of /gnu/emacs/windows/emacs-28

emacs-28.2.zipを展開して、MSYS2のlibgccjit-0.dllとlibisl-23.dllをemacs-28.2/binへ、その他のライブラリやらas.exe、ld.exeやらをemacs-28.2/lib/gcc/へコピーし、early-init.elで (setq native-comp-driver-options (list "-B" (expand-file-name (file-name-concat invocation-directory "../lib/gcc")) )) と指定しただけで問題なくNative Compileできるようになりました。

ちなみに、最初MSYS2最新のlibgccjit-0.dllをbinに入れてもNative Compileが有効になりませんでした((native-comp-available-p)がnil)。

それをTwitterに書いたところ、libisl-23.dllも必要になったとの情報を頂きました。

調べてみると確かに最新のlibgccjit-0.dll内にはlibisl-23.dllという文字列があります。libgccjitのバージョンアップに伴い依存するdllが増えたようです。libisl-23.dllもemacs-28.2/binへコピーしたところnative-comp-available-pがtになりました。

こういうことがあるからバイナリ配布するならlibgccjit(や関連ファイル)も一緒に配布してほしいものです。

(追記:2023-07-31)Emacs 29.1

MS-Windows版 Emacs 29.1へ移行に書きました。dllが二つ増えていただけです。