M1 MacでのシンプルなHomebrew管理

2021/03/10に公開
4

はじめに

:::message note
この記事は2021年3月10日に執筆したものです。
情報が古くなっているケースにご注意ください。
改善点・間違いの指摘と言ったご意見もお待ちしております。
:::

本記事はRosettaを使用する・しない二つの環境のHomebrew管理に関する私なりの解答になっています。今後行う説明の実行環境は以下の通りです。

  • Hardware: Mac mini(M1)
  • MacOS: Big Sur v11.2.2
  • iTerm2: v3.4.4
  • Homebrew: v3.0.5

執筆者はできる限りApple Silicon対応ソフトを使用して、対応していない・バグがあるといった場合にRosettaに逃げる形式を想定しています。

本文は一部知識・実装は以下の記事を参考にさせていただいています。
この記事の方の実装をベースにデフォルトのHomebrewの仕組みに乗っ取りshellenvを活用する形式で管理していきます。
https://zenn.dev/ress/articles/069baf1c305523dfca3d

(2021/3/13追記)
参考元の方との差別点は二つのbrewで同名パッケージを許容するか許容しないかです。
また、細かい部分ですと、私の方はARM側のアーキテクチャ環境では絶対にARMのBrewしか呼べない形式をとっています。参考元の記事の手法も解答の一つだと思うので、読者の方の好みで参考にする記事を決めていただければと思います。

想定している読者

今回はターミナルのデフォルトのシェルであるzshを使った説明を行いますが、bash等でも同様のことができると思います。

  • M1 Mac(Apple Silicon) を使う方
  • zsh を使う方
  • 必要に応じて Rosetta を使う方

モチベーション

手順だけ知りたい方はこの項目を飛ばしてもらっても大丈夫です。

二つのアーキテクチャの存在・混在

Apple SiliconはAppleの発表した最新のアーキテクチャで、以下の長所・短所があります。

  • 長所: 高速・低消費電力である
  • 短所: 対応していないアプリケーションがある

私自身、M1 Macを触ってその長所は感じていますが、短所は開発の大きな問題になりえます。実際Apple Siliconに対応していないアプリケーションはたくさんあります。
その問題の対処としてはRosettaというエミュレートソフトを使うことになります。
Rosetta を使えば、M1 Macであっても、Intelプロセッサ搭載 Mac用に開発された アプリケーション(バイナリ) を使えるようになります。
なのでユーザーはM1 Macに対応していないアプリにぶつかった時はRosettaを使うことで、M1 Macより前の機種で使えたアプリも使えるようになります。

M1 Macの美味しい活用としては、基本はApple Siliconに対応しているアプリケーションをできる限り活用して、対応していない場合はRosettaを活用する形になると思います。極端な話にはなりますがユニバーサルな環境が良い!と考えてずっとRosettaを使うのであればM1 Macじゃなくていいと思います。

つまり現状のM1 Macユーザーは、あらゆるアプリがM1 Macに対応するまでのしばらくの間、二つのアーキテクチャ環境(今回はarm64eとx86_64)を使い分ける必要がある、ということです。ちなみにarm64eがApple Siliconの環境に対応しています。長いので今後はARMと呼びます。

開発環境ぐらいはx86_64で統一しちゃう方が楽、と言う方も少なからずいらっしゃると思います。ただ今回はあくまで将来的にARM環境をベースに置く思想のもとで話をします。また、最悪のケースとしてRosetta側しか使わない方でもほとんど環境を汚染しない手法になっているのでぜひ目を通してもらえればと思います。

Homebrewの対応

この必要に応じてx86_64を使うとなった時にパッケージマネージャーであるHomebrewの扱いが問題になります。Homebrewは最近ARMへのインストールに正式に対応しましたが、Homebrewでインストールできるパッケージが全てARMに対応しているわけではありません。

公式ではARM用のHomebrewとRosettaでエミュレートしてるアーキテクチャ環境のHomebrewでインストール場所が分かれるようになっています。あくまで一時的な対応の結果かもしれませんが、公式でもアーキテクチャで使い分けを推奨していると行っても過言ではありません。

なのでARM環境でのHomebrewではARMに対応したパッケージだけをインストールして、Rosetta環境でのHomebrewではARMに対応していないようなパッケージ等をインストールし、使い分ける形が良いと考えています。

その対処法として複雑にbrewのパスそのものをいじくり回す記事もちらほら見かけましたが、brew周りのPATHを直接触るのは以下のようなバグの原因になります。

  • Rosetta環境なのにARM環境用のHomebrewを参照しちゃう
  • brew コマンドだけはARMの方を参照しているが、brewでインストールしたパッケージ群はRosetta側の方を見てるせいでパッケージがnot foundになる

