# Precompilation guide
This guide has two sections, the first one is intended for precompiler module developers. It covers a minimal example of creating a precompiler module. The second section is intended for library developers who want their library to be able to use precompiled artefacts in a simple way.
## Library Developer
This guide assumes you have already added `elixir_make` to your library and you have written a `Makefile` that compiles the native code in your project. Once your native code compile and works as expected, you are now ready to precompile it.
A full demo project is available on [cocoa-xu/cc_precompiler_example](https://github.com/cocoa-xu/cc_precompiler_example).
### Setup mix.exs
To use a precompiler module such as the `CCPrecompiler` example above, we first add the precompiler (`:cc_precompiler` here) and `:elixir_make` to `deps`.
```elixir
def deps do
[
# ...
{:elixir_make, "~> 0.6", runtime: false},
{:cc_precompiler, "~> 0.1", runtime: false, github: "cocoa-xu/cc_precompiler"}
# ...
]
end
```
Then add `:elixir_make` to the `compilers` list, and set `CCPrecompile` as the value for `make_precompiler`.
```elixir
@version "0.1.0"
def project do
[
# ...
compilers: [:elixir_make] ++ Mix.compilers(),
# elixir_make specific config
make_precompiler: {:nif, CCPrecompiler},
make_precompiler_url: "https://github.com/cocoa-xu/cc_precompiler_example/releases/download/v#{@version}/@{artefact_filename}",
make_precompiler_filename: "nif",
make_precompiler_priv_paths: ["nif.*"],
make_precompiler_unavailable_target: :compile,
# ...
]
end
```
Another required field is `make_precompiled_url`. It is a URL template to the artefact file.
`@{artefact_filename}` in the URL template string will be replaced by corresponding artefact filenames when fetching them. For example, `cc_precompiler_example-nif-2.16-x86_64-linux-gnu-0.1.0.tar.gz`.
Note that there is an optional config key for elixir_make, `make_precompiler_filename`. If the name (file extension does not count) of the shared library is different from your app's name, then `make_precompiler_filename` should be set. For example, if the app name is `"cc_precompiler_example"` while the name shared library is `"nif.so"` (or `"nif.dll"` on windows), then `make_precompiler_filename` should be set as `"nif"`.
Another optional config key is `make_precompiler_priv_paths`. For example, say the `priv` directory is organised as follows in Linux, macOS and Windows respectively,
Also, you can specify how to recover from unavailable targets using the `make_precompiler_unavailable_target` config key. Allowed values are `:compile` and `:ignore`. Defaults to `:compile`.
It is also possible to pass in a 2-arity function to `make_precompiler_unavailable_target`: the first argument is the triplet of the unavailable target, and the second argument is a list that contains all available targets given by the precompiler.
```
# Linux
.
├── assets
│ ├── model.onnx
│ └── data.json
├── lib
│ ├── libpriv1.so
│ ├── libpriv2.so
│ └── libpriv3.so
└── nif.so
# macOS
.
├── assets
│ ├── model.onnx
│ └── data.json
├── lib
│ ├── libpriv1.dylib
│ ├── libpriv2.dylib
│ └── libpriv3.dylib
└── nif.so
# Windows
.
├── assets
│ ├── model.onnx
│ └── data.json
├── lib
│ ├── libpriv1.dll
│ ├── libpriv2.dll
│ └── libpriv3.dll
└── nif.dll
```
By default, everything in `priv` will be included in the precompiled tar file. However, files in `assets` can be very large or platform-independent, therefore, we would like to only include the `nif.so` (`nif.dll`) file and everything in the `lib` directory in the precompiled tar file to reduce the footprint. In this case, we can set `make_precompiler_priv_paths` to `["nif.so", "nif.dll", "lib"]`.
Of course, wildcards (`?`, `**`, `*`) are supported when specifying files. For example, `["nif.*", "lib/*.so", "lib/*.dll", "lib/*.dylib"]` will include `nif.so` (Linux/macOS) or `nif.dll` (Windows), and `.so` or `.dll` files in the `lib` directory.
Directory structures and symbolic links are preserved.
### (Optional) Test the NIF code locally
To test the NIF code locally, you can either set `force_build` to `true` or append `"-dev"` to your NIF library's version string.
```elixir
@version "0.1.0-dev"
def project do
[
# either append `"-dev"` to your NIF library's version string
version: @version,
# or set force_build to true
force_build: true,
# ...
]
end
```
Doing so will ask `elixir_make` to only compile for the current host instead of building for all available targets.
```shell
$ mix compile
cc -shared -std=c11 -O3 -fPIC -I"/usr/local/lib/erlang/erts-13.0.3/include" -undefined dynamic_lookup -flat_namespace -undefined suppress "/Users/cocoa/git/cc_precompiler_example/c_src/cc_precompiler_example.c" -o "/Users/cocoa/Git/cc_precompiler_example/_build/dev/lib/cc_precompiler_example/priv/nif.so"
$ mix test
make: Nothing to be done for `build'.
Generated cc_precompiler_example app
.
Finished in 0.00 seconds (0.00s async, 0.00s sync)
1 test, 0 failures
Randomized with seed 102464
```
### Precompile for available targets
It's possible to either setup a CI task to do the precompilation job or precompile on a local machine and upload the precompiled artefacts.
To precompile for all targets on a local machine:
```shell
MIX_ENV=prod mix elixir_make.precompile
```
Environment variable `ELIXIR_MAKE_CACHE_DIR` can be used to set the cache dir for the precompiled artefacts, for instance, to output precompiled artefacts in the cache directory of the current working directory, `export ELIXIR_MAKE_CACHE_DIR="$(pwd)/cache"`.
To setup a CI task such as GitHub Actions, the following workflow file can be used for reference:
```yml
name: precompile
on:
push:
tags:
- 'v*'
jobs:
linux:
runs-on: ubuntu-latest
env:
MIX_ENV: "prod"
steps:
- uses: actions/checkout@v3
- uses: erlef/setup-beam@v1
with:
otp-version: "25.1"
elixir-version: "1.14"
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential automake autoconf pkg-config bc m4 unzip zip \
gcc g++ \
gcc-i686-linux-gnu g++-i686-linux-gnu \
gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \
gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf \
gcc-riscv64-linux-gnu g++-riscv64-linux-gnu \
gcc-powerpc64le-linux-gnu g++-powerpc64le-linux-gnu \
gcc-s390x-linux-gnu g++-s390x-linux-gnu
- name: Get musl cross-compilers (Optional, use this if you have musl targets to compile)
run: |
for musl_arch in x86_64 aarch64 riscv64
do
wget "https://musl.cc/${musl_arch}-linux-musl-cross.tgz" -O "${musl_arch}-linux-musl-cross.tgz"
tar -xf "${musl_arch}-linux-musl-cross.tgz"
done
- name: Mix Test
run: |
# Optional, use this if you have musl targets to compile
for musl_arch in x86_64 aarch64 riscv64
do
export PATH="$(pwd)/${musl_arch}-linux-musl-cross/bin:${PATH}"
done
mix deps.get
MIX_ENV=test mix test
- name: Create precompiled library
run: |
export ELIXIR_MAKE_CACHE_DIR=$(pwd)/cache
mkdir -p "${ELIXIR_MAKE_CACHE_DIR}"
mix elixir_make.precompile
- uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
cache/*.tar.gz
macos:
runs-on: macos-11
env:
MIX_ENV: "prod"
steps:
- uses: actions/checkout@v3
- name: Install erlang and elixir
run: |
brew install erlang elixir
mix local.hex --force
mix local.rebar --force
- name: Mix Test
run: |
mix deps.get
MIX_ENV=test mix test
- name: Create precompiled library
run: |
export ELIXIR_MAKE_CACHE_DIR=$(pwd)/cache
mkdir -p "${ELIXIR_MAKE_CACHE_DIR}"
mix elixir_make.precompile
- uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
cache/*.tar.gz
```
### Generate checksum file
After CI has finished, you can fetch the precompiled binaries from GitHub.
```shell
$ MIX_ENV=prod mix elixir_make.checksum --all --ignore-unavailable
```
Meanwhile, a checksum file will be generated. In this example, the checksum file will be named as `checksum.exs` in current working directory.
This checksum file is extremely important in the scenario where you need to release a Hex package using precompiled NIFs. It's **MANDATORY** to include this file in your Hex package (by updating the `files` field in the `mix.exs`). Otherwise your package **won't work**.
```elixir
defp package do
[
files: [
# ...
"checksum.exs",
# ...
],
# ...
]
end
```
However, there is no need to track the checksum file in your version control system (git or other).
### (Optional) Test fetched artefacts can work locally
```shell
# delete previously built binaries so that
# elixir_make will try to restore the NIF library
# from the downloaded tarball file
$ rm -rf _build/prod/lib/cc_precompiler_example
# set to prod env and test everything
$ MIX_ENV=prod mix test
==> castore
Compiling 1 file (.ex)
Generated castore app
==> elixir_make
Compiling 5 files (.ex)
Generated elixir_make app
==> cc_precompiler
Compiling 1 file (.ex)
Generated cc_precompiler app
20:47:42.262 [debug] Restore NIF for current node from: /Users/cocoa/Library/Caches/cc_precompiler_example-nif-2.16-aarch64-apple-darwin-0.1.0.tar.gz
==> cc_precompiler_example
Compiling 1 file (.ex)
Generated cc_precompiler_example app
.
Finished in 0.01 seconds (0.00s async, 0.01s sync)
1 test, 0 failures
Randomized with seed 539590
```
## Recommended flow
To recap, the suggested flow is the following:
1. Choose an appropriate precompiler for your NIF library and set all necessary options in the `mix.exs`.
2. (Optional) Test if your NIF library compiles locally.
```shell
mix compile
mix test
```
3. (Optional) Test if your NIF library can precompile to all specified targets locally.
```shell
MIX_ENV=prod mix elixir_make.precompile
```
4. Precompile your library on CI or locally.
```shell
# locally
MIX_ENV=prod mix elixir_make.precompile
# CI
# please see the docs above
```
5. Fetch precompiled binaries from GitHub.
```shell
# only fetch artefact for current host
MIX_ENV=prod mix elixir_make.checksum --only-local --print
# fetch all
MIX_ENV=prod mix elixir_make.checksum --all --print
# to fetch all available artefacts at the moment
MIX_ENV=prod mix elixir_make.checksum --all --print --ignore-unavailable
```
6. (Optional) Test if the downloaded artefacts works as expected.
```shell
rm -rf _build/prod/lib/NIF_LIBRARY_NAME
MIX_ENV=prod mix test
```
6. Update Hex package to include the checksum file.
7. Release the package to Hex.pm (make sure your release includes the correct files).