Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

AirPrint Bridge — Administrator & Developer Guide

Generated May 2026 from sapireli/AirPrint_Bridge at 743085a

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

1. Architecture Overview

AirPrint Bridge is a single-file Bash script (airprint_bridge.sh, v1.3.2) that makes macOS-shared CUPS printers discoverable by iOS and iPadOS via Apple's AirPrint protocol. It works by using macOS's built-in dns-sd command to register each shared printer as a Bonjour mDNS service with the correct AirPrint TXT records. iOS devices on the same local network then see these printers natively — no app, no driver, and no cloud service required.

The project contains two operational components: the main script and a Homebrew formula that packages it for distribution.

macOS host (runs AirPrint Bridge)
┌─────────────────────────────────────────────────────┐
│                                                     │
│  airprint_bridge.sh ─── CUPS subsystem              │
│       │               (lpstat, lpoptions)           │
│       │                                             │
│       ├── check_firewall()    socketfilterfw        │
│       ├── check_cups_permissions()  cupsd.conf      │
│       ├── browse_printers()   lpstat -p             │
│       ├── generate_urf()      lpoptions -l          │
│       ├── resolve_printer()   lpstat -v / -l        │
│       └── generate_script()   → airprint_bridge_launcher.sh
│                                                     │
│  LaunchDaemon (com.sapireli.airprint_bridge.plist)  │
│       └── runs airprint_bridge_launcher.sh          │
│              └── dns-sd -R (one per printer)        │
│                     │                               │
└─────────────────────┼───────────────────────────────┘
                       │  Bonjour / mDNS (_ipp._tcp.,_universal)
        ───────────────┼───────────────
        iOS / iPadOS   │   discovers AirPrint printers

Note

The architecture has zero runtime dependencies beyond what ships with macOS. There is no daemon, no configuration server, no database, and no network proxy. The entire bridge is the output of dns-sd -R commands keeping Bonjour records alive. If the process dies, the LaunchDaemon's KeepAlive=true restarts it automatically.


2. Component Inventory

Component Path Role
Main script airprint_bridge.sh All logic: parsing, CUPS querying, URF generation, script/plist generation, install/uninstall
Homebrew formula Formula/airprint-bridge.rb Packages the script as a Homebrew installable; declares macOS dependency
Homebrew tap setup helper setup_homebrew_tap.sh Developer utility — guides through setting up the tap; not installed to end-user systems
Formula test script test_homebrew.sh Developer utility — validates formula file structure locally
LaunchDaemon plist /Library/LaunchDaemons/com.sapireli.airprint_bridge.plist Generated at install time; manages persistent service lifecycle
Printer registration launcher /usr/local/bin/airprint_bridge_launcher.sh Generated at install time; contains one dns-sd -R command per detected shared printer

3. System Requirements

Item Requirement
OS macOS (any modern version)
Shell /bin/bash
Required tools dns-sd, lpstat, lpoptions, launchctl (all built-in to macOS)
Firewall tool /usr/libexec/ApplicationFirewall/socketfilterfw (built-in)
Privileges root / sudo for install, uninstall, and test modes
CUPS Must be running with at least one shared printer
Homebrew Optional; only required for the formula install path

4. Configuration Reference

4.1 Script Variables

These variables are declared at the top of airprint_bridge.sh and control runtime behavior. There is no external config file.

Variable Default Type Description
SERVICE _ipp._tcp.,_universal String Bonjour service type used in dns-sd -R registration. This must match the AirPrint service type to be discoverable by iOS.
DOMAIN local String mDNS domain passed to dns-sd -R.
SCRIPT airprint_bridge_launcher.sh String Filename of the generated per-printer registration script. Overridable with -f.
LOGGING 0 Boolean (0/1) When 1, log output is written to LOGFILE in addition to stderr. When 0, output goes only to stderr.
LOGFILE airprint_bridge.log String Log file path. Relative to the working directory when the script runs.
SCRIPT_FILE (empty) String Populated by the -f option; overrides SCRIPT.
CUPS_CONF_CHANGED 0 Boolean (0/1) Set to 1 by check_cups_permissions() if /etc/cups/cupsd.conf was modified; used by revert_cups_config() during uninstall.
HAS_COLOR 0 Boolean (0/1) Set to 1 by generate_urf() if the printer reports any color mode (RGB, AdobeRGB, CMYK).
HAS_DUPLEX 0 Boolean (0/1) Set to 1 by generate_urf() if the printer reports any duplex mode other than simplex.
URF (empty) String Populated by generate_urf() with the complete URF capabilities string (e.g., V1.4,SRGB24,PQ3,DM1,MS_LETTER).

