mirror of
https://github.com/kfattic/cupsadmin.git
synced 2026-11-13 19:06:43 -05:00
Canon: CNColorMode color/mono, CNDuplex, PageSize=Letter. HP: HPColorAsGray or HPColorMode; duplex and Letter from the generic profile. Profiles can mark an action unavailable with a reason (Canon department IDs, HP accounting codes). Use Letter Paper only says Fit to Nearest Size when the profile sets a fit option. Offline tests sweep every Canon and HP PPD found in CUPSKIT_PPD_DIRS or /Library/Printers/PPDs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.3 KiB
7.3 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). 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>. 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.