Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 89 additions & 7 deletions .github/workflows/precompile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
workflow_dispatch:

permissions:
contents: write
contents: read

jobs:
build:
Expand All @@ -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 }

Expand Down Expand Up @@ -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 }}
Expand Down Expand Up @@ -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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
9 changes: 8 additions & 1 deletion lib/quickbeam/native.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
17 changes: 17 additions & 0 deletions scripts/smoke_precompiled_native.exs
Original file line number Diff line number Diff line change
@@ -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")
Loading