4.2 CUPS Configuration File

File Format Location Purpose
cupsd.conf Apache-style directives /etc/cups/cupsd.conf Main CUPS daemon config. The script adds Allow @LOCAL to the <Location /> block if absent.
cupsd.conf.bak Same /etc/cups/cupsd.conf.bak Backup created by the script before any auto-modification. Restored by uninstall.
printers.conf Apache-style /etc/cups/printers.conf Read by the script to detect Shared Yes printer entries as a fallback to lpoptions.

4.3 Generated Files

File Location Format Created by
airprint_bridge_launcher.sh /usr/local/bin/airprint_bridge_launcher.sh Bash script generate_script()
LaunchDaemon plist /Library/LaunchDaemons/com.sapireli.airprint_bridge.plist XML plist generate_plist()

LaunchDaemon plist key reference

Key Value Description
Label com.sapireli.airprint_bridge.plist Unique identifier for launchctl
ProgramArguments ["/usr/local/bin/airprint_bridge_launcher.sh"] Script to run
LowPriorityIO true Reduces I/O scheduling priority
Nice 1 Slight CPU de-prioritization
UserName root Runs as root
RunAtLoad true Start automatically when the plist is loaded
KeepAlive true Restart the process if it exits for any reason

5. Deployment

5.1 Install Flow (Step by Step)

When sudo airprint-bridge -i is run, the following sequence executes:

  1. check_dependencies() — verifies dns-sd, lpstat, lpoptions, launchctl are present.
  2. check_cups_permissions() — reads /etc/cups/cupsd.conf; if Allow @LOCAL is absent, backs up the file, injects the directive, validates with cupsd -t, and restarts CUPS via launchctl. Also calls lpadmin -p <name> -u allow:all for any shared printer that restricts users.
  3. check_firewall() — queries socketfilterfw --getglobalstate; if the firewall is enabled and cupsd is not already in the allow list, adds and unblocks it.
  4. browse_printers() — enumerates all printers via lpstat -p; filters to those with printer-is-shared=true (via lpoptions) or Shared Yes (via printers.conf). Populates the PRINTERS array.
  5. generate_script() — for each printer, calls resolve_printer() (which calls generate_urf()), assembles TXT records, and writes a dns-sd -R command to airprint_bridge_launcher.sh. The generated script uses a trap to kill all dns-sd child processes on EXIT, INT, or TERM.
  6. generate_plist() — copies the launcher to /usr/local/bin/, writes the LaunchDaemon plist to /Library/LaunchDaemons/, and loads it with launchctl load -w.

5.2 Uninstall Flow

When sudo airprint-bridge -u is run:

  1. revert_cups_config() — if CUPS_CONF_CHANGED=1 and the backup exists, restores /etc/cups/cupsd.conf.bak and restarts CUPS.
  2. Unloads and deletes /Library/LaunchDaemons/com.sapireli.airprint_bridge.plist.
  3. Removes /usr/local/bin/airprint_bridge_launcher.sh.
  4. Uses pgrep -f "dns-sd -R" to find all running dns-sd processes advertising _ipp._tcp.,_universal; kills each one.
  5. Sleeps 2 seconds to allow Bonjour advertisements to expire from the network.

5.3 Homebrew Distribution

The Homebrew formula at Formula/airprint-bridge.rb taps directly from the main repository. There is no separate tap repository required.

brew tap sapireli/AirPrint_Bridge
brew install airprint-bridge

The formula:

  • Downloads from the repository archive at commit 59e9440a529e99d3000762608af57b0db6a7b62b (version 1.3.2)
  • Installs airprint_bridge.sh to #{bin}/airprint-bridge with mode 0755
  • Declares depends_on :macos
  • Passes the test do block by running airprint-bridge -h and asserting "Usage:" appears in output

Note

The formula's locked commit (59e9440) differs from the documentation's source_commit (743085a). The formula was published at an earlier commit than the current tip. Users who install via Homebrew get the formula-locked version; users who clone from source get the latest commit.


6. Operations

6.1 Service Management

# Check if the LaunchDaemon is loaded
sudo launchctl list | grep airprint

# Start the service
sudo launchctl start com.sapireli.airprint_bridge.plist

# Stop the service
sudo launchctl stop com.sapireli.airprint_bridge.plist

# Check running dns-sd registrations
pgrep -fl "dns-sd -R"

