macOS Catalina で ASDF を使って Erlang 20 をインストール

一筋縄ではいかなかったので記事にしてみます。
Catalina を使い始めた方の助けになればと思います。

まず、何をしようとしていたかを書いておきます。

- Erlang 20.3.8.9 を macOS Catalina にインストールしたい。
    - 古いですね。。
- [asdf-erlang](https://github.com/asdf-vm/asdf-erlang) を使ってインストールしたい。

私がとった方法は[まとめ](#まとめ)に書いてあります。

## ソースから Erlang をコンパイルしてみる

問題の切り分けのために、まず ASDF を使わずに Erlang のコンパイルをしてみました。
このとき生じた問題を記録しておきます。

### Stack check のデフォルト有効化

この問題は Erlang/OTP [PR#2413](https://github.com/erlang/otp/pull/2413) によって対処済です。
詳しくは分かりませんが、コンパイル時の stack check がデフォルトで有効化されたことにより、clang の潜在的なバグが顕在化してしまったようです。
したがって、`CFLAGS` に `-fno-stack-check` を含めればコンパイルエラーを回避できます。

Erlang 22.1.4 で反映されるようなので、それよりも古い Erlang を使いたい場合は、`-fno-stack-check` を含めた `CFLAGS` を自分で設定してコンパイルすれば大丈夫です。
デフォルトの `CFLAGS` は `-O2 -g` らしいので、私は下記のようにしました。

```shell
export CFLAGS="-O2 -g -fno-stack-check"
```

### OpenSSL が見つからない

この問題は Catalina にする前からあったのかもしれないです。。
原因は[ここ](https://github.com/kerl/kerl#compiling-crypto-on-macs)に書いてあります。

Configure するときに明示的に OpenSSL の場所を指定すれば大丈夫です。
私は下記のようにして Homebrew でインストールした OpenSSL を指定しました。

```shell
./configure --with-ssl=/usr/local/opt/openssl
```

### Super Carrier がつくれない

この問題が最も大変でした。
症状としては、コンパイル時に下記のエラーが起きます。

```log
=== Entering application hipe
 VSN  hipe.hrl
 ERLC ../ebin/hipe_rtl.beam
erts_mmap: Failed to create super carrier of size 512 MB
make[3]: *** [../ebin/hipe_rtl.beam] Error 1
make[2]: *** [opt] Error 2
make[1]: *** [opt] Error 2
make: *** [secondary_bootstrap_build] Error 2
```

#### Super Carrier とは

`Failed to create super carrier` とありますが、Super Carrier は何でしょうか？
[ここ](https://github.com/erlang/otp/blob/master/erts/emulator/internal_doc/SuperCarrier.md)に書いてありました。
パフォーマンス向上のために Erlang VM の起動時にある程度の大きさのメモリ領域を確保しておくらしいのですが、そのメモリ領域のことを Super Carrier を呼ぶそうです。

#### HiPE とは

`Entering application hipe` とありますが、HiPE とは何でしょうか？
[この記事](https://qiita.com/sile/items/d750019fcab961bbb0a7)が分かりやすかったです。
Erlang のソースコードをネイティブコードとしてコンパイルしてくれる機能のようです。

そして、コンパイラされたものは Super Carrier に配置されるようです。

#### 32 bit のための配慮が仇となっていた

この問題の原因は、HiPE の生成物が所謂 low 2GB address space に配置されるように、プログラムがこのメモリ領域に Super Carrier をつくろうとしていることです。
よく知りませんが、32 bit システムではこのメモリ領域を使わざるを得ないみたいですね。。

Super Carrier と HiPE に関するコード（のひとつ）は Erlang/OTP リポジトリの [`erts/emulator/sys/common/erl_mmap.c`](https://github.com/erlang/otp/blob/master/erts/emulator/sys/common/erl_mmap.c) にあります。
ここで下記のような関数が定義されています（下記はリポジトリではなく、20.3.8.9 のソースから抜き出しました）。

```cpp
static void *
os_mmap_virtual(char *ptr, UWord size, int exec)
{
    int flags = ERTS_MMAP_VIRTUAL_FLAGS;
    void* res;

#ifdef ERTS_ALC_A_EXEC
    if (exec) {
        ASSERT(!ptr);
        /* OTP-19.0: Nice hack below cut-and-pasted from hipe_amd64.c */

#  ifdef MAP_32BIT
        /* If we got MAP_32BIT (Linux), then use that to ask for low memory */
        flags |= MAP_32BIT;
#  else
        /* FreeBSD doesn't have MAP_32BIT, and it doesn't respect
           a plain map_hint (returns high mappings even though the
           hint refers to a free area), so we have to use both map_hint
           and MAP_FIXED to get addresses below the 2GB boundary.
           This is even worse than the Linux/ppc64 case.
           Similarly, Solaris 10 doesn't have MAP_32BIT,
           and it doesn't respect a plain map_hint. */
        ptr = (char*)(512*1024*1024); /* 0.5GB */

#    if defined(__FreeBSD__) || defined(__sun__)
        flags |= MAP_FIXED;
#    endif
#  endif /* !MAP_32BIT */
    }
#else /* !ERTS_ALC_A_EXEC */
    ASSERT(!exec);
#endif
    res = mmap((void *) ptr, (size_t) size, ERTS_MMAP_VIRTUAL_PROT,
               flags, ERTS_MMAP_FD, 0);
    if (res == (void *) MAP_FAILED)
  return NULL;
    return res;
}
```

macOS は `MAP_32BIT` を持たないため、`ptr = (char*)(512*1024*1024);` が実行されることになります。
`FreeBSD doesn't have MAP_32BIT ...` で始まるコメントにあるように、below the 2GB boundary なメモリ領域を確保するために、関数 `mmap()` に渡すメモリ領域の開始位置 `ptr` を 0.5GB に固定しています。

Catalina で、プログラムが low 2GB address space にメモリが確保されるように指定することができなくなったのか、それとも low 2GB address space に限らずメモリ確保の指定ができなくなったのかは分かりませんが、とにかく、[`MAP_32BIT` を使った条件分岐部分を削除](https://github.com/erlang/otp/commit/250f6d74e645968d036bad14d5b92907ff4d7342)すれば、コンパイルが通るようになります。
ちなみに、ソースコードには `os_mmap_virtual()` の第一引数 `ptr` が `NULL` の場合があり、したがって `mmap()` に渡される `ptr` が `NULL` になる場合があるのですが、このとき確保されるメモリ領域の開始位置はシステムが決めてくれるみたいです。

なお、`MAP_32BIT` を使った条件分岐部分の削除は Erlang/OTP の [PR#1699](https://github.com/erlang/otp/pull/1699) で反映されており、**Erlang 21 以降であればこの問題は起こりません**。
リリースノートには下記のように書いてあります。

> ```text
>  OTP-14951    Application(s): erts
>
>               Removed need for HiPE to allocate native executable
>               memory in low 2GB address space on x86_64. Command line
>               option +MXscs is thereby obsolete and ignored.
> ```

Catalina では 32 bit アプリが使えなくなるという話を聞きますが、この問題もそれに関連した話なのかもしれませんね。。

## kerl を使って Erlang をコンパイルしてみる

asdf-erlang は内部的に [kerl](https://github.com/kerl/kerl) を使っています。
問題の切り分けのために、直接 kerl を使って Erlang をコンパイルしてみました。
ソースコードを変更することができないため、HiPE 自体を無効化するしかありませんでした。。

```shell
export KERL_CONFIGURE_OPTIONS="--disable-hipe --with-ssl=/usr/local/opt/openssl"
./kerl build 20.3.8
```

もしかしたらコンパイルオプションで問題を回避する方法もあるのかもしれませんが、調べきれませんでした。。

## ASDF を使って Erlang をコンパイルしてみる

asdf-erlang を使う場合も `KERL_CONFIGURE_OPTIONS` が効くはずです。
しかし、`KERL_CONFIGURE_OPTIONS` の設定を二通り試してみたのですが、設定が反映されていないようでした。

- `export` する。
- `~/.kerlrc` に書く。

前者の原因はよく分かっていませんが、後者の理由は、asdf-erlang が実際に読みに行く設定ファイルが `~/.asdf/plugins/erlang/kerl-home/.kerlrc` であるためです。
したがって、下記のようにして `KERL_CONFIGURE_OPTIONS` の設定を反映させることができました。

```shell
mkdir -p ~/.asdf/plugins/erlang/kerl-home
echo 'KERL_CONFIGURE_OPTIONS="--disable-hipe --with-ssl=/usr/local/opt/openssl"' > ~/.asdf/plugins/erlang/kerl-home/.kerlrc
```

ちなみに、この問題が kerl の [Issue#320](https://github.com/kerl/kerl/issues/320) と関係しているかな？と思ったのですが、まだ詳しく調べきれていません。

## まとめ

まず、Catalina で ASDF を使って Erlang 20 をインストールしたい場合、HiPE を諦めざるを得ませんでした（少なくとも私の力量では）。
このことを理解した上で、私は下記の方法でインストールしました。

```shell
mkdir -p ~/.asdf/plugins/erlang/kerl-home
echo 'KERL_CONFIGURE_OPTIONS="--disable-hipe --with-ssl=/usr/local/opt/openssl"' > ~/.asdf/plugins/erlang/kerl-home/.kerlrc
export CFLAGS="-O2 -g -fno-stack-check"
asdf install erlang 20.3.8.9
# お掃除
unset CFLAGS
rm ~/.asdf/plugins/erlang/kerl-home/.kerlrc
```

インストールした Erlang は（使っているのは Elixir ですが）今のところ問題なく動作しています。
また、Mojave のときにインストールした Erlang も今のところ問題なく Catalina で動作しています。

ですが、可能であれば、Erlang 21 に移行したほうが良さそうです。。