diff --git a/.github/workflows/precompile.yml b/.github/workflows/precompile.yml index ff73bd63..43185121 100644 --- a/.github/workflows/precompile.yml +++ b/.github/workflows/precompile.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: permissions: - contents: write + contents: read jobs: build: @@ -16,7 +16,9 @@ jobs: matrix: include: - { target: x86_64-linux-gnu, os: ubuntu-latest } + - { target: x86_64-linux-musl, os: ubuntu-latest } - { target: aarch64-linux-gnu, os: ubuntu-latest } + - { target: aarch64-linux-musl, os: ubuntu-latest } - { target: aarch64-macos-none, os: macos-15 } - { target: x86_64-windows-gnu, os: windows-2022 } @@ -69,12 +71,6 @@ jobs: path: artifacts/*.tar.gz if-no-files-found: error - - name: Upload to release - if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@v1 - with: - files: artifacts/*.tar.gz - smoke: needs: build runs-on: ${{ matrix.os }} @@ -112,3 +108,89 @@ jobs: rm checksum-QuickBEAM.Native.exs mix compile mix run scripts/smoke_precompiled.exs + + # Alpine has no runner, so the artifact is loaded in a musl container. The + # project is reduced to the native module: a full checkout would compile + # the Hex dependencies for musl, which is the problem being tested around. + smoke-musl: + name: Smoke ${{ matrix.target }} on Alpine + needs: build + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - { target: x86_64-linux-musl, os: ubuntu-latest, platform: linux/amd64 } + - { target: aarch64-linux-musl, os: ubuntu-24.04-arm, platform: linux/arm64 } + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: ${{ matrix.target }} + path: artifacts + + - name: Load and exercise the precompiled NIF on Alpine + shell: bash + env: + ALPINE_IMAGE: hexpm/elixir:1.18.3-erlang-27.3-alpine-3.21.7 + run: | + smoke_dir="${{ runner.temp }}/quickbeam-musl-smoke" + mkdir -p "$smoke_dir/lib/quickbeam/native" "$smoke_dir/artifacts" "$smoke_dir/scripts" + cp lib/quickbeam/native.ex "$smoke_dir/lib/quickbeam/native.ex" + cp lib/quickbeam/native/platform.ex "$smoke_dir/lib/quickbeam/native/platform.ex" + cp scripts/smoke_precompiled_native.exs "$smoke_dir/scripts/smoke.exs" + cp artifacts/*.tar.gz "$smoke_dir/artifacts/" + + cat > "$smoke_dir/mix.exs" <<'ELIXIR' + defmodule QuickBEAMMuslSmoke.MixProject do + use Mix.Project + + def project do + [ + app: :quickbeam, + version: System.fetch_env!("QUICKBEAM_VERSION"), + elixir: "~> 1.15", + deps: [{:zigler_precompiled, "~> 0.1.6"}] + ] + end + + def application, do: [extra_applications: [:logger, :inets, :ssl, :public_key]] + end + ELIXIR + + quickbeam_version=$(sed -n 's/^ @version "\(.*\)"/\1/p' mix.exs | head -n1) + + docker run --rm --platform "${{ matrix.platform }}" \ + -v "$smoke_dir:/workspace" \ + -w /workspace \ + -e QUICKBEAM_VERSION="$quickbeam_version" \ + -e ZIGLER_PRECOMPILED_GLOBAL_CACHE_PATH=/tmp/quickbeam-precompiled \ + "$ALPINE_IMAGE" /bin/sh -lc ' + set -eu + mix local.hex --force + mix local.rebar --force + mix deps.get + mkdir -p "$ZIGLER_PRECOMPILED_GLOBAL_CACHE_PATH" + cp artifacts/*.tar.gz "$ZIGLER_PRECOMPILED_GLOBAL_CACHE_PATH/" + mix compile + mix run scripts/smoke.exs + ' + + # Artifacts reach the release only after every target loaded and ran. + release: + if: startsWith(github.ref, 'refs/tags/') + needs: [build, smoke, smoke-musl] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - name: Upload artifacts to release + uses: softprops/action-gh-release@v2 + with: + files: artifacts/*.tar.gz diff --git a/CHANGELOG.md b/CHANGELOG.md index 23c5d289..816ff863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Unreleased + +### Added + +- Precompiled NIFs for `x86_64-linux-musl` and `aarch64-linux-musl`, so Alpine images need no Zig. Each release's artifacts are loaded and run on Alpine before they are published ([#24](https://github.com/elixir-volt/quickbeam/issues/24)). + +### Changed + +- Allow `oxc` 0.19. + ## 0.11.2 - 2026-09-30 ### Changed diff --git a/README.md b/README.md index b0ecb9f5..67e1e64f 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ def deps do end ``` -Precompiled NIFs target Zig's baseline CPU for each supported architecture. +Precompiled NIFs target Zig's baseline CPU and cover x86-64 and ARM64 Linux +with glibc or musl (Alpine included), ARM64 macOS, and x86-64 Windows. Source builds require Zig 0.16 and default to the build machine's CPU; set `QUICKBEAM_CPU=baseline` alongside `QUICKBEAM_BUILD=1` for a portable build. When changing CPU or optimization settings, force recompilation with `mix compile --force`. diff --git a/lib/quickbeam/native.ex b/lib/quickbeam/native.ex index 1392691d..4bd52b62 100644 --- a/lib/quickbeam/native.ex +++ b/lib/quickbeam/native.ex @@ -138,7 +138,14 @@ defmodule QuickBEAM.Native do base_url: "https://github.com/elixir-volt/quickbeam/releases/download/v#{@version}", version: @version, force_build: System.get_env("QUICKBEAM_BUILD") in ["1", "true"], - targets: ~w(x86_64-linux-gnu aarch64-linux-gnu aarch64-macos-none x86_64-windows-gnu), + targets: ~w( + x86_64-linux-gnu + x86_64-linux-musl + aarch64-linux-gnu + aarch64-linux-musl + aarch64-macos-none + x86_64-windows-gnu + ), zig_code_path: "quickbeam.zig", optimize: :env, build_flags: @build_flags, diff --git a/mix.exs b/mix.exs index 05e85e32..933679d8 100644 --- a/mix.exs +++ b/mix.exs @@ -73,7 +73,7 @@ defmodule QuickBEAM.MixProject do {:json_codec, "~> 0.3", optional: true}, {:yaml_elixir, "~> 2.12", only: :test}, {:varint, "~> 1.6"}, - {:oxc, "~> 0.17.8 or ~> 0.18.0"}, + {:oxc, "~> 0.17.8 or ~> 0.18.0 or ~> 0.19.0"}, {:npm, "~> 0.7.6", optional: true}, {:mint, "~> 1.10"}, {:mint_web_socket, "~> 1.0"}, diff --git a/mix.lock b/mix.lock index 33119cf7..a0b7eb83 100644 --- a/mix.lock +++ b/mix.lock @@ -29,7 +29,7 @@ "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "npm": {:hex, :npm, "0.7.6", "de8ca3dd26a40fac7ab8a17680088b8d55bab0e0294be9770b029c276361482f", [:mix], [{:hex_solver, "~> 0.2", [hex: :hex_solver, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:npm_semver, "~> 0.1.0", [hex: :npm_semver, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "e0a1ce77e3e49ec108e095aba133bd315e86da298dba26b142a105972141d9ef"}, "npm_semver": {:hex, :npm_semver, "0.1.0", "3ab2c2a151d8c87c364209b2ca1a4fd2ab98507ed61afbd1ea12c1826e67200a", [:mix], [{:hex_solver, "~> 0.2", [hex: :hex_solver, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "77afbc4c523c19a572325190bc4c968ec027e1c6ef8538bcddacf835966072fa"}, - "oxc": {:hex, :oxc, "0.18.0", "0b22ae1ac62f6e985458aae53bb0020756bd790ed07bcda1a89168b266025415", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:json_codec, "~> 0.3", [hex: :json_codec, repo: "hexpm", optional: false]}, {:rustler, "~> 0.36", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "121495bbbaba0807433653d4446cb9ed68bc7db35e6f11a9c82da56f89bcaa6c"}, + "oxc": {:hex, :oxc, "0.19.0", "9786fc88ec3da84da1fec5e08bb94b108e3917288cdac07d62842edfcd2500ac", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:json_codec, "~> 0.3", [hex: :json_codec, repo: "hexpm", optional: false]}, {:rustler, "~> 0.36", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "ffeec25c488d87144fc12e14d30bca92997576303be2458e80e6e84dd756a296"}, "pegasus": {:hex, :pegasus, "0.2.6", "b4af6522326fbb2ffd1bb706e78ec05854fbcb4b03a7fe08f8db2e9de1d0be67", [:mix], [{:nimble_parsec, "~> 1.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0ac159f0ccab7967cf90208327cc8a35788874814c8d78e19d47104d3fc049b9"}, "plug": {:hex, :plug, "1.20.3", "56c480c633ec2ce10140e236e15233bf576e1d323887d7c96711bd02ab5160db", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "be266aee1b8536ef6409d58cf39a3121319f0ec47cfa1b24024485aa0e76ad76"}, "plug_crypto": {:hex, :plug_crypto, "2.2.0", "144014737daaf485407f5ed77daeaad74d651b216a28c87543f8cc7043f8efc8", [:mix], [], "hexpm", "83a95744ab1c75876542b6fab135fcc176280e0f301a111c1f757fddcec95d2c"}, diff --git a/scripts/smoke_precompiled_native.exs b/scripts/smoke_precompiled_native.exs new file mode 100644 index 00000000..5692632b --- /dev/null +++ b/scripts/smoke_precompiled_native.exs @@ -0,0 +1,17 @@ +{:module, QuickBEAM.Native} = Code.ensure_loaded(QuickBEAM.Native) + +resource = QuickBEAM.Native.start_runtime(self(), %{}) +ref = QuickBEAM.Native.eval(resource, "40 + 2", 0, "") + +receive do + {^ref, {:ok, 42}} -> + :ok + + {^ref, other} -> + raise "unexpected eval response: #{inspect(other)}" +after + 30_000 -> raise "NIF eval timed out" +end + +QuickBEAM.Native.stop_runtime(resource) +IO.puts("Precompiled NIF: load, startup, and eval passed")