Tip

After any change to printer sharing, run a full uninstall+reinstall rather than just restarting the service. The registration script is generated once at install time and does not update dynamically when printers are added or removed.

6.2 CUPS Interaction Runbook

Situation Command Notes
List all printers lpstat -p Shows printer names
Check if a printer is shared lpoptions -p <name> Look for printer-is-shared=true
Check printer capabilities lpoptions -l -p <name> Lists all options including ColorModel, Duplex, PageSize
Get device URI lpstat -v <name> Shows ipp://... or usb://... URI
Get printer description and location lpstat -l -p <name> Used for the Bonjour service name
Allow all users on a printer sudo lpadmin -p <name> -u allow:all Done automatically by check_cups_permissions()
Restart CUPS sudo launchctl stop org.cups.cupsd && sudo launchctl start org.cups.cupsd Done automatically after cupsd.conf edits

Note

The script forces export LANG=C and export LC_ALL=C before running to ensure CUPS output is parsed in English regardless of the system locale. This is important on macOS systems configured in non-English locales.

6.3 Monitoring

There is no built-in monitoring. To check health:

# 1. Confirm the LaunchDaemon is loaded
sudo launchctl list | grep airprint

# 2. Confirm dns-sd processes are running (one per shared printer)
pgrep -c "dns-sd"

# 3. Confirm the printer appears on an iOS device
#    (open any app with a print function)

Enable logging to airprint_bridge.log (set LOGGING=1) for a persistent record of what the script found and fixed during the last install.


7. Security

7.1 Privilege Requirements

Caution

All three operational modes (-i, -u, -t) require root. The script modifies files in /etc/cups/, /Library/LaunchDaemons/, and /usr/local/bin/ — all owned by root. There is no privilege separation or sandboxing. Run only on trusted, administrator-controlled machines.

7.2 CUPS Configuration Changes

The script automatically modifies /etc/cups/cupsd.conf to add Allow @LOCAL. This permits any device on the local subnet to submit print jobs to all shared CUPS printers. Evaluate whether this is appropriate for your network before installing.

  • The script creates a backup at /etc/cups/cupsd.conf.bak before any modification.
  • Changes are validated with cupsd -t before CUPS is restarted. If validation fails, the script restores the backup automatically and exits.
  • The uninstaller restores the original configuration if the script had modified it.

Warning

Adding Allow @LOCAL opens CUPS to all devices on the local network segment, not just iOS devices. On untrusted networks (open Wi-Fi, shared office subnets), this may allow unintended print access. Adjust /etc/cups/cupsd.conf manually if you need finer-grained access control after installation.

7.3 Firewall Modification

The script uses socketfilterfw --add and --unblockapp to allow cupsd through the macOS Application Firewall. This is only performed if the firewall is already enabled and cupsd is not yet allowed. No firewall changes are reverted on uninstall.

7.4 Network Exposure

The Bonjour advertisements broadcast on _ipp._tcp.,_universal in the local mDNS domain. These are visible to all devices on the same local network segment. The advertisement includes the printer's make/model, location, description, and capability flags — no credentials, no authentication tokens.


8. Troubleshooting

CUPS config validation fails during install

Cause: The sed edit to inject Allow @LOCAL corrupted the config, or the config had a non-standard structure.

Symptom: Script prints "CUPS config invalid. Restoring backup..." and exits.

Solution:

# The script will have restored the backup automatically.
# Manually edit /etc/cups/cupsd.conf to add Allow @LOCAL:
sudo nano /etc/cups/cupsd.conf
# Find the <Location /> block and add:
#   Allow @LOCAL
# Then restart:
sudo launchctl stop org.cups.cupsd && sudo launchctl start org.cups.cupsd

Warning

If the backup also appears corrupted, the original CUPS config can be recovered by running: sudo cupsctl --no-remote-admin to reset to a known-good state, then re-enabling sharing manually.


LaunchDaemon loads but printers disappear from iOS after reboot

Cause: The airprint_bridge_launcher.sh script was removed, or /usr/local/bin was cleaned by a system tool after install.

Solution:

ls /usr/local/bin/airprint_bridge_launcher.sh
# If missing:
sudo airprint-bridge -u
sudo airprint-bridge -i

launchctl load fails during install

Cause: A plist from a previous install is still loaded, or the plist file has a syntax error.

Solution:

