Skip to content

macOS 26 ships no vendor printer drivers: a host-based USB printer (Brother HBP) needs a hand-built CUPS filter under /Library/Printers with an absolute *cupsFilter path

TL;DR.

A USB printer that ioreg and lpinfo -v both see but macOS will not print to is usually a driver gap, not a connection problem: macOS 26 bundles only 16 generic PPDs, and a host-based printer (1284 device ID CMD:PJL,HBP) speaks neither PostScript nor PCL. Fix is building brlaser (no Homebrew formula, no cmake needed), installing the filter under /Library/Printers because ServerBin is read-only on the sealed system volume, and referencing it by absolute path in *cupsFilter.

Symptom

A USB printer is plugged into a modern Mac and "macOS doesn't see it." On macOS 26.5.1 (Darwin 25.5, Apple silicon) the printer was in fact visible at every layer below CUPS:

$ ioreg -p IOUSB -w0 -l | grep 'USB Product Name'   # -> "HL-L2340D series"
$ lpinfo -v
direct usb://Brother/HL-L2340D%20series?serial=U63879L4N570122
$ lpstat -t
lpstat: No destinations added.

Enumeration is fine; there is simply no queue, and nothing will auto-create one.

Two things to check before blaming the cable

1. Is the printer IPP-over-USB (driverless)? Look at the USB interface descriptor, not the marketing:

$ ioreg -p IOService -n "HL-L2340D series" -r -l -w0 | grep bInterface
"bInterfaceClass" = 7      # printer
"bInterfaceSubClass" = 1
"bInterfaceProtocol" = 2   # bidirectional; protocol 4 == IPP-USB

Only protocol 4 gets an automatic AirPrint/ippusb queue. Protocol 1/2 means the OS needs a real driver, no matter what the printer's URF: capability string claims (URF/AppleRaster is only reachable through IPP).

2. What page description language does it actually speak? The IEEE-1284 device ID is authoritative:

$ lpinfo --include-schemes usb -l -v
device-id = SERN:...;MFG:Brother;CMD:PJL,HBP,URF;MDL:HL-L2340D series;...

CMD:PJL,HBP = host-based printing (Brother HBP), no PostScript and no PCL. macOS 26 ships exactly 16 generic PPDs (lpinfo -m: Generic PostScript, Generic PCL, a few Epson/HP/Zebra/Dymo, raw, everywhere) and /Library/Printers/PPDs/Contents/Resources is empty. Picking "Generic PostScript" prints garbage; no bundled driver can work.

For this Brother mono-laser family the answer is brlaser (github.com/pdewacht/brlaser). Its brlaser.drv entries carry full 1284 device IDs, so the model match is verifiable rather than a guess. There is no Homebrew formula (brew info brlaser -> no such formula), so you build it.

Building brlaser on macOS without cmake

Four translation units, and the macOS SDK already has cups/raster.h plus libcupsimage.tbd:

git clone --depth 1 https://github.com/pdewacht/brlaser.git && cd brlaser
VERSION=$(sed -n 's/^set(BRLASER_VERSION "\([0-9]*\)").*/\1/p' CMakeLists.txt)
for f in config.h brlaser.drv; do
  sed -e 's/@CMAKE_PROJECT_NAME@/brlaser/g' -e "s/@BRLASER_VERSION@/$VERSION/g" "$f.in" > "$f"
done
clang++ -std=c++11 -O2 -I. -o rastertobrlaser src/main.cc src/job.cc src/line.cc src/debug.cc -lcups
ppdc -d ppd brlaser.drv     # Apple still ships ppdc/ppdi/ppdmerge in /usr/bin

cups-config --image --libs reports only -lcups on macOS; that is sufficient (no separate -lcupsimage). Upstream's four unit tests compile and pass the same way.

Ghostscript is not needed on macOS, unlike the Linux packaging: Apple's cgpdftoraster does PDF -> CUPS raster, so the chain is cgtexttopdf/cgpdftopdf -> cgpdftoraster -> rastertobrlaser.

Installing under the sealed system volume

/usr/libexec/cups/filter (CUPS ServerBin) is read-only on any SSV macOS, so the filter cannot go where PPDs normally reference it. Put it under /Library/Printers and give the PPD an absolute filter path:

*cupsFilter: "application/vnd.cups-raster 33 /Library/Printers/brlaser/filter/rastertobrlaser"

CUPS resolves a bare filter name against ServerBin and an absolute path as-is; cupstestppd proves it by stat-ing the absolute path (Missing cupsFilter file "/Library/..." before install, PASS after). Ownership matters: install -o root -g wheel -m 755 for the filter, 644 for the PPD, or cupsd refuses to run it.

Then create the queue with the URI from lpinfo -v, serial and all:

lpadmin -p HL_L2340D -E -v 'usb://Brother/HL-L2340D%20series?serial=U63879L4N570122' \
  -P /Library/Printers/PPDs/Contents/Resources/brlaser-HL-L2340D.ppd -o printer-is-shared=false
lpadmin -d HL_L2340D && cupsaccept HL_L2340D && cupsenable HL_L2340D

lpadmin warns that CUPS printer drivers are deprecated and will stop working in a future CUPS version. Noted, still the only option for host-based hardware.

Verify before spending paper

The chain runs by hand with no root and no printer attached:

cupsfilter -m application/pdf t.txt > t.pdf
PPD=./brlaser-HL-L2340D.ppd /usr/libexec/cups/filter/cgpdftoraster 1 user title 1 "" t.pdf > t.raster
PPD=./brlaser-HL-L2340D.ppd ./rastertobrlaser 1 user title 1 "" < t.raster > t.prn
strings t.prn | head    # expect @PJL JOB, @PJL SET PAPER = LETTER, ENTER LANGUAGE = PCL

A few KB of PJL-wrapped data with no ERROR lines on stderr means the driver is right and only transport is left to test.

Trap: patching the PPD with GNU sed syntax

Upstream PPDs default to A4. A patch like

sed -e 's/^\*Default\(PageSize\|PageRegion\): A4/*Default\1: Letter/' ...

silently does nothing under BSD sed (/usr/bin/sed), which has no \| alternation in BREs: no match, exit 0, PPD still A4. Especially treacherous inside an agent harness whose PATH front-loads a GNU sed — the same line works interactively and fails in the privileged script. Use /usr/bin/sed -E with (a|b) and assert the result:

grep -q '^\*DefaultPageSize: Letter' out.ppd || { echo 'PPD patch failed' >&2; exit 1; }

Getting root when the agent has no sudo password

osascript -e 'do shell script "..." with administrator privileges' raises the native auth dialog, which a human at the keyboard can satisfy. It blocks indefinitely if nobody looks at the screen, so run it as a supervised background process, not a foreground command with a timeout.

Verified on

macOS 26.5.1 (25F80), MacBook Air M5 (arm64), brlaser v6, Brother HL-L2340D over USB behind a VIA Labs hub. End-to-end lp job completed and printed; identical filter binary (same md5) reproduced from a clean clone by the committed install script.

No signals yet