https://github.com/rdrkr/openwrt-printing
AirPrint for GL.iNet Flint 3
https://github.com/rdrkr/openwrt-printing
cups openwrt
Last synced: 11 days ago
JSON representation
AirPrint for GL.iNet Flint 3
- Host: GitHub
- URL: https://github.com/rdrkr/openwrt-printing
- Owner: rdrkr
- License: mit
- Created: 2026-04-14T17:38:16.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-04-14T17:55:36.000Z (about 2 months ago)
- Last Synced: 2026-04-14T19:28:31.916Z (about 2 months ago)
- Topics: cups, openwrt
- Language: Shell
- Homepage: https://github.com/rdrkr/openwrt-printing
- Size: 20.5 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
AirPrint for GL.iNet Flint 3 (GL-BE9300)
[](LICENSE)
[](https://openwrt.org/releases/23.05/start)
[](https://openwrt.org/docs/techref/targets/ipq807x)
[](https://openwrt.org/docs/guide-developer/toolchain/start)
[](https://www.docker.com/)
**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.**
[Pipeline](#-print-pipeline) • [Quick Start](#-quick-start) • [Build](#-build) • [Install](#-install-on-router) • [Configure](#-configure) • [Troubleshooting](#-troubleshooting)
---
## ✨ 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
```markdown
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](#-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 router** — `ssh-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.
```markdown
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
```markdown
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.
```bash
# 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:
```bash
# 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:
```bash
docker exec openwrt-build tail -F /workspace/build.log
```
### Expected Output
```bash
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.
```bash
# 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
```bash
# 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):
```bash
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 \ @ \"**.
---
## 🔧 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:
```bash
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:
```bash
ssh root@192.168.8.1 \
cupsfilter -m application/vnd.cups-postscript \
-p /etc/cups/ppd/.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
- **Vladdrako printing feed**:
- **OpenWrt SDK (23.05.6, ipq807x)**:
- **OpenPrinting / foo2zjs**:
- **HP LaserJet 1022 on OpenPrinting**:
- **TheMMcOfficial — CUPS for OpenWrt**:
---
## 📄 License
[MIT](LICENSE) — see `LICENSE` for full text.