Skip to content

Commit 4840a01

Browse files
authored
Merge pull request #7 from Intellection/downloading-private-assets
Enable RustlerPrecompiled to Fetch NIFs from Exstatic (private repo)
2 parents 128ffdf + bea6f10 commit 4840a01

5 files changed

Lines changed: 142 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11
# Changelog
22

3-
## v0.1.0
3+
All notable changes to this project will be documented in this file.
44

5-
- Initial release.
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
## [v0.1.1] - 2024-02-07
11+
12+
### Added
13+
14+
- **RustlerPrecompiled Integration:**
15+
- Updated `Exstatic.Native` to use RustlerPrecompiled instead of requiring users to build the Rust code themselves.
16+
- Precompiled NIFs are downloaded from GitHub Releases, reducing the need for local Rust dependencies.
17+
- Defined `nif_versions: ["2.16"]`.
18+
19+
- **GitHub Actions CI/CD for Precompiled NIF Builds (`release.yml`):**
20+
- Builds NIFs for macOS & Linux (ARM and x86_64).
21+
- Uses [`philss/rustler-precompiled-action`](https://github.com/philss/rustler-precompiled-action) for cross-compilation and upload.
22+
- Automatically triggers on:
23+
- New GitHub tags (e.g., `v0.1.0`).
24+
- Main branch pushes.
25+
- PRs that modify native code or the `release.yml` file.
26+
- Uploads precompiled NIFs to GitHub Releases when a tag is pushed.
27+
28+
## [v0.1.0] - 2024-02-07
29+
30+
### Added
31+
Initial Release: Introduced Exstatic, a statistical distribution library for Elixir with native Rust implementations.
32+
33+
[Unreleased]: https://github.com/Intellection/exstatic/compare/v0.1.1...HEAD
34+
[v0.1.1]: https://github.com/Intellection/exstatic/compare/v0.1.1...v0.1.0
35+
[v0.1.0]: https://github.com/Intellection/exstatic/releases/tag/v0.1.0

lib/exstatic/native.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
defmodule Exstatic.Native do
22
@moduledoc false
3-
config = Mix.Project.config()
3+
version = Mix.Project.config()[:version]
44

55
use RustlerPrecompiled,
66
otp_app: :exstatic,
77
crate: "exstatic",
8-
base_url: "#{config[:source_url]}/releases/download/v#{config[:version]}",
8+
base_url: {Exstatic.ReleaseHelper, :github_release_url!},
99
force_build: System.get_env("EXSTATIC_BUILD") == "true",
1010
nif_versions: ["2.16"],
11-
version: config[:version],
11+
version: version,
1212
targets: ~w(
1313
aarch64-apple-darwin
1414
x86_64-apple-darwin

lib/exstatic/release_helper.ex

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
defmodule Exstatic.ReleaseHelper do
2+
@moduledoc """
3+
A helper module for fetching precompiled assets from a private GitHub release.
4+
5+
This module is designed to work with `RustlerPrecompiled`, allowing it to dynamically
6+
fetch the correct precompiled NIF binaries from a GitHub release using the GitHub API.
7+
It retrieves the asset ID for a given file name and constructs a download URL with the
8+
necessary authentication headers.
9+
10+
## Required Environment Variables
11+
12+
- `EXSTATIC_GITHUB_TOKEN` – A GitHub personal access token (PAT) with access to private repositories.
13+
"""
14+
15+
@config Mix.Project.config()
16+
@tag "v#{@config[:version]}"
17+
18+
@doc """
19+
Retrieves the GitHub asset download URL and authentication headers.
20+
21+
## Parameters
22+
23+
- `file_name` (String.t()): The name of the asset file to download.
24+
25+
## Returns
26+
27+
- `{download_url, headers}` (tuple): A tuple containing the GitHub asset URL and headers.
28+
29+
## Raises
30+
31+
- `RuntimeError`: If the `EXSTATIC_GITHUB_TOKEN` environment variable is missing or if the asset ID cannot be found.
32+
33+
## Example
34+
35+
iex> Exstatic.ReleaseHelper.github_release_url!("libexstatic-v0.1.1-nif-2.16-aarch64-apple-darwin.so.tar.gz")
36+
{"https://api.github.com/repos/Intellection/exstatic/releases/assets/227243824",
37+
[{"Authorization", "token MY_TOKEN"}, {"Accept", "application/octet-stream"}, {"User-Agent", "exstatic-bot"}]}
38+
"""
39+
@spec github_release_url!(String.t()) :: {String.t(), [{String.t(), String.t()}]}
40+
def github_release_url!(file_name) do
41+
token = github_personal_access_token!()
42+
43+
case fetch_github_asset_id(file_name, token) do
44+
{:ok, asset_id} ->
45+
{asset_url(asset_id),
46+
[
47+
{"Authorization", "token #{token}"},
48+
{"Accept", "application/octet-stream"},
49+
{"User-Agent", "exstatic-bot"}
50+
]}
51+
52+
{:error, reason} ->
53+
raise "Failed to fetch GitHub asset ID: #{reason}"
54+
end
55+
end
56+
57+
defp fetch_github_asset_id(file_name, token) do
58+
headers = [
59+
{"Authorization", "token #{token}"},
60+
{"Accept", "application/vnd.github+json"},
61+
{"User-Agent", "exstatic-bot"}
62+
]
63+
64+
case Tesla.get(release_url(), headers: headers) do
65+
{:ok, %Tesla.Env{status: 200, body: release}} ->
66+
release
67+
|> :json.decode()
68+
|> find_asset_id(file_name)
69+
70+
{:ok, %Tesla.Env{status: status, body: body}} ->
71+
{:error, "Failed to fetch releases: #{status} #{inspect(body)}"}
72+
73+
{:error, reason} ->
74+
{:error, reason}
75+
end
76+
end
77+
78+
defp asset_url(asset_id) do
79+
"https://api.github.com/repos/#{@config[:repo]}/releases/assets/#{asset_id}"
80+
end
81+
82+
defp release_url, do: "https://api.github.com/repos/#{@config[:repo]}/releases/tags/#{@tag}"
83+
84+
defp find_asset_id(release, file_name) do
85+
case Enum.find(release["assets"], fn asset -> asset["name"] == file_name end) do
86+
nil -> {:error, "Asset not found: #{file_name} in release #{@tag}"}
87+
asset -> {:ok, asset["id"]}
88+
end
89+
end
90+
91+
defp github_personal_access_token! do
92+
case System.get_env("EXSTATIC_GITHUB_TOKEN") do
93+
nil -> raise "Missing EXSTATIC_GITHUB_TOKEN environment variable."
94+
token -> token
95+
end
96+
end
97+
end

mix.exs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ defmodule Exstatic.MixProject do
22
use Mix.Project
33

44
@version "0.1.1"
5-
@source_url "https://github.com/intellection/exstatic"
5+
@repo "Intellection/exstatic"
6+
@source_url "https://github.com/#{@repo}"
67

78
def project do
89
[
@@ -18,10 +19,12 @@ defmodule Exstatic.MixProject do
1819
package: package(),
1920
name: "Exstatic",
2021
source_url: @source_url,
22+
repo: @repo,
2123
test_coverage: [
2224
summary: [threshold: 90],
2325
ignore_modules: [
24-
Exstatic.Native
26+
Exstatic.Native,
27+
Exstatic.ReleaseHelper
2528
]
2629
]
2730
]
@@ -75,7 +78,10 @@ defmodule Exstatic.MixProject do
7578
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
7679
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
7780
{:ex_doc, "~> 0.34", only: :dev, runtime: false},
78-
{:sobelow, "~> 0.13", only: [:dev, :test], runtime: false}
81+
{:sobelow, "~> 0.13", only: [:dev, :test], runtime: false},
82+
{:tesla, "~> 1.13.2"},
83+
{:jason, "~> 1.4"},
84+
{:mint, "~> 1.0"}
7985
]
8086
end
8187
end

mix.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@
2323
"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"},
2424
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
2525
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
26+
"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"},
2627
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
2728
}

0 commit comments

Comments
 (0)