← Package generator

Building a module

How to make something the generator packs into your .fw and the installer runs on the PLAY, next to Tailscale, the KVM endpoint, the media player and the UVC converter — which are all modules in everything but packaging.

What a module is

The whole package format is a gzip'd tar with an executable update at the top level, which BirdDog's updater extracts into a temp dir and runs as root with no signature check. Our update is a bash script. A module is a directory it finds at modules/<name>/ inside that package, carrying an install script that it runs — as root, from that directory — once its own payloads are in place.

You hand the generator a .tgz of that directory under Custom modules. It is read in this browser (nothing is uploaded anywhere), checked, and written into the .fw at ./modules/<name>/. The quickest way to a working one is bd-play-module-template: a complete module that installs a heartbeat service, with every line of its install commented.

Layout and module.conf

module.conf          NAME, VERSION, DESCRIPTION, HOMEPAGE — read, never sourced
install              bash; the installer runs it as root with this directory as cwd
uninstall            optional but expected; copy it onto the device so it can be run later
payload/             whatever install copies to /userdata/bd-<name>/
  run.sh               the unit's ExecStart
  bin/                 compiled programs — aarch64 only

Only module.conf and install are required, and only their names are — what goes in payload/ and how install uses it are yours. The archive may hold these at its root, or all inside one top-level directory (what tar czf module.tgz mymodule/ makes); the generator strips that directory. macOS ._ files and .DS_Store are dropped.

module.conf is shell-style KEY=value, one per line, quotes optional. It is parsed with a regular expression here and with sed on the device; nothing in it is executed.

KeyRuleUsed for
NAME required; 1–32 characters of a-z 0-9 -, starting with a letter or digit. Not one of tailscale tailscale-ui kvm cam cam-api play probe modules, which the generator's own payloads use. modules/<NAME>/ in the package; by convention /userdata/bd-<NAME> and bd-<NAME>.service on the device. Two chosen modules with one NAME are refused.
VERSION required; 1–32 characters of A-Z a-z 0-9 . _ + - logged on the device with each install
DESCRIPTIONoptionalshown in the build log here
HOMEPAGEoptionalfor whoever reads the file later

What install gets

The installer runs bash ./install — bash regardless of your shebang — with the module directory as the working directory and this in the environment:

VariableValue
BD_MODULENAME from module.conf
BD_MODULE_VERSIONVERSION from module.conf
BD_MODULE_DIRabsolute path of the module directory (also the cwd)
BD_BUILD_TAGthe package's build tag, as typed on the generator
BD_HARDWAREBirdDog PLAY or BirdDog Pod — anything else has already been refused before any module runs
BD_INSTALL_LOG/tmp/bd-custom-install.log, where your stdout and stderr are appended
bd_logan exported function. bd_log "text" appears in the web UI's live update log as [module:<name>] text, and in the install log. Use it for the two or three lines a person watching the upload should see.

Around your script, the installer has already: checked the hardware ID, remounted the rootfs read-write if it needed to, made sure /userdata exists, and installed whichever built-in payloads were selected — so a module may rely on, say, /userdata/tailscale being present if the user ticked Tailscale, and should cope if they did not.

It then runs your module in a way that means one that fails — exits non-zero, crashes, hangs — cannot take the install down with it:

None of that contains a module that means harm or gets it badly wrong: it runs as root, and it can reboot, stop BirdDogRunner without starting it again, or kill the updater mid-way, and no wrapper will stop it. That is what the rules below are for, and why the generator asks you to read what you package.

Modules run in name order, after the built-in payloads and the rootfs overlay, before the probe report and before the built-in units are enabled. One line per module — name= version= build= rc= — is appended to /userdata/bd-modules.log.

The rules

Nothing enforces these. They are what has kept every unit this project has touched recoverable, and they are the same rules the built-in installer follows.

Limits the format imposes

Building and checking

Any tar will do — tar czf mymodule.tgz mymodule/ — but the template's build.sh packs only what the device needs, as ustar owned by root, and runs its check.sh first: names, path lengths, links, ELF architecture, that every script parses, and a warning if install so much as mentions the recovery path or rebooting. Its CI then has GNU tar list the archive and imports this page's own fw.js to prove the generator will accept it:

./check.sh
./build.sh                                   # dist/<NAME>-<VERSION>.tgz
node scripts/patcher-accepts.mjs dist/*.tgz  # the generator's reader, same messages as here

Then choose the .tgz under Custom modules on the generator. Each chosen file is read and checked immediately and listed with a tick or the reason it was refused; a refused file blocks the build rather than being dropped quietly, because you chose it. The build log names each module and where it lands in the package. If you want to see exactly what will run, the .fw is a plain tarball: tar tzvf BirdDog_PLAY-custom-*.fw.

On the device

  1. Upload the .fw on the PLAY's firmware page. Your bd_log lines appear in its live log as [module:<name>] ….
  2. Read http://<play-ip>/static/bd-probe.txt — the probe report now ends with one line per module and its exit code, and works without SSH.
  3. Everything your install printed is in /tmp/bd-custom-install.log; journalctl -u bd-<name> has the service.
  4. To remove a module built from the template: bash /userdata/bd-<name>/uninstall.

Worked examples

Every payload this generator ships is a module in all but packaging, and each is public: