A subprocess's output reaching the terminal as it is produced — faithful
for valid UTF-8, with anything unreadable shown as �, and unable to stop
the task that runs it, whatever bytes it carries.
The port hands System.cmd/3's collectable flat 65536-byte chunks, so a
boundary falls wherever the byte count lands — regularly inside one of the
box-drawing characters a compiler diagnostic is framed with. The device
those bytes go to is a unicode one, and stays one when stdout is redirected
to a file, so half a codepoint reaching it raises instead of printing.
IO.stream/2's second argument is no lever against that: :line and a
byte count alike shape the read direction only, and the collectable it
returns writes every chunk it is handed straight to the device whichever
one is passed — so the line-at-a-time framing that argument suggests was
never real, and tuning it was never a fix.
This collectable therefore never presents an incomplete or invalid sequence
to the device. It holds back a trailing sequence the bytes so far still
allow to be a codepoint — at most three bytes — and prepends it to the next
chunk; a sequence a byte has already ruled out has its first byte written
as � and the remainder re-split behind it. Nothing else is read out of
the bytes: no lines, no escape sequences, no assumption about what the
subprocess prints. That is what keeps a carriage-return progress redraw on
screen as it arrives, where a collectable buffering by line shows docker's
fast layers once, at the end.
What it collects must be iodata: System.cmd/3 opens its port with
:binary, so that is what it is handed. Chardata is not accepted, and the
narrowing is silent rather than loud — a codepoint list is read as bytes,
so codepoints 128–255 come out as � and anything larger raises in
IO.iodata_to_binary/1.
The device is a field rather than a fixed :stdio because a StringIO
enforces the same encoding: a test can prove the property against one
without capturing the run's own output.