5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

5
Last updated at Posted at 2024-12-14

同梱ライブラリーの正しい処理方法が判明したので記事に反映しました(2026/4/15)

今年の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)では、CFLAGSLDFLAGS-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、というかELFバイナリー5は非常に柔軟でやりやすいです。というのも、リンク時に-Lオプションで指定したライブラリーのサーチパスは、実際の実行時には参照されず、ダイナミックローダーの設定に従ってライブラリーを探す仕様になっているためです。

一方で、macOSのMach-Oバイナリーについては、基本的にライブラリーのビルド時に指定したインストール先ディレクトリーにあることが前提とされています。Linuxと同様に、環境変数やオプションでconfigureに任意のライブラリーパスを渡すことで、そこにあるライブラリーをリンクさせようとすることはできますが、場所が食い違っている場合リンク時にエラーとなってしまいます。

アプリケーションにライブラリーを同梱する場合、パッケージ作成用の仮ツリーとして通常ビルド環の下にEmacs.appを掘り、その配下にインストールしたライブラリーを参照してもらう形で、続くライブラリーや実行ファイルをビルドする必要があるため、この仕様だと困ったことになるのです。

エラーの原因とMach-Oの動的リンクの仕様

実行ファイルにせよライブラリーにせよ、リンク時に-Lオプションで指定したパスの中から、-lオプションで指定したライブラリーファイルを探すのは、Linuxでのリンカーの挙動と同じですが、この際、リンク対象のライブラリーに設定されているinstall_nameと呼ばれるメタ情報の値を参照しており、この値を実行時にライブラリーを動的リンクする際のパスとして埋め込みます。そして、実行時のみならず、リンク時にも($LIBDIRからライブラリーを探してきているにもかかわらず)install_nameからライブラリーをロードしようとするらしく、この挙動のためにリンク時に引っかかってしまうのです。

通常、Darwin向けにライブラリーをmakeする際には、$LIBDIRの値をinstall_nameとして設定するようにMakefileが作られており、これゆえ「ライブラリーをmake installした際のインストール先以外に置くとリンク時にエラーとなる」という現象が起きるのです。

以下は模式的なMakefileの断片です。ELFでの通常メジャーバージョンを含むライブラリースタブが埋め込まれるのと同様のカラクリが、Mach-Oではinstall_nameの値を使って行われるのですが、この際ライブラリーのファイル名だけでなくパスまで拘束されてしまうことが読み取れるかと思います。

libXXX.dylib: $(OBJS)
        $(CC) $(LDFLAGS) -dynamiclib -Wl,-install_name,$(LIBDIR)/libXXX.$(SOVER).dylib -o $@

共有ライブラリー作成時の対応

特に同梱ライブラリーのように、実行ファイルとの相対位置にあるライブラリーをロードしたいケースでは、このinstall_nameのパスを変更することで回避できます。具体的には、install_nameのディレクトリー名を@rpathという値にしてしまうのが一般的なやり方です。

configureで生成したMakefileの場合、内部で$LIBDIRから$libdirの値が作られていて、この値がリンク時に参照されることが多いため、ビルド時のみmakeにこの値を渡してやるとうまくいく場合があります。もちろんLIBDIR=@rpathでいけちゃう場合もありますが。

$ make libdir=@rpath

中には、ビルド直前にMakefile等を書き換える以外に方法がない場合もあります。例えば、Gnu Automake/Autoconfで生成されたMakefileでは、configure時に生成されるlibtoolスクリプトにライブラリーのリンクを任せることになりますが、こいつは$libdirの値を見てくれるにもかかわらず、これの値が絶対パスでないとエラーになるというちょっと都合の悪い仕様になっているため、この場合はもうlibtoolを書き換えてしまうのが早いです。

# libtool内で、\$rpathとして参照しているので、ここを書き換えてしまう
sed -ie '/-install_name/s/\\\$rpath/@rpath/' libtool

なお、otool -Dinstall_nameの値を参照することができます。

