3
Help us understand the problem. What are the problem?

posted at

updated at

Apple Silicon Mac + Rosetta 2 + Dockerで arm64(aarch64)/x86_64 とmacOS/Linuxの組み合わせで自動テストする方法(Elixirだけじゃなく汎用の方法も紹介するよ)

はじめに

Docker for Macがバージョン4.3.0になって,Apple Siliconで Rosetta 2 不要になったという知らせを受けて,いろいろ試して,ARMバイナリであればトラブルなく動くことに感心しました。

なお,Log4jの脆弱性の問題により,Docker for Mac 4.3.1以降を推奨します。詳しくはこちらを参照ください。

いろいろいじってみてふと思いついたのが,「Dockerがあれば,macOSとLinuxの自動テストを1台でできるのでは?」「Rosetta 2があれば,Apple Siliconであれば,arm64(aarch64)とamd64(x86_64)の自動テストを同時にできるのでは?」というアイデアでした。さっそく試してみたところ,一定の成果を得たので,公開します。これを,JaSST'21 KyushuでLTすることにしました。この記事は,その成果物を公開するものです。

この記事は fukuoka.ex Elixir/Phoenix Advent Calendar 2021 19日目の記事です。昨日は @Yoosuke さんのLiveViewを使って簡単にステートフルなタイピングゲームアプリを作ろう!後編でした。

macOSで動作しているかの確認方法

シェルだと次のような感じです。

uname -s

もし,Darwinが返ってくればmacOSです。

Elixirだと次のような感じです。

:os.type()

もし,{:unix, :darwin}が返ってくればmacOSです。

CPUのアーキテクチャの確認

シェルだと次のような感じです。

uname -m

macOSのときにもし,arm64が返ってくればmacOSのApple Siliconです。

Elixirだと次のような感じです。

:erlang.system_info(:system_architecture)
|> List.to_string()
|> String.split("-")
|> hd

macOSのときにもし,aarch64が返ってくればApple Siliconです。

Rosetta 2のインストール確認

シェルだと次のような感じです。

if test "`pkgutil --files com.apple.pkg.RosettaUpdateAuto`" != ''; then
        # 条件成立
fi

Elixirだと次のような感じです。

    case System.find_executable("pkgutil") do
      nil ->
        # インストールされていない
        :ok

      _ ->
        case System.cmd("pkgutil", ["--files", "com.apple.pkg.RosettaUpdateAuto"]) do
          {"", 0} ->
            # インストールされていない
            :ok

          {_, 0} ->
            # 条件成立
            :ok

          _ ->
            # インストールされていない
            :ok
        end
    end

Rosetta 2を起動してのx86_64上でのテスト実行

シェルだと次のような感じです。

env /usr/bin/arch -x86_64 /bin/sh test.sh # test.sh はテストスクリプト

Elixirだと次のような感じです。

System.cmd("env", ["/usr/bin/arch", "-x86_64", "mix", "test"] ++ args)

arg には mix test に渡すパラメータをリストで与えます。

Docker が存在かつ起動しているかの確認

シェルだと次のような感じです。

    if test "`which docker`" != ''; then
        docker ps > /dev/null 2>&1 && \
        # 存在かつ起動している
    fi

Elixirだと次のような感じです。

    case System.find_executable("docker") do
      nil ->
        # 存在しない
        :ok

      _ ->
        case System.cmd("docker", ["ps"], stderr_to_stdout: true) do
          {_, 1} ->
            # 存在しているが起動していない
            :ok

          _ ->
            # 存在かつ起動している
            :ok

        end
    end

1台のMacで,複数の環境をテストするテスティングスクリプト

次のようなシェルスクリプトを使うと,自動でElixirと1台のMacで,複数の環境をテストすることができます。

#!/bin/sh

function call_test () {
    /bin/sh test.sh
}

function call_test_by_x86_64 () {
    if test "`pkgutil --files com.apple.pkg.RosettaUpdateAuto`" != ''; then
        env /usr/bin/arch -x86_64 /bin/sh test.sh
    fi
}

function call_test_by_docker () {
    if test "`which docker`" != ''; then
        docker ps > /dev/null 2>&1 && \
        docker build -t astesting . && \
        docker run -it --rm astesting test.sh && \
        docker rmi astesting
    fi
}

case `uname -s` in 
    Darwin)
        case `uname -m` in
        arm64)
            call_test
            call_test_by_x86_64
            call_test_by_docker
            ;;
        x86_64)
            call_test
            call_test_by_docker
            ;;
        esac 
        ;;
    *) ;;
esac

Elixirと1台のMacで,複数の環境をテストする mix test.astesting を提供するhexライブラリAstesting

Hexに公開しました。

GitHubレポジトリはこちらです。

ドキュメントはこちらです。

インストール方法はREADMEに書いてあります。

実行するには次のようにします。

mix test.astesting

