Apple silicon Macに移行したらdeasyncのnpm installでエラーになった話

# はじめに

Intel Macで開発していたWebアプリケーションの開発環境をApple silicon Macにも構築しようとしたところ、フロントエンドのコンテナ起動時に以下のようなエラーが表示されました。

```shell:エラーログ(抜粋)
> deasync@0.1.24 install /app/node_modules/deasync
> node ./build.js

gyp ERR! find Python 
gyp ERR! find Python Python is not set from command line or npm configuration
gyp ERR! find Python Python is not set from environment variable PYTHON
```

どうやら Python が見つからなかったようですが、フロントエンドは TypeScript で開発しており Python は使っていません。

* フロントエンドで使っていないはずの Python がなぜ必要なのか？
* Intel Mac での実行時にはなぜ同様のエラーが発生しなかったのか？
* Docker を使っているのになぜ環境差が発生してしまったのか？

と疑問に思ったので調べてみました。

# フロントエンドの使用技術

* Docker
* Node.js
* TypeScript
* Vue.js

# 詳細

以下が出力されたエラーログの全体です。

<details><summary>エラーログ(全体)</summary>

```

> deasync@0.1.24 install /app/node_modules/deasync
> node ./build.js

gyp ERR! find Python 
gyp ERR! find Python Python is not set from command line or npm configuration
gyp ERR! find Python Python is not set from environment variable PYTHON
gyp ERR! find Python checking if "python" can be used
gyp ERR! find Python - "python" is not in PATH or produced an error
gyp ERR! find Python checking if "python2" can be used
gyp ERR! find Python - "python2" is not in PATH or produced an error
gyp ERR! find Python checking if "python3" can be used
gyp ERR! find Python - "python3" is not in PATH or produced an error
gyp ERR! find Python 
gyp ERR! find Python **********************************************************
gyp ERR! find Python You need to install the latest version of Python.
gyp ERR! find Python Node-gyp should be able to find and use Python. If not,
gyp ERR! find Python you can try one of the following options:
gyp ERR! find Python - Use the switch --python="/path/to/pythonexecutable"
gyp ERR! find Python   (accepted by both node-gyp and npm)
gyp ERR! find Python - Set the environment variable PYTHON
gyp ERR! find Python - Set the npm configuration variable python:
gyp ERR! find Python   npm config set python "/path/to/pythonexecutable"
gyp ERR! find Python For more information consult the documentation at:
gyp ERR! find Python https://github.com/nodejs/node-gyp#installation
gyp ERR! find Python **********************************************************
gyp ERR! find Python 
gyp ERR! configure error 
gyp ERR! stack Error: Could not find any Python installation to use
gyp ERR! stack     at PythonFinder.fail (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/find-python.js:307:47)
gyp ERR! stack     at PythonFinder.runChecks (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/find-python.js:136:21)
gyp ERR! stack     at PythonFinder.<anonymous> (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/find-python.js:179:16)
gyp ERR! stack     at PythonFinder.execFileCallback (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/find-python.js:271:16)
gyp ERR! stack     at exithandler (child_process.js:326:5)
gyp ERR! stack     at ChildProcess.errorhandler (child_process.js:338:5)
gyp ERR! stack     at ChildProcess.emit (events.js:375:28)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:275:12)
gyp ERR! stack     at onErrorNT (internal/child_process.js:467:16)
gyp ERR! stack     at processTicksAndRejections (internal/process/task_queues.js:82:21)
gyp ERR! System Linux 5.15.49-linuxkit
gyp ERR! command "/usr/local/bin/node" "/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "rebuild"
gyp ERR! cwd /app/node_modules/deasync
gyp ERR! node -v v14.17.1
gyp ERR! node-gyp -v v5.1.0
gyp ERR! not ok 
Build failed

npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! deasync@0.1.24 install: `node ./build.js`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the deasync@0.1.24 install script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
```

</details>

気になる箇所は以下の2点です。