ビルド後にinstall_name_tool-idコマンドで書き換える方法もあるのですが、このコマンドは複数アーキテクチャー向けのユニバーサルバイナリーには対応していないため、これを使う場合、lipoで結合する前に行う必要があります。

実行ファイル作成時の対応

このようにしてライブラリー内のinstall_name@rpathの相対としてセットすると、リンク時に埋め込まれるライブラリーパスも@rpathの相対として書き込まれます。問題はこの@rpathが何者かなのですが、これは、実行ファイルのバイナリーのヘッダー部分(正確にはMach-Oヘッダーに続くロードコマンドと呼ばれる領域)にメタデータとして埋め込まれます。

こちらも、実行ファイルのリンク時に以下のようにオプションを追加することで埋め込むことができます。デフォルトでは何も設定されていないので、LDFLAGSに追加してconfigureに渡してしまえばだいたい大丈夫です。以下の例では、@rpathの値を@executable_path/libとして埋め込むようになるため、実行ファイルと同じディレクトリー階層に掘ったlibの中にライブラリーを放り込んでおけば、ビルド時も実行時もこれを参照してくれるようになります。

LDFLAGS="-arch arm64 -arch x86_64 -Wl,-rpath,@executable_path/lib" ./configure

場合によっては、実際にバイナリーが生成される場所や、同一ソースから他に生成してリンクするライブラリーがある場合はこれの格納先などに、同梱したいライブラリーのリンクを張らないとリンク時に引っかかる場合がありますが、このあたりはmakeのエラーメッセージを見ながら試行錯誤すればいいでしょう。

例えばEmacsの場合、最初にリンクして作った実行ファイル(temacs)を起動して、ELispのバイトコンパイルやダンプを行う関係で、temacsが作られるsrc以下に、libへのリンクを張らないとダンプ時にエラーが出ます。

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

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

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

@rpath@executable_path/libに設定した場合、Emacs.app/Contents/MacOS以下に実行ファイルのEmacsが格納されているので、ライブラリーはEmacs.app/Contents/MacOS/libに格納することとなります。ライブラリーのビルド時に--prefix--libdirで調整することになるでしょう。なお、lib以下を全部配置する必要はなく、実行時にはinstall_nameで確認できる名前のファイルだけがあればOKで、それ以外7は削除してしまっても構いません。

あとは、hdiutilで、

$ 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パスの変更8などを行っています。site-lispパスは、アプリとは独立したところに置いておかないと、アップデート時に潰されたりするのは困りますからね…

他にもいろいろとカスタマイズ可能になっています。詳しくはREADMEをどうぞ。 どこかにlibgccjit同梱してGccEmacsに対応させてしまう猛者とかいませんかね… とりあえずtreesitは対応追加しましたが、Emacs30はちょっと安定していないようです9。が、今後こちらに移行していくだろうから、準備は整えておきたいと思っています。

参考文献

  1. workブランチ側で最新版への追従は行われており、自前ビルド用の差分を抽出するなり、そのままチェックアウトするなりでビルドできます。また、Mac Portsでemacs-mac-app-develを指定することでもインストールできます。

  2. 独自UI側で元からスムーズスクロールに対応しているため、無効化しても問題はありませんので、(and window-system (not (eq window-system 'mac)))等の条件で有効化するようにすれば、実用上大きな支障はありません。

  3. macOSにはOpenSSLが標準で入っていますが、EmacsはOpenSSLに対応していません。

  4. makeはそのまま実行しても大丈夫でした。

  5. もしかしたら、Linuxのダイナミックリンカー(ld-linux.so)側の機能かもしれません。

  6. MacPort版では--with-macの指定になります。

  7. バージョン番号を持たない拡張子.dylibのみのファイルや、.la.aの拡張子を持つファイルはリンク時のみしか参照せず、実行時には不要な場合が多いです。

  8. デフォルトでは/Library/Application Support/Emacs/site-lispにしています。

  9. bashやMarkdown、JSONは普通に動いてますが、JSやCはハイライトが効かず、Rubyはエラーが出てしまいます。構文解析はできているようなので、おそらくはELisp側の問題で、次バージョンでは解消されるのではないかなと…

5
2
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
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?