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
77 changes: 77 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Build Precompiled NIFs

on:
push:
branches:
- main
paths:
- "native/**"
- ".github/workflows/release.yml"
tags:
- "v*"
pull_request:
paths:
- ".github/workflows/release.yml"
workflow_dispatch:

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: ["2.15"]
job:
- { target: aarch64-apple-darwin, os: macos-14 }
- { target: x86_64-apple-darwin, os: macos-13 }
- { target: aarch64-unknown-linux-gnu, os: ubuntu-22.04, use-cross: true }
- { target: aarch64-unknown-linux-musl, os: ubuntu-22.04, use-cross: true }
- { target: x86_64-unknown-linux-gnu, os: ubuntu-22.04 }
- { target: x86_64-unknown-linux-musl, os: ubuntu-22.04, use-cross: true }

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Extract project version
shell: bash
run: |
# Extract version from mix.exs @version attribute
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

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Build the project
id: build-crate
uses: philss/rustler-precompiled-action@v1.1.4
with:
project-name: ecto_libsql
project-version: ${{ env.PROJECT_VERSION }}
target: ${{ matrix.job.target }}
nif-version: ${{ matrix.nif }}
use-cross: ${{ matrix.job.use-cross }}
cross-version: "from-source"
project-dir: "native/ecto_libsql"

- name: Artifact upload
uses: actions/upload-artifact@v4
with:
name: ${{ steps.build-crate.outputs.file-name }}
path: ${{ steps.build-crate.outputs.file-path }}

- name: Publish archives and packages
uses: softprops/action-gh-release@v2
with:
files: |
${{ steps.build-crate.outputs.file-path }}
if: startsWith(github.ref, 'refs/tags/')
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ resolver = "2"

members = ["native/ecto_libsql"]
exclude = ["native/ecto_libsql/fuzz"]

[profile.release]
lto = "thin"
22 changes: 20 additions & 2 deletions lib/ecto_libsql/native.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,27 @@ defmodule EctoLibSql.Native do

"""

use Rustler,
mix_config = Mix.Project.config()
version = mix_config[:version]
github_url = mix_config[:package][:links]["GitHub"]
mode = if Mix.env() in [:dev, :test], do: :debug, else: :release

use RustlerPrecompiled,
otp_app: :ecto_libsql,
crate: :ecto_libsql
crate: "ecto_libsql",
version: version,
base_url: "#{github_url}/releases/download/v#{version}",
targets: ~w(
aarch64-apple-darwin
aarch64-unknown-linux-gnu
aarch64-unknown-linux-musl
x86_64-apple-darwin
x86_64-unknown-linux-gnu
x86_64-unknown-linux-musl
),
nif_versions: ["2.15"],
mode: mode,
force_build: System.get_env("ECTO_LIBSQL_BUILD") in ["1", "true"]

# Raw NIF functions - implemented in Rust (native/ecto_libsql/src/lib.rs)
# These all raise :nif_not_loaded errors until the NIF is loaded
Expand Down
8 changes: 6 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ defmodule EctoLibSql.MixProject do

@version "0.8.9"
@source_url "https://github.com/ocean/ecto_libsql"
@dev? String.ends_with?(@version, "-dev")
@force_build? System.get_env("ECTO_LIBSQL_BUILD") in ["1", "true"]

Comment on lines +6 to 8
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

def project do
[
Expand Down Expand Up @@ -55,7 +57,8 @@ defmodule EctoLibSql.MixProject do
{:ecto_sql, "~> 3.11"},
{:ex_doc, "~> 0.31", only: :dev, runtime: false},
{:jason, "~> 1.4"},
{:rustler, "~> 0.37.1"},
{:rustler, "~> 0.37.1", optional: not (@dev? or @force_build?)},
{:rustler_precompiled, "~> 0.8"},
{:sobelow, "~> 0.13", only: [:dev, :test], runtime: false},
{:stream_data, "~> 1.0", only: [:dev, :test]}
]
Expand All @@ -64,7 +67,8 @@ defmodule EctoLibSql.MixProject do
defp package() do
[
name: "ecto_libsql",
files: ~w(lib priv .formatter.exs mix.exs README.md LICENSE CHANGELOG.md USAGE.md native),
files:
~w(lib priv .formatter.exs mix.exs README.md LICENSE CHANGELOG.md USAGE.md ECTO_MIGRATION_GUIDE.md SECURITY.md native checksum-*.exs),
licenses: ["Apache-2.0"],
links: %{
"GitHub" => @source_url,
Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
%{
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"},
"credo": {:hex, :credo, "1.7.16", "a9f1389d13d19c631cb123c77a813dbf16449a2aebf602f590defa08953309d4", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d0562af33756b21f248f066a9119e3890722031b6d199f22e3cf95550e4f1579"},
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
Expand All @@ -16,6 +17,7 @@
"makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"rustler": {:hex, :rustler, "0.37.1", "721434020c7f6f8e1cdc57f44f75c490435b01de96384f8ccb96043f12e8a7e0", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "24547e9b8640cf00e6a2071acb710f3e12ce0346692e45098d84d45cdb54fd79"},
"rustler_precompiled": {:hex, :rustler_precompiled, "0.8.4", "700a878312acfac79fb6c572bb8b57f5aae05fe1cf70d34b5974850bbf2c05bf", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "3b33d99b540b15f142ba47944f7a163a25069f6d608783c321029bc1ffb09514"},
"sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"},
"stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
Expand Down
20 changes: 20 additions & 0 deletions native/ecto_libsql/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Cross-compilation configuration for rustler_precompiled NIF builds.
#
# macOS: Use dynamic lookup for Erlang NIF symbols (resolved at load time).
# musl: Disable static CRT linking for compatibility with BEAM's dynamic loader.

[target.'cfg(target_os = "macos")']
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]

[target.x86_64-unknown-linux-musl]
rustflags = [
"-C", "target-feature=-crt-static",
]

[target.aarch64-unknown-linux-musl]
rustflags = [
"-C", "target-feature=-crt-static",
]
2 changes: 1 addition & 1 deletion native/ecto_libsql/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ crate-type = ["cdylib", "rlib"]

[dependencies]
libsql = { version = "0.9.29", features = ["encryption", "replication"] }
rustler = "0.37.0"
rustler = { version = "0.37.1", default-features = false, features = ["derive", "nif_version_2_15"] }
tokio = "1.45.1"
uuid = "1.17.0"
bytes = "1.5"
Expand Down