今年のEmacs Advent Calendarの出だしは
Emacsどうやってインストールしていますか?
でした。
私はWindows環境では公式のZIPを展開してtr-imeだけ適用したものを使用、いっぽうでmacOS環境では.dmgを自前でビルドしてそれを入れております。
というわけで今回は.dmgを作るお話をしたいと思います。
なぜわざわざ.dmgを作るのか
macOSであれば、brew install
なりport install
なりすれば色々選択肢があるご時世でわざわざ自前ビルド(しかも.dmgで)している理由は主に2つあります。
理由1 - 欲しい仕様のものがない
上記の@tadsanさんの記事にもあるとおり、macOS環境で簡単にインストールできるGUI版のEmacsは事実上3種類あります。
MacPort版Emacs
Metal対応の独自UIを実装されていて、IMEイベントにも対応している等、非常にいい感じなのですが、メンテナーの方がご多忙のようで、29.1の後の正式リリースがなされていません1。
現状、Emacs29で導入されたpixel-scroll-precision-modeとの食い合わせがよくないため、無効化が必要なのですが2、無効化した状態でも、fringe領域にビットマップを置いた場合にビットマップの途中でスクロールを止められず、うまく操作できない現象が確認されており、例えばこれを活用したgit-gutter-fringe.el等との相性が悪いという状況です。
Emacs Plus
今年のEmacs Advent Calendarの初日から推されているEmacs Plusですが、標準のUI実装であるため上記問題が起きないのはいいとして、IMEパッチが統合されていません。もちろん、単にインライン入力を行うだけであればもはやパッチは不要なのですが、個人的には入力モードに応じてカーソルの形状を変えたいので、イベントは欲しいのです。
Cask版Emacs
こちらはユニバーサルバイナリーの.dmgパッケージとしても提供されています。完全無添加なのが売りですが、私はIMEパッチは欲しいです。
MacPort版のスムーズスクロール周りが修正されればそれでよさそうな気もしますが、現状ではEmacs PlusにIMEパッチを追加した状態が個人的にはベターかなと考えています。それならば自前でビルドしましょうか、となるわけです。
理由2 - 布教のため
.dmgを作ろうと思った理由は、単に技術的興味やパッケージ化することによる管理の容易さもあるのですが、大きな要因として布教というお題目があります。
自前でパッチを集めてお好みのビルドに仕上げたものを.dmg化しておくことで、このご時世にEmacs使ってみたいと宣う奇特な方が現れたときに、これをポンと提供することができるわけです。その人が、Homebrewを必ずしも導入していることは期待しないほうがいいと思っていますので。
以上の理由により、
- 必要なライブラリーを同梱し、.dmgだけで動作するように調整し
- 必要なパッチを当てて、お好みのビルドオプションでビルドし
- まだまだIntel環境も残ってるだろうからユニバーサルバイナリーを作る
というのをゴールとしました。
そうそう、自分でビルドすれば、というよりはおそらく.dmg化してインストールすれば、Sequoiaにうっかりアップグレードしていても問題ありませんw
ユニバーサルバイナリーの作り方
なんかもう全然Emacsじゃなくなっていますが…
一般的に、Xcodeで指定してビルドすればそうなるようになっていますが、ここではAutoconf+makeの世界になるので、その方法は使えません。
Xcodeのコマンドラインツール(clang)では、CFLAGS
とLDFLAGS
に-arch
オプションを渡すことでクロスコンパイルが可能です。結論から言ってしまうと、ここで-arch
を2つ渡すことでユニバーサルバイナリーになります。つまり、
$ CFLAGS='-arch arm64 -arch x86_64' LDFLAGS='-arch arm64 -arch x86_64' ./configure
とすれば終わりです。終わりのはずでした…
複数アーキテクチャーでのビルドができないケース
macOSが標準で持っていないライブラリーのうち、Emacsにぜひともリンクしておきたいものの筆頭格といえばやはりGnuTLSかと思います3。GnuTLSが要求するNettleも必要です。
が、こいつらはどちらも-arch
を2つ付けてビルドすると止まってしまいます。
$ 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
コマンドでユニバーサルバイナリーにまとめる必要があります。
$ 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。
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
を掘って、本来のインストール先からシンボリックリンクを張る」方法しかないかなと思っています。
# 実際はエラーリカバリーもきちんとしたほうがいい
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ができます。今回は同梱ライブラリー群があるので、これらとインストールツリーをマージしないといけませんが。
$ tar cs - -C nextstep/Emacs.app/Contents . | tar xpf - -C path/to/pkgroot/Applications/Emacs.app/Contents
あとは、hdiutilsで、
$ 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
のように固めれば出来上がりです。
以上を…
スクリプト化してまとめております。よろしければどうぞ。オリジナルパッチを追加するのも簡単だと思います。
上記でやったこと以外にも、MacPort版で公開しているアイコンへの差し替えや、site-lispパスの変更6などを行っています。site-lispパスは、アプリとは独立したところに置いておかないと、アップデート時に潰されたりするのは困りますからね…
他にもいろいろとカスタマイズ可能になっています。詳しくはREADMEをどうぞ。 どこかにlibgccjit同梱してGccEmacsに対応させてしまう猛者とかいませんかね…
-
workブランチ側で最新版への追従は行われており、自前ビルド用の差分を抽出するなり、そのままチェックアウトするなりでビルドできます。また、Mac Portsでemacs-mac-app-develを指定することでもインストールできます。 ↩
-
独自UI側で元からスムーズスクロールに対応しているため、無効化しても問題はありませんので、
(and window-system (not (eq window-system 'mac)))
等の条件で有効化するようにすれば、実用上大きな支障はありません。 ↩ -
macOSにはOpenSSLが標準で入っていますが、EmacsはOpenSSLに対応していません。 ↩
-
makeはそのまま実行しても大丈夫でした。 ↩
-
MacPort版では
--with-mac
の指定になります。 ↩ -
デフォルトでは
/Library/Application Support/Emacs/site-lisp
にしています。 ↩