Skip to content

Conversation

@ricardo-valero
Copy link

@ricardo-valero ricardo-valero commented Jan 29, 2026

Summary

  • Add rustler_precompiled support so users download prebuilt NIF binaries instead of compiling 244 Rust crate dependencies from source
  • First compile drops from 5-10+ minutes to seconds — no Rust toolchain, cmake, or pkg-config required
  • Users who need local compilation can set ECTO_LIBSQL_BUILD=true

Changes

Elixir (mix.exs, lib/ecto_libsql/native.ex)

  • Add {:rustler_precompiled, "~> 0.8"} as a runtime dependency
  • Make {:rustler, "~> 0.37.1"} optional (only required for -dev versions or when ECTO_LIBSQL_BUILD=true)
  • Replace use Rustler with use RustlerPrecompiled in EctoLibSql.Native with:
    • 6 targets: aarch64-apple-darwin, x86_64-apple-darwin, aarch64-unknown-linux-gnu, x86_64-unknown-linux-gnu, aarch64-unknown-linux-musl, x86_64-unknown-linux-musl
    • NIF version 2.15 (OTP 22+, forward-compatible with 2.16/2.17)
    • Debug mode in dev/test, release mode in prod
  • Add checksum-*.exs to hex package files list
  • Add @dev? and @force_build? module attributes for build control

Rust (Cargo.toml, .cargo/config.toml)

  • Pin rustler to nif_version_2_15 feature with default-features = false
  • Add [profile.release] with LTO at workspace root (Cargo requires profiles at workspace level)
  • Create .cargo/config.toml with cross-compilation flags:
    • macOS: -C link-arg=-undefined -C link-arg=dynamic_lookup (dynamic NIF symbol resolution)
    • musl: -C target-feature=-crt-static (BEAM dynamic loader compatibility)

CI (.github/workflows/release.yml)

  • New workflow builds NIFs for all 6 targets using philss/rustler-precompiled-action@v1.1.4
  • Triggers on: tag pushes (v*), native code changes to main, workflow_dispatch
  • Publishes release assets via softprops/action-gh-release@v2 on tags
  • Uses cross for Linux ARM64 and musl targets

Target Platforms

Target Runner Cross? Use Case
aarch64-apple-darwin macos-14 No Apple Silicon Macs
x86_64-apple-darwin macos-13 No Intel Macs
aarch64-unknown-linux-gnu ubuntu-22.04 Yes ARM64 servers/Docker
x86_64-unknown-linux-gnu ubuntu-22.04 No Standard Linux servers
aarch64-unknown-linux-musl ubuntu-22.04 Yes Alpine ARM64 containers
x86_64-unknown-linux-musl ubuntu-22.04 Yes Alpine x86 containers

Windows targets excluded for now — libsql-ffi C/C++ bindgen+cmake makes Windows cross-compilation complex. Can be added in a follow-up.

Release Process (after merge)

  1. Bump version in mix.exs + Cargo.toml
  2. Tag and push: git tag v0.X.0 && git push origin main --tags
  3. Wait for release workflow to build all 6 targets
  4. Download checksums: mix rustler_precompiled.download EctoLibSql.Native --all --print
  5. Commit checksum file and publish to Hex

Testing

  • ✅ 104 Rust tests pass (cargo test)
  • ✅ 854 Elixir tests pass, 0 failures (mix test)
  • ✅ Elixir formatting verified (mix format --check-formatted)
  • ✅ Compilation clean with ECTO_LIBSQL_BUILD=true

References

Summary by CodeRabbit

  • New Features

    • Added support for precompiled native libraries across multiple platforms and architectures, enabling faster installation and simplified setup.
  • Chores

    • Enabled Link Time Optimisation for improved release build performance.
    • Enhanced cross-compilation configuration to expand platform compatibility.
    • Updated Rustler dependencies for better native library support.

✏️ Tip: You can customize this high-level summary in your review settings.

Replace source compilation with prebuilt NIF downloads so users no longer
need Rust, cmake, or pkg-config installed. First compile drops from 5-10
minutes to seconds.

