An open API service indexing awesome lists of open source software.

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

Awesome Lists containing this project

README

          

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

[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![OpenWrt 23.05](https://img.shields.io/badge/openwrt-23.05-00b5e2)](https://openwrt.org/releases/23.05/start)
[![Target: aarch64](https://img.shields.io/badge/target-aarch64__cortex--a53-F37626)](https://openwrt.org/docs/techref/targets/ipq807x)
[![Toolchain: GCC 12.3](https://img.shields.io/badge/toolchain-gcc%2012.3.0%20%2B%20musl%201.2.4-7952B3)](https://openwrt.org/docs/guide-developer/toolchain/start)
[![Platform: Docker](https://img.shields.io/badge/platform-docker-2496ED)](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.