From 18d30ccf7d70a1d21eba99fa103160e8dee28c02 Mon Sep 17 00:00:00 2001 From: Brook <71849503+Primebrook@users.noreply.github.com> Date: Sat, 8 Feb 2025 13:46:55 +0000 Subject: [PATCH 1/7] release helper --- lib/exstatic/native.ex | 6 +-- lib/exstatic/release_helper.ex | 72 ++++++++++++++++++++++++++++++++++ mix.exs | 7 +++- 3 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 lib/exstatic/release_helper.ex diff --git a/lib/exstatic/native.ex b/lib/exstatic/native.ex index c274fea..cd7072a 100644 --- a/lib/exstatic/native.ex +++ b/lib/exstatic/native.ex @@ -1,14 +1,14 @@ defmodule Exstatic.Native do @moduledoc false - config = Mix.Project.config() + version = Mix.Project.config()[:version] use RustlerPrecompiled, otp_app: :exstatic, crate: "exstatic", - base_url: "#{config[:source_url]}/releases/download/v#{config[:version]}", + base_url: {Exstatic.ReleaseHelper, :github_release_url!}, force_build: System.get_env("EXSTATIC_BUILD") == "true", nif_versions: ["2.16"], - version: config[:version], + version: version, targets: ~w( aarch64-apple-darwin x86_64-apple-darwin diff --git a/lib/exstatic/release_helper.ex b/lib/exstatic/release_helper.ex new file mode 100644 index 0000000..27d4e8f --- /dev/null +++ b/lib/exstatic/release_helper.ex @@ -0,0 +1,72 @@ +defmodule Exstatic.ReleaseHelper do + @config Mix.Project.config() + @tag "v#{@config[:version]}" + + @spec github_release_url!(String.t()) :: {String.t(), [{String.t(), String.t()}]} + def github_release_url!(file_name) do + start_finch() + + token = System.fetch_env!("EXSTATIC_GITHUB_TOKEN") + + case fetch_github_asset_id(file_name) do + {:ok, asset_id} -> + {asset_url(asset_id), + [ + {"Authorization", "token #{token}"}, + {"Accept", "application/octet-stream"}, + {"User-Agent", "exstatic-bot"} + ]} + + {:error, reason} -> + raise "Failed to fetch GitHub asset ID: #{reason}" + end + end + + defp fetch_github_asset_id(file_name) do + start_finch() + + token = github_personal_access_token!() + + headers = [ + {"Authorization", "token #{token}"}, + {"Accept", "application/vnd.github+json"}, + {"User-Agent", "exstatic-bot"} + ] + + case Req.get(release_url(), headers: headers) do + {:ok, %{status: 200, body: release}} -> + case Enum.find(release["assets"], fn asset -> asset["name"] == file_name end) do + nil -> {:error, "Asset not found: #{file_name} in release #{@tag}"} + asset -> {:ok, asset["id"]} + end + + {:ok, %{status: status, body: body}} -> + {:error, "Failed to fetch releases: #{status} #{inspect(body)}"} + + {:error, reason} -> + {:error, reason} + end + end + + defp asset_url(asset_id) do + "https://uploads.github.com/repos/#{@config[:repo]}/releases/#{asset_id}/assets" + end + + defp release_url, do: "https://api.github.com/repos/#{@config[:repo]}/releases/tags/#{@tag}" + + defp github_personal_access_token! do + case System.get_env("EXSTATIC_GITHUB_TOKEN") do + nil -> + raise "Missing EXSTATIC_GITHUB_TOKEN environment variable (GitHub personal access token)." + + token -> + token + end + end + + defp start_finch do + unless Process.whereis(Req.Finch) do + {:ok, _} = Finch.start_link(name: Req.Finch) + end + end +end diff --git a/mix.exs b/mix.exs index f443d1c..38242f8 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,8 @@ defmodule Exstatic.MixProject do use Mix.Project @version "0.1.1" - @source_url "https://github.com/intellection/exstatic" + @repo "Intellection/exstatic" + @source_url "https://github.com/#{@repo}" def project do [ @@ -75,7 +76,9 @@ defmodule Exstatic.MixProject do {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, {:ex_doc, "~> 0.34", only: :dev, runtime: false}, - {:sobelow, "~> 0.13", only: [:dev, :test], runtime: false} + {:sobelow, "~> 0.13", only: [:dev, :test], runtime: false}, + {:finch, "~> 0.19"}, + {:req, "~> 0.5.0"} ] end end From 10ff8b3434ab6d7109e1c7e96408479e5a356b22 Mon Sep 17 00:00:00 2001 From: Brook <71849503+Primebrook@users.noreply.github.com> Date: Sat, 8 Feb 2025 13:59:05 +0000 Subject: [PATCH 2/7] docs --- lib/exstatic/release_helper.ex | 44 ++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/lib/exstatic/release_helper.ex b/lib/exstatic/release_helper.ex index 27d4e8f..4085b5a 100644 --- a/lib/exstatic/release_helper.ex +++ b/lib/exstatic/release_helper.ex @@ -1,7 +1,39 @@ defmodule Exstatic.ReleaseHelper do + @moduledoc """ + A helper module for fetching precompiled assets from a private GitHub release. + + This module is designed to work with `RustlerPrecompiled`, allowing it to dynamically + fetch the correct precompiled NIF binaries from a GitHub release using the GitHub API. + It retrieves the asset ID for a given file name and constructs a download URL with the + necessary authentication headers. + + ## Required Environment Variables + + - `EXSTATIC_GITHUB_TOKEN` – A GitHub personal access token (PAT) with access to private repositories. + """ @config Mix.Project.config() @tag "v#{@config[:version]}" + @doc """ + Retrieves the GitHub asset download URL and authentication headers. This function first ensures that then Finch + HTTP client is started. It then fetches the asset ID for the given file name from the GitHub API and constructs a download URL. + + ## Parameters + - `file_name` (String.t()): The name of the asset file to download. + + ## Returns + - `{"https://api.github.com/repos/.../assets/{asset_id}", headers}` (tuple): + A tuple containing the download URL and required HTTP headers. + + ## Raises + - `RuntimeError`: If the EXSTATIC_GITHUB_TOKEN environment variable is missing or if the asset ID cannot be found. + + ## Example + + iex> Exstatic.ReleaseHelper.github_release_url!("libexstatic-v0.1.1-nif-2.16-aarch64-apple-darwin.so.tar.gz") + {"https://api.github.com/repos/Intellection/exstatic/releases/assets/227243824", + [{"Authorization", "token MY_TOKEN"}, {"Accept", "application/octet-stream"}, {"User-Agent", "exstatic-bot"}]} + """ @spec github_release_url!(String.t()) :: {String.t(), [{String.t(), String.t()}]} def github_release_url!(file_name) do start_finch() @@ -35,10 +67,7 @@ defmodule Exstatic.ReleaseHelper do case Req.get(release_url(), headers: headers) do {:ok, %{status: 200, body: release}} -> - case Enum.find(release["assets"], fn asset -> asset["name"] == file_name end) do - nil -> {:error, "Asset not found: #{file_name} in release #{@tag}"} - asset -> {:ok, asset["id"]} - end + find_asset_id(release, file_name) {:ok, %{status: status, body: body}} -> {:error, "Failed to fetch releases: #{status} #{inspect(body)}"} @@ -54,6 +83,13 @@ defmodule Exstatic.ReleaseHelper do defp release_url, do: "https://api.github.com/repos/#{@config[:repo]}/releases/tags/#{@tag}" + defp find_asset_id(release, file_name) do + case Enum.find(release["assets"], fn asset -> asset["name"] == file_name end) do + nil -> {:error, "Asset not found: #{file_name} in release #{@tag}"} + asset -> {:ok, asset["id"]} + end + end + defp github_personal_access_token! do case System.get_env("EXSTATIC_GITHUB_TOKEN") do nil -> From 2bdf0c7764d45d88feec7a4fa46f096af2c7c46c Mon Sep 17 00:00:00 2001 From: Brook <71849503+Primebrook@users.noreply.github.com> Date: Sat, 8 Feb 2025 14:00:05 +0000 Subject: [PATCH 3/7] credo --- lib/exstatic/release_helper.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/exstatic/release_helper.ex b/lib/exstatic/release_helper.ex index 4085b5a..507ea3c 100644 --- a/lib/exstatic/release_helper.ex +++ b/lib/exstatic/release_helper.ex @@ -102,7 +102,7 @@ defmodule Exstatic.ReleaseHelper do defp start_finch do unless Process.whereis(Req.Finch) do - {:ok, _} = Finch.start_link(name: Req.Finch) + {:ok, _pid} = Finch.start_link(name: Req.Finch) end end end From 922455aabda131d4b45a0acd73ef400d91ddb1c4 Mon Sep 17 00:00:00 2001 From: Brook <71849503+Primebrook@users.noreply.github.com> Date: Sat, 8 Feb 2025 18:29:29 +0000 Subject: [PATCH 4/7] Tesla > Req --- lib/exstatic/release_helper.ex | 49 +++++++++++++--------------------- mix.exs | 9 ++++--- mix.lock | 1 + 3 files changed, 26 insertions(+), 33 deletions(-) diff --git a/lib/exstatic/release_helper.ex b/lib/exstatic/release_helper.ex index 507ea3c..9694fd5 100644 --- a/lib/exstatic/release_helper.ex +++ b/lib/exstatic/release_helper.ex @@ -11,22 +11,24 @@ defmodule Exstatic.ReleaseHelper do - `EXSTATIC_GITHUB_TOKEN` – A GitHub personal access token (PAT) with access to private repositories. """ + @config Mix.Project.config() @tag "v#{@config[:version]}" @doc """ - Retrieves the GitHub asset download URL and authentication headers. This function first ensures that then Finch - HTTP client is started. It then fetches the asset ID for the given file name from the GitHub API and constructs a download URL. + Retrieves the GitHub asset download URL and authentication headers. ## Parameters + - `file_name` (String.t()): The name of the asset file to download. ## Returns - - `{"https://api.github.com/repos/.../assets/{asset_id}", headers}` (tuple): - A tuple containing the download URL and required HTTP headers. + + - `{download_url, headers}` (tuple): A tuple containing the GitHub asset URL and headers. ## Raises - - `RuntimeError`: If the EXSTATIC_GITHUB_TOKEN environment variable is missing or if the asset ID cannot be found. + + - `RuntimeError`: If the `EXSTATIC_GITHUB_TOKEN` environment variable is missing or if the asset ID cannot be found. ## Example @@ -36,11 +38,9 @@ defmodule Exstatic.ReleaseHelper do """ @spec github_release_url!(String.t()) :: {String.t(), [{String.t(), String.t()}]} def github_release_url!(file_name) do - start_finch() - - token = System.fetch_env!("EXSTATIC_GITHUB_TOKEN") + token = github_personal_access_token!() - case fetch_github_asset_id(file_name) do + case fetch_github_asset_id(file_name, token) do {:ok, asset_id} -> {asset_url(asset_id), [ @@ -54,22 +54,20 @@ defmodule Exstatic.ReleaseHelper do end end - defp fetch_github_asset_id(file_name) do - start_finch() - - token = github_personal_access_token!() - + defp fetch_github_asset_id(file_name, token) do headers = [ {"Authorization", "token #{token}"}, {"Accept", "application/vnd.github+json"}, {"User-Agent", "exstatic-bot"} ] - case Req.get(release_url(), headers: headers) do - {:ok, %{status: 200, body: release}} -> - find_asset_id(release, file_name) + case Tesla.get(release_url(), headers: headers) do + {:ok, %Tesla.Env{status: 200, body: release}} -> + release + |> :json.decode() + |> find_asset_id(file_name) - {:ok, %{status: status, body: body}} -> + {:ok, %Tesla.Env{status: status, body: body}} -> {:error, "Failed to fetch releases: #{status} #{inspect(body)}"} {:error, reason} -> @@ -78,7 +76,7 @@ defmodule Exstatic.ReleaseHelper do end defp asset_url(asset_id) do - "https://uploads.github.com/repos/#{@config[:repo]}/releases/#{asset_id}/assets" + "https://api.github.com/repos/#{@config[:repo]}/releases/assets/#{asset_id}" end defp release_url, do: "https://api.github.com/repos/#{@config[:repo]}/releases/tags/#{@tag}" @@ -92,17 +90,8 @@ defmodule Exstatic.ReleaseHelper do defp github_personal_access_token! do case System.get_env("EXSTATIC_GITHUB_TOKEN") do - nil -> - raise "Missing EXSTATIC_GITHUB_TOKEN environment variable (GitHub personal access token)." - - token -> - token - end - end - - defp start_finch do - unless Process.whereis(Req.Finch) do - {:ok, _pid} = Finch.start_link(name: Req.Finch) + nil -> raise "Missing EXSTATIC_GITHUB_TOKEN environment variable." + token -> token end end end diff --git a/mix.exs b/mix.exs index 38242f8..98fad69 100644 --- a/mix.exs +++ b/mix.exs @@ -19,10 +19,12 @@ defmodule Exstatic.MixProject do package: package(), name: "Exstatic", source_url: @source_url, + repo: @repo, test_coverage: [ summary: [threshold: 90], ignore_modules: [ - Exstatic.Native + Exstatic.Native, + Exstatic.ReleaseHelper ] ] ] @@ -77,8 +79,9 @@ defmodule Exstatic.MixProject do {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, {:ex_doc, "~> 0.34", only: :dev, runtime: false}, {:sobelow, "~> 0.13", only: [:dev, :test], runtime: false}, - {:finch, "~> 0.19"}, - {:req, "~> 0.5.0"} + {:tesla, "~> 1.13.2"}, + {:jason, "~> 1.4"}, + {:mint, "~> 1.0"} ] end end diff --git a/mix.lock b/mix.lock index 5c996c6..fd2a451 100644 --- a/mix.lock +++ b/mix.lock @@ -23,5 +23,6 @@ "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"}, + "tesla": {:hex, :tesla, "1.13.2", "85afa342eb2ac0fee830cf649dbd19179b6b359bec4710d02a3d5d587f016910", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "960609848f1ef654c3cdfad68453cd84a5febecb6ed9fed9416e36cd9cd724f9"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, } From f30b9ce338afd4f8c6d2c9e3b13c5c3512f463a3 Mon Sep 17 00:00:00 2001 From: Brook <71849503+Primebrook@users.noreply.github.com> Date: Sat, 8 Feb 2025 19:22:42 +0000 Subject: [PATCH 5/7] changelog entry for 0.1.0 --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af61942..1c999e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog -## v0.1.0 +All notable changes to this project will be documented in this file. -- Initial release. +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [v0.1.0] - 2024-07-02 + +### Added +Initial Release: Introduced Exstatic, a statistical distribution library for Elixir with native Rust implementations. From 84a2813769221e0b7c03f24c81f2f1e455904270 Mon Sep 17 00:00:00 2001 From: Brook <71849503+Primebrook@users.noreply.github.com> Date: Sat, 8 Feb 2025 19:25:39 +0000 Subject: [PATCH 6/7] changelog entry for 0.1.1 release --- CHANGELOG.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c999e8..e35be42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [v0.1.0] - 2024-07-02 +## [v0.1.1] - 2024-02-07 + +### Added + +- **RustlerPrecompiled Integration:** + - Updated `Exstatic.Native` to use RustlerPrecompiled instead of requiring users to build the Rust code themselves. + - Precompiled NIFs are downloaded from GitHub Releases, reducing the need for local Rust dependencies. + - Defined `nif_versions: ["2.16"]`. + +- **GitHub Actions CI/CD for Precompiled NIF Builds (`release.yml`):** + - Builds NIFs for macOS & Linux (ARM and x86_64). + - Uses [`philss/rustler-precompiled-action`](https://github.com/philss/rustler-precompiled-action) for cross-compilation and upload. + - Automatically triggers on: + - New GitHub tags (e.g., `v0.1.0`). + - Main branch pushes. + - PRs that modify native code or the `release.yml` file. + - Uploads precompiled NIFs to GitHub Releases when a tag is pushed. + +## [v0.1.0] - 2024-02-07 ### Added Initial Release: Introduced Exstatic, a statistical distribution library for Elixir with native Rust implementations. From bea6f1022cf2a363f608c8dd8927537116a93779 Mon Sep 17 00:00:00 2001 From: Brook <71849503+Primebrook@users.noreply.github.com> Date: Sat, 8 Feb 2025 19:28:58 +0000 Subject: [PATCH 7/7] add links --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e35be42..fe8a3fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,3 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added Initial Release: Introduced Exstatic, a statistical distribution library for Elixir with native Rust implementations. + +[Unreleased]: https://github.com/Intellection/exstatic/compare/v0.1.1...HEAD +[v0.1.1]: https://github.com/Intellection/exstatic/compare/v0.1.1...v0.1.0 +[v0.1.0]: https://github.com/Intellection/exstatic/releases/tag/v0.1.0