From 84ae3d4ac6eb51fd5bb08e1efa28167d713e5431 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Thu, 20 Aug 2026 11:46:18 -0400 Subject: [PATCH 1/3] fix(EXT-7): bundle musl NIF for Linux Burrito releases and container rustler_precompiled resolves the precompiled NIF target at compile time from the build host's system_architecture. Ubuntu runners report a glibc triplet, so the glibc mdex_native NIF was being bundled into Linux Burrito releases and the container image. The container runs Alpine (musl), and Burrito's own musl runtime is also musl-based, so the glibc NIF would fail to load at runtime on musl systems. Fix: pre-compile mdex_native with TARGET_ABI=musl before running mix release on each Linux target. mix release then sees mdex_native already compiled in _build/prod and skips it, picking up the musl .so. TARGET_ABI is scoped to just the mdex_native compile step because cc_precompiler (used by exqlite) also reads that env var but behaves incorrectly when only TARGET_ABI is set without TARGET_ARCH and TARGET_OS. Also add an Alpine/musl container smoke test after building the image to verify the musl NIF actually loads before publishing. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/main.yaml | 47 ++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 81065ed..8790c04 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -66,7 +66,10 @@ jobs: # (used by mdex_native) downloads the correct precompiled NIF for the # target platform. Cross-compiling from Ubuntu caused the Linux NIF to # be bundled into macOS/Windows releases, making MDEx unavailable at - # runtime (see CRY-40). + # runtime (see CRY-40). Linux targets also get a pre-compile step that + # forces the musl NIF variant, since Ubuntu runners auto-select the + # glibc NIF but Burrito Linux binaries run on musl-based systems (see + # EXT-7). matrix: include: - target: macos_aarch64 @@ -104,6 +107,25 @@ jobs: version: "0.16.0" - run: mix deps.get + - + # rustler_precompiled resolves the NIF target at compile time from + # :erlang.system_info(:system_architecture), which is always glibc on + # Ubuntu runners. Burrito bundles its own musl runtime for Linux and + # the container runs Alpine - both need the musl NIF, not glibc. + # TARGET_ABI=musl tells rustler_precompiled to download the musl + # variant. cc_precompiler (used by exqlite) also reads TARGET_ABI but + # only activates the override when TARGET_ARCH and TARGET_OS are ALSO + # set, so setting TARGET_ABI alone here is safe. Pre-compiling just + # this dep (not running mix release with the env) keeps exqlite's own + # compile path unaffected. mix release sees mdex_native already + # compiled and skips it, bundling the musl .so from _build/prod. See + # EXT-7. + if: startsWith(matrix.target, 'linux_') + name: Pre-compile mdex_native with the musl NIF + env: + MIX_ENV: prod + TARGET_ABI: musl + run: mix deps.compile mdex_native - name: Build the ${{ matrix.target }} target run: MIX_ENV=prod BURRITO_TARGET=${{ matrix.target }} mix release lc @@ -316,6 +338,16 @@ jobs: - run: mix deps.get working-directory: app + - + # Same reasoning as the burrito-build pre-compile step: Ubuntu runners + # default to the glibc NIF, but the container image runs Alpine (musl). + # See EXT-7. + name: Pre-compile mdex_native with the musl NIF + env: + MIX_ENV: prod + TARGET_ABI: musl + run: mix deps.compile mdex_native + working-directory: app - name: Build the linux_x86_64 target (container's payload) run: MIX_ENV=prod BURRITO_TARGET=linux_x86_64 mix release lc @@ -325,6 +357,19 @@ jobs: env: APP_VERSION: ${{ needs.burrito-package.outputs.tag_name }} run: ./ci/build_image.sh "${{ needs.burrito-package.outputs.tag_name }}" + - + # Verify the musl NIF actually loads on Alpine before publishing. + # Analogous to the linux_aarch64 native smoke test in burrito-build: + # `lc version` boots the OTP app, loads all NIFs (exqlite via + # elixir_make/Zig, mdex_native via rustler_precompiled with the musl + # NIF bundled by the pre-compile step above), prints the version, and + # exits 0. No API key or network needed. ci/build_image.sh prefers + # Podman, so the image lives in Podman's local storage; we run it + # directly rather than loading a tarball. See EXT-7. + name: Smoke-test the container image boots and NIFs load on Alpine/musl + timeout-minutes: 1 + run: | + podman run --rm "linear-cli:${{ needs.burrito-package.outputs.tag_name }}" lc version - # ci/build_image.sh prefers Podman over Docker (both are present on # GitHub-hosted runners), so the image above lives only in Podman's From 4e9bdfacfd27399a1cb5f5721c52b8461492adf4 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Thu, 20 Aug 2026 11:52:58 -0400 Subject: [PATCH 2/3] fix(EXT-7): fix pre-compile dep order and smoke test daemon mode - Add rustler_precompiled to the pre-compile commands: mix deps.compile only compiles the named deps (not their transitive deps), so mdex_native's compile-time dependency on RustlerPrecompiled must be listed explicitly. - Add -e LINEAR_CLI_DAEMON=false to the container smoke test: the image bakes in LINEAR_CLI_DAEMON=true, which causes the app to start as a daemon (ignoring the version arg and never exiting), timing out the step. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/main.yaml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 8790c04..8c5213f 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -118,14 +118,16 @@ jobs: # set, so setting TARGET_ABI alone here is safe. Pre-compiling just # this dep (not running mix release with the env) keeps exqlite's own # compile path unaffected. mix release sees mdex_native already - # compiled and skips it, bundling the musl .so from _build/prod. See - # EXT-7. + # compiled and skips it, bundling the musl .so from _build/prod. + # rustler_precompiled is listed first because mix deps.compile only + # compiles the named deps (not their transitive deps), and mdex_native + # uses it via `use RustlerPrecompiled` at compile time. See EXT-7. if: startsWith(matrix.target, 'linux_') name: Pre-compile mdex_native with the musl NIF env: MIX_ENV: prod TARGET_ABI: musl - run: mix deps.compile mdex_native + run: mix deps.compile rustler_precompiled mdex_native - name: Build the ${{ matrix.target }} target run: MIX_ENV=prod BURRITO_TARGET=${{ matrix.target }} mix release lc @@ -341,12 +343,13 @@ jobs: - # Same reasoning as the burrito-build pre-compile step: Ubuntu runners # default to the glibc NIF, but the container image runs Alpine (musl). - # See EXT-7. + # rustler_precompiled listed first for the same reason as there. See + # EXT-7. name: Pre-compile mdex_native with the musl NIF env: MIX_ENV: prod TARGET_ABI: musl - run: mix deps.compile mdex_native + run: mix deps.compile rustler_precompiled mdex_native working-directory: app - name: Build the linux_x86_64 target (container's payload) @@ -365,11 +368,14 @@ jobs: # NIF bundled by the pre-compile step above), prints the version, and # exits 0. No API key or network needed. ci/build_image.sh prefers # Podman, so the image lives in Podman's local storage; we run it - # directly rather than loading a tarball. See EXT-7. + # directly rather than loading a tarball. LINEAR_CLI_DAEMON is + # overridden to false (the image bakes in true so the default CMD + # starts the daemon; without this override the app ignores `version` + # and stays alive as a daemon, timing out the step). See EXT-7. name: Smoke-test the container image boots and NIFs load on Alpine/musl timeout-minutes: 1 run: | - podman run --rm "linear-cli:${{ needs.burrito-package.outputs.tag_name }}" lc version + podman run --rm -e LINEAR_CLI_DAEMON=false "linear-cli:${{ needs.burrito-package.outputs.tag_name }}" lc version - # ci/build_image.sh prefers Podman over Docker (both are present on # GitHub-hosted runners), so the image above lives only in Podman's From 12675d3a23ad085603e0312a1d028a0963d4e3c1 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Thu, 20 Aug 2026 11:55:45 -0400 Subject: [PATCH 3/3] fix(EXT-7): correct inaccurate cc_precompiler comment The comment claimed cc_precompiler only activates its TARGET_ABI override when all three of TARGET_ARCH, TARGET_OS, TARGET_ABI are set. That is wrong: its current_target_from_env fires when any of the three is set. The step is actually safe because (a) env: blocks are step-scoped so mix release does not inherit TARGET_ABI, and (b) mix deps.compile only compiles the named deps so cc_precompiler/exqlite never run during this step. Flagged by code review. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/main.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 8c5213f..a4ef606 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -113,11 +113,11 @@ jobs: # Ubuntu runners. Burrito bundles its own musl runtime for Linux and # the container runs Alpine - both need the musl NIF, not glibc. # TARGET_ABI=musl tells rustler_precompiled to download the musl - # variant. cc_precompiler (used by exqlite) also reads TARGET_ABI but - # only activates the override when TARGET_ARCH and TARGET_OS are ALSO - # set, so setting TARGET_ABI alone here is safe. Pre-compiling just - # this dep (not running mix release with the env) keeps exqlite's own - # compile path unaffected. mix release sees mdex_native already + # variant. cc_precompiler (used by exqlite) also reads TARGET_ABI, but + # is safe here for two reasons: (a) the env: block is step-scoped, so + # mix release in the next step does not inherit TARGET_ABI, and (b) mix + # deps.compile only compiles the named deps, not exqlite, so + # cc_precompiler never runs during this step. mix release sees mdex_native already # compiled and skips it, bundling the musl .so from _build/prod. # rustler_precompiled is listed first because mix deps.compile only # compiles the named deps (not their transitive deps), and mdex_native