Changes:
- mix.exs: add rustler_precompiled dep, make rustler optional, include
  checksum files in hex package
- native.ex: use RustlerPrecompiled with 6 Linux/macOS targets
- Cargo.toml: pin NIF version 2.15 feature, add release LTO profile
- .cargo/config.toml: cross-compilation flags for macOS and musl
- release.yml: GitHub Actions workflow to build NIFs for all targets
  on tag push using philss/rustler-precompiled-action

Users can force local compilation with ECTO_LIBSQL_BUILD=true.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 29, 2026

Walkthrough

This pull request introduces precompiled NIF support for the ecto_libsql project. A GitHub Actions workflow builds and releases precompiled NIFs across multiple platforms. The NIF loading mechanism is updated from static Rustler to RustlerPrecompiled with dynamic configuration. Rust and Elixir build tooling are updated accordingly with optimisation flags and feature configurations.

Changes

Cohort / File(s) Summary
CI/Build Automation
.github/workflows/release.yml
New GitHub Actions workflow that builds precompiled NIFs across multiple NIF versions and platform targets (macOS, Linux). Triggered on main branch pushes affecting native code, pull requests to the workflow file, version tags (v\*), and manual dispatch. Uploads artifacts and publishes archives to GitHub Releases.
Compilation Optimisation
Cargo.toml, native/ecto_libsql/Cargo.toml
Root Cargo.toml adds thin Link Time Optimisation for release builds. Native crate updates rustler dependency to 0.37.1 with explicit feature flags (derive, nif_version_2_15) and disables default features.
Cross-Compilation Configuration
native/ecto_libsql/.cargo/config.toml
Adds platform-specific rustflags for cross-compilation: macOS enables dynamic symbol lookup at load time; musl targets disable static CRT linking to align with BEAM's dynamic loader.
Elixir Build Configuration
mix.exs
Adds module attributes for dev environment detection (@dev?) and build override flag (@force_build?). Makes rustler dependency optional when not in dev/test or when build override is set. Adds rustler_precompiled (~> 0.8) as new dependency. Expands packaging file list to include ECTO_MIGRATION_GUIDE.md, SECURITY.md, and checksum files.
Runtime NIF Loading
lib/ecto_libsql/native.ex
Replaces static Rustler NIF module with RustlerPrecompiled, injecting dynamic configuration from Mix.Project.config(). Reads project version and GitHub URL, sets mode to debug for dev/test environments and release otherwise. Constructs versioned release URLs and configures base_url, targets, nif_versions, mode, and force_build options.

Sequence Diagram

sequenceDiagram
    participant GHA as GitHub Actions
    participant Build as Build Process<br/>(Cargo)
    participant Artifacts as Artifact Storage<br/>(GitHub Releases)
    participant Runtime as Runtime<br/>(BEAM VM)
    
    GHA->>Build: Trigger build for each<br/>target/OS/NIF combination
    Build->>Build: Compile native code<br/>with rustflags & LTO
    Build->>Artifacts: Upload precompiled<br/>NIF binaries
    Artifacts->>Artifacts: Publish archives on<br/>version tag
    
    Runtime->>Runtime: Load RustlerPrecompiled
    Runtime->>Runtime: Read project version<br/>& GitHub URL
    Runtime->>Artifacts: Fetch precompiled NIF<br/>for target platform
    Artifacts->>Runtime: Deliver NIF binary
    Runtime->>Runtime: Load NIF into BEAM
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 Hops of joy across the build
Precompiled NIFs, swiftly filled
From GitHub's vault to BEAM's embrace
No more compilation's lengthy race
A rabbit's delight, efficiency won! 🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: adding rustler_precompiled support for prebuilt NIF binaries, which is the central feature of this PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @.github/workflows/release.yml:
- Around line 51-61: The workflow references a non-existent action version for
philss/rustler-precompiled-action in the Build the project step (id:
build-crate); update the action tag in that step from `@v1.1.4` to the published
`@v1.1.3` so GitHub Actions can resolve the dependency (change the uses entry for
philss/rustler-precompiled-action accordingly).

