How to add a Buildroot package to NBPR

Copy Markdown View Source

This guide takes you from "I want some upstream binary in my Nerves rootfs" to "the binary is published as :nbpr_<name> in the nbpr Hex organisation". It assumes you know what Nerves and Buildroot are, can build a Nerves firmware, and have a clone of this repo.

If the package isn't in upstream Buildroot mainline, this flow won't work — mix nbpr.new reads metadata from a mainline Buildroot tree. A vendored-package guide is on the to-do list; for now, treat out-of-tree packages as out of scope here.

Prerequisites

  • A clone of nerves-project/nbpr.
  • Elixir 1.16+ and OTP 27+.
  • A Nerves target you can build against (rpi4, bbb, etc. — pick one you have hardware for, or use qemu_arm for a host-only smoke test).
  • Docker installed locally if you're not on Linux. The source-build path uses the canonical Nerves build container.

1. Confirm the package is in upstream Buildroot

Open deps/nerves_system_br/ after mix deps.get and check package/<name>/:

ls deps/nerves_system_br/package/<name>/

You should see at minimum a <name>.mk and a Config.in. If the directory doesn't exist, the package isn't in mainline — stop here and follow the vendored-package guide instead.

2. Resolve deps for a target

The generator reads the Buildroot pin from the workspace's deps/. Pull those in:

MIX_TARGET=rpi4 mix deps.get

Any real target works — pick one that's already in the workspace mix.exs deps(). The first run downloads ~50 MB of Buildroot source into ~/.local/share/nerves/nbpr/; subsequent mix nbpr.new runs reuse the cache.

3. Scaffold the package

Run the generator with the upstream Buildroot package name (no nbpr_ prefix — the generator adds it):

mix nbpr.new <name>

This creates packages/nbpr_<name>/ with:

  • mix.exs — version, licence, description, dependency on :nbpr and any auto-detected :nbpr_* siblings already in the workspace.
  • lib/nbpr/<name>.ex — the package's metadata module, doing use NBPR.BrPackage.
  • README.md — stub with upstream description and links.
  • test/ — a smoke test asserting the metadata is well-formed.

The generator pre-fills the upstream version, SPDX-validated licences, homepage, and description directly from the Buildroot tree. You don't edit those by hand.

If a Buildroot licence string isn't a valid SPDX identifier (e.g. GPL-2.0+), the generator stops and prints suggestions. Re-run with --licenses "GPL-2.0-or-later" to override.

4. Review auto-detected sibling dependencies

The generator parses the upstream package's _DEPENDENCIES and select BR2_PACKAGE_* directives. For each dep it finds:

  • If packages/nbpr_<dep>/ already exists in the workspace, the dep is added to the new package's mix.exs automatically.
  • If not, the generator prints a warning listing the unresolved deps.

You decide what to do with each unresolved dep:

  • Provided by the base Nerves system (ncurses, openssl, zlib, libc, etc.) — ignore. They're already in the rootfs.
  • Not provided by base, not yet packaged in NBPR — go scaffold them too, recursively. mix nbpr.new <dep> for each, then come back and add them to your package's deps via the same nbpr_dep/2 helper.

5. Declare daemons, kernel modules, and build options

Open lib/nbpr/<name>.ex. The default scaffold gives you:

defmodule NBPR.<Name> do
  @moduledoc "..."

  use NBPR.BrPackage,
    version: 1,
    br_package: "<name>",
    description: "...",
    artifact_sites: [{:ghcr, "ghcr.io/<owner>/<repo>"}]
end

Extend it as the package needs. The full option schema is in NBPR.BrPackage's moduledoc. The common extensions are:

  • Daemons (the package runs a long-lived process like dnsmasq) — add a daemons: declaration. See NBPR.BrPackage's moduledoc for the schema; :nbpr_dnsmasq is the canonical example.
  • Kernel modules (out-of-tree .ko files) — add a kernel_modules: declaration. The macro generates an Application that runs modprobe for each at boot.
  • Build options (Buildroot kconfig you want to expose to consumers, e.g. --enable-fips) — add a build_opts: schema. Consumers override via their app's config/target.exs per target.

Per-extension how-tos for each of these are on the to-do list. For now, follow the schema in NBPR.BrPackage's moduledoc and copy from an existing package that does the same thing.

For a basic CLI-tool package (jq, htop, strace), no extra declarations are needed.

6. Build locally to verify

From the workspace root:

MIX_TARGET=rpi4 mix nbpr.build NBPR.<Name> -o /tmp/build

On first run this pulls the Nerves build container (~1 GB), then runs Buildroot for the package. Subsequent runs are faster — Buildroot caches its working tree per target/system-version.

A successful build leaves a nbpr_<name>-<version>-<system>-<key>.tar.gz in /tmp/build. If the tarball is there, the package built. If not, the build runner prints the offending step. Buildroot's per-package logs live under ~/.local/share/nerves/nbpr/build/<system>-<br-vsn>/<package>-build.log and friends usually point at the root cause.

7. Smoke-test in a Nerves project

Point a real Nerves project at your local checkout via a path-dep:

# In your test Nerves project's mix.exs
defp deps do
  [
    # ...
    {:nbpr, path: "../path/to/nbpr/nbpr"},
    {:nbpr_<name>, path: "../path/to/nbpr/packages/nbpr_<name>"}
  ]
end

Then mix firmware and deploy. On the device, exercise the binary via System.cmd/2 (or, for daemon-bearing packages, confirm the daemon module is supervised and running).

8. Open a PR

Commit conventions (also documented in CONTRIBUTING.md):

  • Conventional commits: improvement(packages): add nbpr_<name>.
  • One commit per logical change. Don't squash unrelated work.
  • Don't bypass commit hooks.

CI runs the package matrix on push: every (package × target × system version) is built. If anything fails, the PR shouldn't merge.

9. After merge — automatic release

Once your PR lands on main:

  1. The build matrix runs for the new package. Successful builds publish the prebuilt artefact to GHCR.
  2. After the build succeeds, the auto-release workflow detects that the package's local @version is ahead of Hex (because it's brand-new on Hex), creates a nbpr_<name>-v<version> tag, and dispatches the release workflow.
  3. The release workflow publishes the package to the nbpr Hex organisation.

You don't tag or publish manually.

Common gotchas

  • host-* dependencies are build-host-only. The generator filters them out automatically; you shouldn't see them in your generated mix.exs.

  • Conditional _DEPENDENCIES += foo lines (gated by ifeq on kconfig) are deliberately skipped by the dep parser — they depend on user kconfig choices, not intrinsic package wiring. If your package needs one of these unconditionally, declare the sibling dep manually in mix.exs after scaffolding.

  • Make-variable references like $(TARGET_NLS_DEPENDENCIES) in the upstream _DEPENDENCIES line aren't resolved statically. Same workaround as above if the dep is mandatory.

  • Buildroot versions like 2.91 aren't valid Hex semver. The generator pads to 2.91.0 automatically. Subsequent nbpr-side rebuilds of the same upstream version go in the patch position (2.91.1, 2.91.2, …).

  • Buildroot package names with hyphens (e.g. kernel-modules) map to underscored module names (NBPR.KernelModules) and underscored Hex package names (nbpr_kernel_modules). The generator handles the mapping; pass the BR-style hyphenated name to mix nbpr.new.