①
> npm ERR! deasync@0.1.24 install: \`node ./build.js\`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the deasync@0.1.24 install script.

`deasync` は npm パッケージです。
どうやらこのパッケージのインストールに失敗しているようです。

②
> gyp ERR! find Python Python is not set from command line or npm configuration
gyp ERR! find Python Python is not set from environment variable PYTHON
gyp ERR! find Python checking if "python" can be used
gyp ERR! find Python - "python" is not in PATH or produced an error
gyp ERR! find Python checking if "python2" can be used
gyp ERR! find Python - "python2" is not in PATH or produced an error
gyp ERR! find Python checking if "python3" can be used
gyp ERR! find Python - "python3" is not in PATH or produced an error


Python を探しています。

> gyp ERR! find Python Node-gyp should be able to find and use Python. If not,

Python を必要としているのは `node-gyp` のようです。
`node-gyp` も npm パッケージです。

それぞれについて調べていきます。

### ① deasync

[GitHub](https://github.com/abbr/deasync) で README.md を見てみると、

> The core of deasync is written in C++.
(deasync のコア部分は C++ で書かれています。)


> Except on a few platforms + Node version combinations where binary distribution is included, DeAsync uses node-gyp to compile C++ source code so you may need the compilers listed in node-gyp.
(バイナリが配布されている一部のプラットフォームと Node.js のバージョンの組み合わせを除き、DeAsync は C++ ソースコードのコンパイルに node-gyp を使用するので、node-gyp に記載されているコンパイラが必要になる場合があります。)

とあります。

一部 C++ で書かれているため実行するにはバイナリが必要ですが、一部の環境向けには事前にコンパイルして配布してくれているようです。
バイナリ配布の対象となっている環境を[一覧ページ](https://github.com/abbr/deasync-bin)で確認します。

```:コンパイル済みバイナリ一覧(抜粋)
darwin-x86-node-0.10
linux-la32-node-0.10
linux-x64-node-0.10
win32-la32-node-0.10
...
```

バイナリが格納されたディレクトリがたくさんあり、`[OS]-[CPUアーキテクチャ]-[Node.jsバージョン]` の形式で名前が付けられています。
このディレクトリ名の中に、自分の環境に該当するものがあるかを探します。
(今回はコンテナ上で `npm install` を実行しているので、コンテナの環境に合うものを探します。)

##### OS と Node.jsバージョン

異なる環境であっても、同じ Docker イメージを使っていれば「OS」と「Node.jsバージョン」は同じになります。
今回は `node:14.17.1-alpine3.13` を使っているので、OS が `Linux`、Node.jsバージョンが `14.17.1` となります。

##### CPUアーキテクチャ

しかし、「CPUアーキテクチャ」は環境によって異なる場合があります。
Docker の[公式ドキュメント](https://docs.docker.jp/docker-for-mac/multi-arch.html)に以下の記載があります。

> マルチ・アーキテクチャをサポートしているイメージの実行時、 docker は自動的に OS とアーキテクチャに一致した、対応しているイメージを選択します。

使用しているイメージ(`node:14.17.1-alpine3.13`)が対応しているアーキテクチャを Docker Hub で確認すると、`linux/amd64` と `linux/arm64/v8` の両方がサポートされていました。
Intel Mac のCPUアーキテクチャは `amd64(x64)` 、Apple silicon Mac は `arm64` です。
つまり、Intel Mac では `linux/amd64` のイメージが、Apple silicon Mac では `linux/arm64/v8` のイメージがそれぞれ選択されることになります。

上記をもとに先程の一覧ページを確認すると、Intel Mac では `linux-x64-node-14` が該当しますが、Apple silicon Mac には該当するものがありません。
したがって Apple silicon Mac で `deasync` を利用する為には、 `node-gyp` を使ってコンパイルする必要があります。

### ② node-gyp

こちらも [GitHub](https://github.com/nodejs/node-gyp#installation) で README.md を見てみると、OS(Unix、macOS、Windows)ごとに必要なものが記載されています。
(Linux の場合は Unix の内容と同じで問題ないと思います。)

ここでは必要なものとして以下の3つが挙げられています。

* Python
* make
* C/C++ コンパイラ

これで、Apple silicon Mac でだけ Python が必要になった理由が分かりました。

# 解決方法

エラーの原因が分かったので、解決方法を考えてみます。

### ① Docker の platform オプションを指定する

実行時に `platform` オプションで `linux/amd64` を指定することで、`amd64` 用のコンテナを実行することができます。
これにより `amd64` コンテナ上で `npm install` を実行することになり、配布されているバイナリ(`linux-x64-node-14`)を使うことができます。

ただしこの方法は [Docker の公式ドキュメント](https://matsuand.github.io/docs.docker.jp.onthefly/desktop/mac/apple-silicon/#known-issues)で以下のように言及されており、可能であれば避けた方が良さそうです。

> 上のことをまとめると、Arm ベースのマシン上において Intel ベースのコンテナーを動作させているのは「ベストエフォート」にすぎない、
言い換えればできる範囲での対応でしかない、ということです。
したがって Apple silicon 上では可能なら arm64 コンテナーの実行をお勧めします。

### ② node-gyp の実行に必要なものをインストールする

`deasync` のインストールを実行するコンテナに、`node-gyp` の実行に必要な Python 等をインストールしてやれば、`arm64` 用のバイナリをコンパイルできるようになり、今回のエラーは発生しなくなるかと思います。(未検証)



### (その他) ARM向けバイナリの配布を待つ

`deasync` が `arm64` 用のバイナリも配布してくれるようになれば、Apple silicon 環境でも今回のエラーは発生しなくなるはずです。
ただし、いつ対応されるのか、そもそも対応されるのかは分かりません。

# まとめ

`deasync` は一部 C++ で書かれているため、予め用意されているバイナリが使用できない環境では自前でコンパイルする必要があり、コンパイルに使用するツール `node-gyp` を実行する為に Python が必要だった。

##### Dockerについて
* Dockerは、ホストマシンと同じCPUアーキテクチャのイメージを取得してくる。
* CPUアーキテクチャはIntel Macが `amd64`、Apple silicon Macは `arm64`。

##### deasyncについて

* npmパッケージは JavaScript 以外の言語でも記述でき、バイナリを含めることもできる。
* `deasync` というnpmパッケージはコア部分がC++で書かれており、いくつかの環境向けにバイナリを配布しているが、**自身の環境に一致するバイナリが配布されていない場合、インストール時に `node-gyp` を使って自身の環境でコンパイルされる**。
(現時点で `arm64` 向けのバイナリは配布されていない。)
* `node-gyp` の実行には、Python 等が必要となる。