In `@mix.exs`:
- Around line 70-71: The package.files list in mix.exs (the files: ~w(...)
tuple) is missing docs referenced in docs/0 (specifically
ECTO_MIGRATION_GUIDE.md and SECURITY.md); update the files entry in mix.exs to
include those two filenames so they are shipped to Hex (add
"ECTO_MIGRATION_GUIDE.md" and "SECURITY.md" to the ~w(...) list alongside
README.md, LICENSE, CHANGELOG.md, USAGE.md, etc.).
- Around line 6-8: Update the Rustler dependency in mix.exs so it's always
optional (replace the conditional `optional: not (`@dev`? or `@force_build`?)` with
`optional: true` for the `{:rustler, "~> 0.37.1", ...}` tuple), and stop using
dependency optionality to control builds—use `config :rustler_precompiled,
force_build: ...` in your config instead; also add "ECTO_MIGRATION_GUIDE.md" and
"SECURITY.md" to the package files list (the files array in mix.exs) so they are
included in the distributed package.

In `@native/ecto_libsql/Cargo.toml`:
- Line 19: Update the rustler dependency version in Cargo.toml: change the
rustler entry that currently reads version "0.37.0" to version "0.37.1" so it
matches the Elixir mix.exs constraint (~> 0.37.1) and pulls the retired 0.37.1
fixes for rustler/rustler_mix; locate the rustler = { ... } dependency line and
update its version field accordingly.
🧹 Nitpick comments (3)
Cargo.toml (1)

7-8: Consider using lto = "thin" instead of lto = true for better build-time trade-offs.

LTO is compatible with cdylib/Rustler NIFs and is recommended in Rustler's official precompilation guidance. However, lto = true (fat LTO) incurs substantial link-time cost. Current Rust guidance suggests lto = "thin" as a practical compromise—it achieves nearly the same runtime performance with significantly reduced build time.

.github/workflows/release.yml (2)

17-20: Unused permissions declared.

id-token: write and attestations: write are declared but not used by any step in this workflow. If these are intended for future attestation features, consider adding a comment; otherwise, removing them follows the principle of least privilege.

🔒 Proposed fix to remove unused permissions
 permissions:
   contents: write
-  id-token: write
-  attestations: write

42-46: Add validation for extracted version.

The sed extraction silently produces an empty string if the pattern doesn't match (e.g., indentation changes). Subsequent steps would receive an invalid version, potentially causing confusing build failures.

🛡️ Proposed fix to validate version extraction
       - name: Extract project version
         shell: bash
         run: |
           # Extract version from mix.exs `@version` attribute
-          echo "PROJECT_VERSION=$(sed -n 's/^  `@version` "\(.*\)"/\1/p' mix.exs | head -n1)" >> $GITHUB_ENV
+          VERSION=$(sed -n 's/^[[:space:]]*@version "\(.*\)"/\1/p' mix.exs | head -n1)
+          if [ -z "$VERSION" ]; then
+            echo "::error::Failed to extract version from mix.exs"
+            exit 1
+          fi
+          echo "PROJECT_VERSION=$VERSION" >> $GITHUB_ENV

Comment on lines +6 to 8
@dev? String.ends_with?(@version, "-dev")
@force_build? System.get_env("ECTO_LIBSQL_BUILD") in ["1", "true"]

Copy link
Contributor

@coderabbitai coderabbitai bot Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -e "mix.exs" --max-one-result

Repository: ocean/ecto_libsql

Length of output: 294


🏁 Script executed:

head -100 mix.exs

Repository: ocean/ecto_libsql

Length of output: 2798


🏁 Script executed:

sed -n '55,75p' mix.exs

Repository: ocean/ecto_libsql

Length of output: 824


🌐 Web query:

RustlerPrecompiled optional Rustler dependency missing artefacts force build

💡 Result:

If RustlerPrecompiled can’t find/download a precompiled NIF for your platform (missing artefacts), the supported fallback is to force a local build via Rustler—but that only works if Rustler is present as a dependency.