今回はほぼ既存のbrewのデフォルトの仕組みに乗っ取った手法をとることで、直接パスを変更する複雑さ等を回避します。具体的にはshellenvを活用することで今回はこの二つのHomebrew管理をログイン時に切り替えることで、できる限り単純に・快適に実現していきたいと思います。
シンプルイズベスト!

前提

  • それぞれのアーキテクチャ環境でHomebrewがインストールされている
    (それぞれのアーキテクチャ環境でbrewのインストールコマンドを叩くだけでいいです)
  • Rosetta環境でターミナルを立ち上げることができる
  • ターミナルでのアーキテクチャ切り替え方法は分かっている

本題: Homebrewをアーキテクチャごとに独立管理し切り替える

管理方法としては二つのHomebrewをインストールし、アーキテクチャごとに使い分ける方法です。
ほぼ既存のbrewのデフォルトの仕組みに乗っ取った手法に沿った形式となっています。
ログイン時にアーキテクチャを判定し、使うbrewを切り替えます。

まず、Homebrewではインストール時に使用してるアーキテクチャ環境でデフォルトのインストール場所が以下のように異なります。また、それぞれのbrewで同名のパッケージがインストールされても、もちろん異なるディレクトリに入ります。

  • arm64e -> /opt/homebrew/bin/brew
  • x86_64 -> /usr/local/bin/brew

Homebrewインストール時にはこんな出力を見かけたと思います。下記はARM環境でHomebrewをインストールしたときの出力例です。

==> Next steps:
- Add Homebrew to your PATH in /Users/<user_name>/.zprofile:
    echo 'eval $(/opt/homebrew/bin/brew shellenv)' >> /Users/<user_name>/.zprofile
    eval $(/opt/homebrew/bin/brew shellenv)
- Run `brew help` to get started
- Further documentation: 
    https://docs.brew.sh

以上から分かるようにbrewのパスは公式の手順に従った場合~/.zprofileにて設定されることになります。.zprofileはzshにログインした際に1度だけ実行される設定ファイルです。

既にbrew導入済みの方は~/.zprofileには以下のような記述があるかと思います。brewのshellenvというコマンドがHomebrew周りのパスを追加してくれます。

~/.zprofile
eval $(/opt/homebrew/bin/brew shellenv)

ただこの状態でRosettaのアーキテクチャ環境を使ったりすると、Brew環境が混在しておかしくなります。
だからといって以下の設定にすることも間違いです。両方のアーキテクチャ環境でHomebrewをインストールし、愚直に手順を進めると最終的には以下のような設定になっているかと思います。基本的にパスは先頭からマッチングが行われるので、片方のbrewのshellenvコマンドによって追加されたパスが優先されてしまいます。

~/.zprofile
eval $(/opt/homebrew/bin/brew shellenv) # ARM環境
eval $(/usr/local/bin/brew shellenv) # Rosetta環境

下記のコードが今回の手法の結論です。
実際にはログイン時のアーキテクチャ環境をチェックし、それぞれのshellenvを動作させることが一番元の動作に近く簡潔です。以下が今回記述するファイルです。

~/.zprofile
ARCH=$(uname -m)
if [[ $ARCH == arm64 ]]; then
    echo "Current Architecture: $ARCH"
	eval $(/opt/homebrew/bin/brew shellenv)
elif [[ $ARCH == x86_64 ]]; then
    echo "Current Architecture: $ARCH"
	eval $(/usr/local/bin/brew shellenv)
fi

ARMの場合とrosetta環境をuname -mで確認しそれぞれ走らせるshell envを分岐させる、これだけです。
brewコマンドへのパスだけを弄っても、brewでインストールしたパッケージを参照するときのパスがバグることが想定されたりとパスをダイレクトで操作するとバグの原因になりがちなので、できる限り元の公式の仕様通りshellenvを活用した形式を勧めます。

※今回の例ではログイン時にどちらのアーキテクチャかechoで表示させてますが、zsh promptとかで表示させる方がスマートだし間違えないと思います。(ARMの時だけ特定のマークを出現させる、とか)

アーキテクチャの切り替え時の注意点

私は以下のエイリアスを使うことでそれぞれの環境を切り替えています。--loginをつけることで~/.zprofileを切り替え時に読み込んでくれます。

~/.zshrc
alias arm="exec arch -arch arm64e /bin/zsh --login"
alias x64="exec arch -arch x86_64 /bin/zsh --login"

