macOS向けにユニバーサルバイナリーのEmacsを自前ビルドする話

今年のEmacs Advent Calendarの出だしは

::: note info
Emacsどうやってインストールしていますか？
:::

でした。

https://qiita.com/tadsan/items/ca3494c4827334de25f3

私はWindows環境では公式のZIPを展開して[tr-ime](https://github.com/trueroad/tr-emacs-ime-module)だけ適用したものを使用、いっぽうでmacOS環境では.dmgを自前でビルドしてそれを入れております。

というわけで今回は.dmgを作るお話をしたいと思います。

なぜわざわざ.dmgを作るのか
--------------------------

macOSであれば、`brew install`なり`port install`なりすれば色々選択肢があるご時世でわざわざ自前ビルド(しかも.dmgで)している理由は主に2つあります。

### 理由1 - 欲しい仕様のものがない

上記の@tadsanさんの記事にもあるとおり、macOS環境で簡単にインストールできるGUI版のEmacsは事実上3種類あります。

#### [MacPort版Emacs](https://bitbucket.org/mituharu/emacs-mac/src/master/)

Metal対応の独自UIを実装されていて、IMEイベントにも対応している等、非常にいい感じなのですが、メンテナーの方がご多忙のようで、29.1の後の正式リリースがなされていません[^1]。

現状、Emacs29で導入されたpixel-scroll-precision-modeとの食い合わせがよくないため、無効化が必要なのですが[^2]、無効化した状態でも、fringe領域にビットマップを置いた場合にビットマップの途中でスクロールを止められず、うまく操作できない現象が確認されており、例えばこれを活用したgit-gutter-fringe.el等との相性が悪いという状況です。

#### [Emacs Plus](https://github.com/d12frosted/homebrew-emacs-plus)

今年のEmacs Advent Calendarの初日から推されているEmacs Plusですが、標準のUI実装であるため上記問題が起きないのはいいとして、IMEパッチが統合されていません。もちろん、単にインライン入力を行うだけであればもはやパッチは不要なのですが、個人的には入力モードに応じてカーソルの形状を変えたいので、イベントは欲しいのです。

#### [Cask版Emacs](https://emacsformacosx.com/)

こちらはユニバーサルバイナリーの.dmgパッケージとしても提供されています。完全無添加なのが売りですが、私はIMEパッチは欲しいです。

MacPort版のスムーズスクロール周りが修正されればそれでよさそうな気もしますが、現状ではEmacs PlusにIMEパッチを追加した状態が個人的にはベターかなと考えています。それならば自前でビルドしましょうか、となるわけです。

### 理由2 - 布教のため

.dmgを作ろうと思った理由は、単に技術的興味やパッケージ化することによる管理の容易さもあるのですが、大きな要因として布教というお題目があります。

自前でパッチを集めてお好みのビルドに仕上げたものを.dmg化しておくことで、このご時世にEmacs使ってみたいと宣う奇特な方が現れたときに、これをポンと提供することができるわけです。その人が、Homebrewを必ずしも導入していることは期待しないほうがいいと思っていますので。

以上の理由により、

* 必要なライブラリーを同梱し、.dmgだけで動作するように調整し
* 必要なパッチを当てて、お好みのビルドオプションでビルドし
* まだまだIntel環境も残ってるだろうからユニバーサルバイナリーを作る

というのをゴールとしました。

そうそう、自分でビルドすれば、というよりはおそらく.dmg化してインストールすれば、Sequoiaにうっかりアップグレードしていても問題ありませんｗ

ユニバーサルバイナリーの作り方
------------------------------

~~なんかもう全然Emacsじゃなくなっていますが…~~

一般的に、Xcodeで指定してビルドすればそうなるようになっていますが、ここではAutoconf+makeの世界になるので、その方法は使えません。

Xcodeのコマンドラインツール(clang)では、`CFLAGS`と`LDFLAGS`に`-arch`オプションを渡すことでクロスコンパイルが可能です。結論から言ってしまうと、ここで`-arch`を2つ渡すことでユニバーサルバイナリーになります。つまり、

```console
$ CFLAGS='-arch arm64 -arch x86_64' LDFLAGS='-arch arm64 -arch x86_64' ./configure
```

とすれば終わりです。終わりのはずでした…

### 複数アーキテクチャーでのビルドができないケース

macOSが標準で持っていないライブラリーのうち、Emacsにぜひともリンクしておきたいものの筆頭格といえばやはりGnuTLSかと思います[^3]。GnuTLSが要求するNettleも必要です。

が、こいつらはどちらも`-arch`を2つ付けてビルドすると止まってしまいます。

```console
$ make
  :
/usr/bin/m4 ./m4-utils.m4 ./asm.m4 config.m4 machine.m4 chacha-core-internal.asm >chacha-core-internal.s
gcc -I.  -DHAVE_CONFIG_H -arch arm64 -arch x86_64 -ggdb3 -Wall -W -Wno-sign-compare   -Wmissing-prototypes -Wmissing-declarations -Wstrict-prototypes   -Wpointer-arith -Wbad-function-cast -Wnested-externs -fPIC -MT chacha-core-internal.o -MD -MP -MF chacha-core-internal.o.d  -c chacha-core-internal.s
chacha-core-internal.s:77:2: error: invalid instruction mnemonic 'adr'
 adr x3, .Lrot24
 ^~~
  :
```

なんと**アセンブラのコードが含まれている**ため、正しくビルドできない模様。これは仕方ないですね…

このような場合は、各アーキテクチャーでビルドし、`lipo`コマンドでユニバーサルバイナリーにまとめる必要があります。

```console
$ lipo -create arm64/hogehoge.dylib x86_64/hogehoge.dylib -output universal/hogehoge.dylib
```

`lipo`コマンドは2つのインプットを別のファイルに書き出す構造なので、別々にビルドしてmake installし、どちらか片方はさらに別のパスにもmake installした状態で、findとfileでバイナリーファイルだけを抽出してlipoを実行する、という方法を取ると定型的に処理できます。configureで、`--prefix`にはEmacs.appの下のリソースパスを指定します。また、正しいアセンブラコードを選択するには、configureそのものもターゲットのアーキテクチャー上で走らせる必要があります[^4]。

```bash
CFLAGS="-arch arm64" LDFLAGS="-arch arm64" arch -arm64 ./configure --prefix=/Applications/Emacs.app/Contents/MacOS
make -j4
DESTDIR=path/to/pkgroot make install
DESTDIR=path/to/pkgroot_arm64 make install
# distcleanではダメな場合がある(その場合はソース再展開)
make distclean

CFLAGS="-arch x86_64" LDFLAGS="-arch x86_64" arch -x86_64 ./configure --prefix=/Applications/Emacs.app/Contents/MacOS
make -j4
DESTDIR=path/to/pkgroot_x86_64 make install

cd path/to/pkgroot
BINARY_FILES=()
for file in `find . -type f -print`; do
  case `file -b --mime-type $file` in
  application/*binary*)
    BINARY_FILES+=(${file#./})
    ;;
  esac
done

for file in ${BINARY_FILES[@]}; do
  lipo -create path/to/pkgroot_arm64/$file path/to/pkgroot_x86_64/$file -output path/to/pkgroot/$file
done
```

同梱ライブラリーをリンクする際の問題
------------------------------------

共有ライブラリーのリンクの仕方はOSによって色々癖がありますが、例えばLinux、というかGnu Libtoolはこのへん非常に柔軟でやりやすいです。というのも、リンク時に`-L`オプションで指定したライブラリーのサーチパスは、実際の実行時には参照されず、ダイナミックローダーの設定に従ってライブラリーを探す仕様になっているためです。

一方で、macOSについては、基本的にライブラリーのビルド時に指定したインストール先ディレクトリーにあることが前提とされています。configureに環境変数で渡すことで、別の場所にあっても認識はできますが、リンク時にエラーとなります。

いい解決方法があればいいのですが、今のところ、「一旦`/Applications/Emacs.app`を掘って、本来のインストール先からシンボリックリンクを張る」方法しかないかなと思っています。

```bash
# 実際はエラーリカバリーもきちんとしたほうがいい
LIBDIR=/Applications/Emacs.app/Contents/MacOS/lib
if [ -d $LIBDIR ]; then
  # インストール済なので退避する
  mv $LIBDIR ${LIBDIR}_
  # ビルドしているライブラリーパスにリンクを張る
  ln -s path/to/pkgroot$LIBDIR $LIBDIR
  # ライブラリーを参照するものをビルド
  make
  # 戻す
  rm -f $LIBDIR
  mv ${LIBDIR}_ $LIBDIR
else
  # インストールしていないので仮にアプリケーションディレクトリーを掘る
  mkdir -p /Applications/Emacs.app/Contents/MacOS
  # ビルドしているライブラリーパスにリンクを張る
  ln -s path/to/pkgroot$LIBDIR $LIBDIR
  # ライブラリーを参照するものをビルド
  make
  # 戻す
  rm -f $LIBDIR
  rmdir /Applications/Emacs.app/Contents/MacOS
  rmdir /Applications/Emacs.app/Contents
  rmdir /Applications/Emacs.app
fi
```

ビルドできたら.dmgにする
------------------------

幸い、Emacsはconfigureのオプションに`--with-ns`[^5]を指定して、make→make installすればEmacs.appができます。今回は同梱ライブラリー群があるので、これらとインストールツリーをマージしないといけませんが。

```console
$ tar cs - -C nextstep/Emacs.app/Contents . | tar xpf - -C path/to/pkgroot/Applications/Emacs.app/Contents
```

あとは、hdiutilで、

```console
$ ln -s /Applications path/to/pkgroot/Applications
$ hdiutil create -ov -srcfolder path/to/pkgroot/Applications -fs HFS+ -format UDBZ -volname Emacs path/to/pkgroot/Emacs-xx.y.dmg
```

のように固めれば出来上がりです。

以上を…
--------

スクリプト化してまとめております。よろしければどうぞ。オリジナルパッチを追加するのも簡単だと思います。

https://github.com/jado4810/emacs-dmg-build

上記でやったこと以外にも、MacPort版で公開しているアイコンへの差し替えや、site-lispパスの変更[^6]などを行っています。site-lispパスは、アプリとは独立したところに置いておかないと、アップデート時に潰されたりするのは困りますからね…

他にもいろいろとカスタマイズ可能になっています。詳しくはREADMEをどうぞ。 ~~どこかにlibgccjit同梱してGccEmacsに対応させてしまう猛者とかいませんかね…~~

[^1]:workブランチ側で最新版への追従は行われており、自前ビルド用の差分を抽出するなり、そのままチェックアウトするなりでビルドできます。また、Mac Portsで[emacs-mac-app-devel](https://ports.macports.org/port/emacs-mac-app-devel/)を指定することでもインストールできます。
[^2]:独自UI側で元からスムーズスクロールに対応しているため、無効化しても問題はありませんので、`(and window-system (not (eq window-system 'mac)))`等の条件で有効化するようにすれば、実用上大きな支障はありません。
[^3]:macOSにはOpenSSLが標準で入っていますが、EmacsはOpenSSLに対応していません。
[^4]:makeはそのまま実行しても大丈夫でした。
[^5]:MacPort版では`--with-mac`の指定になります。
[^6]:デフォルトでは`/Library/Application Support/Emacs/site-lisp`にしています。