Streams a .fw to fwup --apply --framing via an Erlang Port.
Library-shape: no host dependencies.
Wire protocol
Both directions are length-prefixed: [4-byte BE length | payload].
Input from us:
[length | bytes]records carrying.fwpayload[0](zero-length record) marks end-of-stream
Output from fwup (each record's payload begins with a 2-byte type code):
PR+ 2-byte BE percent — progressOK+ 2 bytes reserved — successER+ UTF-8 error message — errorWN+ UTF-8 warning — non-fatal warning
We deliberately do not pass --exit-handshake: in framing mode
with an Erlang Port, the handshake byte deadlocks because fwup
doesn't see EOF on stdin while the Port is alive. fwup exits
cleanly on the zero-length terminator without the handshake.
Source for the framing format: nerves_firmware_http and
ssh_subsystem_fwup use the same parser. We rely on
Port.command/2's blocking semantics for back-pressure: when fwup's
stdin buffer is full, the write blocks until fwup drains.
Caller contract
Callers MUST serialize apply/2 invocations. Each call spawns one
fwup process against devpath; concurrent applies to the same
device are undefined.
Isolation from port death
The Erlang Port is opened and owned by an isolated spawn_monitor
worker, not by the calling process. If the fwup OS process dies
mid-stream (broken pipe / :epipe — bad .fw, disk full, early
exit), the linked port delivers an EXIT signal only to that worker
(which traps exits), never to the caller. The caller only ever
receives the worker's {ref, result} message or, if the worker
itself crashes unexpectedly, a :DOWN message — both are folded
into the documented :ok | {:error, term()} contract.
Accepted risks
- No
--exit-handshake(see above) — the finalERtext can race behind:exit_status, so an error exit may report a nil/stale message. :stderr_to_stdoutmerges stderr into the framed stream. A chatty stderr line could theoretically desynchronize frame parsing or stall it. Accepted because fwup in framing mode keeps stderr quiet in practice.- A mid-stream fwup death no longer crashes the caller: it is
surfaced as
{:error, {:fwup_port_exit, reason}}(trapped exit signal from the dying port) or{:error, :fwup_port_closed}(write attempted after the port was already gone), because the port itself is owned by an isolated monitored worker (see above) rather than by the caller.
Summary
Functions
Applies fw_path via fwup, blocking until fwup exits.
Types
Functions
Applies fw_path via fwup, blocking until fwup exits.
Options
:devpath— block device to write to. Required for real hardware; for the host-side smoke test it can be a path to a file used as a target.:task— the fwup task name (default"upgrade").:progress— callback invoked as{:flashing, percent}while fwup applies the firmware.:chunk_size— read chunk size forfw_path(default 65536).:fwup_path— explicit path to the fwup executable. Defaults toSystem.find_executable("fwup").:extra_args— extra fwup CLI args appended after the fixed args (default[]). Hook for future delta-update fwup flags like--unsafe.
Returns :ok on exit status 0; {:error, term()} otherwise.