平常運転

アニソンが好き

過去記事とかは記事一覧で見れます

Apple M1 Mac で Go を動かす

このエントリははてなエンジニア Advent Calender の15日目です(遅刻)。

qiita.com

昨日?前日?は id:ma2saka さんのAWSコストエクスプローラーAPIと気軽につきあう(2020) with Next.js でした。

qiita.com

このエントリでは、 Apple M1 搭載の Mac で Go を動かすという話をします。

今年は Mac ユーザーにとっては Apple Silicon への CPU アーキテクチャの移行が始まるというビッグイヤーになりました。先月発売された Apple M1 搭載の MacBook Air / MacBook Pro / Mac mini をワクワクしながら購入した方も多いかと思います。わたしもです。
これらの Apple M1 の Mac では、これまでの Mac 上で動いていた amd64(x86_64) 向けバイナリはネイティブに動作せず、新たに arm64 アーキテクチャ向けにビルドし直すか、あるいは macOS に組み込まれた互換レイヤーである Rosetta を介して実行する必要があります。

ところで(唐突)、我々 Web アプリケーションエンジニアとしての関心は、日々のソフトウェア開発環境を Apple M1 Mac の上に構築できるかということにあると思います。まあ結論から言うと Docker が動かないので現代はだいたいダメそうとか、まだ Homebrew も正式には arm64 をサポートしてないとかで結局ガッツリ環境を移すのはもう少し先になりそうなのですが、今日はそのうち Go (golang) に少しスポットを当ててみることにします。

この記事を書いた時点での Go の最新版は 1.15 (1.15.6) ですが、Go 1.15 系においてはまだ Go 自身が arm64 Mac でネイティブに動かないほか、arm64 Mac 向けのビルド / クロスビルドを行うこともできません。おそらく来年の2月頃?[要出展]にリリースされるであろう Go 1.16 では対応するはずなので、そこまではステイということになります。とはいえ、 arm64 Mac 向けの対応は進んできており、最新のソースコードからビルドすれば arm64 Mac で Go 自身を動かすことも、Go で書かれたプログラムを arm64 Mac 向けにビルドすることもできるようになっています。
なので、今 Apple M1 Mac の上で Go を使うには、前述の Rosetta を介して amd64 向けのバイナリを実行するか、ソースコードからビルドするかのどちらかを行えばよい、ということになります。

Rosetta で使う

最初に書いてしまうと Rosettaamd64 向けのバイナリを動かすとだいたい動きます。おつかれさまでした。 Rosetta はよくできている。。

Rosetta で Go をインストールするにはどうするのが楽かというと、 Rosetta で Homebrew をインストールするのが多分一番楽です。どうせ他のコマンドなどを入れる必要がでてきて Rosetta Homebrew からは逃げられない、というのが 2020年末時点での見解です。。
ここでは詳説しませんが、ターミナルエミュレータ自体を Rosetta で起動するのが一番手っ取り早そうです。個人的には arch コマンドをよく使います。

qiita.com

[asato@initial01 ~]$ alias x86brew
alias x86brew='arch -x86_64 /usr/local/bin/brew'

Rosetta を介して amd64(x86_64) バイナリの Go を動作させると、(他の Rosetta 経由のコマンドがそうであるように)コマンド内からは Intel Mac であるかのように見えます。なので GOARCH=amd64 になる。

[asato@initial01 ~]$ file /usr/local/bin/go
/usr/local/bin/go: Mach-O 64-bit executable x86_64
[asato@initial01 ~]$ /usr/local/bin/go env | grep GOARCH
GOARCH="amd64"
[asato@initial01 ~]$

試しに手元で Go のコードをちょっと書いたりする程度ならおそらくこれでひとまずは十分でしょう。

ソースコードからビルドする

Rosetta を介することでひとまず Go が動く環境は手に入ったわけですが、折角なのでネイティブで動かしたいところです。このエントリ序盤で触れたように、最新のソースコードを用いれば arm64 でネイティブに動く Go をビルドすることが出来ますし、それを用いることで arm64 向けのバイナリを go build できるようになります。

golang.org

ただし、ここにおいて忘れてはいけないのは、 Go のビルドには Go が必要になる、ということです*1

The Go toolchain is written in Go. To build it, you need a Go compiler installed.

https://golang.org/doc/install/source#go14