Apple Silicon Mac で Rosetta 2 をインストールしていて,かつ Docker Desktop for Macを起動していると次のように自動で arm64 macOS, x86_64 macOS, aarch64 Linux の3つの環境で mix test を実行してくれます。

% mix test.astesting
make: Nothing to be done for `all'.
...

Finished in 0.03 seconds (0.00s async, 0.03s sync)
1 doctest, 2 tests, 0 failures

Randomized with seed 723087
testing on x86_64
==> elixir_make
Compiling 1 file (.ex)
Generated elixir_make app
==> astesting
Compiling 2 files (.ex)
Generated astesting app
==> test_astesting
make: Nothing to be done for `all'.
Generated test_astesting app
...

Finished in 0.05 seconds (0.00s async, 0.05s sync)
1 doctest, 2 tests, 0 failures

Randomized with seed 999685
testing on Docker
[+] Building 3.6s (7/7) FINISHED                                                
 => [internal] load build definition from Dockerfile336                    0.4s
 => => transferring dockerfile: 206B                                       0.0s
 => [internal] load .dockerignore                                          0.7s
 => => transferring context: 2B                                            0.0s
 => [internal] load metadata for docker.io/library/elixir:1.13.0-alpine    2.4s
 => [auth] library/elixir:pull token for registry-1.docker.io              0.0s
 => [1/2] FROM docker.io/library/elixir:1.13.0-alpine@sha256:3745a095bd61  0.0s
 => CACHED [2/2] RUN apk update &&     apk add alpine-sdk &&     mix loca  0.0s
 => exporting to image                                                     0.3s
 => => exporting layers                                                    0.0s
 => => writing image sha256:0b11cae0f80c7eb500f525382e1598917ab2fb69831d8  0.1s
 => => naming to docker.io/library/astesting429                            0.1s
Resolving Hex dependencies...
Dependency resolution completed:
Unchanged:
  astesting 0.1.5
  elixir_make 0.6.3
* Getting astesting (Hex package)
* Getting elixir_make (Hex package)
==> elixir_make
Compiling 1 file (.ex)
Generated elixir_make app
==> astesting
Compiling 2 files (.ex)
Generated astesting app
==> test_astesting
mkdir -p /work/_build/test/lib/test_astesting/priv
mkdir -p /work/_build/test/lib/test_astesting/obj
cc -I/usr/local/lib/erlang/usr/include -fPIC -std=c11 -O3 -Wall -Wextra -Wno-unused-function -Wno-unused-parameter -Wno-missing-field-initializers c_src/libnif.c -MM -MP -MF /work/_build/test/lib/test_astesting/obj/libnif.d
CC libnif.o
cc -c -I/usr/local/lib/erlang/usr/include -fPIC -std=c11 -O3 -Wall -Wextra -Wno-unused-function -Wno-unused-parameter -Wno-missing-field-initializers -o /work/_build/test/lib/test_astesting/obj/libnif.o c_src/libnif.c
LD libnif.so
g++ -o /work/_build/test/lib/test_astesting/priv/libnif.so /work/_build/test/lib/test_astesting/obj/libnif.o -L/usr/local/lib/erlang/usr/lib -fPIC -shared  
Compiling 1 file (.ex)
Generated test_astesting app
...

Finished in 0.02 seconds (0.00s async, 0.02s sync)
1 doctest, 2 tests, 0 failures

Randomized with seed 968834
Untagged: astesting429:latest
Deleted: sha256:0b11cae0f80c7eb500f525382e1598917ab2fb69831d8db8662e3d9ba8ecf124

将来課題

なお,このテストは,elixir_makeを入れてNIFをコンパイルする場合を示していますが,x86_64 macOSの場合でも,arm64のNIFを生成してリンクしています。arm64向けのErlang VMをHomebrewやasdfでインストールしている場合は,そうしないとリンクエラーになってしまうのです。

なぜそうなるかというと,Rosetta2を起動してx86_64モードで起動しているにもかかわらず,インストールされているErlangがarm64バイナリなので,arm64モードで実行することになるからです。これを解決するには,x86_64バイナリのErlangをインストールする必要があります。

その確実な方法をいろいろ調べたところ,asdfを使ってインストールするのが良さそうでした。asdfでは,環境変数の設定次第でx86_64バイナリとarm64バイナリが共存できそうです。ただし,OpenSSLなど,Erlangからリンクするライブラリもx86_64バイナリにする必要がありそうで,それを確実にインストールするにはHomebrewを使ってx86_64バイナリを共存させる必要があるという感じになり,結構な大ごとになります。

別のアイデアとしては,前述の問題の解決でasdfを使うのですから,バージョンの異なるElixirやErlangで自動テストするようにも発展できそうです。

おわりに

この記事では,Rosetta 2 と Docker により,1台のMacで,複数の環境をテストする方法を紹介しました。

明日は @iyanayatudaze さんの"phx.server"コマンドの実装を追い掛けるです。お楽しみに。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
3
Help us understand the problem. What are the problem?