できるだけ広範囲のmacOS環境でネイティブ実行できるjqバイナリをビルドする

# `jq` とは

`jq` コマンドは，便利なJSONパーサーとして（そして単なるパーサーを超えてプログラミング言語としても）知られています。`jq` で何ができるかは，次のページが参考になるでしょう。

* [公式サイト](https://jqlang.github.io/jq/)
* [Wikipedia (en)](https://en.wikipedia.org/wiki/Jq_(programming_language))
* [とほほのjq入門](https://www.tohoho-web.com/ex/jq.html)

# `jq` のインストール法3選
さて，macOS に `jq` をインストールする方法としては，次のような方法が考えられます。

1. Homebrew や MacPorts といったパッケージ管理システムを利用する
1. 公式配布バイナリを利用する
1. 自分でソースコードからビルドする

既に Homebrew や MacPorts を使っている方であれば，1. が簡単でしょう。

```sh:Homebrewの場合
$ brew install jq
```

```sh:MacPortsの場合
$ sudo port install jq
```

# パッケージ管理システムによるインストールの欠点

自分一人で使う分には Homebrew や MacPorts で困らないのですが，この `jq` のバイナリを自作アプリに同梱して配布するなど，他のマシンへと移植する場合を考えると，バイナリのポータビリティが気になります。

## ライブラリの依存性

### MacPortsの場合

例えば，MacPorts でインストールされる `/opt/local/bin/jq` が何に依存しているか，その依存ライブラリを調べてみましょう。`otool` コマンドが役立ちます。

```sh:MacPorts でインストールされる jq バイナリの依存ライブラリの調査
$ otool -L /opt/local/bin/jq
/opt/local/bin/jq:
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1336.0.0)
	/opt/local/lib/libonig.5.dylib (compatibility version 10.0.0, current version 10.0.0)
```

`/opt/local/lib/` から始まるパスが現れていることから，この `jq` バイナリは，MacPorts でインストールされる `oniguruma` ライブラリ（これは正規表現ライブラリ[鬼車](https://ja.wikipedia.org/wiki/%E9%AC%BC%E8%BB%8A_(%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA))です）に依存してしまっていることが分かります。これでは，この `jq` バイナリを単に他のマシンにコピーするだけでは動きません。

### Homebrewの場合

Homebrew でインストールされる `/usr/local/bin/jq` についても同様に，Homebrew の依存関係解決に基づきインストールされる `oniguruma` ライブラリの存在に依拠しています。

```sh:Homebrew でインストールされる jq バイナリの依存ライブラリの調査
$ otool -L /usr/local/bin/jq
/usr/local/bin/jq:
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.100.3)
	/usr/local/opt/oniguruma/lib/libonig.5.dylib (compatibility version 9.0.0, current version 9.0.0)
```

## バイナリの対象アーキテクチャ

また，バイナリの対象アーキテクチャを調べてみましょう。`file` コマンドが使えます。

```sh:MacPorts でインストールされる jq バイナリの対象CPUアーキテクチャの確認
$ file /opt/local/bin/jq
/opt/local/bin/jq: Mach-O 64-bit executable arm64
```

この MacPorts は Apple Silicon 上で実行しているので，`arm64`バイナリが生成されています。これでは，Intel Mac 上にこのバイナリを持っていったときに実行できません。

逆に，`x86_64` バイナリを Apple Silicon 上で実行するのは，Rosetta 2 をインストールしてあれば可能ではありますが，Rosetta 2 がインストールされているマシンでなければ動きませんし，動作パフォーマンスも下がるので，好ましくありません。

## 起動OSの最低バージョン

このバイナリが起動できる最低OSバージョンを調べてみます。`otool -l` が役立ちます。

```sh:MacPorts でインストールされる jq バイナリの最低動作要件の確認
$ otool -l /opt/local/bin/jq | grep minos
    minos 14.0
```

この MacPorts は，macOS 14 Sonoma 上で実行しているため，その上でデフォルトで生成されたバイナリは，起動できるOSバージョンの最低値が macOS 14 と設定されてしまっています。これでは，macOS 13 以前の環境に持っていったときに使えません。Homebrew の場合も同様に，実行しているホストOSを最低動作環境とするバイナリがインストールされます。

# 公式配布バイナリ

次に，`jq` の公式配布バイナリの状況を調べてみます。公式サイトでは，`AMD64` (Intel CPU) 向けバイナリと `ARM64` (Apple Silicon) 向けバイナリがそれぞれ配布されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41739/d2c3fe22-2876-e0f9-958c-583e095778d4.png)

最新の jq Ver.1.7 の公式バイナリについて，それぞれの状況を調べてみましょう。

## Intel CPU 向け公式バイナリ
```sh:Intel CPU 向け公式バイナリの依存ライブラリの調査
$ file jq-macos-amd64
jq-macos-amd64: Mach-O 64-bit executable x86_64
$ otool -L jq-macos-amd64
jq-macos-amd64:
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.100.3)
$ otool -l jq-macos-amd64 | grep minos
    minos 13.0
```

## Apple Silicon 向け公式バイナリ
```sh:Apple Silicon 向け公式バイナリの依存ライブラリの調査
$ file jq-macos-arm64
jq-macos-arm64: Mach-O 64-bit executable arm64
$ otool -L jq-macos-arm64
jq-macos-arm64:
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.100.3)
$ otool -l jq-macos-arm64 | grep minos
    minos 13.0
```

このように，Intel CPU 向けバイナリ，Apple Silicon 向けバイナリのどちらも，

* 単一アーキテクチャ用バイナリ
* 依存性はOS標準のライブラリのみ
* 要求OSバージョンは macOS 13 Ventura 以降

となっていることが分かります。macOS 13 Ventura は，本記事執筆時点では最新OSの1つ前のバージョンです。ちょっと要求水準が高めですね。

# もっと広範囲の環境で動くようにしたい

他のマシンへのポータビリティを高めるべく，次のような条件を見たす `jq` バイナリが欲しいです。

* **Intel CPU / Apple Silicon のどちらでもネイティブ起動できる Universal Binary である**
* **OS標準のライブラリにのみしか依存しない**
* **できるだけ広い範囲のOS環境で起動できる**

このようなバイナリを得るためには，ソースコードから自前で `jq` をビルドする必要があるでしょう。


# ソースコードからの `jq` バイナリのビルド

[公式サイトの説明](https://jqlang.github.io/jq/download/)によると，バイナリのビルド＆インストール方法は次のように説明されています。

```sh
git clone --recursive https://github.com/jqlang/jq.git
cd jq
autoreconf -i
./configure
make
sudo make install
```

この案内に沿ってバイナリのビルドを進めましょう。

## `autoreconf` できるようにする

このためには，`autoreconf` コマンドが必要です。そのためには，`autoconf` をインストールしておきましょう。ここは Homebrew や MacPorts を使うのが簡単です。

```sh:Homebrewの場合
$ brew install autoconf 
```

```sh:MacPortsの場合
$ sudo port install autoconf 
```

続きをやってみます。

```
$ git clone --recursive https://github.com/jqlang/jq.git
$ cd jq
$ autoreconf -i
Can't exec "aclocal": No such file or directory at /usr/local/Cellar/autoconf/2.71/share/autoconf/Autom4te/FileUtils.pm line 274.
autoreconf: error: aclocal failed with exit status: 2
```

何やらエラーが出ました。これは `autoreconf` は `aclocal` も必要とするからです。このためには，`automake` をインストールしておきましょう。`aclocal` は `automake` に付随してインストールされます。

```sh:Homebrewの場合
$ brew install automake 
```

```sh:MacPortsの場合
$ sudo port install automake 
```

改めて `autoreconf` にチャレンジします。

```sh
$ autoreconf -i
src/Makefile.am:24: error: Libtool library used but 'LIBTOOL' is undefined
```

またエラーが出ました。今度は `libtoool` をインストールします。

```sh:Homebrewの場合
$ brew install libtool 
```

```sh:MacPortsの場合
$ sudo port install libtool 
```

これで，`autoreconf -i` が通るようになります。


## まずはビルドしてみる

では，この状態でビルドしてみましょう。`configure` & `make` をしてみます。

```sh
$ ./configure --disable-shared \
  --enable-static \
  --with-oniguruma=builtin  
$ make
```

`--disable-shared --enable-static --with-oniguruma=builtin` の部分は，onigurumaなど外部ライブラリへの依存関係をなくすための指定です。

すると，同ディレクトリ内に `jq` バイナリが生成されます。

まずは依存ライブラリを確認します。

```sh
$ otool -L jq
jq:
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1336.0.0)
```

システム標準のライブラリのみを参照しており，期待通りです。

ただし，この方法でビルドするだけでは，

* ホストマシンと同じCPUアーキテクチャのみを動作対象とする
* ホストマシンのOSバージョンを最低動作要件とする

バイナリが生成されてしまいます。これでは困るので，対象アーキテクチャと最低動作要件を明示指定したバイナリを作りましょう。
そのためには，`CFLAGS`, `CXXFLAGS`, `LDFLAGS` にオプションを付け加えます。

## Apple Silicon 向けバイナリのビルド

Apple Silicon 搭載 Mac は，最低でも macOS 11 Big Sur です。よって，動作要件としては macOS 11 以上を指定すれば十分です。

clangの `-arch` オプションによるクロスコンパイル機能を使えば，コンパイルを実行しているホストマシンのCPUが Intel CPU / Apple Silicon のいずれであっても，`x86_64` / `arm64` の双方のバイナリを生成できます。

次のように，`CFLAGS`, `CXXFLAGS`, `LDFLAGS` のオプションを指定して，`autoreconf -i` を実行します。

```sh
$ export CFLAGS="-arch arm64 -mmacosx-version-min=11.0"
$ export CXXFLAGS="-arch arm64 -mmacosx-version-min=11.0"
$ export LDFLAGS="-arch arm64 -mmacosx-version-min=11.0"
$ autoreconf -i
```

次に `configure` スクリプトを実行します。ただし，クロスコンパイルになる場合（ホストマシンのアーキテクチャと生成バイナリのアーキテクチャが異なる場合）は，`configure` スクリプトの実行に，`--host` オプションでホストマシンのアーキテクチャを明示指定しておく必要があります。ここでは，`--host=$(uname -m)` によって，ホストマシンのアーキテクチャを常に明示指定しておくことにしましょう。

```sh
$ ./configure --disable-shared \
  --enable-static \
  --with-oniguruma=builtin \
  --host=$(uname -m)
```

そしていよいよ `make` します。既に `make` を実行していたのであれば，一旦 `make clean` しておきましょう。

```sh
$ make clean
$ make
```

そして，生成された `jq` バイナリの状況をチェックします。

### 依存ライブラリのチェック
```sh
$ otool -L jq
jq:
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1336.0.0)
```

### 対象アーキテクチャのチェック
```sh
$ file jq
jq: Mach-O 64-bit executable arm64
```

### 最低動作OS要件のチェック
```sh
$ otool -l jq | grep minos
    minos 11.0
```

いずれも期待通りの結果となりました。このバイナリを別名で退避しておきます。

```sh
$ cp jq jq-arm64
```

## Intel CPU 向けバイナリのビルド

Intel CPU の Mac は昔からあります。最初期のものは MacOS X 10.5 Leopard のあたりです。よって，動作要件を MacOS X 10.5 Leopard 以上とするバイナリを生成してみましょう。

```sh
$ export CFLAGS="-arch x86_64 -mmacosx-version-min=10.5"
$ export CXXFLAGS="-arch x86_64 -mmacosx-version-min=10.5"
$ export LDFLAGS="-arch x86_64 -mmacosx-version-min=10.5"
$ autoreconf -i
```

そして，`configure` スクリプトを実行します。

```sh
$ ./configure --disable-shared \
  --enable-static \
  --with-oniguruma=builtin \
  --host=$(uname -m)
```

そして `make` します。

```sh
$ make clean
$ make
```

そして，生成された `jq` バイナリの状況をチェックします。

### 依存ライブラリのチェック
```sh
$ otool -L jq
jq:
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1336.0.0)
```

### 対象アーキテクチャのチェック
```sh
$ file jq
jq: Mach-O 64-bit executable x86_64
```

### 最低動作OS要件のチェック

最低動作OS要件のチェックは `otool -l` で行えますが，少し昔のOS用のバイナリの場合，出力の形式が異なり，`grep minos` で探せません。`otool -l` の出力の中の `LC_VERSION_MIN_MACOSX` という行の2行下の `version` でチェックします。


```sh
$ otool -l jq
（中略）
      cmd LC_VERSION_MIN_MACOSX
  cmdsize 16
  version 10.5
      sdk 14.0
（後略）
```

すると，正しく MacOS X 10.5 Leopard を最低動作要件とするバイナリが生成されていることが分かります。

このバイナリを別名で退避しておきます。

```sh
$ cp jq jq-x86_64
```

## Universal Binary として結合する

最後に，こうして得られた

* macOS 11.0 Big Sur 以上で動作する Apple Silicon 向けバイナリ `jq-arm64`
* MacOS X 10.5 Leopard 以上で動作する Intel CPU 向けバイナリ `jq-x86_64`

を Universal Binary として結合しましょう。バイナリの結合や分解には `lipo` コマンドを使います。

```sh
$ lipo -create -output jq jq-arm64 jq-x86_64
```

こうして得られた `jq` バイナリを確認します。

```
$ file jq
jq: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64]
jq (for architecture x86_64):	Mach-O 64-bit executable x86_64
jq (for architecture arm64):	Mach-O 64-bit executable arm64
```

確かに Universal Binary となっています！

これで，めでたく**できるだけ広範囲の macOS 環境でネイティブ実行できる `jq` バイナリ**が得られました！これならば，どこの環境に持っていっても確実に動作するので，安心して頒布可能です。以上のような，クロスコンパイルも用いた Universal Binary のビルド方法は，`jq` に限らず，広く活用できることでしょう。

# スクリプト化

ここまでの流れで確立した，**`jq` をソースコードからビルドし，できるだけ広範囲の macOS 環境でネイティブ実行できる Universal Binary を得る**という流れを，1つのシェルスクリプトにまとめておきましょう。（`autoconf`, `autoreconf` が使える状態になっていることが前提ｔ。）

```bash
#!/bin/bash -x

CONFIGURE_FLAGS=(
  --with-oniguruma=builtin
  --disable-shared
  --enable-static
  --host=$(uname -m)
)

export CC="/usr/bin/clang"
export CXX="/usr/bin/clang++"
export LD="/usr/bin/clang"

WORKDIR="$(mktemp -d)"
OUTDIR=$WORKDIR/output
rm -rf $WORKDIR
mkdir -p $OUTDIR
echo "Building in: $WORKDIR"
git clone --recursive https://github.com/jqlang/jq.git "$WORKDIR/jq"
cd "$WORKDIR/jq"

# 1. autoreconf の実行
autoreconf -i

# 2. arm64 ビルド (macOS 11 以上)
echo "Building arm64..."
export CFLAGS="-arch arm64 -mmacosx-version-min=11.0"
export CXXFLAGS="$CFLAGS"
export LDFLAGS="$CFLAGS"
./configure "${CONFIGURE_FLAGS[@]}"
make clean
make
if [ $? -ne 0 ]; then
  echo "Build failed: jq (arm64)"
  exit 1
fi
cp jq "$WORKDIR/jq-arm64"

# 3. x86_64 ビルド (MacOS X 10.5 以上)
echo "Building x86_64..."
export CFLAGS="-arch x86_64 -mmacosx-version-min=10.5"
export CXXFLAGS="$CFLAGS"
export LDFLAGS="$CFLAGS"
./configure "${CONFIGURE_FLAGS[@]}"
make clean
make
if [ $? -ne 0 ]; then
  echo "Build failed: jq (x86_64)"
  exit 1
fi
cp jq "$WORKDIR/jq-x86_64"

# 4. Universal Binary を作成
echo "Creating universal binary..."
cd $WORKDIR
lipo -create -output $OUTDIR/jq jq-arm64 jq-x86_64
strip $OUTDIR/jq

echo "Done! jq is now a Universal Binary."
open -R $OUTDIR/jq
```