~/.zprofileを使わない方法も存在しそうですが、Homebrewではデフォルトでは~/.zprofileを使うため、それに習って今回はこの仕様になっています。

想定している実行結果としては以下の形になります。

$ uname -m
x86_64
$ which brew
/usr/local/bin/brew
$ arm
Current Architecture: arm64
$ which brew
/opt/homebrew/bin/brew

あとはそれぞれのアーキテクチャ環境で必要なパッケージをインストールするだけです。
今回の管理方法の問題点は、同一パッケージであっても別々のディレクトリにインストールされる、という点ですね。それに付随して2重にインストールするパッケージによってはディスク容量を結構食う可能性があります。

例えばどちらのアーキテクチャ環境でも fzf と言ったパッケージは使いたいです。その場合はそれぞれのbrewでinstallすることになりますが、これは二つのbrewの独立性を保つために致し方ないと割り切っています。また、最終的にはRosetta側のbrewが不要になる日が来ると思うので、その時にはそちら側のbrewを消すだけで分かりやすいです。

まとめ

今回の手法

M1 Macの恩恵を最大限受けるにはarm64eとx86_64環境をそれぞれ独立で管理するのが良いと考えました。そして、Homebrewはアーキテクチャ環境によってインストール場所が異なるので、それに沿った形式の管理手法を用意しました。今回の手法の要約は以下の2行になります。

  1. アーキテクチャ情報をuname -mで確認して
  2. ターミナルシェルにログインした時にどちらのHomebrewのshellenvを実行するか分岐させる

アーキテクチャ情報の確認や分岐を行わないと元のHomebrewの動作そのままになりますね。

手法の注意点

ターミナル上でアーキテクチャ環境を切り替える時は--loginをつけないといけません。
これをやらないと~/.zprofileが読まれないからです。

別のdotfileでshellenvコマンドを叩かせる形式も考えられますが、元のbrewでは.zprofileを使っているので、そこは揃えることを個人的には勧めます。
また、今回紹介したように切り替えに使用する専用のaliasを用意することをオススメします。

できる限りARM対応のソフトウェア・パッケージを活用して恩恵を受けていきましょう!

Discussion

RessRess

記事へのリンクありがとうございます。

brew shellenv の内容はこのようになっており

brew shellenv を設定しなくてもbrew configで確認すると適切に設定されるようになっています

なのでshellenvは設定しなくてもいい感じに使用できますよ!
(shellenvを設定すると$PATHの書き換えを行いますのでそこだけ書くのもいいかもしれませんね)

junkudojunkudo

コメントありがとうございます!
出力まで添えていただいて感謝です。

なるほど、brew configで設定もしてくれるんですね。
ただ現状の設定が出力されるだけだと勘違いしていました。ただ、そうなるとconfigshellenvの差別化点が難しいですね。

Ressさんの記事を見るに、想定している使い方としては、brew configコマンドを叩くことで使用するbrewを切り替えるかんじですかね。ただ個人の思想としてはアーキテクチャ切り替えとbrew切り替えを別々に扱うと、使用中アーキテクチャ環境と使用中brewで不整合を出しちゃいそうなのが不安ですね。

私は以下の思想ベースで~/.zprofileでのみbrewの切り替えが発生するような形式をとっています。

  1. ARMのアーキテクチャ環境ではARMのbrewしか触れないようにしたい(=それぞれのアーキテクチャはそれぞれ対応したbrewしか見えないようにしたい)
  2. できる限り既存のbrewのパス設定の仕組みに揃えたい

configを使う場面はどちらのbrewを使ってるか分かる情報が欲しい場面に限定されそうなので、ARMアーキテクチャでは/opt/homebrewしか触らない僕の形式ではshellenvで事足りるかも?と思ってます。

RessRess

brew config は現在の設定を表示するコマンドです。誤解を招いてしまいすみません。

私の方式としては今後全てがARMなるという前提で/opt/homebrew/binを使用しています
また必要に応じて/usr/local/bin/brewを呼び出しx86環境も用意しているといった感じです

上記で言いたかったことは「eval $(brew shellenv)をわざわざ呼び出さなくてもbrewコマンドがいい感じに設定してくれるよ!」ってことです

junkudojunkudo

なるほど、こちらこそ変に解釈してしまってすいません。
理解です、ありがとうございます!

今やっと理解したのですがRessさんの手法は、両方の環境で同名パッケージのインストールを許さない形式なんですね。確かにそれならわざわざshellenvを叩く必要はないですね。

僕の場合は両方の環境で同じパッケージを使いたいケースがあったので、そこらへんで思想が違いますね。