mirror of
https://github.com/kfattic/cupsadmin.git
synced 2026-11-13 19:06:43 -05:00
Judge the delete by the read-back (is the queue gone?) instead of lpadmin's exit status. Deleting a queue that was removed elsewhere before the sidebar refreshed made lpadmin fail with 'The printer or class does not exist', and the app showed a failure alert although the queue was gone. Now the status bar shows 'Deleted <queue>' and os_log notes it was already deleted. Unit tests for all four outcomes, plus an opt-in live test (CUPSKIT_LIVE_DELETE=1) on a name that never existed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.5 KiB
7.5 KiB
cupsadmin
A native replacement for the CUPS web interface (http://localhost:631) that Apple removed in
macOS 27. cupsd, lpadmin, lpstat, lpoptions and IPP Everywhere queues still work; only the
browser admin pages are gone. Two front ends share one library:
- CupsKit (
Sources/CupsKit) — IPP client, PPD parser, CUPS tool wrappers, read-back checks. Pure logic: returns values or throws, never prints. cupsadmin(Sources/cupsadmin) — the CLI, a thin layer over CupsKit.- CUPS Admin.app (
Sources/CUPSAdminApp) — the SwiftUI app. The target isCUPSAdminApp, notCUPSAdmin: APFS is case-insensitive, soSources/CUPSAdminis the same folder asSources/cupsadminand the two executables would collide in.build/.
Private maintainer notes, if present, are in CLAUDE.local.md (gitignored).
Architecture
- Reads are IPP with a small hand-written encoder/decoder (RFC 8010,
CupsKit/IPP): CUPS-Get-Printers, Get-Printer-Attributes, Get-Jobs, Get-Job-Attributes, Cancel-Job, CUPS-Get-Default. This gives typed data (state reasons,*-default,*-supported, job state) instead of localizedlpstattext. - Transport is the Unix socket
/private/var/run/cupsd, not TCP 631. On macOS 27 launchd socket-activates cupsd only on that socket; cupsd binds port 631 itself while running and exits after about a minute idle, so TCP fails whenever it's asleep. URLSession can't use AF_UNIX, soUnixSocketHTTP.swiftspeaks minimal HTTP/1.1 over POSIX sockets. PPDs are fetched the same way (GET /printers/<queue>.ppd). - Writes shell out through
CupsTools:lpadmin,cupsenable/cupsdisable,cupsaccept/cupsreject,cancel,lp,lpmove,lpinfo.lpadminalready handles_lpadminauthorization and IPP Everywhere PPD generation. - Queue defaults are written with
lpadmin -p <queue> -o key=value(into the queue's PPD / printers.conf, for every user) — neverlpoptions.~/.cups/lpoptionsis only read, to point out a per-user override. - Driver filter check (
DriverFilters.swift): resolves each*cupsFilter/*cupsFilter2program (bare names under/usr/libexec/cups/filter, skippingmaxsize(n)and-) and reads its Mach-O header directly — any arm64 CPU type counts as native, including subtypeslipocan't name. Intel-only → "Driver needs Rosetta"; not found → "Driver filter missing" (shown first). - PPD options keep their
*OpenGroup, UI type and*ParamCustomtype/range. Custom values are written asCustom.<value>and quoted when they contain spaces (unquoted,lpadminsilently truncates). - Quick actions are vendor-neutral.
QuickActions.swiftdefines the actions; what each writes comes fromResources/driver-profiles.json(embedded in code — the CLI is a single binary), matched on the PPD's*Manufacturer/*NickName, vendor profiles first andgeneric(standard PPD keywords, IPP Everywhere*-defaultattributes) last. A pair is used only if that queue supports the keyword and value; otherwise the action is unavailable with the reason. A profile'sunavailablemap gives the reason an action can't be done with that driver (e.g. Canon department IDs); itsinputsmap relabels the typed value and can add an optional second value ({value2}, e.g. the Xerox account ID). Use Letter Paper is titled ", Fit to Nearest Size" only when the profile also sets a fit option.cupsadmin ppdreport <queue | file.ppd[.gz]>shows a driver's keywords and which actions its profile enables.
Build and test
swift build # debug build of everything
.build/debug/cupsadmin printers # run the CLI
./test.sh # Swift Testing via the Command Line Tools (see flags inside)
./build.sh --build-only # universal CLI + app bundle, ad-hoc signed, no identities needed
./build.sh --screenshots # regenerate docs/screenshots/app-jobs.png from temporary demo queues
./build.sh # release: sign, payload-free PKG, notarize, staple, spctl
- Only the Command Line Tools are required. Universal builds are per-triple
(
swift build -c release --triple arm64-apple-macosx14.0andx86_64-…) pluslipo;--arch arm64 --arch x86_64needs full Xcode. - Package: tools-version 6.0, Swift 5 language mode, macOS 14 minimum.
build.shreadsCODESIGN_APP_IDENTITY,CODESIGN_PKG_IDENTITY,NOTARY_PROFILE(and optionalCODESIGN_TEAM_ID) from the environment or an untrackedbuild.env. Never hard-code identities.- PKG layout: the app is a payload (
pkgbuild --rootwith a stagingApplications/that is really root:admin 775 and--ownership preserve,BundleIsRelocatablefalse), so it gets a receipt and the package records the system's own ownership.--ownership recommendedwould record Applications as root:wheel — don't use it. The CLI is not a payload: the binary ships in Scripts andpkg/postinstallcopies it, because a/usr/local/binpayload entry would reset a Homebrew-owned folder. The build verifies both BOM ownership entries. The app is notarized and stapled before packaging; the PKG after. - The app icon master is
Icon/AppIcon-1024.png;build.shgeneratesAppIcon.icnsfrom it. Don't redraw it. - Live tests are opt-in and write to queues:
CUPSKIT_LIVE_QUEUE=<queue>,CUPSKIT_LIVE_JOBS=<a>,<b>,CUPSKIT_LIVE_QUICK=<ricoh>,<generic>,CUPSKIT_LIVE_DELETE=1(deletes a queue name that never existed; writes nothing). Use throwaway queues (e.g.lpd://127.0.0.1/…, paused) and delete them afterwards; the live tests restore what they change. Offline tests use vendor PPDs fromCUPSKIT_PPD_DIRS(colon-separated, e.g. Canon/HP PPDs extracted withpkgutil --expand-full, not installed) and/Library/Printers/PPDs, and skip when they aren't there.
Conventions
- US spelling everywhere — code, UI, CLI output, docs (color, canceled, gray).
- No third-party dependencies. No Xcode project.
- CLI status lines on every exit path:
started HH:MM:SS, exactly oneOK: …orERROR: …, andfinished HH:MM:SS (total N min)— on stderr, so tables on stdout pipe cleanly. Exit 0 success, 1 runtime error or not applied, 2 usage error. Existing command output is a contract: don't change it casually. Scripts (build.sh,test.sh) print the same started/finished lines. - Read back after every write. Re-query cupsd and compare; a change that didn't take is reported
(
NOT APPLIED, non-zero exit, or a red X with the reason) — never silent. - App feedback: every action updates the bottom status line with its timing ("Set 3 options on
Office in 0.4 s", "ERROR: …") and logs the same through
os_log(subsystemedu.wku.cupsadmin.app). Failures show the tool output and, for permission errors, the_lpadminfix. The exact command is visible before it runs. Destructive actions confirm and name the object. - Secrets: passcode/password options written as queue defaults land in a world-readable PPD — warn,
and mask them in displayed commands. Never commit
build.env,dist/, notary output orCLAUDE.local.md. - Native SwiftUI and macOS idioms only:
NavigationSplitView, sortable/resizableTable,Formwith.formStyle(.grouped), standard sheets, alerts and confirmation dialogs, SF Symbols, system fonts, semantic colors (.primary,.secondary,.green/.blue/.redstate dots),ContentUnavailableViewempty states. No custom chrome, no web-style cards, no emoji. Must look right in light and dark mode. Tables and text are selectable and copyable.