fix: save full EXLA build cache (object files + libexla.so) in CI image
Some checks failed
Build and Push / Build CI test image (push) Successful in 35s
Build and Push / Build and Push Docker Image (push) Failing after 5m33s

EXLA's cached_make copies libexla.so but Make still compiles .o files
when they're missing from deps/exla/cache/ (they don't survive
mix deps.get). Save the pre-built cache/ directory at /opt/exla-cache/
in the CI image and restore it after mix deps.get in the test step.
With both .o files and libexla.so present, Make skips all C++ compilation.
This commit is contained in:
Graham McIntire 2026-08-05 10:05:41 -05:00
parent 88c46b9c1d
commit af648c0330
2 changed files with 21 additions and 27 deletions

View file

@ -141,6 +141,7 @@ jobs:
sh -euc '\ sh -euc '\
mkdir -p /app && cd /app && tar xf - && \ mkdir -p /app && cd /app && tar xf - && \
mix deps.get && \ mix deps.get && \
cp -a /opt/exla-cache/cache /app/deps/exla/cache && \
mix ecto.create --quiet && \ mix ecto.create --quiet && \
mix ecto.migrate --quiet && \ mix ecto.migrate --quiet && \
mix test' mix test'

View file

@ -3,67 +3,60 @@
# Pre-built CI test image. Extends the hexpm/elixir image with: # Pre-built CI test image. Extends the hexpm/elixir image with:
# * build-essential + git + curl + ca-certificates + cdo # * build-essential + git + curl + ca-certificates + cdo
# * hex + rebar # * hex + rebar
# * Precompiled EXLA NIF cached at ~/.cache/xla/ # * Precompiled EXLA NIF + object files at /opt/exla-cache/
# #
# When mix test runs with this image, EXLA's cached_make finds the # EXLA's cached_make copies libexla.so to deps/exla/cache/, but Make
# precompiled libexla.so and skips the OOM-prone g++ C++ compilation # still tries to compile .o files when they're missing (they don't
# entirely. Rebuild this image whenever mix.exs changes elixir/exla/xla # survive mix deps.get). We save the full cache/ directory and restore
# versions or c_src/ files change (the cache key auto-invalidates). # it in the test step before mix test runs.
# #
# Built by .forgejo/workflows/build-ci-image.yaml. # Rebuild whenever mix.exs changes elixir/exla/xla versions or c_src/
# files change (the cache key auto-invalidates).
#
# Built by .forgejo/workflows/build.yaml (build-ci-image job).
ARG ELIXIR_IMAGE="docker.io/hexpm/elixir:1.20.1-erlang-29.0.2-debian-trixie-20260518-slim" ARG ELIXIR_IMAGE="docker.io/hexpm/elixir:1.20.1-erlang-29.0.2-debian-trixie-20260518-slim"
FROM ${ELIXIR_IMAGE} AS builder FROM ${ELIXIR_IMAGE} AS builder
# Install build tools and runtime test deps (cdo is needed by tests).
RUN export DEBIAN_FRONTEND=noninteractive && \ RUN export DEBIAN_FRONTEND=noninteractive && \
apt-get update -qq && \ apt-get update -qq && \
apt-get install -y -qq git curl ca-certificates build-essential cdo && \ apt-get install -y -qq git curl ca-certificates build-essential cdo && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Install hex + rebar
RUN mix local.hex --force && mix local.rebar --force RUN mix local.hex --force && mix local.rebar --force
# Precompile EXLA to populate ~/.cache/xla/ with both the downloaded
# XLA archive and the compiled libexla.so. We only need mix.exs +
# mix.lock + vendor + minimal config — no full project checkout.
WORKDIR /tmp/exla_warm WORKDIR /tmp/exla_warm
# Copy only what's needed for deps.get + deps.compile exla
COPY mix.exs mix.lock /tmp/exla_warm/ COPY mix.exs mix.lock /tmp/exla_warm/
COPY vendor /tmp/exla_warm/vendor COPY vendor /tmp/exla_warm/vendor
# Minimal config — nx needs :default_backend to exist
RUN mkdir -p /tmp/exla_warm/config && \ RUN mkdir -p /tmp/exla_warm/config && \
printf 'import Config\nconfig :nx, :default_backend, EXLA.Backend\n' > /tmp/exla_warm/config/config.exs printf 'import Config\nconfig :nx, :default_backend, EXLA.Backend\n' > /tmp/exla_warm/config/config.exs
# Fetch and compile all deps. EXLA's cached_make compiler calls # Compile all deps. EXLA's cached_make downloads the XLA archive, compiles
# Mix.Tasks.Compile.ElixirMake at build time, which requires elixir_make # the C++ NIF, and caches libexla.so at ~/.cache/xla/exla/{key}/.
# to be compiled first. mix deps.compile without arguments handles
# dependency ordering automatically. The cached_make step downloads the
# XLA archive, compiles the C++ NIF, and caches libexla.so at
# ~/.cache/xla/exla/{cache_key}/libexla.so.
RUN mix deps.get && mix deps.compile RUN mix deps.get && mix deps.compile
# Clean up the temp project — the cache in ~/.cache/xla/ persists in # Save the full build cache (libexla.so + .o files + xla_extension lib)
# the image and is what matters. # so Make finds all targets already built when restored in the test step.
RUN rm -rf /tmp/exla_warm RUN mkdir -p /opt/exla-cache && \
cp -a deps/exla/cache /opt/exla-cache/cache
FROM ${ELIXIR_IMAGE} AS final FROM ${ELIXIR_IMAGE} AS final
# Install runtime test deps
RUN export DEBIAN_FRONTEND=noninteractive && \ RUN export DEBIAN_FRONTEND=noninteractive && \
apt-get update -qq && \ apt-get update -qq && \
apt-get install -y -qq git curl ca-certificates build-essential cdo && \ apt-get install -y -qq git curl ca-certificates build-essential cdo && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Install hex + rebar
RUN mix local.hex --force && mix local.rebar --force RUN mix local.hex --force && mix local.rebar --force
# Copy the precompiled EXLA cache from the builder stage. This is the # XLA archive download + cached libexla.so (cached_make fast path).
# ~/.cache/xla/ directory containing the XLA archive download and the
# compiled libexla.so keyed by exact Elixir/ERTS/exla/xla/c_src versions.
COPY --from=builder /root/.cache/xla /root/.cache/xla COPY --from=builder /root/.cache/xla /root/.cache/xla
# Pre-built EXLA object files + libexla.so + xla_extension/ lib.
# Restored in the test step: cp -a /opt/exla-cache/cache /app/deps/exla/cache
COPY --from=builder /opt/exla-cache /opt/exla-cache
WORKDIR /app WORKDIR /app