diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ce11b33 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,87 @@ +name: CI +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +env: + EXSTATIC_BUILD: true + MIX_ENV: test + +jobs: + test: + strategy: + matrix: + include: + - otp_version: "27.2" + elixir_version: "1.18" + os: ubuntu-latest + + runs-on: ${{ matrix.os }} + name: Test on Elixir ${{ matrix.elixir_version }} / OTP ${{ matrix.otp_version }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Elixir and Erlang + uses: erlef/setup-beam@v1 + with: + otp-version: ${{ matrix.otp_version }} + elixir-version: ${{ matrix.elixir_version }} + + - name: Cache Mix dependencies + uses: actions/cache@v4 + id: cache-deps + with: + path: | + deps + _build + _build/test + key: mix-${{ runner.os }}-${{ matrix.elixir_version }}-${{ matrix.otp_version }}-${{ hashFiles('mix.lock') }} + restore-keys: | + mix-${{ runner.os }}-${{ matrix.elixir_version }}-${{ matrix.otp_version }}- + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: native/exstatic + + - name: Install dependencies + env: + HEX_API_KEY: ${{ secrets.HEX_API_KEY }} + run: mix deps.get + + - name: Compile + run: mix compile --warnings-as-errors + + - name: Check for unused packages + run: mix deps.unlock --check-unused + + - name: Check code formatting + run: mix format --check-formatted + + - name: Run Credo (Code Linter) + run: mix credo --strict + + - name: Run Dialyzer (Static Analysis) + run: mix dialyzer + + - name: Check for abandoned dependencies + run: mix hex.audit + + - name: Check for outdated dependencies + run: mix hex.outdated --within-requirements || true + + - name: Run tests + run: mix test + + - name: Run tests with coverage + run: mix test --cover --export-coverage default + + - name: Check test coverage + run: mix test.coverage + + - name: Scan for security vulnerabilities + run: mix sobelow --exit --threshold medium diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69e0a1e..317c4ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,20 +1,87 @@ -name: Release +name: Compile & Upload NIFs + on: - release: - types: [published] + push: + branches: + - main + paths: + - "native/**" + - ".github/workflows/release.yml" + tags: + - "*" + pull_request: + paths: + - ".github/workflows/release.yml" + workflow_dispatch: + jobs: - publish: - name: Publish - runs-on: ubuntu-22.04 + compile_nifs: + name: ${{ matrix.job.platform }} (NIF ${{ matrix.nif }}) + runs-on: ${{ matrix.job.os }} + + permissions: + contents: write + id-token: write + + strategy: + fail-fast: false + matrix: + nif: ["2.16"] + job: + - { target: aarch64-apple-darwin, os: macos-13, platform: "macOS (ARM64)" } + - { target: aarch64-unknown-linux-gnu, os: ubuntu-22.04, use-cross: true, platform: "Linux (ARM64)" } + - { target: x86_64-apple-darwin, os: macos-13, platform: "macOS (x86_64)" } + - { target: x86_64-unknown-linux-gnu, os: ubuntu-22.04, platform: "Linux (x86_64)" } + + env: + EXSTATIC_BUILD: true # Ensure RustlerPrecompiled forces a build + steps: - - uses: actions/checkout@v4 - - uses: erlef/setup-beam@v1 + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable with: - elixir-version: 1.18 - otp-version: 27.2 - - name: Fetch dependencies - run: mix deps.get - - name: Publish package - env: - HEX_API_KEY: ${{ secrets.HEX_API_KEY }} - run: mix hex.publish --organization ${{ vars.HEX_ORG }} --replace --yes + toolchain: stable + targets: ${{ matrix.job.target }} + + - name: Install Cross for cross-compilation + if: ${{ matrix.job.use-cross }} + run: cargo install cross --git https://github.com/cross-rs/cross + + - name: Extract project version + shell: bash + run: | + echo "PROJECT_VERSION=$(sed -n 's/^ @version "\(.*\)"/\1/p' mix.exs | head -n1)" >> $GITHUB_ENV + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: v0-precomp + shared-key: ${{ matrix.job.target }}-${{ matrix.nif }} + workspaces: | + native/exstatic + + - name: Compile the NIFs + id: build-crate + uses: philss/rustler-precompiled-action@v1.1.4 + with: + project-name: exstatic + project-version: ${{ env.PROJECT_VERSION }} + target: ${{ matrix.job.target }} + nif-version: ${{ matrix.nif }} + use-cross: ${{ matrix.job.use-cross }} + project-dir: "native/exstatic" + + - name: Upload compiled NIF artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.build-crate.outputs.file-name }} + path: ${{ steps.build-crate.outputs.file-path }} + + - name: Publish NIFs to GitHub Releases + uses: softprops/action-gh-release@v2 + with: + files: | + ${{ steps.build-crate.outputs.file-path }} + if: startsWith(github.ref, 'refs/tags/') diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index cbe4714..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: Test -on: - push: - branches: [main] - pull_request: - branches: [main] -jobs: - test: - name: Test (Elixir ${{ matrix.elixir }} | OTP ${{ matrix.otp }}) - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - - elixir: 1.18.x - otp: 27 - os: ubuntu-22.04 - env: - MIX_ENV: test - steps: - - name: Setup Elixir - uses: erlef/setup-beam@v1 - with: - elixir-version: ${{ matrix.elixir }} - otp-version: ${{ matrix.otp }} - - - name: Checkout code - uses: actions/checkout@v4 - - - name: Cache dependencies - uses: actions/cache@v4 - id: cache-deps - with: - path: | - deps - _build - key: | - mix-${{ runner.os }}-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('**/mix.lock') }} - restore-keys: | - mix-${{ runner.os }}-${{ matrix.elixir }}-${{ matrix.otp }}- - - name: Install dependencies - env: - HEX_API_KEY: ${{ secrets.HEX_API_KEY }} - run: mix deps.get - - - name: Compile - run: mix compile --warnings-as-errors - - - name: Check for unused packages - run: mix deps.unlock --check-unused - - - run: mix format --check-formatted - - - run: mix credo --strict - - - run: mix dialyzer - - - name: Check for abandonded packages - run: mix hex.audit - - - name: Check outdated dependencies - run: mix hex.outdated --within-requirements || true - - - name: Check for vulnerable packages - run: mix hex.audit - - - name: Run tests - run: mix test - - - name: Run tests (with coverage) - run: mix test --cover --export-coverage default - - - name: Check coverage - run: mix test.coverage - - - name: Scan for security vulnerabilities - run: mix sobelow --exit --threshold medium - - publish: - name: Publish - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - uses: erlef/setup-beam@v1 - with: - elixir-version: 1.18 - otp-version: 27.2 - - name: Fetch dependencies - run: mix deps.get - - name: Publish package - env: - HEX_API_KEY: ${{ secrets.HEX_API_KEY }} - run: mix hex.publish --organization ${{ vars.HEX_ORG }} --dry-run --replace --yes diff --git a/lib/exstatic/native.ex b/lib/exstatic/native.ex index 9bcd5a3..968a2d6 100644 --- a/lib/exstatic/native.ex +++ b/lib/exstatic/native.ex @@ -1,6 +1,14 @@ defmodule Exstatic.Native do @moduledoc false - use Rustler, otp_app: :exstatic, crate: "exstatic" + version = Mix.Project.config()[:version] + + use RustlerPrecompiled, + otp_app: :exstatic, + crate: "exstatic", + base_url: "https://github.com/intellection/exstatic/releases/download/v#{version}", + force_build: System.get_env("EXSTATIC_BUILD") == "true", + nif_versions: ["2.16"], + version: version @spec normal_pdf(float(), float(), float()) :: float() def normal_pdf(_mean, _std_dev, _x), do: :erlang.nif_error(:nif_not_loaded) diff --git a/mix.exs b/mix.exs index 5ee458f..1a9aa8b 100644 --- a/mix.exs +++ b/mix.exs @@ -61,7 +61,7 @@ defmodule Exstatic.MixProject do [ name: "exstatic", organization: "zappi", - files: ~w(lib .formatter.exs mix.exs README*), + files: ~w(lib mix.exs README.md checksum-*.exs), licenses: ["MIT"], links: %{"GitHub" => @source_url} ] @@ -70,7 +70,8 @@ defmodule Exstatic.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:rustler, "~> 0.35.1", runtime: false}, + {:rustler_precompiled, "~> 0.8"}, + {:rustler, ">= 0.0.0", optional: true}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, {:ex_doc, "~> 0.34", only: :dev, runtime: false}, diff --git a/mix.lock b/mix.lock index ff0a6bb..5c996c6 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.11", "4bbd584741601eb658007339ea730b082cc61f3554cf2e8f39bf693a11b49073", [:mix], [], "hexpm", "e03990b4db988df56262852f20de0f659871c35154691427a5047f4967a16a62"}, "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [: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", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, @@ -19,6 +20,7 @@ "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "req": {:hex, :req, "0.5.8", "50d8d65279d6e343a5e46980ac2a70e97136182950833a1968b371e753f6a662", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d7fc5898a566477e174f26887821a3c5082b243885520ee4b45555f5d53f40ef"}, "rustler": {:hex, :rustler, "0.35.1", "ec81961ef9ee833d721dafb4449cab29b16b969a3063a842bb9e3ea912f6b938", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "3713b2e70e68ec2bfa8291dfd9cb811fe64a770f254cd9c331f8b34fa7989115"}, + "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.2", "5f25cbe220a8fac3e7ad62e6f950fcdca5a5a5f8501835d2823e8c74bf4268d5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "63d1bd5f8e23096d1ff851839923162096364bac8656a4a3c00d1fff8e83ee0a"}, "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, diff --git a/native/exstatic/Cargo.toml b/native/exstatic/Cargo.toml index f6d5815..7a99f66 100644 --- a/native/exstatic/Cargo.toml +++ b/native/exstatic/Cargo.toml @@ -10,5 +10,5 @@ path = "src/lib.rs" crate-type = ["cdylib"] [dependencies] -rustler = "0.35.1" +rustler = { version = "0.35.1", default-features = false, features = ["nif_version_2_16"] } statrs = "0.18.0"