Кросс-сборка Elixir-релизов amd64→arm64
Введение
Как обычно, начнём с «Зачем?»
Всё очень просто: Elixir-приложение я пишу на машине с amd64, а запускать релиз я буду на arm64 (RaspberryPi).
И — чтобы не потерять рецепт — оставлю тут заметочку.
А есть простой путь?
Нет. Elixir сам в такое не умеет, а — учитывая тот факт, что в Elixir-релизе лежит весь Erlang/OTP — вариант как со скриптовыми языками не прокатит.
Да, конечно, можно собирать релиз без бинарей и требовать установки Erlang/OTP на машине, где оно будет запускаться, но это не наш метод. Нам надо tar -xf release.tar.gz && ./bin/release start
.
А как?
Тут нам на помощь приходят две технологии: Docker и qemu. Конечно, вместо докера можно использовать связку Buildah и Podman, но я для такого уже староват.
Что-то мешает?
Да.
Казалось бы, можно просто добавить --platform=linux/arm64
в образ FROM elixir:_version_
, но, увы, есть одна проблема: qemu не совместим с JIT-компилятором Erlang’а.
А отключается JIT только при сборке Erlang/OTP.
Что делать?
Собирать руками Erlang/OTP и Elixir, принудительно отключив JIT.
Получится что-то примерно такое:
FROM --platform=linux/arm64 ubuntu:jammy-20250126
## Build OTP + Elixir
RUN apt update && \
apt install --no-install-recommends -y apt-utils libncurses-dev libwxgtk3.0-gtk3-dev libssl-dev openssl ca-certificates inotify-tools build-essential wget && \
apt clean
ENV OTP_VERSION="27.2.4" ELIXIR_VERSION="1.18.2"
ENV BUILD_PATH=/root/elixir \
OTP_URL="https://github.com/erlang/otp/releases/download/OTP-$OTP_VERSION/otp_src_$OTP_VERSION.tar.gz" \
ELIXIR_VERSION="1.18.2" \
ELIXIR_URL="https://github.com/elixir-lang/elixir/archive/v$ELIXIR_VERSION.tar.gz"
RUN mkdir -p $BUILD_PATH && \
rm -rf $BUILD_PATH && \
mkdir -p $BUILD_PATH/
RUN cd $BUILD_PATH/ && \
wget "$OTP_URL" && tar -xf "otp_src_$OTP_VERSION.tar.gz" && \
cd "$BUILD_PATH/otp_src_$OTP_VERSION" && \
./configure --disable-jit && make && make install
RUN cd $BUILD_PATH/ && \
wget $ELIXIR_URL && tar -xf "v$ELIXIR_VERSION.tar.gz" && \
cd "$BUILD_PATH/elixir-$ELIXIR_VERSION" && \
make && make install
Ну и дальше собираем релиз в этом образе
## Build the app
RUN mkdir /app
WORKDIR /app
ARG RELEASE_NAME=my_app
ENV MIX_ENV=prod RELEASE_NAME=${RELEASE_NAME}
COPY ./mix.exs ./mix.lock /app
RUN mix deps.get && mix deps.compile
RUN mix esbuild.install && mix tailwind.install
COPY . /app
RUN mix compile
RUN mix assets.deploy && mix phx.digest
RUN mix release $RELEASE_NAME
RUN tar -C "./_build/prod/rel/${RELEASE_NAME}" -cf release.tar ./
Собираем образ и вытаскиваем релиз из докера
docker build -t my_app_arm64 . -f Dockerfile.arm64
docker run --rm -v $(pwd):/build my_app_arm64 bash -c "cp /app/release.tar /build/release.tar"
Работать будет долго
Очень долго. Надо потерпеть. Или купить машинку для сборки вместо ноутбука.
Получилось не всё
Шаг RUN mix assets.deploy
у меня застревает — думаю, проще ассеты будет собрать на родной платформе (там всё равно кроме css и js ничего нет) и скопировать в образ перед сборкой релиза.
Для этого в контейнере с родной архитектурой делаем MIX_ENV=prod mix do assets.deploy, mix phx.digest
, а в Dockerfile.arm64 вместо RUN mix assets.deploy
делаем COPY ./priv/static /app/priv/static
.
Полная команда получается такая:
docker build -t my_app .
docker run --rm -v $(pwd):/app my_app bash -c "MIX_ENV=prod mix assets.deploy"
docker build -t my_app_arm64 . -f Dockerfile.arm64
docker run --rm -v $(pwd):/build my_app_arm64 bash -c "cp /app/release.tar /build/release.tar"