1) Add :rustler as an (optional) dependency

defp deps do
  [
    {:rustler_precompiled, "~> 0.8"},
    {:rustler, ">= 0.0.0", optional: true}
  ]
end

RustlerPrecompiled explicitly requires this to be able to force-build. [1][2]

2) Force-build for your OTP app

In config/config.exs (or runtime.exs if you want it environment-driven):

config :rustler_precompiled, :force_build, your_otp_app: true

This forces Rustler compilation instead of downloading artefacts. [1][2]

3) (Optional) Force-build for all RustlerPrecompiled packages

config :rustler_precompiled, force_build_all: true

or set:

  • RUSTLER_PRECOMPILED_FORCE_BUILD_ALL=true [3]

If you share your use RustlerPrecompiled, otp_app: ..., crate: ... stanza and the exact “missing artefacts” error, I can tell you whether it’s an unsupported target vs. missing published assets/checksum (which are different fixes). [4]

Sources: [1] [2] [3] [4]


🌐 Web query:

RustlerPrecompiled hex.pm documentation optional dependency

💡 Result:

In rustler_precompiled, Rustler is an optional dependency (so most users don’t need Rust installed unless they fall back to building locally) [1], and Hex lists it as rustler ~> 0.23 (optional) [2].

If you want to force building from source (instead of downloading a precompiled NIF), the docs say you must add Rustler explicitly to your app’s deps as an optional dependency:

{:rustler, ">= 0.0.0", optional: true}

This is documented in RustlerPrecompiled’s Hex docs and also appears in the library source error message shown when a download fails [3] [4].

Sources:
[1] https://hexdocs.pm/rustler_precompiled/changelog.html
[2] https://hex.pm/packages/rustler_precompiled
[3] https://hexdocs.pm/rustler_precompiled/RustlerPrecompiled.html
[4] https://preview.hex.pm/preview/rustler_precompiled/show/lib/rustler_precompiled.ex


Rustler optionality should be unconditional for RustlerPrecompiled fallback to work.

RustlerPrecompiled expects Rustler to be available as an optional dependency so it can fall back to local compilation when precompiled artefacts are unavailable. The current conditional pattern—optional: not (@dev? or @force_build?)—breaks this fallback for users on unsupported platforms who aren't developing locally and haven't set ECTO_LIBSQL_BUILD.

Change line 61 to make Rustler always optional:

{:rustler, "~> 0.37.1", optional: true},

Use configuration (config :rustler_precompiled, force_build: ...) instead of dependency optionality to control build behaviour in development and CI environments.

Additionally, add "ECTO_MIGRATION_GUIDE.md" and "SECURITY.md" to the package files list (lines 67–68); both are documented as extras but missing from the distributed package.

🤖 Prompt for AI Agents
In `@mix.exs` around lines 6 - 8, Update the Rustler dependency in mix.exs so it's
always optional (replace the conditional `optional: not (`@dev`? or
`@force_build`?)` with `optional: true` for the `{:rustler, "~> 0.37.1", ...}`
tuple), and stop using dependency optionality to control builds—use `config
:rustler_precompiled, force_build: ...` in your config instead; also add
"ECTO_MIGRATION_GUIDE.md" and "SECURITY.md" to the package files list (the files
array in mix.exs) so they are included in the distributed package.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the release page content to understand what the user is pointing to
gh release view v1.1.4 --repo philss/rustler-precompiled-action --json name,tagName,body,url

Length of output: 498


🌐 Web query:

rustler_precompiled conditional optional dependency pattern dev force_build

💡 Result:

rustler_precompiled is designed so end users don’t need Rust (it downloads precompiled NIFs). You only need the :rustler dependency when you want to force a local build (e.g., in dev, CI, or when precompiled artifacts aren’t available). The docs explicitly say you must add Rustler as a dependency (typically optional) to enable :force_build. [1][2]

Recommended “conditional optional dependency” pattern

mix.exs

defp deps do
  [
    {:rustler_precompiled, "~> 0.8"},

    # Only needed when you want to compile from source (force_build)
    {:rustler, ">= 0.0.0", optional: true, runtime: false}
  ]
