Precompiled NIF Distribution

View Source

C3nif can ship prebuilt .so / .dylib / .dll archives so downstream users don't need a c3c toolchain to install your library. This mirrors the workflow of rustler_precompiled for Rust NIFs: you build once per target on CI, upload the archives to a release URL, commit a checksum manifest, and consumers get a fast, verified install.

Concepts

  • Target triple — short c3c target name like linux-x64, macos-aarch64, windows-x64. The consumer's host is detected at install time; you ship one archive per target you want to support.
  • Artifact archivelib<Module>-<version>-<triple>.tar.gz containing the compiled shared library.
  • Checksum manifestchecksum-<version>.exs mapping each archive filename to its SHA-256 digest. This file is committed into the consumer's repository so downloads are verified end-to-end.

Consumer opt-in

In a downstream project:

defmodule MyApp.Nif do
  use C3nif,
    otp_app: :my_app,
    precompiled: [
      base_url: "https://github.com/me/my_app/releases/download/v0.1.0",
      version: "0.1.0",
      checksums_path: Path.expand("../../priv/checksum-0.1.0.exs", __DIR__)
    ]

  ~n"""
  module mynif;
  import c3nif;
  // ...
  """
end

At mix compile time, Mix.Tasks.Compile.C3nif will:

  1. Detect the host triple (linux-x64, macos-aarch64, etc).
  2. Load the checksum manifest and find the expected digest for lib<Module>-<version>-<triple>.tar.gz.
  3. Download the archive from {base_url}/{filename} (cached under $XDG_CACHE_HOME/c3nif_precompiled).
  4. Verify the SHA-256 against the manifest.
  5. Extract the shared library into the OTP app's priv/ directory.

If any step fails and :force_build is left at its default (true), the compile falls back to a local c3c build. Set :force_build to false to turn a fetch failure into a hard error.

Maintainer workflow

As a library maintainer producing the artifacts:

  1. Build the matrix locally or in CI:

    mix compile                    # populate the C3nif manifest
    mix c3nif.precompile           # builds for all default targets
    

    Default targets are returned by C3nif.Precompiled.default_targets/0. Override with --target:

    mix c3nif.precompile --target linux-x64 --target macos-aarch64
    

    Other useful flags:

    • --module Elixir.MyApp.Nif — build a single module
    • --version 0.1.0 — override the version string (defaults to the OTP app's mix.exs version)
    • --output-dir priv/precompiled — where archives and checksums land
  2. Upload the archives to your release hosting (e.g. a GitHub release tagged v0.1.0). The files are at priv/precompiled/lib<Module>-<version>-<triple>.tar.gz.

  3. Commit the checksum file. The task writes priv/precompiled/checksum-<version>.exs. Commit this file into your repository. Consumers reference it via :checksums_path.

  4. Bump the version for each release. The checksum file is version-pinned, so old consumers keep working.

Cross-compilation notes

mix c3nif.precompile invokes c3c build --target <triple> for each target. This relies on whatever cross toolchains are installed on the build host — typically you'll run this inside a CI matrix where each job has access to the right clang / cc for its target.

A minimal GitHub Actions setup might use the matrix.include strategy to run one job per (runner, target) pair:

strategy:
  matrix:
    include:
      - {runner: ubuntu-latest, target: linux-x64}
      - {runner: ubuntu-24.04-arm, target: linux-aarch64}
      - {runner: macos-13, target: macos-x64}
      - {runner: macos-14, target: macos-aarch64}
      - {runner: windows-latest, target: windows-x64}

Each job runs mix c3nif.precompile --target ${{ matrix.target }} and uploads the resulting archive plus partial checksum file as an artifact; a final "release" job merges the per-target checksum files into a single checksum-<version>.exs and publishes everything to the release.

Troubleshooting

  • unsupported_host — the consumer is running on a CPU/OS combo the library doesn't ship binaries for. They can either build from source (which is the default fallback) or file an issue asking for a new target in your next release.
  • Checksum mismatch — either the hosted artifact was re-uploaded without bumping the version, or the download was corrupted. Delete the cached file under $XDG_CACHE_HOME/c3nif_precompiled and retry; if the mismatch persists, the artifact has been tampered with.
  • checksum_entry_missing — the checksum manifest doesn't list the host's target. You need to re-run the precompile task with that target included and publish the updated manifest.