sudo launchctl unload /Library/LaunchDaemons/com.sapireli.airprint_bridge.plist 2>/dev/null
sudo rm /Library/LaunchDaemons/com.sapireli.airprint_bridge.plist 2>/dev/null
sudo airprint-bridge -i

dns-sd processes not killed on uninstall

Cause: pgrep -f "dns-sd -R" found PIDs but kill failed (processes owned by root, or already exited).

Symptom: Duplicate AirPrint entries may persist on iOS temporarily after uninstall.

Solution:

sudo pkill -f "dns-sd -R"

Bonjour advertisements expire from the iOS cache within a few seconds of the advertising process exiting.

Note

The uninstall function already uses sudo kill <pid> for each matching process. If the issue persists, the iOS AirPrint cache will expire naturally within 30-60 seconds — no persistent state is stored on the iOS device.


Firewall re-enables after macOS update and blocks printing

Cause: macOS security updates occasionally reset the Application Firewall allow list.

Solution:

Run a reinstall to re-apply the firewall exception:

sudo airprint-bridge -u
sudo airprint-bridge -i

9. Developer Reference

9.1 Repository Structure

AirPrint_Bridge/
├── airprint_bridge.sh         # Main script (v1.3.2) — all logic lives here
├── Formula/
│   └── airprint-bridge.rb     # Homebrew formula
├── homebrew-airprint-bridge/
│   └── README.md              # Tap README (empty directory in current tree)
├── setup_homebrew_tap.sh      # Dev utility: guides tap setup
├── test_homebrew.sh           # Dev utility: validates formula file structure
├── HOMEBREW_SETUP.md          # Documentation for the Homebrew tap setup
├── LICENSE                    # MIT
└── .gitignore

9.2 Function Reference

All functions are defined in airprint_bridge.sh.

Function Lines (approx.) Description
log() 3 Writes to stderr; also appends to $LOGFILE if LOGGING=1
check_dependencies() 8 Verifies dns-sd, lpstat, lpoptions, launchctl are in $PATH
check_firewall() 15 Checks firewall state; adds cupsd to allow list if needed
check_cups_permissions() 35 Ensures Allow @LOCAL in cupsd.conf and all-user access on shared printers
revert_cups_config() 10 Restores cupsd.conf.bak if the script modified it; used in uninstall and on validation failure
usage() 8 Prints usage message and exits with code 1
browse_printers() 40 Enumerates shared printers via lpstat -p + lpoptions/printers.conf; populates PRINTERS[] array
generate_urf() 120 Calls lpoptions -l; parses color mode, print quality, orientation, duplex, media size, media type, and input slot into a URF capabilities string
resolve_printer() 50 Resolves device URI, port, description, location, make/model, calls generate_urf(), builds TXT_RECORDS[] array
generate_script() 25 Iterates PRINTERS[], calls resolve_printer() for each, writes a dns-sd -R command per printer into airprint_bridge_launcher.sh
generate_plist() 40 Copies launcher to /usr/local/bin/, writes LaunchDaemon plist, loads with launchctl load -w
uninstall() 35 Calls revert_cups_config(), unloads/removes plist, removes launcher, kills dns-sd processes
install() 15 Calls check_dependencies, check_cups_permissions, check_firewall, browse_printers, generate_script, generate_plist
test_run() 12 Same as install() but runs generate_script then executes it in the foreground instead of installing a LaunchDaemon

9.3 URF Capability Mapping

The generate_urf() function maps CUPS lpoptions -l output to AirPrint URF codes. This is the core translation logic.

Color Mode → URF

CUPS ColorModel value URF code HAS_COLOR
*Gray* or *Black* W8 0
*RGB* or *Color* SRGB24 1
*AdobeRGB* ADOBERGB24 1
*CMYK* CMYK32 1
CUPS cupsPrintQuality value URF code
*Draft* PQ1
*Normal* PQ2
*High* PQ3
*Photo* or *Best* PQ4

Duplex Mode → URF

CUPS Duplex value URF code HAS_DUPLEX
*None*, *Off*, *Simplex* DM1 0
*DuplexNoTumble* DM2 1
*DuplexTumble* DM3 1
*DuplexManual* DM4 1

Media Size → URF

CUPS PageSize value URF code
*Letter* MS_LETTER
*Legal* MS_LEGAL
*A4* MS_A4
*A3* MS_A3
*A5* MS_A5
*A6* MS_A6
*B5* MS_B5
*Executive* MS_EXECUTIVE
*Tabloid* MS_TABLOID
*4x6* or *4X6* MS_4X6
*5x7* or *5X7* MS_5X7

