From 433f2ce00b2a752db603c09304b123c2a26ef3bc Mon Sep 17 00:00:00 2001 From: Ricardo Valero de la Rosa <55701657+ricardo-valero@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:09:43 -0600 Subject: [PATCH 1/2] feat: add rustler_precompiled support for prebuilt NIF binaries 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. --- .github/workflows/release.yml | 74 +++++++++++++++++++++++++++ .gitignore | 3 ++ Cargo.toml | 3 ++ lib/ecto_libsql/native.ex | 22 +++++++- mix.exs | 8 ++- mix.lock | 2 + native/ecto_libsql/.cargo/config.toml | 20 ++++++++ native/ecto_libsql/Cargo.toml | 2 +- 8 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 native/ecto_libsql/.cargo/config.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..74672989 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,74 @@ +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 + id-token: write + attestations: 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 + echo "PROJECT_VERSION=$(sed -n 's/^ @version "\(.*\)"/\1/p' mix.exs | head -n1)" >> $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/') diff --git a/.gitignore b/.gitignore index de6a37a6..b223df70 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ z_ecto_libsql_test* # Implementation summaries and temporary docs TEST_AUDIT_REPORT.md TEST_COVERAGE_ISSUES_CREATED.md + +/.direnv/ +.envrc diff --git a/Cargo.toml b/Cargo.toml index 50058cc4..a4d8fd08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,3 +3,6 @@ resolver = "2" members = ["native/ecto_libsql"] exclude = ["native/ecto_libsql/fuzz"] + +[profile.release] +lto = true diff --git a/lib/ecto_libsql/native.ex b/lib/ecto_libsql/native.ex index 2a1ac78c..9623f9e5 100644 --- a/lib/ecto_libsql/native.ex +++ b/lib/ecto_libsql/native.ex @@ -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 diff --git a/mix.exs b/mix.exs index 36ab087a..215a5040 100644 --- a/mix.exs +++ b/mix.exs @@ -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"] def project do [ @@ -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]} ] @@ -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 native checksum-*.exs), licenses: ["Apache-2.0"], links: %{ "GitHub" => @source_url, diff --git a/mix.lock b/mix.lock index 4b5cd56d..08df823a 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, @@ -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"}, diff --git a/native/ecto_libsql/.cargo/config.toml b/native/ecto_libsql/.cargo/config.toml new file mode 100644 index 00000000..42285d42 --- /dev/null +++ b/native/ecto_libsql/.cargo/config.toml @@ -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", +] diff --git a/native/ecto_libsql/Cargo.toml b/native/ecto_libsql/Cargo.toml index 8f8ff7cb..89c68881 100644 --- a/native/ecto_libsql/Cargo.toml +++ b/native/ecto_libsql/Cargo.toml @@ -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.0", default-features = false, features = ["derive", "nif_version_2_15"] } tokio = "1.45.1" uuid = "1.17.0" bytes = "1.5" From a7f6e7a50c9ee0acfcfdaf1d530a58eae13f47ce Mon Sep 17 00:00:00 2001 From: Ricardo Valero de la Rosa <55701657+ricardo-valero@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:26:50 -0600 Subject: [PATCH 2/2] fix: address PR review feedback and remove unrelated .gitignore change - 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 --- .github/workflows/release.yml | 9 ++++++--- .gitignore | 3 --- Cargo.toml | 2 +- mix.exs | 2 +- native/ecto_libsql/Cargo.toml | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 74672989..fbe00cbf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,8 +16,6 @@ on: permissions: contents: write - id-token: write - attestations: write jobs: build_release: @@ -43,7 +41,12 @@ jobs: 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 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable diff --git a/.gitignore b/.gitignore index b223df70..de6a37a6 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,3 @@ z_ecto_libsql_test* # Implementation summaries and temporary docs TEST_AUDIT_REPORT.md TEST_COVERAGE_ISSUES_CREATED.md - -/.direnv/ -.envrc diff --git a/Cargo.toml b/Cargo.toml index a4d8fd08..b0dc003b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,4 +5,4 @@ members = ["native/ecto_libsql"] exclude = ["native/ecto_libsql/fuzz"] [profile.release] -lto = true +lto = "thin" diff --git a/mix.exs b/mix.exs index 215a5040..b860efea 100644 --- a/mix.exs +++ b/mix.exs @@ -68,7 +68,7 @@ defmodule EctoLibSql.MixProject do [ name: "ecto_libsql", files: - ~w(lib priv .formatter.exs mix.exs README.md LICENSE CHANGELOG.md USAGE.md native checksum-*.exs), + ~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, diff --git a/native/ecto_libsql/Cargo.toml b/native/ecto_libsql/Cargo.toml index 89c68881..79afc666 100644 --- a/native/ecto_libsql/Cargo.toml +++ b/native/ecto_libsql/Cargo.toml @@ -16,7 +16,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] libsql = { version = "0.9.29", features = ["encryption", "replication"] } -rustler = { version = "0.37.0", default-features = false, features = ["derive", "nif_version_2_15"] } +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"