なので、Go をビルドするためにはまず Go を用意する必要があります(循環参照)。既に Go のバイナリが提供されてる環境ではそのバイナリを利用することになりますが、それがない場合はまず Bootstrap 用の Go コンパイラをビルドする必要があります。

  • そのへんの Go を使って、Go の最新のソースコードから Bootstrap 用の arm64 Go コンパイラをビルドする
    • この時ビルドした Bootstrap 用の Go 自体も多分動くには動くけど、一応これは Boostrap 用という設定っぽい
  • Apple M1 Mac において、 Bootstrap 用の arm64 Go を用いて arm64 Go (本番)をビルドする

最初の手順は一般には既に Go がインストールされてる他の環境(例えばあなたの隣にある Intel Mac)でクロスビルドしたりすることになるでしょうが、 Apple M1 Mac の場合は Rosetta によってインストールされた Go が使えて、これなら他のマシンの手を借りずにビルドすることができます。 Golang の過去の issue でも取り上げられていた手順を参考にやっていきます。

github.com

github.com

/opt/go あたりにソースコードを checkout してはじめましょう。(以後ディレクトリのパーミッションはよしなにしてください。多分一時的に chmod 777 /opt とかする必要はあるかも)
まずは Bootstrap 用の Go をビルドします。/opt/go/src/ で ./bootstrap.bash を実行すれば bootstrap 用の環境が用意されます。このとき arch コマンドで明示的に Rosetta 経由で実行しないと途中で詰まってしまってが終わらない感じがします。

$ cd /opt/go
$ cd ./src
$ arch --x86_64 env GOOS=darwin GOARCH=arm64 ./bootstrap.bash # /opt/go-darwin-arm64-bootstrap ディレクトリと、 /opt/go-darwin-arm64-bootstrap.tbz ができる
#### Copying to ../../go-darwin-arm64-bootstrap

#### Cleaning ../../go-darwin-arm64-bootstrap
Removing VERSION.cache
Removing bin/
Removing pkg/
Removing src/cmd/cgo/zdefaultcc.go
Removing src/cmd/go/internal/cfg/zdefaultcc.go
Removing src/cmd/go/internal/cfg/zosarch.go
Removing src/cmd/internal/objabi/zbootstrap.go
Removing src/go/build/zcgo.go
Removing src/runtime/internal/sys/zversion.go

#### Building ../../go-darwin-arm64-bootstrap

Building Go cmd/dist using /usr/local/Cellar/go/1.15.5/libexec. (go1.15.5 darwin/amd64)
Building Go toolchain1 using /usr/local/Cellar/go/1.15.5/libexec.
Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
Building Go toolchain2 using go_bootstrap and Go toolchain1.
Building Go toolchain3 using go_bootstrap and Go toolchain2.
Building packages and commands for host, darwin/amd64.
Building packages and commands for target, darwin/arm64.
----
Bootstrap toolchain for darwin/arm64 installed in /opt/go-darwin-arm64-bootstrap.
Building tbz.

この行程を他のマシンで行った場合は tbz を Apple M1 Mac に移して展開することになりますが、幸い我々は同じマシンなので、bootstrap 用のディレクトリをそのまま使います。この時点で /opt/go-darwin-arm64-bootstrap/bin/go に既に arm64 バイナリの Go が生成されています。

[asato@initial01 ~]$ /opt/go-darwin-arm64-bootstrap/bin/go version
go version devel +5a25a3fd1d Tue Dec 15 02:35:59 2020 +0000 darwin/arm64

後はこの bootstrap 用の Go を使って本物(?)の Go をビルドしましょう。 /opt/go/src に戻って、先ほど作った bootstrap 用の Go に PATH を通しながら ./all.bash を実行します。

$ cd /opt/go/src
$ PATH=/somewhere/go-darwin-arm64-bootstrap/bin:$PATH GOARCH=arm64 ./all.bash
Building Go cmd/dist using /opt/go-darwin-arm64-bootstrap. (devel +3298300ddf Tue Dec 15 13:59:00 2020 +0000 darwin/arm64)
Building Go toolchain1 using /opt/go-darwin-arm64-bootstrap.
Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
Building Go toolchain2 using go_bootstrap and Go toolchain1.
Building Go toolchain3 using go_bootstrap and Go toolchain2.
Building packages and commands for darwin/arm64.
(以後テストが走ります。きっと全部通る)
ALL TESTS PASSED
---
Installed Go for darwin/arm64 in /opt/go
Installed commands in /opt/go/bin
*** You need to add /opt/go/bin to your PATH.

