Skip to content

Commit 1fc7e15

Browse files
authored
Merge pull request #75 from ausimian/feature/precompiled-nif
Precompiled NIF: hex consumers download the linked NIF, source builds use an MLX git dep
2 parents c11df76 + 75e0e70 commit 1fc7e15

12 files changed

Lines changed: 581 additions & 443 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,15 @@ jobs:
3232
matrix:
3333
include:
3434
- os: macos-14
35-
variant: no-jit
35+
variant: aot
3636
- os: macos-26
3737
variant: jit
3838
env:
3939
MIX_ENV: test
40+
EMILY_MLX_VARIANT: ${{ matrix.variant }}
4041
steps:
4142
- uses: actions/checkout@v6
4243

43-
- name: Select MLX variant
44-
if: matrix.variant == 'jit'
45-
run: |
46-
cat > config/local.exs <<'EOF'
47-
import Config
48-
config :emily, mlx_variant: :jit
49-
EOF
50-
5144
- name: Setup Beam
5245
uses: erlef/setup-beam@v1
5346
with:
@@ -100,33 +93,6 @@ jobs:
10093
# that no Nx op on the transformer critical path has regressed.
10194
- run: mix test --only conformance
10295

103-
# Tagged-release gate: exercise the Hex tarball the way a downstream
104-
# consumer would — unpack it into a throwaway project, consume it via
105-
# a path dep, download the MLX prebuilt, and run a sanity snippet
106-
# against Emily.Backend + Emily.Compiler. Catches missing package
107-
# files or a broken prebuilt release before `mix hex.publish`.
108-
smoke-test:
109-
name: Hex package smoke test
110-
if: github.event_name == 'push' && github.ref_type == 'tag'
111-
runs-on: macos-14
112-
steps:
113-
- uses: actions/checkout@v6
114-
115-
- name: Setup Beam
116-
uses: erlef/setup-beam@v1
117-
with:
118-
version-file: .tool-versions
119-
version-type: strict
120-
121-
- name: Cache MLX prebuilt
122-
uses: actions/cache@v5
123-
with:
124-
path: ~/Library/Caches/emily
125-
key: mlx-smoke-${{ runner.os }}-${{ hashFiles('c_src/**', 'Makefile', 'mix.exs') }}
126-
restore-keys: mlx-smoke-${{ runner.os }}-
127-
128-
- run: scripts/smoke-test-package.sh
129-
13096
# Manual-only job: exercises the heavy `*_full` conformance and
13197
# training variants excluded from the default suite (see
13298
# test/test_helper.exs). These download full-size checkpoints
@@ -142,22 +108,15 @@ jobs:
142108
matrix:
143109
include:
144110
- os: macos-14
145-
variant: no-jit
111+
variant: aot
146112
- os: macos-26
147113
variant: jit
148114
env:
149115
MIX_ENV: test
116+
EMILY_MLX_VARIANT: ${{ matrix.variant }}
150117
steps:
151118
- uses: actions/checkout@v6
152119

153-
- name: Select MLX variant
154-
if: matrix.variant == 'jit'
155-
run: |
156-
cat > config/local.exs <<'EOF'
157-
import Config
158-
config :emily, mlx_variant: :jit
159-
EOF
160-
161120
- name: Setup Beam
162121
uses: erlef/setup-beam@v1
163122
with:

.github/workflows/release-mlx.yml

Lines changed: 0 additions & 99 deletions
This file was deleted.

.github/workflows/release-nif.yml

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
name: Release NIF
2+
3+
# Builds the precompiled NIF for each supported (variant × target)
4+
# cell and publishes the resulting tarballs. Two triggers:
5+
#
6+
# * `push` on a bare-semver tag (e.g. 0.3.0) — the publication run.
7+
# Tarballs and matching `.sha256` sidecars land on a draft GitHub
8+
# release named after the tag, at the URL mix.exs's
9+
# `compile.emily_nif` step fetches from.
10+
#
11+
# * `workflow_dispatch` — manual rebuild against any branch. Useful
12+
# for debugging or reproducing a release without cutting a new
13+
# tag. Artefacts go to workflow run storage (retention 90 days);
14+
# no GitHub release is touched.
15+
#
16+
# Consumers verify each tarball against the sidecar fetched from
17+
# the same release, so there is no checksum list to bake into
18+
# mix.exs on the maintainer side.
19+
20+
on:
21+
push:
22+
tags: ['[0-9]+.[0-9]+.[0-9]+*']
23+
workflow_dispatch:
24+
25+
permissions:
26+
contents: write
27+
28+
jobs:
29+
# Resolves the target version once and — on tag trigger only —
30+
# creates the draft release up front so the matrix cells don't race
31+
# on `gh release create`. On workflow_dispatch the release step is
32+
# a no-op; there's no tag to create one against.
33+
prepare-release:
34+
runs-on: ubuntu-latest
35+
outputs:
36+
version: ${{ steps.version.outputs.version }}
37+
steps:
38+
- uses: actions/checkout@v6
39+
40+
- name: Resolve version
41+
id: version
42+
run: |
43+
if [[ "${{ github.event_name }}" == "push" ]]; then
44+
VERSION="${GITHUB_REF_NAME}"
45+
else
46+
VERSION=$(grep -m1 '@version' mix.exs | awk -F'"' '{print $2}')
47+
if [[ -z "$VERSION" ]]; then
48+
echo "error: could not parse @version from mix.exs" >&2
49+
exit 1
50+
fi
51+
fi
52+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
53+
echo "Resolved version: $VERSION"
54+
55+
- name: Create or reuse draft release
56+
if: github.event_name == 'push'
57+
env:
58+
GH_TOKEN: ${{ github.token }}
59+
TAG: ${{ steps.version.outputs.version }}
60+
REPO: ${{ github.repository }}
61+
run: |
62+
if ! gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then
63+
gh release create "$TAG" \
64+
--repo "$REPO" \
65+
--draft \
66+
--title "Emily $TAG" \
67+
--notes "Precompiled NIF tarballs for macOS arm64 (AOT + JIT variants)."
68+
fi
69+
70+
build:
71+
needs: prepare-release
72+
strategy:
73+
fail-fast: false
74+
matrix:
75+
include:
76+
# AOT runs on macos-14 so the metallib works back to older
77+
# macOS; JIT runs on macos-26 because MLX's NAX kernel
78+
# sources transitively include
79+
# <MetalPerformancePrimitives/MetalPerformancePrimitives.h>,
80+
# first shipped in the macOS 26.2 SDK.
81+
- { variant: aot, target: macos-arm64, runs-on: macos-14 }
82+
- { variant: jit, target: macos-arm64, runs-on: macos-26 }
83+
runs-on: ${{ matrix.runs-on }}
84+
env:
85+
MIX_ENV: dev
86+
EMILY_MLX_VARIANT: ${{ matrix.variant }}
87+
VERSION: ${{ needs.prepare-release.outputs.version }}
88+
steps:
89+
- uses: actions/checkout@v6
90+
91+
- uses: erlef/setup-beam@v1
92+
with:
93+
version-file: .tool-versions
94+
version-type: strict
95+
96+
- name: Cache deps
97+
uses: actions/cache@v5
98+
with:
99+
path: deps
100+
key: deps-${{ runner.os }}-${{ hashFiles('mix.lock') }}
101+
restore-keys: deps-${{ runner.os }}-
102+
103+
# MLX install dir keyed by the NIF sources, Makefile, build
104+
# script, and the pinned MLX version in mix.exs.
105+
- name: Cache MLX install
106+
uses: actions/cache@v5
107+
with:
108+
path: ~/Library/Caches/emily
109+
key: mlx-release-${{ runner.os }}-${{ matrix.variant }}-${{ hashFiles('c_src/**', 'Makefile', 'mix.exs', 'scripts/build-mlx.sh') }}
110+
restore-keys: mlx-release-${{ runner.os }}-${{ matrix.variant }}-
111+
112+
- run: mix deps.get
113+
114+
- run: mix compile
115+
116+
- name: Package NIF tarball
117+
id: pack
118+
env:
119+
VARIANT: ${{ matrix.variant }}
120+
TARGET: ${{ matrix.target }}
121+
run: |
122+
ASSET="emily-nif-${VERSION}-${VARIANT}-${TARGET}.tar.gz"
123+
PRIV="_build/dev/lib/emily/priv"
124+
125+
if [[ ! -d "$PRIV" ]]; then
126+
echo "error: expected $PRIV after mix compile" >&2
127+
exit 1
128+
fi
129+
130+
# Flat tarball (no wrapping dir) — mix.exs's fetch_nif/1
131+
# extracts with `tar -xzf ... -C priv` and expects files at
132+
# the top level.
133+
(cd "$PRIV" && tar czf "$GITHUB_WORKSPACE/$ASSET" libemily.* mlx.metallib)
134+
135+
shasum -a 256 "$ASSET" > "${ASSET}.sha256"
136+
echo "asset=$ASSET" >> "$GITHUB_OUTPUT"
137+
138+
{
139+
echo "## ${VARIANT}-${TARGET}"
140+
echo ""
141+
echo '```'
142+
cat "${ASSET}.sha256"
143+
echo '```'
144+
} >> "$GITHUB_STEP_SUMMARY"
145+
146+
# On tag push, publish to the draft GitHub release so consumers
147+
# can fetch. On workflow_dispatch, stash as workflow artefacts
148+
# for maintainer collection — no release side-effects.
149+
- name: Upload to draft release
150+
if: github.event_name == 'push'
151+
env:
152+
GH_TOKEN: ${{ github.token }}
153+
TAG: ${{ needs.prepare-release.outputs.version }}
154+
REPO: ${{ github.repository }}
155+
ASSET: ${{ steps.pack.outputs.asset }}
156+
run: gh release upload "$TAG" "$ASSET" "${ASSET}.sha256" --repo "$REPO" --clobber
157+
158+
- name: Upload as workflow artefact
159+
if: github.event_name == 'workflow_dispatch'
160+
uses: actions/upload-artifact@v4
161+
with:
162+
name: ${{ steps.pack.outputs.asset }}
163+
path: |
164+
${{ steps.pack.outputs.asset }}
165+
${{ steps.pack.outputs.asset }}.sha256
166+
retention-days: 90

.gitignore

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@ emily-*.tar
2727
/priv/mlx/
2828
/priv/mlx.metallib
2929

30-
# Per-checkout config overrides (see config/config.exs)
31-
/config/local.exs
32-
3330
# Dialyzer PLTs
3431
/priv/plts/
3532

0 commit comments

Comments
 (0)