Skip to content

rdrkr/openwrt-printing

Repository files navigation

AirPrint for GL.iNet Flint 3 (GL-BE9300)

License: MIT OpenWrt 23.05 Target: aarch64 Toolchain: GCC 12.3 Platform: Docker

Cross-compile CUPS + cups-filters + Poppler/Ghostscript for a GL.iNet Flint 3 router, turning a USB-connected printer into an AirPrint-discoverable network printer for iOS and macOS. Ships a reference foo2zjs driver validated end-to-end against an HP LaserJet 1022 — swap it for any other CUPS driver (hplip, splix, gutenprint, …) to target a different printer.

PipelineQuick StartBuildInstallConfigureTroubleshooting


✨ What This Is

OpenWrt's official feeds don't ship CUPS, cups-filters, Ghostscript, Poppler, or foo2zjs for the aarch64_cortex-a53 target used by the Flint 3. This project provides a reproducible Docker-based cross-compile pipeline that produces installable .ipk packages plus a foo2zjs tarball, then installs and configures them on the router for AirPrint.

The target router is a GL.iNet GL-BE9300 (Flint 3) running GL.iNet firmware v4.8.4 (OpenWrt 23.05-SNAPSHOT, QSDK v12.5) on a Qualcomm IPQ5332 (4× Cortex-A53, 1 GB RAM).

Printer-agnostic by design

Everything in the print pipeline except the final driver is printer-agnostic — CUPS, cups-filters (including the project's pdftops shim), Ghostscript, Poppler, Avahi, and the AirPrint advertisement scripts work unchanged for any USB printer that has a CUPS PPD. The reference build has been validated end-to-end on an HP LaserJet 1022 (ZjStream protocol) using the foo2zjs driver, but foo2zjs is the only printer-family-specific component. To target a different printer:

  • HP PCL/PostScript: install hplip from the upstream printing feed and use its PPD.
  • Samsung / Xerox Phaser (QPDL): use splix instead of foo2zjs.
  • Canon / Epson / Brother inkjet: use gutenprint (the feed ships it).
  • Any PostScript-native printer: no driver needed — ship its PPD directly.

The only files you need to change are scripts/build-foo2zjs.sh (swap the driver source) and scripts/install-foo2zjs.sh (swap the tarball payload); configure-cups.sh and configure-airprint.sh are printer-agnostic apart from the Avahi TXT-record strings, which are sourced from environment variables (PRINTER_NAME, PRINTER_MODEL, etc.).


🖨️ Print Pipeline

iOS / macOS device
  → AirPrint (IPP + mDNS discovery via Avahi)
    → CUPS (print server, port 631)
      → pdftops shim (PDF → PostScript via Ghostscript) ──┐  printer-agnostic
      → cups-filters (PostScript / raster conversion)     │
        → foomatic-rip (PPD-driven filter chain)        ──┘
          → printer driver (foo2zjs / hplip / splix / …)  ← printer-specific
            → USB backend (/dev/usb/lp0 or usb:// URI)
              → physical printer (e.g. HP LaserJet 1022)

Poppler is the primary PDF backend ("Plan A"). Ghostscript is attempted but treated as optional — its aarch64 cross-compile is historically fragile, and cups-filters works with Poppler alone. Because cups-filters 1.0.37 does not build its own pdftops against Poppler 23.x on this target, configure-cups.sh installs a small Ghostscript-based pdftops shim — see Troubleshooting for the full diagnosis.


🚀 Quick Start

Prerequisites

  • Docker — works with Docker Desktop, Colima, or any Docker daemon.
  • An x86_64 Linux host is strongly recommended. The OpenWrt SDK ships only x86_64 binaries; on Apple Silicon they run under Rosetta, and shell-heavy host-tool builds (especially gettext/gnulib-tool) become the bottleneck — expect hours instead of minutes.
  • SSH key access to the routerssh-copy-id -i ~/.ssh/id_ed25519.pub root@192.168.8.1.

Router Specs

Detail Value
Model GL.iNet GL-BE9300 (Flint 3)
SoC Qualcomm IPQ5332
Architecture aarch64_cortex-a53_neon-vfpv4 (runtime)
CPU / RAM 4× Cortex-A53 / 1 GB
Firmware GL.iNet v4.8.4 (OpenWrt 23.05-SNAPSHOT, QSDK 12.5)
Toolchain match GCC 12.3.0, musl 1.2.4

🏗️ Architecture

The build runs inside a long-lived Docker container (openwrt-build, ubuntu:22.04, --platform linux/amd64). All SDK state lives in a named Docker volume (openwrt-sdk-vol) rather than a bind mount — this avoids macOS virtiofs permission issues with the SDK's symlinks and restricted files. The project directory is bind-mounted at /host for the scripts directory and for writing output artifacts.

Host (Mac or Linux)
  scripts/*.sh       ─┐
  output/*.ipk       ◄│──  /host   ┐
                     │              │
                     │              │  openwrt-build  (ubuntu:22.04 linux/amd64)
                     │              │
  openwrt-sdk-vol  ──┴──  /workspace ┴─►  OpenWrt SDK 23.05.6 ipq807x
                                          + feeds (base, packages, printing, luci)
                                          + build_dir/, staging_dir/, bin/

Project Layout

openwrt-printing/
├── scripts/
│   ├── bootstrap.sh              # One-shot wrapper: build + install + configure
│   ├── setup-container.sh        # Create openwrt-build container + named volume
│   ├── fetch-sdk.sh              # Download + extract OpenWrt 23.05.6 ipq807x SDK
│   ├── patch-sdk.sh              # Overlay full gnulib tree onto SDK snapshot (gettext fix)
│   ├── prepare-feeds.sh          # Wire Vladdrako printing feed, update + install
│   ├── configure-sdk.sh          # Write SDK .config for cups/filters/poppler/gs
│   ├── build-stack.sh            # Cross-compile poppler + cups + gs + cups-filters
│   ├── build-foomatic-rip.sh     # Cross-compile foomatic-rip (missing from cups-filters 1.0.37)
│   ├── build-foo2zjs.sh          # Cross-compile foo2zjs binary + wrapper + PPD
│   ├── install-on-router.sh      # scp *.ipk → opkg install on router
│   ├── install-foomatic-rip.sh   # scp tarball → extract on router
│   ├── install-foo2zjs.sh        # scp tarball → extract on router
│   ├── configure-cups.sh         # Write cupsd.conf, pdftops shim, open firewall
│   └── configure-airprint.sh     # Write Avahi service file for _ipp._tcp
├── output/                       # Produced .ipk + foo2zjs tarball (gitignored)
├── build/                        # Cached SDK tarball (gitignored)
├── CLAUDE.md                     # Agent context: plan, URLs, troubleshooting
├── LICENSE                       # MIT
└── README.md                     # This file

📦 Build

All scripts are idempotent and resume-friendly. Re-running after a failure is safe.

One-shot deploy (recommended)

scripts/bootstrap.sh wraps the full pipeline — build, install, configure, lpadmin, AirPrint publish — behind a single CLI. Every printer-facing input is a flag; defaults target the reference HP LaserJet 1022. Run with --help for the full flag list.

# Reference HP LaserJet 1022 setup (defaults for all flags):
./scripts/bootstrap.sh

# Different printer — override the identity + driver wiring:
./scripts/bootstrap.sh \
  --router root@192.168.8.1 \
  --printer-name Brother_HLL2350DW \
  --printer-model "Brother HL-L2350DW" \
  --printer-duplex T \
  --device-uri usb://Brother/HL-L2350DW \
  --ppd /usr/share/cups/model/Brother-HL-L2350DW.ppd

# Re-run only the router-side steps (the build output is still in output/):
./scripts/bootstrap.sh --skip-build

# Re-push only the Avahi / CUPS config (packages already installed):
./scripts/bootstrap.sh --skip-build --skip-install

Running the stages manually

If you prefer to drive each stage yourself — for debugging or when re-using pieces of the pipeline in another project — the stage scripts are designed to be called directly, in this order:

# First-time environment bootstrap (~5 min on Linux amd64, ~15 min under Rosetta)
./scripts/setup-container.sh
./scripts/fetch-sdk.sh
./scripts/patch-sdk.sh        # gnulib overlay — see Troubleshooting
./scripts/prepare-feeds.sh
./scripts/configure-sdk.sh

# Cross-compile the stack (single `make -jN` invocation with all four targets —
# poppler, cups, ghostscript, cups-filters — so sibling packages run in parallel
# under one jobserver).
./scripts/build-stack.sh

# Cross-compile foomatic-rip (cups-filters 1.0.37 doesn't ship it).
./scripts/build-foomatic-rip.sh

# Cross-compile foo2zjs (direct toolchain invocation — not wrapped as .ipk;
# produces a tarball that unpacks straight into /usr/lib/cups/filter/ and
# /usr/share/cups/model/ on the router).
./scripts/build-foo2zjs.sh

Watching the Build

The build logs to /workspace/build.log inside the container. Stream it from a second terminal:

docker exec openwrt-build tail -F /workspace/build.log

Expected Output

output/
├── cups_*.ipk
├── cups-client_*.ipk
├── libcups_*.ipk
├── openprinting-cups-filters_*.ipk
├── libpoppler_*.ipk
├── ghostscript_*.ipk            # if the GS build succeeded
├── liblcms2_*.ipk, libpng_*.ipk, libtiff_*.ipk, ...
└── foo2zjs-hp-lj1022.tar.gz

🔌 Install on Router

The router's DISTRIB_ARCH is aarch64_cortex-a53_neon-vfpv4 (note the _neon-vfpv4 suffix), but the upstream OpenWrt SDK emits packages tagged aarch64_cortex-a53. install-on-router.sh handles this by adding an extra arch aarch64_cortex-a53 200 line to /etc/opkg.conf so opkg accepts both arch tags. ABI-wise this is safe — NEON + VFPv4 are mandatory parts of ARMv8-A, so the cortex-a53 build runs identically on the Flint 3.

# SSH key must be in place first:
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@192.168.8.1

# Install the .ipk stack + foo2zjs:
./scripts/install-on-router.sh
./scripts/install-foo2zjs.sh

⚙️ Configure

# CUPS: bind to 0.0.0.0:631, allow LAN, open firewall, install pdftops shim
./scripts/configure-cups.sh

# AirPrint: Avahi _ipp._tcp._universal service record. All printer-facing
# strings are env-overridable; defaults target the reference HP LJ 1022.
PRINTER_NAME=My_Printer \
PRINTER_MODEL="Brother HL-L2350DW" \
PRINTER_DESCRIPTION="Brother HL-L2350DW on GL-BE9300" \
./scripts/configure-airprint.sh

Then add the printer through the CUPS admin UI at http://192.168.8.1:631/admin, or via lpadmin on the router. Example (reference HP LaserJet 1022 setup):

ssh root@192.168.8.1 \
  lpadmin -p HP_LaserJet_1022 -E \
  -v usb://HP/LaserJet%201022 \
  -P /usr/share/cups/model/HP-LaserJet_1022.ppd

For a different printer, substitute the queue name, USB URI (see lpinfo -v on the router), and PPD path. The printer then appears on iOS and macOS as an AirPrint destination named "AirPrint <PRINTER_MODEL> @ <hostname>".


🔧 Troubleshooting

Build is slow / looks stuck

If you're building on Apple Silicon via Rosetta, gettext-full's host build runs gnulib-tool — a shell script that spawns thousands of short-lived processes. Each fork roundtrips through Rosetta, so a single gnulib import can take over an hour. The build isn't stuck; it's emulation-bound. Move the build to a native x86_64 Linux host and it finishes in under an hour.

Verify it's still progressing:

docker exec openwrt-build bash -c '
  L1=$(wc -c < /workspace/build.log); sleep 10
  L2=$(wc -c < /workspace/build.log)
  echo "bytes added in 10s: $((L2 - L1))"'

gnulib-tool: module root-uid doesn't exist

The SDK's bundled staging_dir/host/share/gnulib/ is a curated 2017 snapshot and omits a handful of modules that gettext-0.21.1's autogen.sh imports (notably root-uid). scripts/patch-sdk.sh fixes this by overlaying Ubuntu's gnulib package (apt-installed inside the container) with rsync --ignore-existing, adding the missing module descriptors without touching the SDK's own files. It also clears build_dir/hostpkg/gettext-0.21.1 so gettext reimports with the patched tree. Idempotent — re-running is a no-op after the first pass.

poppler CMake: Boost recommended for Splash. Use ENABLE_BOOST=OFF to skip.

Vladdrako's poppler 23.11.0 Makefile does not pass ENABLE_BOOST=OFF, so CMake hard-fails when Boost ≥ 1.71 is not installed. The Splash backend isn't needed — cups-filters uses poppler's core API — so scripts/patch-sdk.sh appends -DENABLE_BOOST=OFF to the CMake options and clears the stale .configured stamp. Idempotent.

cups configure: --with-tls=openssl was specified but neither OpenSSL nor LibreSSL were found

The Vladdrako cups Makefile has its --with-tls conditional inverted: --with-tls=$(if $(LIBCUPS_OPENSSL),gnutls,openssl) — so selecting GnuTLS in menuconfig actually passes openssl to configure. scripts/patch-sdk.sh swaps the two branches so the selected TLS backend is respected. We default to GnuTLS because libgnutls is pre-staged by the SDK; OpenSSL requires also selecting libopenssl.

nspr fails with "write jobserver: Bad file descriptor"

This is a known GCC 12 LTO/jobserver fd bug triggered at high parallelism. build-stack.sh automatically retries failed packages at -j1 when the parallel build fails.

Ghostscript fails to cross-compile

Known-fragile. The pipeline uses Poppler as the primary PDF backend; Ghostscript failure is treated as non-fatal (cups-filters works with Poppler alone).

Printer not discovered by iOS / macOS

  1. Check Avahi is running: ssh root@192.168.8.1 pgrep avahi-daemon
  2. Verify mDNS traffic is allowed on the LAN side: ssh root@192.168.8.1 nft list chain inet fw4 input_lan
  3. Confirm the service advertises: dns-sd -B _ipp._tcp local (from macOS)

CUPS: No filter to convert from application/pdf / iOS shows online but never prints

cups-filters 1.0.37 does not build its pdftops / pdftopdf / pdftoraster filters against Poppler 23.x on aarch64 — the Poppler C++ headers moved enough that those filter sources fail to compile, so the .ipk ships without them. Without pdftops, CUPS has no chain from application/pdf to application/vnd.cups-postscript (which every foomatic-rip-driven PPD expects as input). The first visible symptom is that application/pdf and image/urf are missing from ipptool … document-format-supported — and because iOS AirPrint probes that list before submitting a job, the printer shows "online" but no Create-Job ever reaches CUPS (tcpdump shows only Get-Printer-Attributes requests, no job data). configure-cups.sh fixes this by installing a small Ghostscript-based /usr/lib/cups/filter/pdftops shim and registering it in /etc/cups/mime.convs at cost 50, which makes PDF and URF appear in document-format-supported and unblocks iOS job submission. Verify after configure-cups.sh with:

ssh root@192.168.8.1 \
  cupsfilter -m application/vnd.cups-postscript \
    -p /etc/cups/ppd/<QUEUE>.ppd -i application/pdf --list-filters /dev/null
# Expected output: pdftops

opkg install refuses packages — architecture mismatch

Confirm /etc/opkg.conf on the router contains arch aarch64_cortex-a53 200. install-on-router.sh adds this line idempotently on every run.


🛡️ Notes on ABI and Source Compatibility

  • SDK target ipq807x is used as the closest upstream match for the router's ipq53xx. Both use Cortex-A53 with identical ABI (NEON + VFPv4 + crypto extensions); packages compiled for ipq807x run unchanged on ipq53xx.
  • Runtime linker mismatch risk: the router uses ld-musl-aarch64.so.1 from its shipped musl 1.2.4 build; the SDK produces binaries linked against the same. Confirmed compatible.
  • Kernel version on the router (5.4.213) affects only kernel modules. This project ships userspace only — no kmods — so kernel skew is irrelevant.

📚 Key References


📄 License

MIT — see LICENSE for full text.

Contributors

Languages