Skip to content
Open
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
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ jobs:
otp: ["27.1.2"]
env:
MIX_ENV: test
# Force source build. CI validates the source path end-to-end;
# precompiled artifacts are exercised separately in release.yml.
ORTEX_BUILD: "true"
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v6
- uses: erlef/setup-beam@v1
with:
otp-version: ${{ matrix.otp }}
Expand All @@ -33,8 +36,11 @@ jobs:
name: macOS
env:
MIX_ENV: test
# Force source build. CI validates the source path end-to-end;
# precompiled artifacts are exercised separately in release.yml.
ORTEX_BUILD: "true"
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v6
- name: Install
run: |
brew update
Expand Down
107 changes: 107 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
name: Build precompiled NIFs

on:
push:
branches:
- main
paths:
# Only run on main when the native crate or this workflow itself
# changes. Tag pushes always run (via the tags trigger below).
- "native/**"
- ".github/workflows/release.yml"
tags:
- "v*"
pull_request:
paths:
- "native/**"
- ".github/workflows/release.yml"

# softprops/action-gh-release creates the GitHub release on tag
# pushes, which requires write access to repository contents. Since
# 2023 the default GITHUB_TOKEN scope has been read-only, so the
# permission must be granted explicitly.
permissions:
contents: write

jobs:
build_release:
name: NIF ${{ matrix.nif }} - ${{ matrix.job.target }} (${{ matrix.job.os }})
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
# NIF version 2.16 covers OTP 24-26; 2.17 covers OTP 27-28.
# Both contemporary versions are built so consumers across the
# range get a precompiled artifact.
nif: ["2.16", "2.17"]
job:
# Apple Silicon. macos-14 runners are arm64 natively.
- { target: aarch64-apple-darwin, os: macos-14 }
# Intel Mac. macos-13 was retired by GitHub on Dec 4 2025;
# macos-15-intel is the replacement and is the last available
# Intel-architecture macOS runner (supported through Aug 2027).
# See https://github.com/actions/runner-images/issues/13045.
- { target: x86_64-apple-darwin, os: macos-15-intel }
# Linux x86_64. Built on ubuntu-22.04 — produces a binary
# that requires glibc >= 2.35. Could be moved to a
# manylinux2014 container if older-distro support becomes
# a goal.
- { target: x86_64-unknown-linux-gnu, os: ubuntu-22.04 }
# Linux ARM64 via cross-rs (cross-compiled from x86_64 runner).
- { target: aarch64-unknown-linux-gnu, os: ubuntu-22.04, use-cross: true }
# Windows x86_64 (MSVC ABI). Covers >95% of Windows users.
- { target: x86_64-pc-windows-msvc, os: windows-2022 }

steps:
- name: Checkout source
uses: actions/checkout@v6

- name: Extract project version from mix.exs
id: version
shell: bash
run: |
version=$(sed -n 's/^ @version "\(.*\)"$/\1/p' mix.exs | head -1)
if [ -z "$version" ]; then
echo "Failed to extract @version from mix.exs" >&2
exit 1
fi
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "Project version: ${version}"

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.job.target }}

# `philss/rustler-precompiled-action` is the standard build +
# package step used by every rustler_precompiled crate
# (Tokenizers, Polars, Explorer, ...). It runs cargo (or cross
# for Linux ARM64), then tarballs the resulting NIF with the
# naming convention RustlerPrecompiled expects on the consumer
# side. With ort 2.0.0-rc.x and stable rustc onnxruntime is
# statically linked into the NIF, so the single-file tarball
# the action produces is the complete artifact.
- name: Build NIF
id: build-crate
uses: philss/rustler-precompiled-action@v1.1.5
with:
project-name: ortex
project-version: ${{ steps.version.outputs.version }}
target: ${{ matrix.job.target }}
nif-version: ${{ matrix.nif }}
use-cross: ${{ matrix.job.use-cross }}
project-dir: "native/ortex"

- name: Upload artifact (workflow run)
uses: actions/upload-artifact@v7
with:
name: ${{ steps.build-crate.outputs.file-name }}
path: ${{ steps.build-crate.outputs.file-path }}

- name: Attach to GitHub release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v3
with:
draft: true
files: ${{ steps.build-crate.outputs.file-path }}

5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ ortex-*.tar

# Temporary files, for example, from tests.
/tmp/

# Source-build artifacts — populated by `cargo build` + copy_ort_libs/0
# when ORTEX_BUILD=true. Precompiled-path consumers receive priv/native/
# contents from the published tarballs at install time, not via git.
/priv/native/
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Changelog

## v0.1.11

### Added