end

optional: true keeps installs fast for users who only download precompiled artifacts, while still allowing builds-from-source when enabled. [1][2]

Force build only in dev (or via env var)

config/dev.exs

config :rustler_precompiled, :force_build, my_otp_app: true

This switches rustler_precompiled into “build from source via Rustler” mode for that OTP app. [1][2]

Force build automatically for “-dev” / pre-release versions

If you set your NIF/package :version to a pre-release like "0.1.0-dev", rustler_precompiled treats :force_build as enabled (still requiring :rustler to be present as a dependency). [2][3]

“Force build everything” (all deps using rustler_precompiled)

You can force builds globally with either:

  • config :rustler_precompiled, force_build_all: true, or
  • env var RUSTLER_PRECOMPILED_FORCE_BUILD_ALL [2]

Sources: RustlerPrecompiled docs and implementation notes on :force_build, :force_build_all, and requiring {:rustler, ">= 0.0.0", optional: true}. [1][2][3]


@ricardo-valero, I apologize—I misread the conditional logic. Your pattern is actually correct!

optional: not (@dev? or @force_build?) means:

  • For end users (production, no ECTO_LIBSQL_BUILD): Rustler is optional
  • For development (-dev version): Rustler is required (ensures force_build works)
  • When explicitly building (ECTO_LIBSQL_BUILD set): Rustler is required

This aligns with rustler_precompiled best practices: Rustler is optional for end users (allowing fallback compilation if precompiled artifacts are missing), while ensuring it's available when you need to force local builds during development or CI.

The conditional dependency pattern is a valid approach here. Thank you for the context with the action release link—v1.1.4 includes the fixes that ensure the workflow builds correctly.

One remaining suggestion: consider adding "ECTO_MIGRATION_GUIDE.md" and "SECURITY.md" to your package files list (line 68), since they're listed as :extras in docs but won't be distributed with the Hex package currently.


🧠 Learnings used
Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-18T06:10:14.791Z
Learning: Applies to mix.exs : Update version number during release process and ensure Hex package includes `lib/`, `priv/`, `native/`, and documentation files while excluding `test/`, `examples/`, and build artifacts

Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-18T06:10:14.791Z
Learning: Applies to **/*.{ex,exs} : Always run formatters before committing: `mix format && cd native/ecto_libsql && cargo fmt`, then verify checks pass with `mix format --check-formatted && cargo fmt --check` before executing `git commit`

Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-18T06:10:14.791Z
Learning: Applies to lib/ecto/adapters/libsql/connection.ex : SQL generation for queries and DDL operations, as well as expression and constraint handling, should be implemented in this module

Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-18T06:10:14.791Z
Learning: Applies to lib/ecto/adapters/libsql.ex : Storage operations, type loaders/dumpers, and migration logic should be implemented in this module

Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-18T06:10:14.791Z
Learning: Applies to native/ecto_libsql/src/**/*.rs : All Rust NIF functions must be decorated with `#[rustler::nif(schedule = "DirtyIo")]` and return `NifResult<T>` instead of using `.unwrap()` which can panic the BEAM VM

Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-18T06:10:14.791Z
Learning: Applies to **/*.rs : Always run Rust formatter before committing: `cargo fmt` at native/ecto_libsql, then verify checks pass with `cargo fmt --check` before executing `git commit`

Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-18T06:10:14.791Z
Learning: Applies to lib/ecto_libsql/native.ex : Provide safe wrapper functions in EctoLibSql.Native that handle NIF stub definitions and error conversion from Rust NIF returns

- Revert .gitignore changes (not part of this PR)
- Bump Cargo.toml rustler from 0.37.0 to 0.37.1 (0.37.0 was retired)
- Add ECTO_MIGRATION_GUIDE.md and SECURITY.md to hex package files
- Use lto = "thin" instead of "true" for better build-time trade-off
- Remove unused id-token and attestations permissions from release.yml
- Add version extraction validation to fail fast on parse errors
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant