Skip to content

Commit 39af35e

Browse files
authored
Use a precompiled NIF (#28)
This change introduces the ability to download a precompiled NIF file and skip the need to have Rust toolchain installed. It is going to work for the majority of popular Operational Systems. For those that are not supported, the user can still force compilation by setting the "HTML5EVER_BUILD" environment variable to "true" or "1". The following targets are supported: aarch64-apple-darwin - MacOS on M1 x86_64-apple-darwin x86_64-unknown-linux-gnu x86_64-unknown-linux-musl arm-unknown-linux-gnueabihf x86_64-pc-windows-gnu x86_64-pc-windows-msvc The supported NIF versions are "2.14", "2.15" and "2.16". With this change, every time a new Git tag is pushed to the main repository a new release is made. So we need to push tags before publishing the repository to Hex.pm. This is due the release workflow that have to build files before we use them.
1 parent 8ba6337 commit 39af35e

File tree

11 files changed

+676
-10
lines changed

11 files changed

+676
-10
lines changed

.github/workflows/release.yml

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
name: Build release
2+
3+
env:
4+
MIN_SUPPORTED_RUST_VERSION: "1.55.0"
5+
6+
on:
7+
push:
8+
tags:
9+
- '*'
10+
11+
defaults:
12+
run:
13+
# Sets the working dir for "run" scripts.
14+
# Note that this won't change the directory for actions (tasks with "uses").
15+
working-directory: ./native/html5ever_nif
16+
17+
jobs:
18+
build_release:
19+
name: NIF ${{ matrix.job.nif }} - ${{ matrix.job.target }} (${{ matrix.job.os }})
20+
runs-on: ${{ matrix.job.os }}
21+
strategy:
22+
fail-fast: false
23+
matrix:
24+
job:
25+
# NIF version 2.16
26+
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04 , nif: "2.16", use-cross: true }
27+
- { target: aarch64-apple-darwin , os: macos-10.15 , nif: "2.16" }
28+
- { target: x86_64-apple-darwin , os: macos-10.15 , nif: "2.16" }
29+
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 , nif: "2.16" }
30+
- { target: x86_64-unknown-linux-musl , os: ubuntu-20.04 , nif: "2.16", use-cross: true }
31+
- { target: x86_64-pc-windows-gnu , os: windows-2019 , nif: "2.16" }
32+
- { target: x86_64-pc-windows-msvc , os: windows-2019 , nif: "2.16" }
33+
# NIF version 2.15
34+
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04 , nif: "2.15", use-cross: true }
35+
- { target: aarch64-apple-darwin , os: macos-10.15 , nif: "2.15" }
36+
- { target: x86_64-apple-darwin , os: macos-10.15 , nif: "2.15" }
37+
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 , nif: "2.15" }
38+
- { target: x86_64-unknown-linux-musl , os: ubuntu-20.04 , nif: "2.15", use-cross: true }
39+
- { target: x86_64-pc-windows-gnu , os: windows-2019 , nif: "2.15" }
40+
- { target: x86_64-pc-windows-msvc , os: windows-2019 , nif: "2.15" }
41+
# NIF version 2.14
42+
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04 , nif: "2.14", use-cross: true }
43+
- { target: aarch64-apple-darwin , os: macos-10.15 , nif: "2.14" }
44+
- { target: x86_64-apple-darwin , os: macos-10.15 , nif: "2.14" }
45+
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 , nif: "2.14" }
46+
- { target: x86_64-unknown-linux-musl , os: ubuntu-20.04 , nif: "2.14", use-cross: true }
47+
- { target: x86_64-pc-windows-gnu , os: windows-2019 , nif: "2.14" }
48+
- { target: x86_64-pc-windows-msvc , os: windows-2019 , nif: "2.14" }
49+
50+
env:
51+
RUSTLER_NIF_VERSION: ${{ matrix.job.nif }}
52+
steps:
53+
- name: Checkout source code
54+
uses: actions/checkout@v2
55+
56+
- name: Install prerequisites
57+
shell: bash
58+
run: |
59+
case ${{ matrix.job.target }} in
60+
arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;;
61+
aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;;
62+
esac
63+
64+
- name: Extract crate information
65+
shell: bash
66+
run: |
67+
echo "PROJECT_NAME=$(sed -n 's/^name = "\(.*\)"/\1/p' Cargo.toml | head -n1)" >> $GITHUB_ENV
68+
# Get the project version from mix.exs
69+
echo "PROJECT_VERSION=$(sed -n 's/^ @version "\(.*\)"/\1/p' ../../mix.exs | head -n1)" >> $GITHUB_ENV
70+
71+
- name: Install Rust toolchain
72+
uses: actions-rs/toolchain@v1
73+
with:
74+
toolchain: stable
75+
target: ${{ matrix.job.target }}
76+
override: true
77+
profile: minimal
78+
79+
- name: Show version information (Rust, cargo, GCC)
80+
shell: bash
81+
run: |
82+
gcc --version || true
83+
rustup -V
84+
rustup toolchain list
85+
rustup default
86+
cargo -V
87+
rustc -V
88+
89+
- name: Download cross from GitHub releases
90+
uses: giantswarm/[email protected]
91+
if: ${{ matrix.job.use-cross }}
92+
with:
93+
binary: "cross"
94+
version: "v0.2.1"
95+
download_url: "https://github.com/rust-embedded/cross/releases/download/${version}/cross-${version}-x86_64-unknown-linux-gnu.tar.gz"
96+
tarball_binary_path: "${binary}"
97+
smoke_test: "${binary} --version"
98+
99+
- name: Build
100+
shell: bash
101+
run: |
102+
if [ "${{ matrix.job.use-cross }}" == "true" ]; then
103+
cross build --release --target=${{ matrix.job.target }}
104+
else
105+
cargo build --release --target=${{ matrix.job.target }}
106+
fi
107+
108+
- name: Rename lib to the final name
109+
id: rename
110+
shell: bash
111+
run: |
112+
LIB_PREFIX="lib"
113+
case ${{ matrix.job.target }} in
114+
*-pc-windows-*) LIB_PREFIX="" ;;
115+
esac;
116+
117+
# Figure out suffix of lib
118+
# See: https://doc.rust-lang.org/reference/linkage.html
119+
LIB_SUFFIX=".so"
120+
case ${{ matrix.job.target }} in
121+
*-apple-darwin) LIB_SUFFIX=".dylib" ;;
122+
*-pc-windows-*) LIB_SUFFIX=".dll" ;;
123+
esac;
124+
125+
CICD_INTERMEDIATES_DIR=$(mktemp -d)
126+
127+
# Setup paths
128+
LIB_DIR="${CICD_INTERMEDIATES_DIR}/released-lib"
129+
mkdir -p "${LIB_DIR}"
130+
LIB_NAME="${LIB_PREFIX}${{ env.PROJECT_NAME }}${LIB_SUFFIX}"
131+
LIB_PATH="${LIB_DIR}/${LIB_NAME}"
132+
133+
# Copy the release build lib to the result location
134+
cp "target/${{ matrix.job.target }}/release/${LIB_NAME}" "${LIB_DIR}"
135+
136+
# Final paths
137+
# In the end we use ".so" for MacOS in the final build
138+
# See: https://www.erlang.org/doc/man/erlang.html#load_nif-2
139+
LIB_FINAL_SUFFIX="${LIB_SUFFIX}"
140+
case ${{ matrix.job.target }} in
141+
*-apple-darwin) LIB_FINAL_SUFFIX=".so" ;;
142+
esac;
143+
144+
# Check Html5ever.Precompiled.target/0 - lib/html5ever/precompiled.ex
145+
LIB_FINAL_NAME="${LIB_PREFIX}${PROJECT_NAME}-v${PROJECT_VERSION}-nif-${RUSTLER_NIF_VERSION}-${{ matrix.job.target }}${LIB_FINAL_SUFFIX}"
146+
147+
# Copy lib to final name on this directory
148+
cp "${LIB_PATH}" "${LIB_FINAL_NAME}"
149+
150+
tar -cvzf "${LIB_FINAL_NAME}.tar.gz" "${LIB_FINAL_NAME}"
151+
152+
# Passes the path relative to the root of the project.
153+
LIB_FINAL_PATH="native/html5ever_nif/${LIB_FINAL_NAME}.tar.gz"
154+
155+
# Let subsequent steps know where to find the lib
156+
echo ::set-output name=LIB_FINAL_PATH::${LIB_FINAL_PATH}
157+
echo ::set-output name=LIB_FINAL_NAME::${LIB_FINAL_NAME}.tar.gz
158+
159+
- name: "Artifact upload"
160+
uses: actions/upload-artifact@v2
161+
with:
162+
name: ${{ steps.rename.outputs.LIB_FINAL_NAME }}
163+
path: ${{ steps.rename.outputs.LIB_FINAL_PATH }}
164+
165+
- name: Publish archives and packages
166+
uses: softprops/action-gh-release@v1
167+
with:
168+
files: |
169+
${{ steps.rename.outputs.LIB_FINAL_PATH }}

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ def deps do
1616
end
1717
```
1818

19+
By default **you don't need Rust installed** because the lib will try to download
20+
a precompiled NIF file. In case you want to force compilation set the
21+
`HTML5EVER_BUILD` environment variable to `true` or `1`. Alternatively you can also set the
22+
application env `:build_from_source` to `true` in order to force the build:
23+
24+
```elixir
25+
config :html5ever, Html5ever, build_from_source: true
26+
```
27+
1928
## License
2029

2130
Licensed under either of

lib/html5ever.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
defmodule Html5ever do
22
@moduledoc """
33
The html5ever is an HTML parser written in Rust.
4+
5+
By default this lib will try to use a precompiled NIF
6+
from the GitHub releases page. This way you don't need
7+
to have the Rust toolchain installed.
8+
In case no precompiled file is found an error is raised.
9+
10+
You can force the precompilation to occur by setting the
11+
value of the `HTML5EVER_BUILD` environment variable to
12+
"true" or "1".
413
"""
514

615
@doc """

lib/html5ever/native.ex

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,42 @@
11
defmodule Html5ever.Native do
22
@moduledoc false
3+
require Logger
4+
5+
mix_config = Mix.Project.config()
6+
version = mix_config[:version]
7+
github_url = mix_config[:package][:links]["GitHub"]
8+
9+
rustler_opts = [otp_app: :html5ever, crate: "html5ever_nif", mode: :release]
10+
env_config = Application.get_env(rustler_opts[:otp_app], Html5ever, [])
11+
12+
opts =
13+
if System.get_env("HTML5EVER_BUILD") in ["1", "true"] or env_config[:build_from_source] do
14+
rustler_opts
15+
else
16+
case Html5ever.Precompiled.download_or_reuse_nif_file(
17+
rustler_opts,
18+
base_url: "#{github_url}/releases/download/v#{version}",
19+
version: version
20+
) do
21+
{:ok, new_opts} ->
22+
new_opts
23+
24+
{:error, error} ->
25+
error =
26+
"Error while downloading precompiled NIF: #{error}\n\nSet HTML5EVER_BUILD=1 env var to compile the NIF from scratch. You can also configure this application to force compilation:\n\n config :html5ever, Html5ever, build_from_source: true\n"
27+
28+
if Mix.env() == :prod do
29+
raise error
30+
else
31+
Logger.debug(error)
32+
rustler_opts
33+
end
34+
end
35+
end
336

437
# This module will be replaced by the NIF module after
538
# loaded. It throws an error in case the NIF can't be loaded.
6-
use Rustler, otp_app: :html5ever, crate: "html5ever_nif", mode: :release
39+
use Rustler, opts
740

841
def parse_sync(_binary), do: err()
942
def parse_async(_binary), do: err()

0 commit comments

Comments
 (0)