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 useqemu_armfor 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.getAny 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:nbprand any auto-detected:nbpr_*siblings already in the workspace.lib/nbpr/<name>.ex— the package's metadata module, doinguse 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'smix.exsautomatically. - 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 samenbpr_dep/2helper.
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>"}]
endExtend 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 adaemons:declaration. SeeNBPR.BrPackage's moduledoc for the schema;:nbpr_dnsmasqis the canonical example. - Kernel modules (out-of-tree
.kofiles) — add akernel_modules:declaration. The macro generates anApplicationthat runsmodprobefor each at boot. - Build options (Buildroot kconfig you want to expose to consumers,
e.g.
--enable-fips) — add abuild_opts:schema. Consumers override via their app'sconfig/target.exsper 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/buildOn 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>"}
]
endThen 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:
- The build matrix runs for the new package. Successful builds publish the prebuilt artefact to GHCR.
- After the build succeeds, the auto-release workflow detects that the
package's local
@versionis ahead of Hex (because it's brand-new on Hex), creates anbpr_<name>-v<version>tag, and dispatches the release workflow. - The release workflow publishes the package to the
nbprHex 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 generatedmix.exs.Conditional
_DEPENDENCIES += foolines (gated byifeqon 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 inmix.exsafter scaffolding.Make-variable references like
$(TARGET_NLS_DEPENDENCIES)in the upstream_DEPENDENCIESline aren't resolved statically. Same workaround as above if the dep is mandatory.Buildroot versions like
2.91aren't valid Hex semver. The generator pads to2.91.0automatically. 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 tomix nbpr.new.