* Precompiled NIF artifacts for common platforms via [`rustler_precompiled`](https://hex.pm/packages/rustler_precompiled). Consumers on `aarch64-apple-darwin`, `x86_64-apple-darwin`, `x86_64-unknown-linux-gnu` (glibc >= 2.35), `aarch64-unknown-linux-gnu` (glibc >= 2.35), and `x86_64-pc-windows-msvc` no longer need a Rust toolchain — `mix deps.get` downloads a prebuilt NIF binary instead. `onnxruntime` is statically linked into the artifact so there's no separate runtime install.

* `ORTEX_BUILD=true` environment variable to force source compilation, useful for development, unsupported targets, or when building with a non-default execution-provider feature flag (`cuda`, `tensorrt`, `coreml`, `directml`).

* `RELEASE.md` documents the publish process — version bump, tag push, CI matrix, checksum-file regeneration, smoke-test, hex publish.

### Changed

* `:rustler` is now an `optional` dependency. Consumers using precompiled NIFs don't pull it in.

* `:rustler_precompiled` added as a runtime dependency.

* `lib/ortex/native.ex` branches on `ORTEX_BUILD` between `use RustlerPrecompiled` (default) and `use Rustler` + the legacy `compile_crate/copy_ort_libs` dance (source path). Both paths are tested in CI.

* New `.github/workflows/release.yml` builds a 5-target × 2 NIF-version matrix on tag push and uploads tarballs to a draft GitHub release.

### Notes for consumers

If you've been pinning `ortex` at `~> 0.1.10` and your platform is in the supported precompile list above, upgrading to `0.1.11` will silently switch you from a 30-90s Rust build to a sub-second NIF download on every `mix deps.compile`. Behaviour is otherwise unchanged.

If your platform is **not** in the supported list, `mix` falls back to source build automatically — no action needed, but you'll keep needing a Rust toolchain. Consider opening an issue if your target should be added.

## v0.1.10

* Support Elixir 1.19 — switch to `Inspect.Algebra.to_doc` syntax (#48 by @zentourist).

## v0.1.9 and earlier

See git history.
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,34 @@ iex> result |> Nx.backend_transfer() |> Nx.argmax(axis: 1)
```elixir
def deps do
[
{:ortex, "~> 0.1.10"}
{:ortex, "~> 0.1.11"}
]
end
```

You will need [Rust](https://www.rust-lang.org/tools/install) for compilation to succeed.
### Precompiled NIFs

Starting with `0.1.11`, Ortex ships **precompiled NIF artifacts** for common platforms. If you're on one of the supported targets you do **not** need a Rust toolchain — `mix deps.get` downloads a prebuilt binary directly.

Supported targets:

* `aarch64-apple-darwin` (Apple Silicon macOS)
* `x86_64-apple-darwin` (Intel macOS)
* `x86_64-unknown-linux-gnu` (Linux x86_64, glibc >= 2.35)
* `aarch64-unknown-linux-gnu` (Linux ARM64, glibc >= 2.35)
* `x86_64-pc-windows-msvc` (Windows x86_64, MSVC ABI)

`onnxruntime` is statically linked into the precompiled NIF, so the artifact is a single self-contained shared library — no separate runtime install required.

### Source build

You'll need [Rust](https://www.rust-lang.org/tools/install) and a working C toolchain to build from source. This happens automatically when:

* Your target tuple isn't in the precompiled list above (e.g. musl Linux, 32-bit ARM, FreeBSD).
* You explicitly request it with `ORTEX_BUILD=true`:

```bash
ORTEX_BUILD=true mix deps.compile ortex --force
```

The source path is the same flow that's shipped since 0.1.0 — `cargo build` downloads `onnxruntime` and the resulting NIF is dropped into `priv/native/`. Useful for development on the NIF crate itself, or when you need a feature flag (`cuda`, `tensorrt`, `coreml`, `directml`) that the precompiled CPU-only artifact doesn't include.
116 changes: 116 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Release process

Ortex publishes a Rust NIF that depends on `onnxruntime`. As of `ort` 2.0.0-rc.x, `onnxruntime` is statically linked into the NIF binary, so the artifact tarballs contain a single self-contained shared library and no sidecar dependencies.

This document is the runbook for cutting a release.

## Supported precompile targets

| Target | Runner | Notes |
|---|---|---|
| `aarch64-apple-darwin` | `macos-14` | Apple Silicon, native build |
| `x86_64-apple-darwin` | `macos-13` | Intel Mac, native build |
| `x86_64-unknown-linux-gnu` | `ubuntu-22.04` | glibc >= 2.35; older distros fall back to source build |
| `aarch64-unknown-linux-gnu` | `ubuntu-22.04` (cross) | Cross-compiled via `cross-rs` |
| `x86_64-pc-windows-msvc` | `windows-2022` | Native build, MSVC ABI |

For each target we publish two NIF-version variants (`2.16` covering OTP 24-26, `2.17` covering OTP 27+), so each release produces 10 tarballs.

Targets outside this matrix automatically fall back to source build via the existing `ORTEX_BUILD` path. Consumers on those targets need a Rust toolchain — same as before precompilation existed.

## Cutting a release — step by step

### 1. Bump version

Edit `mix.exs` `@version` and update `CHANGELOG.md`. Commit on a release branch.

### 2. Push a pre-release tag

```bash
git tag v0.1.11-pre1
git push origin v0.1.11-pre1
```

The tag-push trigger in `.github/workflows/release.yml` runs the matrix build. Each successful job uploads its tarball to a **draft** GitHub release. Watch the workflow run; if any cell fails, fix and retag (`v0.1.11-pre2`, etc.).

### 3. Promote the draft release to a GitHub release (still pre-release flag)

In the GitHub UI, find the draft release for the pre-release tag and untoggle "Save as draft" (keep the "Set as a pre-release" flag). The artifacts are now downloadable at stable URLs.

### 4. Generate the checksum file

From a clean checkout of the release branch:

```bash
mix deps.get
mix rustler_precompiled.download Ortex.Native --all --print > checksum-Elixir.Ortex.Native.exs
```

The command pulls each tarball from the GitHub release URL pattern (defined by `base_url:` in `lib/ortex/native.ex`), computes its SHA-256, and writes the resulting map. Inspect the diff — every supported target × NIF-version combination should appear.

Commit the updated `checksum-Elixir.Ortex.Native.exs`.

### 5. Smoke-test the precompiled path locally

In a separate scratch directory:

```bash
# Without ORTEX_BUILD set — should download and use the precompiled NIF
mix new ortex_smoke && cd ortex_smoke
cat <<'EOF' >> mix.exs
# In deps/0:
{:ortex, path: "../ortex"}
EOF
mix deps.compile ortex --force
```

Should succeed without invoking Rust. If you have `cargo` on `PATH`, temporarily move it (`PATH=${PATH//$(dirname $(which cargo)):/}`) to confirm precompilation isn't silently falling back to source build.

### 6. Verify source fallback regression

```bash
ORTEX_BUILD=true mix deps.compile ortex --force
```

Should exercise the legacy path — `Rustler.Compiler.compile_crate/2` runs, `Ortex.Util.copy_ort_libs/0` runs (no-op when onnxruntime is statically linked), tests pass.

### 7. Promote the pre-release to a real release

Once smoke tests pass, push the final tag (`v0.1.11`), let the workflow re-run, promote the draft to a non-pre-release GitHub release. The checksum file already in the repo references the same artifact filenames; verify the SHA-256s still match (they should — same crate, same matrix).

### 8. Publish to Hex

```bash
mix hex.publish
```

The package now contains the `checksum-Elixir.Ortex.Native.exs` file; consumers will fetch precompiled NIFs by default.

## Updating the precompile matrix

Adding a new target means three things:

1. Add a new entry to `precompiled_targets` in `lib/ortex/native.ex`.
2. Add a corresponding `{ target: ..., os: ..., use-cross?: ... }` row to the matrix in `.github/workflows/release.yml`.
3. Re-run the release process — the new target's checksums end up in the next checksum file regeneration.

Removing a target is the inverse, with one caveat: existing consumers pinned to an older version of Ortex still rely on the artifact existing at the old release URL. Don't delete artifacts from past releases.

## Falling back to source build

Consumers who hit the source-build path get there in two ways:

1. Their target tuple isn't in the `precompiled_targets` list. RustlerPrecompiled detects this and prints a notice, then triggers a source build automatically.
2. They explicitly set `ORTEX_BUILD=true` before `mix deps.compile`. This is documented in the README as the manual override.

Both paths require a Rust toolchain and exercise the original `Rustler.Compiler.compile_crate/2` + `Ortex.Util.copy_ort_libs/0` flow. That path remains supported indefinitely — the precompiled artifacts are an optimisation, not a replacement.

## Common failure modes

* **"checksum mismatch" during install** — the GitHub release artifact was modified or rebuilt without the checksum file being regenerated. Solution: re-run step 4.

* **"no precompiled NIF available for ..."** — consumer is on a target outside the matrix. Solution: instruct them to set `ORTEX_BUILD=true` and ensure they have a Rust toolchain installed.

* **CI matrix cell fails for one target only** — fix it specifically; other targets' artifacts uploaded by the same workflow run are still valid. Re-tagging only re-runs *all* cells, which is wasteful but safe.

* **Old glibc on a Linux consumer** — the precompiled `x86_64-unknown-linux-gnu` artifact requires glibc >= 2.35 (set by the `ubuntu-22.04` runner image). To support older glibc, the workflow's Linux-x86_64 cell can be moved to a `manylinux_2_28` (or older) container; this is a follow-up worth doing if user reports come in.
21 changes: 21 additions & 0 deletions checksum-Elixir.Ortex.Native.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SHA-256 checksums for the precompiled NIF artifacts published to
# the project's GitHub releases. RustlerPrecompiled refuses to
# download any artifact whose checksum is not pinned here — this
# file is the integrity boundary between consumers and the release
# tarballs.
#
# This file is auto-populated as part of the release process; do not
# hand-edit. To regenerate after a tag's CI run finishes and the
# draft release has been promoted:
#
# ORTEX_BUILD=true mix rustler_precompiled.download \
# Ortex.Native --all
#
# (The ORTEX_BUILD=true is needed on the first run when the file is
# empty — without it, mix can't compile lib/ortex/native.ex because
# the precompiled-NIF download path tries to verify against an empty
# checksum map. Setting it forces source build during the bootstrap
# compile, after which the download task runs and writes the file.)
#
# Commit the result before running mix hex.publish.
%{}
Loading
Loading