Skip to content

Commit 3172d4e

Browse files
bougymanclaude
andauthored
fix(EXT-7): bundle musl NIF for Linux Burrito releases and container (#163)
## Summary - Pre-compile `mdex_native` with `TARGET_ABI=musl` before `mix release` on Linux targets so the musl precompiled NIF is bundled instead of the glibc one detected from Ubuntu runners - Same pre-compile step added to the `container` job (which also builds on Ubuntu but runs on Alpine) - Add an Alpine/musl container smoke test (`podman run ... lc version`) after building the image to verify the musl NIF loads before publishing ## Background `rustler_precompiled` resolves which precompiled NIF to bundle at **compile time** from `:erlang.system_info(:system_architecture)`. Ubuntu runners report a glibc triplet, so the glibc `mdex_native` NIF was being baked into Linux Burrito releases and the container image. The container runs Alpine 3.22 (musl), so the glibc NIF fails to load at runtime. `mdex_native` v0.2.7 ships both `x86_64-unknown-linux-gnu` and `x86_64-unknown-linux-musl` (and aarch64 equivalents). Setting `TARGET_ABI=musl` before compiling `mdex_native` selects the musl variant. `mix release` then sees `mdex_native` already compiled in `_build/prod/` and skips recompilation, bundling the musl `.so`. `TARGET_ABI` is scoped to the `mix deps.compile mdex_native` step rather than set globally on `mix release` because `cc_precompiler` (used by `exqlite`) also reads that env var and produces a malformed triplet (`--musl`) when `TARGET_ARCH`/`TARGET_OS` are absent. CRY-40 (v1.5.1) fixed the cross-OS NIF mismatch (Linux NIF going into macOS/Windows releases). This PR fixes the musl/glibc mismatch within Linux. ## Test plan - [x] YAML validates syntactically (`python3 yaml.safe_load`) - [x] `mix format --check-formatted` clean - [x] `mix credo --strict` clean - [x] Tests: same 7 pre-existing `GitTest` failures (require a specific git remote state), 300/307 pass — unchanged from main - [ ] CI: `burrito-build` Linux legs pre-compile mdex_native with musl NIF then build - [ ] CI: container smoke test runs `podman run ... lc version` on Alpine and exits 0 Closes EXT-7 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 31359ef commit 3172d4e

1 file changed

Lines changed: 52 additions & 1 deletion

File tree

.github/workflows/main.yaml

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ jobs:
6666
# (used by mdex_native) downloads the correct precompiled NIF for the
6767
# target platform. Cross-compiling from Ubuntu caused the Linux NIF to
6868
# be bundled into macOS/Windows releases, making MDEx unavailable at
69-
# runtime (see CRY-40).
69+
# runtime (see CRY-40). Linux targets also get a pre-compile step that
70+
# forces the musl NIF variant, since Ubuntu runners auto-select the
71+
# glibc NIF but Burrito Linux binaries run on musl-based systems (see
72+
# EXT-7).
7073
matrix:
7174
include:
7275
- target: macos_aarch64
@@ -104,6 +107,27 @@ jobs:
104107
version: "0.16.0"
105108
-
106109
run: mix deps.get
110+
-
111+
# rustler_precompiled resolves the NIF target at compile time from
112+
# :erlang.system_info(:system_architecture), which is always glibc on
113+
# Ubuntu runners. Burrito bundles its own musl runtime for Linux and
114+
# the container runs Alpine - both need the musl NIF, not glibc.
115+
# TARGET_ABI=musl tells rustler_precompiled to download the musl
116+
# variant. cc_precompiler (used by exqlite) also reads TARGET_ABI, but
117+
# is safe here for two reasons: (a) the env: block is step-scoped, so
118+
# mix release in the next step does not inherit TARGET_ABI, and (b) mix
119+
# deps.compile only compiles the named deps, not exqlite, so
120+
# cc_precompiler never runs during this step. mix release sees mdex_native already
121+
# compiled and skips it, bundling the musl .so from _build/prod.
122+
# rustler_precompiled is listed first because mix deps.compile only
123+
# compiles the named deps (not their transitive deps), and mdex_native
124+
# uses it via `use RustlerPrecompiled` at compile time. See EXT-7.
125+
if: startsWith(matrix.target, 'linux_')
126+
name: Pre-compile mdex_native with the musl NIF
127+
env:
128+
MIX_ENV: prod
129+
TARGET_ABI: musl
130+
run: mix deps.compile rustler_precompiled mdex_native
107131
-
108132
name: Build the ${{ matrix.target }} target
109133
run: MIX_ENV=prod BURRITO_TARGET=${{ matrix.target }} mix release lc
@@ -316,6 +340,17 @@ jobs:
316340
-
317341
run: mix deps.get
318342
working-directory: app
343+
-
344+
# Same reasoning as the burrito-build pre-compile step: Ubuntu runners
345+
# default to the glibc NIF, but the container image runs Alpine (musl).
346+
# rustler_precompiled listed first for the same reason as there. See
347+
# EXT-7.
348+
name: Pre-compile mdex_native with the musl NIF
349+
env:
350+
MIX_ENV: prod
351+
TARGET_ABI: musl
352+
run: mix deps.compile rustler_precompiled mdex_native
353+
working-directory: app
319354
-
320355
name: Build the linux_x86_64 target (container's payload)
321356
run: MIX_ENV=prod BURRITO_TARGET=linux_x86_64 mix release lc
@@ -325,6 +360,22 @@ jobs:
325360
env:
326361
APP_VERSION: ${{ needs.burrito-package.outputs.tag_name }}
327362
run: ./ci/build_image.sh "${{ needs.burrito-package.outputs.tag_name }}"
363+
-
364+
# Verify the musl NIF actually loads on Alpine before publishing.
365+
# Analogous to the linux_aarch64 native smoke test in burrito-build:
366+
# `lc version` boots the OTP app, loads all NIFs (exqlite via
367+
# elixir_make/Zig, mdex_native via rustler_precompiled with the musl
368+
# NIF bundled by the pre-compile step above), prints the version, and
369+
# exits 0. No API key or network needed. ci/build_image.sh prefers
370+
# Podman, so the image lives in Podman's local storage; we run it
371+
# directly rather than loading a tarball. LINEAR_CLI_DAEMON is
372+
# overridden to false (the image bakes in true so the default CMD
373+
# starts the daemon; without this override the app ignores `version`
374+
# and stays alive as a daemon, timing out the step). See EXT-7.
375+
name: Smoke-test the container image boots and NIFs load on Alpine/musl
376+
timeout-minutes: 1
377+
run: |
378+
podman run --rm -e LINEAR_CLI_DAEMON=false "linear-cli:${{ needs.burrito-package.outputs.tag_name }}" lc version
328379
-
329380
# ci/build_image.sh prefers Podman over Docker (both are present on
330381
# GitHub-hosted runners), so the image above lives only in Podman's

0 commit comments

Comments
 (0)