これで見事に完成です。最後のメッセージに出てきたように /opt/go/bin に PATH を通せば、無事に arm64 ネイティブの go が得られます。よかったですね。

[asato@initial01 ~]$ file /opt/go/bin/go
/opt/go/bin/go: Mach-O 64-bit executable arm64
[asato@initial01 ~]$ which go
/opt/go/bin/go
[asato@initial01 ~]$ go env | grep GOARCH
GOARCH="arm64"
[asato@initial01 ~]$ go version
go version devel +3298300ddf Tue Dec 15 13:59:00 2020 +0000 darwin/arm64

冒頭でも触れたように、実際に Web の開発を全力でやるにはまだ環境が整っていないので限定的にしか確かめていませんが、それとなくは動いているようです。 mackerel-agent もビルドできて動いているようです、という話をこのあと 12/16 の Mackerel アドベントカレンダーで書きます(予告)。

github.com
qiita.com

よかったですね

この話を書こうと思い立った11月下旬頃はまだうまく動かない事象がいろいろあって大変だったのですが、(たとえばバイナリに Code Signing が必要なので go test がろくに動かない回 #42684 とか…)今や割と普通に動いてしまうので面白みのないエントリになってしまいました。ふつうに動くようになるまでの先人の努力に感謝ですね。この流れだとイヤミっぽくなってしまうけどそういうことはなくて本当に感謝している。

ひととおりコマンドを追試しながら書いたので再現可能な手順になっているはずだと思いますが、何か誤り等見つけた場合はご連絡いただけると幸いです。

本当はこの後 Go で書かれたアプリケーションのパフォーマンスの話をしたかったのですが、今朝 macOS 11.1 にしたら arm64 ネイティブ側の Homebrew の挙動がぶっ壊れてしまったのでタイムアップとなりました。そのうちどこかで書くかもしれません。

明日の担当は id:papix さんです。と書いてるうちに日付が変わってしまったので「今日」ですね……

おまけ: ビルド時間

折角なので bootstrap を実行するビルド時間を記録しておきました。他のアプリも起動している環境だし正確な値にはほど遠いですが、雰囲気を見る程度ということで。

MacBook Pro (16-inch, 2019)

16インチ、メモリ32GBモデルの豪腕です。株式会社はてなの業務ではこれを使用しています。

$ time GOOS=darwin GOARCH=arm64 ./bootstrap.bash
(略)
real	1m59.248s
user	5m14.465s
sys	1m5.458s

MacBook Pro (13-inch, 2016, Four Thunderbolt 3 Ports)

もう4年前のモデルになってしまった。この度 Mac mini と入れ換えで私物メインマシンの座を退きました。

$ time GOOS=darwin GOARCH=arm64 ./bootstrap.bash
(略)
real	4m52.503s
user	7m39.413s
sys	1m15.057s

流石にだいぶ見劣りしますね。

Mac mini (M1, 2020) (Rosetta)

今回の本命です。

$ arch --x86_64 env time GOOS=darwin GOARCH=arm64 ./bootstrap.bash
(略)
real	2m9.915s
user	5m39.360s
sys	0m57.954s

Rosetta 経由でも Intel MacBook Pro 16-inch と遜色ないビルド時間のようでした。もうこれでいいんじゃない?

Mac mini (M1, 2020) (Native)

ではネイティブは?というのが気になると思いますが、

$ time PATH=/opt/go/bin:$PATH GOOS=darwin GOARCH=arm64 ./bootstrap.bash
#### Copying to ../../go-darwin-arm64-bootstrap

#### Cleaning ../../go-darwin-arm64-bootstrap
Removing VERSION.cache
Removing bin/
Removing pkg/
Removing src/cmd/cgo/zdefaultcc.go
Removing src/cmd/dist/dist
Removing src/cmd/internal/objabi/zbootstrap.go

#### Building ../../go-darwin-arm64-bootstrap

Building Go cmd/dist using /opt/go. (devel +3298300ddf Tue Dec 15 13:59:00 2020 +0000 darwin/arm64)
# runtime/internal/sys
/opt/go/src/runtime/internal/sys/stubs.go:16:30: undefined: StackGuardMultiplierDefault

real	0m1.895s
user	0m0.150s
sys	0m1.670s

bootstrap のビルドに失敗してしまいました。気になるけど既にアドベントカレンダーの締切を過ぎているのでこの辺で。。

*1:現時点では Homebrew を使っても Go をソースからビルドできないのはこのためで、結局 Go をビルドするための Go のバイナリをどこかから入手してこなければいけない