The URF version prefix is always V1.4. If no codes are generated (printer reports no recognized capabilities), URF is set to none.

9.4 AirPrint TXT Record Structure

The resolve_printer() function assembles the following TXT records for each printer's dns-sd -R call:

TXT key Value source Example
txtvers Hardcoded 1 txtvers=1
qtotal Hardcoded 1 qtotal=1
rp printers/<printer_name> rp=printers/HP_LaserJet
ty printer-make-and-model from lpoptions ty=HP LaserJet P1102w
product Same as ty wrapped in parentheses product=(HP LaserJet P1102w)
note <location> via <hostname> note=Office via MyMac
pdl Hardcoded pdl=application/pdf,image/jpeg,image/urf
URF Output of generate_urf() URF=V1.4,SRGB24,PQ3,DM1,MS_LETTER
Color T if HAS_COLOR=1, else F Color=T
Duplex T if HAS_DUPLEX=1, else F Duplex=F

The port used in dns-sd -R is extracted from the printer's IPP/IPPS device URI. If the URI does not include a port, port 631 (the CUPS default) is used.

9.5 Code Patterns and Conventions

  • Locale forcing: export LANG=C; export LC_ALL=C at the top of the script ensures all CUPS tool output is in English, making the awk and grep parsing patterns reliable regardless of system locale.
  • Job control suppression: set +m disables job control messages ([1] Done ...) to keep output clean when background dns-sd processes exit.
  • Backup-before-edit: CUPS config is only modified after creating a .bak copy. The revert_cups_config() function is called on both normal uninstall and on config validation failure — defensive programming against partial edits.
  • Array-based printer collection: PRINTERS[] and TXT_RECORDS[] are Bash arrays. generate_script() iterates over PRINTERS[] and calls resolve_printer() per entry, keeping printer state in global variables (TXT_RECORDS, URF, HAS_COLOR, HAS_DUPLEX).
  • getopts parsing: CLI flags are parsed with getopts ":iutf:h". The leading : puts getopts in silent error mode; unknown options fall through to the *) usage ;; case.

9.6 Testing

The repository includes two developer utility scripts:

test_homebrew.sh — validates formula file structure locally before pushing:

  • Checks that Formula/ directory exists
  • Checks that Formula/airprint-bridge.rb exists
  • Checks that airprint_bridge.sh exists
  • Prints the formula's description, homepage, and version lines

setup_homebrew_tap.sh — guides through the tap setup workflow:

  • Checks Homebrew is installed
  • Verifies the Formula/ directory and formula file exist
  • Prints the exact brew tap and brew install commands

The Homebrew formula itself includes a test do block:

test do
  output = shell_output("#{bin}/airprint-bridge -h 2>&1", 1)
  assert_match "Usage:", output
end

This runs during brew test airprint-bridge and confirms the script is installed and executable.

There are no automated unit tests for the main airprint_bridge.sh logic. Testing is primarily manual via sudo airprint-bridge -t.

9.7 Potential Improvements

  • Dynamic printer refresh: The current architecture requires a full uninstall+reinstall to pick up newly shared or removed printers. A --refresh mode that re-runs browse_printers() and regenerates the launcher without a full reinstall would improve the user experience.
  • Unattended CUPS config hardening: check_cups_permissions() reverts the CUPS config on uninstall only if CUPS_CONF_CHANGED=1. If the user ran the script multiple times or manually edited the config between installs, the backup may not reflect the user's intended state.
  • Idempotent dns-sd management: The uninstall uses pgrep to find dns-sd processes advertising $SERVICE. If multiple unrelated dns-sd -R instances exist on the system, the current grep "$SERVICE" filter on the process args is the only guard against over-killing.
  • Per-printer logging: All printers share the same $LOGFILE. Adding per-printer log entries with timestamps would make diagnosing intermittent issues easier.
  • Firewall change tracking: The script does not track whether it modified the firewall, so firewall changes are never reverted on uninstall. A FIREWALL_CHANGED flag mirroring CUPS_CONF_CHANGED would make cleanup deterministic.

9.8 Contributing

The repository is hosted at https://github.com/sapireli/AirPrint_Bridge under the MIT license.

Before submitting a pull request:

  1. Test all three modes (-i, -u, -t) on a clean macOS installation.
  2. Verify that test_homebrew.sh passes without errors.
  3. Run brew test airprint-bridge if the formula was modified.
  4. Ensure CUPS_CONF_CHANGED and backup/restore logic is exercised by your change.