From 118686ccdf1405f34f2f5b203ee19201c8dffc2d Mon Sep 17 00:00:00 2001 From: Kip Cole Date: Mon, 27 Apr 2026 08:26:14 +1000 Subject: [PATCH 1/2] Add precompiled NIF artifacts via rustler_precompiled (#27) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consumers on aarch64/x86_64 macOS, aarch64/x86_64 Linux glibc, and x86_64 Windows MSVC no longer need a Rust toolchain — Ortex downloads a prebuilt NIF from the GitHub release. Source build remains the fallback for unlisted targets and via ORTEX_BUILD=true. * lib/ortex/native.ex: use RustlerPrecompiled (handles both download and force_build paths). Ortex.Util.copy_ort_libs still runs after source build to stage libonnxruntime when the toolchain produces a dynamically-linked NIF. * mix.exs: add :rustler_precompiled, make :rustler optional, ship CHANGELOG.md and the checksum file in the package. * .github/workflows/release.yml: matrix builds 5 targets × 2 NIF versions on tag push, custom packaging step bundles libonnxruntime when present (no-op for static-linked builds), uploads to a draft release. * .github/workflows/ci.yml: forces source build via ORTEX_BUILD=true so PR validation isn't dependent on release artifacts existing. * checksum-Elixir.Ortex.Native.exs, RELEASE.md, CHANGELOG.md, README.md: pinned checksums, release runbook, changelog entry, precompile target list and source-build instructions. Verified end-to-end on Debian Bookworm with no Rust toolchain via a mix deps.compile + tinymodel.onnx inference test in Docker. --- .github/workflows/ci.yml | 10 +- .github/workflows/release.yml | 223 +++++++++++++++++++++++++++++++ .gitignore | 5 + CHANGELOG.md | 35 +++++ README.md | 29 +++- RELEASE.md | 116 ++++++++++++++++ checksum-Elixir.Ortex.Native.exs | 21 +++ lib/ortex/native.ex | 81 ++++++++--- lib/ortex/util.ex | 95 +++++++++---- mix.exs | 40 ++++-- mix.lock | 8 +- 11 files changed, 604 insertions(+), 59 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 CHANGELOG.md create mode 100644 RELEASE.md create mode 100644 checksum-Elixir.Ortex.Native.exs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a97f4a..6438609 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 }} @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bd88177 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,223 @@ +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. See the comment below about + # tightening this with a manylinux 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 }} + + - name: Install cross (Linux ARM64) + if: matrix.job.use-cross + shell: bash + run: cargo install --git https://github.com/cross-rs/cross --tag v0.2.5 cross + + # Build the NIF directly with cargo. With the rust-toolchain + # action's stable rustc, the `ort` 2.0.0-rc.x crate links + # onnxruntime statically into the NIF on every platform — the + # output is a single self-contained shared library (~20-25 MB). + # No `libonnxruntime` sidecar exists in target/release/ or + # anywhere else on disk after this step; the packaging step + # below correctly produces a single-file tarball as a result. + - name: Build NIF + shell: bash + working-directory: native/ortex + env: + RUSTLER_NIF_VERSION: ${{ matrix.nif }} + run: | + set -euo pipefail + if [ "${{ matrix.job.use-cross }}" = "true" ]; then + # Cross-compilation needs explicit --target. Output goes + # to target//release/. + cross build --release --target "${{ matrix.job.target }}" + else + # Native build. The runner's host arch matches + # matrix.job.target by design, so default cargo behaviour + # produces the right binary. Output goes to target/release/. + cargo build --release + fi + + # Stage the right files into a tarball matching RustlerPrecompiled's + # expected layout. Naming and contents follow the conventions in + # rustler_precompiled.ex (lib_name_with_ext/2 + lib_prefix/1): + # + # * Tarball name: lib-v-nif--.so.tar.gz + # on Linux/macOS, -v-nif--.dll.tar.gz + # on Windows. macOS keeps the .so suffix even though the NIF + # is a Mach-O dylib — RustlerPrecompiled normalises on .so. + # + # * Top-level files in the tarball: the renamed NIF, plus any + # libonnxruntime sidecars cargo produced. With the current + # `ort 2.0.0-rc.x` build under stable rustc, onnxruntime is + # statically linked on every platform, so the sidecar loop + # finds nothing and the tarball is single-file. The loop + # still runs as forward compatibility — if a future ort + # version reverts to dynamic linking we'll auto-bundle the + # runtime alongside the NIF without code changes. + # + # The loop deliberately EXCLUDES libonnxruntime_providers_cuda + # and ..._tensorrt — those are >450 MB and only useful with + # the matching cargo features enabled. CPU-only is the default + # precompile target; GPU users opt into source build via + # ORTEX_BUILD=true. + - name: Package artifact + id: pkg + shell: bash + env: + VERSION: ${{ steps.version.outputs.version }} + TARGET: ${{ matrix.job.target }} + NIF: ${{ matrix.nif }} + run: | + set -euo pipefail + + case "$TARGET" in + *-pc-windows-msvc|*-pc-windows-gnu) + prefix="" + nif_src_ext="dll" + tarball_ext="dll" + runtime_ext="dll" + ;; + *-apple-darwin) + prefix="lib" + nif_src_ext="dylib" + tarball_ext="so" + runtime_ext="dylib" + ;; + *-unknown-linux-*) + prefix="lib" + nif_src_ext="so" + tarball_ext="so" + runtime_ext="so" + ;; + *) + echo "Unsupported target: $TARGET" >&2 + exit 1 + ;; + esac + + NAME="${prefix}ortex-v${VERSION}-nif-${NIF}-${TARGET}" + TARBALL="${NAME}.${tarball_ext}.tar.gz" + + # Build output location depends on whether --target was used. + # Native builds (cargo build --release) land in target/release/. + # Cross builds (cross build --release --target X) land in + # target//release/. The "Build NIF" step controls which. + if [ "${{ matrix.job.use-cross }}" = "true" ]; then + BUILD_DIR="native/ortex/target/${TARGET}/release" + else + BUILD_DIR="native/ortex/target/release" + fi + + mkdir -p pkg + cd pkg + rm -f -- * + + # Copy the NIF, renaming to the long versioned filename + # RustlerPrecompiled expects. -L follows symlinks (cargo + # sometimes leaves the .so as a symlink into deps/). + cp -L "../${BUILD_DIR}/${prefix}ortex.${nif_src_ext}" "${NAME}.${tarball_ext}" + + # Bundle libonnxruntime sidecars if present. Exclude GPU + # provider libraries (huge; not used by CPU-only NIFs). + shopt -s nullglob + for src in "../${BUILD_DIR}/${prefix}onnxruntime"*."${runtime_ext}"*; do + base="$(basename "$src")" + case "$base" in + *providers_cuda*|*providers_tensorrt*) + echo "Excluding GPU provider library: $base" + ;; + *) + echo "Bundling: $base" + cp -L "$src" "$base" + ;; + esac + done + + tar -czf "$TARBALL" -- * + + echo "=== Final tarball contents ===" + tar -tzvf "$TARBALL" + echo "=== Tarball size ===" + ls -lh "$TARBALL" + + echo "tarball=pkg/${TARBALL}" >> "$GITHUB_OUTPUT" + echo "tarball-name=${TARBALL}" >> "$GITHUB_OUTPUT" + + - name: Upload artifact (workflow run) + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.pkg.outputs.tarball-name }} + path: ${{ steps.pkg.outputs.tarball }} + + - name: Attach to GitHub release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v3 + with: + draft: true + files: ${{ steps.pkg.outputs.tarball }} diff --git a/.gitignore b/.gitignore index 46366ee..1de9cce 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3830966 --- /dev/null +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 53c54fc..b0ec274 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..0c9dca6 --- /dev/null +++ b/RELEASE.md @@ -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. diff --git a/checksum-Elixir.Ortex.Native.exs b/checksum-Elixir.Ortex.Native.exs new file mode 100644 index 0000000..ccd9721 --- /dev/null +++ b/checksum-Elixir.Ortex.Native.exs @@ -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. +%{} diff --git a/lib/ortex/native.ex b/lib/ortex/native.ex index ee16c07..9befc4a 100644 --- a/lib/ortex/native.ex +++ b/lib/ortex/native.ex @@ -1,30 +1,71 @@ defmodule Ortex.Native do @moduledoc false - @rustler_version Application.spec(:rustler, :vsn) |> to_string() |> Version.parse!() - - # We have to compile the crate before `use Rustler` compiles the crate since - # cargo downloads the onnxruntime shared libraries and they are not available - # to load or copy into Elixir's during the on_load or Elixir compile steps. - # In the future, this may be configurable in Rustler. - if Version.compare(@rustler_version, "0.30.0") in [:gt, :eq] do - Rustler.Compiler.compile_crate(:ortex, Application.compile_env(:ortex, __MODULE__, []), - otp_app: :ortex, - crate: :ortex - ) - else - Rustler.Compiler.compile_crate(__MODULE__, otp_app: :ortex, crate: :ortex) - end + mix_config = Mix.Project.config() + version = mix_config[:version] + github_url = mix_config[:package][:links]["GitHub"] - Ortex.Util.copy_ort_libs() - - use Rustler, + # `RustlerPrecompiled` handles both paths internally: + # + # * Default path: download the matching prebuilt artifact from the + # project's GitHub release and extract it into priv/native/. + # No Rust toolchain required. + # + # * `force_build: true` path: fall through to `use Rustler` which + # compiles the crate from source. Triggered by the `ORTEX_BUILD` + # env var or the standard `:rustler_precompiled, :force_build` + # application config. Consumers who want this path must add + # `{:rustler, ">= 0.0.0", optional: true}` to their own deps, + # since `:rustler` is `optional: true` in this library. + # + # The `ort` 2.0.0-rc.x crate links onnxruntime statically into the + # NIF on every supported platform with stable rustc, so the NIF is + # self-contained and no `libonnxruntime` sidecar handling is needed + # on either path. + use RustlerPrecompiled, otp_app: :ortex, crate: :ortex, - skip_compilation?: true + version: version, + base_url: "#{github_url}/releases/download/v#{version}", + targets: ~w( + aarch64-apple-darwin + x86_64-apple-darwin + x86_64-unknown-linux-gnu + aarch64-unknown-linux-gnu + x86_64-pc-windows-msvc + ), + force_build: System.get_env("ORTEX_BUILD") in ["1", "true"], + nif_versions: ["2.16", "2.17"] + + # Stage `libonnxruntime` next to the NIF when source-building. With + # some toolchains (notably the rustc that ships preinstalled on + # GitHub-hosted Ubuntu runners) the `ort` crate produces a NIF that + # dynamically links `libonnxruntime` and expects to find it via the + # NIF's rpath ($ORIGIN / @loader_path), i.e. in `priv/native/`. The + # helper finds the sidecar files in the cargo target dir and copies + # them across. + # + # Idempotent + harmless on the other paths: + # + # * Precompiled artifact path: cargo wasn't invoked, so there's + # no target dir, the helper finds nothing and is a no-op. The + # prebuilt NIF tarball already contains whatever sidecars the + # particular target needs. + # + # * Static-link source build (e.g. release.yml's stable rustc): + # cargo target dir exists but has no `libonnxruntime` files + # because the runtime is linked directly into the NIF. Nothing + # to copy. Helper is a no-op. + # + # * Dynamic-link source build (e.g. ci.yml's runner-default + # rustc): cargo target dir contains `libonnxruntime.so` (often + # as symlinks into the ort cache). Helper resolves and copies + # them. Without this step the NIF fails to load at runtime + # with `libonnxruntime.so: cannot open shared object file`. + Ortex.Util.copy_ort_libs() - # When loading a NIF module, dummy clauses for all NIF function are required. - # NIF dummies usually just error out when called when the NIF is not loaded, as that should never normally happen. + # NIF dummies — required so calls compile cleanly before the NIF + # is loaded. Real implementations are provided by the loaded NIF. def init(_model_path, _execution_providers, _optimization_level), do: :erlang.nif_error(:nif_not_loaded) diff --git a/lib/ortex/util.ex b/lib/ortex/util.ex index 2914943..a3be3ba 100644 --- a/lib/ortex/util.ex +++ b/lib/ortex/util.ex @@ -1,35 +1,84 @@ defmodule Ortex.Util do @moduledoc false + + require Logger + @doc """ - Copies the libraries downloaded during the ORT build into a path that - Elixir can use + Copies the libonnxruntime libraries downloaded by the `ort` crate + during cargo build into the application's `priv/native/` directory, + so the NIF's rpath (`$ORIGIN` / `@loader_path`) can resolve them at + load time on platforms where onnxruntime is dynamically linked. + + The cargo target directory's location depends on the Rustler version: + + * **Rustler ≥ 0.30** builds in the natural cargo location at + `/target/release/`. For Ortex itself, that resolves + to `/native/ortex/target/release/`. For consumers, it + resolves to `/deps/ortex/native/ortex/target/release/`. + + * **Rustler < 0.30** staged artifacts at + `/native//release/`. Kept as a fallback for + anyone still on that path. + + The function tries each known layout in order, copies whatever + libonnxruntime files it finds, and warns if no candidate directory + contained any. Static-link platforms (notably macOS with the + `ort` crate's default download strategy) legitimately produce no + files to copy — the warning is informational rather than fatal. """ def copy_ort_libs() do - build_root = Path.absname(:code.priv_dir(:ortex)) |> Path.dirname() + destination_dir = Path.join([:code.priv_dir(:ortex), "native"]) + pattern = "libonnxruntime*.#{shared_lib_ext()}*" - rust_env = - case Path.join([build_root, "native/ortex/release"]) |> File.ls() do - {:ok, _} -> "release" - _ -> "debug" - end + case find_runtime_dir() do + nil -> + Logger.debug( + "Ortex.Util.copy_ort_libs/0: no cargo target directory found in any " <> + "expected location. This is normal on platforms where onnxruntime " <> + "is statically linked into the NIF (e.g. macOS)." + ) - # where the libonnxruntime files are stored - rust_path = Path.join([build_root, "native/ortex", rust_env]) + :ok - onnx_runtime_paths = - case :os.type() do - {:win32, _} -> Path.join([rust_path, "libonnxruntime*.dll*"]) - {:unix, :darwin} -> Path.join([rust_path, "libonnxruntime*.dylib*"]) - {:unix, _} -> Path.join([rust_path, "libonnxruntime*.so*"]) - end - |> Path.wildcard() + rust_path -> + rust_path + |> Path.join(pattern) + |> Path.wildcard() + |> Enum.each(fn src -> + dest = Path.join([destination_dir, Path.basename(src)]) + File.cp!(src, dest) + end) + end + end - # where we need to copy the paths - destination_dir = Path.join([:code.priv_dir(:ortex), "native"]) + # Walks the candidate cargo target directories in priority order and + # returns the first one that exists, or nil. The candidates cover both + # in-repo builds (Ortex testing itself) and consumer builds (Ortex as + # a dep), and both modern (`target/release/`) and legacy + # (`/native//release/`) Rustler layouts. + defp find_runtime_dir do + build_root = Path.absname(:code.priv_dir(:ortex)) |> Path.dirname() + # build_root is `_build//lib/ortex`. Going four levels up + # reaches the project root (Ortex repo) or consumer project root. + project_root = build_root |> Path.join("../../../..") |> Path.expand() + + [ + # Rustler ≥ 0.30 — Ortex consumed as a dependency. + Path.join([project_root, "deps", "ortex", "native", "ortex", "target", "release"]), + # Rustler ≥ 0.30 — Ortex building itself in its own repo. + Path.join([project_root, "native", "ortex", "target", "release"]), + # Rustler < 0.30 — legacy staged location under build_root. + Path.join([build_root, "native", "ortex", "release"]) + ] + |> Enum.map(&Path.expand/1) + |> Enum.find(&File.dir?/1) + end - onnx_runtime_paths - |> Enum.map(fn x -> - File.cp!(x, Path.join([destination_dir, Path.basename(x)])) - end) + defp shared_lib_ext do + case :os.type() do + {:win32, _} -> "dll" + {:unix, :darwin} -> "dylib" + {:unix, _} -> "so" + end end end diff --git a/mix.exs b/mix.exs index 0ff357e..147d46c 100644 --- a/mix.exs +++ b/mix.exs @@ -1,21 +1,24 @@ defmodule Ortex.MixProject do use Mix.Project + @version "0.1.11" + @source_url "https://github.com/elixir-nx/ortex" + def project do [ app: :ortex, - version: "0.1.10", + version: @version, elixir: "~> 1.14", start_permanent: Mix.env() == :prod, deps: deps(), # Docs name: "Ortex", - source_url: "https://github.com/elixir-nx/ortex", - homepage_url: "http://github.com/elixir-nx/ortex", + source_url: @source_url, + homepage_url: @source_url, docs: [ main: "readme", - extras: ["README.md"] + extras: ["README.md", "CHANGELOG.md"] ], package: package() ] @@ -31,7 +34,16 @@ defmodule Ortex.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:rustler, "~> 0.27"}, + # Rustler is required only for source-compiled builds (when + # `ORTEX_BUILD=true` is set or no precompiled artifact exists for + # the target). Made optional so consumers using precompiled NIFs + # don't need a Rust toolchain on PATH. Kept at the original + # `~> 0.27` constraint that the source-build path was tested + # against — bumping the floor showed no benefit and risked + # changing build-output paths in ways `Ortex.Util.copy_ort_libs/0` + # couldn't follow. + {:rustler, "~> 0.27", optional: true}, + {:rustler_precompiled, "~> 0.8"}, {:nx, "~> 0.6"}, {:tokenizers, "~> 0.4", only: :dev}, {:ex_doc, "0.29.4", only: :dev, runtime: false}, @@ -42,10 +54,22 @@ defmodule Ortex.MixProject do defp package do [ - files: ~w(lib .formatter.exs mix.exs README* LICENSE* native/ortex/src/ config/config.exs - native/ortex/Cargo.lock native/ortex/Cargo.toml native/ortex/.cargo/config.toml), + files: ~w( + lib + .formatter.exs + mix.exs + README* + CHANGELOG* + LICENSE* + checksum-*.exs + native/ortex/src/ + config/config.exs + native/ortex/Cargo.lock + native/ortex/Cargo.toml + native/ortex/.cargo/config.toml + ), licenses: ["MIT"], - links: %{"GitHub" => "https://github.com/elixir-nx/ortex"}, + links: %{"GitHub" => @source_url}, description: "ONNX Runtime bindings for Elixir" ] end diff --git a/mix.lock b/mix.lock index 8b9284d..895abcd 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "axon": {:hex, :axon, "0.5.1", "1ae3a2193df45e51fca912158320b2ca87cb7fba4df242bd3ebe245504d0ea1a", [:mix], [{:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}, {:kino_vega_lite, "~> 0.1.7", [hex: :kino_vega_lite, repo: "hexpm", optional: true]}, {:nx, "~> 0.5.0", [hex: :nx, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: true]}], "hexpm", "d36f2a11c34c6c2b458f54df5c71ffdb7ed91c6a9ccd908faba909c84cc6a38e"}, "axon_onnx": {:hex, :axon_onnx, "0.4.0", "7be4b5ac7a44340ec65eb59c24122a8fe2aa8105da33b3321a378b455a6cd9c6", [:mix], [{:axon, "~> 0.5", [hex: :axon, repo: "hexpm", optional: false]}, {:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}, {:protox, "~> 1.6.10", [hex: :protox, repo: "hexpm", optional: false]}], "hexpm", "b98c84e5656caf156ef8998296836349a62bc35598f05cc21eececbbef022d09"}, - "castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"}, + "castore": {:hex, :castore, "1.0.18", "5e43ef0ec7d31195dfa5a65a86e6131db999d074179d2ba5a8de11fe14570f55", [:mix], [], "hexpm", "f393e4fe6317829b158fb74d86eb681f737d2fe326aa61ccf6293c4104957e34"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.7", "77de20ac77f0e53f20ca82c563520af0237c301a1ec3ab3bc598e8a96c7ee5d9", [:mix], [{:elixir_make, "~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2768b28bf3c2b4f788c995576b39b8cb5d47eb788526d93bd52206c1d8bf4b75"}, "complex": {:hex, :complex, "0.5.0", "af2d2331ff6170b61bb738695e481b27a66780e18763e066ee2cd863d0b1dd92", [:mix], [], "hexpm", "2683bd3c184466cfb94fad74cbfddfaa94b860e27ad4ca1bffe3bff169d91ef1"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, @@ -12,15 +12,15 @@ "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, "exla": {:hex, :exla, "0.6.1", "a4400933a04d018c5fb508c75a080c73c3c1986f6c16a79bbfee93ba22830d4d", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nx, "~> 0.6.1", [hex: :nx, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:xla, "~> 0.5.0", [hex: :xla, repo: "hexpm", optional: false]}], "hexpm", "f0e95b0f91a937030cf9fcbe900c9d26933cb31db2a26dfc8569aa239679e6d4"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, "nx": {:hex, :nx, "0.6.2", "f1d137f477b1a6f84f8db638f7a6d5a0f8266caea63c9918aa4583db38ebe1d6", [:mix], [{:complex, "~> 0.5", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ac913b68d53f25f6eb39bddcf2d2cd6ea2e9bcb6f25cf86a79e35d0411ba96ad"}, "protox": {:hex, :protox, "1.6.10", "41d0b0c5b9190e7d5e6a2b1a03a09257ead6f3d95e6a0cf8b81430b526126908", [:mix], [{:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "9769fca26ae7abfc5cc61308a1e8d9e2400ff89a799599cee7930d21132832d9"}, - "rustler": {:hex, :rustler, "0.29.1", "880f20ae3027bd7945def6cea767f5257bc926f33ff50c0d5d5a5315883c084d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "109497d701861bfcd26eb8f5801fe327a8eef304f56a5b63ef61151ff44ac9b6"}, - "rustler_precompiled": {:hex, :rustler_precompiled, "0.7.0", "5d0834fc06dbc76dd1034482f17b1797df0dba9b491cef8bb045fcaca94bcade", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "fdf43a6835f4e4de5bfbc4c019bfb8c46d124bd4635fefa3e20d9a2bbbec1512"}, + "rustler": {:hex, :rustler, "0.37.3", "5f4e6634d43b26f0a69834dd1d3ed4e1710b022a053bf4a670220c9540c92602", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a6872c6f53dcf00486d1e7f9e046e20e01bf1654bdacc4193016c2e8002b32a2"}, + "rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "tokenizers": {:hex, :tokenizers, "0.4.0", "140283ca74a971391ddbd83cd8cbdb9bd03736f37a1b6989b82d245a95e1eb97", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "ef1a9824f5a893cd3b831c0e5b3d72caa250d2ec462035cc6afef6933b13a82e"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, From 2c3a2c6b66c3fae11c803e4c78ff6aac35fb0834 Mon Sep 17 00:00:00 2001 From: Kip Cole Date: Mon, 27 Apr 2026 08:46:06 +1000 Subject: [PATCH 2/2] Add precompiled NIF artifacts via rustler_precompiled (#27) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consumers on aarch64/x86_64 macOS, aarch64/x86_64 Linux glibc, and x86_64 Windows MSVC no longer need a Rust toolchain — Ortex downloads a prebuilt NIF from the GitHub release. Source build remains the fallback for unlisted targets and via ORTEX_BUILD=true. * lib/ortex/native.ex: use RustlerPrecompiled (handles both download and force_build paths). Ortex.Util.copy_ort_libs still runs after source build to stage libonnxruntime when the toolchain produces a dynamically-linked NIF. * mix.exs: add :rustler_precompiled, make :rustler optional, ship CHANGELOG.md and the checksum file in the package. * .github/workflows/release.yml: matrix builds 5 targets × 2 NIF versions on tag push via philss/rustler-precompiled-action, uploads to a draft release. * .github/workflows/ci.yml: forces source build via ORTEX_BUILD=true so PR validation isn't dependent on release artifacts existing. * checksum-Elixir.Ortex.Native.exs, RELEASE.md, CHANGELOG.md, README.md: pinned-checksums placeholder, release runbook, changelog entry, precompile target list and source-build instructions. Verified end-to-end on Debian Bookworm with no Rust toolchain via a mix deps.compile + tinymodel.onnx inference test in Docker. --- .github/workflows/release.yml | 164 +++++----------------------------- 1 file changed, 24 insertions(+), 140 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bd88177..85b78f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,9 +43,9 @@ jobs: # 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. See the comment below about - # tightening this with a manylinux container if older distro - # support becomes a goal. + # 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 } @@ -73,151 +73,35 @@ jobs: with: targets: ${{ matrix.job.target }} - - name: Install cross (Linux ARM64) - if: matrix.job.use-cross - shell: bash - run: cargo install --git https://github.com/cross-rs/cross --tag v0.2.5 cross - - # Build the NIF directly with cargo. With the rust-toolchain - # action's stable rustc, the `ort` 2.0.0-rc.x crate links - # onnxruntime statically into the NIF on every platform — the - # output is a single self-contained shared library (~20-25 MB). - # No `libonnxruntime` sidecar exists in target/release/ or - # anywhere else on disk after this step; the packaging step - # below correctly produces a single-file tarball as a result. + # `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 - shell: bash - working-directory: native/ortex - env: - RUSTLER_NIF_VERSION: ${{ matrix.nif }} - run: | - set -euo pipefail - if [ "${{ matrix.job.use-cross }}" = "true" ]; then - # Cross-compilation needs explicit --target. Output goes - # to target//release/. - cross build --release --target "${{ matrix.job.target }}" - else - # Native build. The runner's host arch matches - # matrix.job.target by design, so default cargo behaviour - # produces the right binary. Output goes to target/release/. - cargo build --release - fi - - # Stage the right files into a tarball matching RustlerPrecompiled's - # expected layout. Naming and contents follow the conventions in - # rustler_precompiled.ex (lib_name_with_ext/2 + lib_prefix/1): - # - # * Tarball name: lib-v-nif--.so.tar.gz - # on Linux/macOS, -v-nif--.dll.tar.gz - # on Windows. macOS keeps the .so suffix even though the NIF - # is a Mach-O dylib — RustlerPrecompiled normalises on .so. - # - # * Top-level files in the tarball: the renamed NIF, plus any - # libonnxruntime sidecars cargo produced. With the current - # `ort 2.0.0-rc.x` build under stable rustc, onnxruntime is - # statically linked on every platform, so the sidecar loop - # finds nothing and the tarball is single-file. The loop - # still runs as forward compatibility — if a future ort - # version reverts to dynamic linking we'll auto-bundle the - # runtime alongside the NIF without code changes. - # - # The loop deliberately EXCLUDES libonnxruntime_providers_cuda - # and ..._tensorrt — those are >450 MB and only useful with - # the matching cargo features enabled. CPU-only is the default - # precompile target; GPU users opt into source build via - # ORTEX_BUILD=true. - - name: Package artifact - id: pkg - shell: bash - env: - VERSION: ${{ steps.version.outputs.version }} - TARGET: ${{ matrix.job.target }} - NIF: ${{ matrix.nif }} - run: | - set -euo pipefail - - case "$TARGET" in - *-pc-windows-msvc|*-pc-windows-gnu) - prefix="" - nif_src_ext="dll" - tarball_ext="dll" - runtime_ext="dll" - ;; - *-apple-darwin) - prefix="lib" - nif_src_ext="dylib" - tarball_ext="so" - runtime_ext="dylib" - ;; - *-unknown-linux-*) - prefix="lib" - nif_src_ext="so" - tarball_ext="so" - runtime_ext="so" - ;; - *) - echo "Unsupported target: $TARGET" >&2 - exit 1 - ;; - esac - - NAME="${prefix}ortex-v${VERSION}-nif-${NIF}-${TARGET}" - TARBALL="${NAME}.${tarball_ext}.tar.gz" - - # Build output location depends on whether --target was used. - # Native builds (cargo build --release) land in target/release/. - # Cross builds (cross build --release --target X) land in - # target//release/. The "Build NIF" step controls which. - if [ "${{ matrix.job.use-cross }}" = "true" ]; then - BUILD_DIR="native/ortex/target/${TARGET}/release" - else - BUILD_DIR="native/ortex/target/release" - fi - - mkdir -p pkg - cd pkg - rm -f -- * - - # Copy the NIF, renaming to the long versioned filename - # RustlerPrecompiled expects. -L follows symlinks (cargo - # sometimes leaves the .so as a symlink into deps/). - cp -L "../${BUILD_DIR}/${prefix}ortex.${nif_src_ext}" "${NAME}.${tarball_ext}" - - # Bundle libonnxruntime sidecars if present. Exclude GPU - # provider libraries (huge; not used by CPU-only NIFs). - shopt -s nullglob - for src in "../${BUILD_DIR}/${prefix}onnxruntime"*."${runtime_ext}"*; do - base="$(basename "$src")" - case "$base" in - *providers_cuda*|*providers_tensorrt*) - echo "Excluding GPU provider library: $base" - ;; - *) - echo "Bundling: $base" - cp -L "$src" "$base" - ;; - esac - done - - tar -czf "$TARBALL" -- * - - echo "=== Final tarball contents ===" - tar -tzvf "$TARBALL" - echo "=== Tarball size ===" - ls -lh "$TARBALL" - - echo "tarball=pkg/${TARBALL}" >> "$GITHUB_OUTPUT" - echo "tarball-name=${TARBALL}" >> "$GITHUB_OUTPUT" + 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.pkg.outputs.tarball-name }} - path: ${{ steps.pkg.outputs.tarball }} + 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.pkg.outputs.tarball }} + files: ${{ steps.build-crate.outputs.file-path }} +