3 Commits

Author SHA1 Message Date
kfattic
cf0241beac Package 1.1.1: app as a payload with system ownership, CLI via postinstall
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 13:59:05 -05:00
kfattic
42c1381ebf README: About section
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 13:32:33 -05:00
kfattic
d81de4a531 README: Options screenshot, captions, describe only what exists
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 13:30:09 -05:00
6 changed files with 138 additions and 88 deletions

View File

@@ -53,10 +53,12 @@ swift build # debug build of everything
- Package: tools-version 6.0, Swift 5 language mode, macOS 14 minimum.
- `build.sh` reads `CODESIGN_APP_IDENTITY`, `CODESIGN_PKG_IDENTITY`, `NOTARY_PROFILE` (and optional
`CODESIGN_TEAM_ID`) from the environment or an untracked `build.env`. Never hard-code identities.
- The PKG is payload-free (`pkgbuild --nopayload --scripts`): `pkg/postinstall` installs the CLI to
`/usr/local/bin` and the notarized, stapled app to `/Applications`. A payload's "." entry is recorded as
root:wheel for its install location, which would reset an existing `/usr/local/bin` (Homebrew) or
`/Applications` (root:admin 775). The app is notarized and stapled before packaging; the PKG after.
- PKG layout: the app is a payload (`pkgbuild --root` with a staging `Applications/` that is really
root:admin 775 and `--ownership preserve`, `BundleIsRelocatable` false), so it gets a receipt and the
package records the system's own ownership. `--ownership recommended` would record Applications as
root:wheel — don't use it. The CLI is not a payload: the binary ships in Scripts and `pkg/postinstall`
copies it, because a `/usr/local/bin` payload 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.sh` generates `AppIcon.icns` from 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

View File

@@ -1,19 +1,23 @@
# CUPS Admin for macOS
![CUPS Admin showing the jobs on a printer](docs/screenshots/app-jobs.png)
*Jobs: hold, release, move or cancel jobs on any queue.*
![CUPS Admin showing a Ricoh queue's options](docs/screenshots/app-options.png)
*Options: the queue's driver settings as a form, applied with `lpadmin` and read back.*
**CUPS Admin** is a native macOS app that puts back the CUPS web interface (`http://localhost:631`) Apple removed in macOS 27 — `cupsd`, `lpadmin` and IPP Everywhere queues all still work, only the browser admin page is gone. The sidebar lists every queue with its state and active jobs; select one to see its status, hold, release, move or cancel its jobs, and change its default options the way the old "Set Default Options" page did, including per-driver settings like Ricoh user codes. Common changes (color or black & white, duplex, Letter paper, a user code, the default printer) are one-click Quick Actions, and every change is read back from cupsd so nothing fails silently. It ships with `cupsadmin`, a command-line tool for scripts, Mosyle/Jamf/Munki commands and quick checks. No third-party dependencies; builds with the Xcode Command Line Tools alone.
## Install
> Status: the CLI is stable. The app (1.1) covers printers, jobs, queue options and quick actions; printer discovery, classes, the server page and the Attributes/Log tabs are still to come — see [Releases](../../releases).
> Status: The CLI and app are both usable today; the app doesn't yet cover every page of the old web interface.
Download the signed, notarized PKG from the latest [release](../../releases). It installs:
- `/usr/local/bin/cupsadmin`
- `/Applications/CUPS Admin.app`
The package is payload-free and installs both via a postinstall script, so it never changes the ownership of an existing `/usr/local/bin` (Homebrew on Intel Macs is safe) or of `/Applications`. For Munki, use an `installs` array pointing at `/usr/local/bin/cupsadmin` and `/Applications/CUPS Admin.app`; there is no payload file list to key off.
The app is a normal payload install with a receipt (`pkgutil --files edu.wku.cupsadmin`). The CLI is installed by a postinstall script instead, so the package never changes the ownership of an existing `/usr/local/bin` (Homebrew on Intel Macs is safe). For Munki, only the CLI needs an `installs` array entry, pointing at `/usr/local/bin/cupsadmin`; the app is covered by the receipt.
Requires macOS 14 or later. Universal (Apple silicon and Intel).
@@ -60,7 +64,7 @@ Which keywords an action writes comes from a driver profile (`Sources/CupsKit/Re
| Ricoh M C251FW PS | ✓ color, black & white, duplex, Letter (no user code) |
| Ricoh PCL (e.g. SP 3710DN) | ✓ duplex, Letter (generic) |
| Any other PPD, IPP Everywhere / AirPrint | ✓ color, black & white, duplex, Letter where the queue supports them (generic) |
| Canon, HP, Xerox, Konica Minolta user/department codes | not yet — contribute a profile |
| Canon, HP, Xerox, Konica Minolta user/department codes | no built-in profile — add one (below) |
### Adding your printer's driver
@@ -98,7 +102,7 @@ VERSION=1.2.3 ./build.sh
Or put those three lines (without `export`) in a `build.env` next to `build.sh`; it's read if present and gitignored. Variables already set in the environment win.
`build.sh` builds a universal binary (per-architecture `swift build` + `lipo`), signs the CLI and app with the hardened runtime, notarizes and staples the app, builds the payload-free PKG, notarizes and staples it, and verifies both with `spctl`. Use `./build.sh --build-only` to skip signing and notarization. `./build.sh --screenshots` regenerates the README screenshot from three temporary demo queues. Run `./test.sh` for the test suite (`CUPSKIT_LIVE_QUEUE=<throwaway queue>` enables the live tests; `CUPSKIT_LIVE_JOBS` and `CUPSKIT_LIVE_QUICK` take two throwaway queues each for the job and quick-action tests).
`build.sh` builds a universal binary (per-architecture `swift build` + `lipo`), signs the CLI and app with the hardened runtime, notarizes and staples the app, builds the PKG (app as payload, CLI via postinstall), notarizes and staples it, and verifies both with `spctl`. Building the PKG asks for admin rights once, to give the staged `Applications` folder the system's root:admin ownership. Use `./build.sh --build-only` to skip signing and notarization. `./build.sh --screenshots` regenerates the README screenshot from three temporary demo queues. Run `./test.sh` for the test suite (`CUPSKIT_LIVE_QUEUE=<throwaway queue>` enables the live tests; `CUPSKIT_LIVE_JOBS` and `CUPSKIT_LIVE_QUICK` take two throwaway queues each for the job and quick-action tests).
## Notes for admins
@@ -106,8 +110,8 @@ Or put those three lines (without `export`) in a `build.env` next to `build.sh`;
- PPD files are world-readable. A locked-print password or login password set as a queue default is stored in plain text; the tool warns before doing that. User codes are accounting codes, not secrets.
- New queues default to `printer-is-shared=false`. `lpadmin` defaults to shared; this tool doesn't.
## License
## About
MIT — see `LICENSE`.
Built at Western Kentucky University by Kurt Fattic to replace the CUPS admin pages for a fleet of about 1,000 Macs, and written with Claude. MIT licensed — see `LICENSE`. Not affiliated with Apple or OpenPrinting.
Built at Western Kentucky University by Kurt Fattic, with Claude. Not affiliated with Apple or OpenPrinting.
Bug reports and driver profiles for other printers are welcome through [Issues](../../issues) and [Pull Requests](../../pulls).

174
build.sh
View File

@@ -1,10 +1,13 @@
#!/bin/bash
# Universal release build -> signed CLI + signed, notarized, stapled "CUPS Admin.app" -> signed payload-free PKG
# (postinstall installs /usr/local/bin/cupsadmin and /Applications/CUPS Admin.app) -> notarize -> staple -> spctl.
# Universal release build -> signed CLI + signed, notarized, stapled "CUPS Admin.app" -> signed PKG
# (payload: /Applications/CUPS Admin.app; postinstall: /usr/local/bin/cupsadmin) -> notarize -> staple -> spctl.
# The pkg step needs admin rights once (sudo in Terminal, or an administrator password dialog) to make the
# staging Applications folder root:admin 775 so the package records the system's real ownership.
# Usage: ./build.sh full pipeline (VERSION=1.2.0 ./build.sh to set the version)
# ./build.sh --build-only build CLI and app, ad-hoc signed; no identities, no pkg, no notarization
# ./build.sh --screenshots build like --build-only, then regenerate docs/screenshots/app-jobs.png
# (creates three demo queues on 127.0.0.1 with held jobs, deletes them after)
# ./build.sh --screenshots build like --build-only, then regenerate docs/screenshots/app-jobs.png and
# app-options.png (creates four demo queues on 127.0.0.1 — one on Ricoh's
# IM C4500 PPD — with held jobs, and deletes them after)
#
# Signing settings come from the environment, or from an untracked build.env next to this script
# (plain KEY=value lines; a variable already set in the environment wins):
@@ -43,8 +46,10 @@ step() { STATUS="ERROR: failed at: $1"; echo; echo "==> $1"; }
# --- screenshots (--screenshots) ------------------------------------------------
# Demo queues with generic names so the README never shows real queue names or users.
DEMO_QUEUES="Front_Office Library_Color Lab_Mono"
SHOT="docs/screenshots/app-jobs.png"
DEMO_QUEUES="Copy_Room Front_Office Library_Color Lab_Mono"
SHOT_JOBS="docs/screenshots/app-jobs.png"
SHOT_OPTIONS="docs/screenshots/app-options.png"
RICOH_DEMO_PPD="/Library/Printers/PPDs/Contents/Resources/RICOH IM C4500"
APP_DOMAIN="edu.wku.cupsadmin.app"
SAVED_PREFS=""
@@ -60,21 +65,57 @@ cleanup_screenshots() {
fi
}
# capture_app <queue> <tab> <file>: launch the app on one queue and tab, capture its window.
capture_app() {
local queue="$1" tab="$2" file="$3" window="" finder
osascript -e 'quit app "CUPS Admin"' >/dev/null 2>&1 || true
for _ in 1 2 3 4 5; do pgrep -f "CUPS Admin.app/Contents/MacOS" >/dev/null || break; sleep 1; done
defaults write "$APP_DOMAIN" selectedQueue "$queue"
defaults write "$APP_DOMAIN" detailTab "$tab"
defaults write "$APP_DOMAIN" "NSWindow Frame main" "120 120 1400 860 $SCREEN "
open "$APP" --args --only-queues "${DEMO_QUEUES// /,}"
finder=$(mktemp -t cupsadmin-window).swift
cat > "$finder" <<'SWIFT'
import CoreGraphics
let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []
if let w = windows.first(where: { ($0[kCGWindowOwnerName as String] as? String) == "CUPS Admin" && ($0[kCGWindowLayer as String] as? Int) == 0 }),
let id = w[kCGWindowNumber as String] as? Int { print(id) }
SWIFT
for _ in $(seq 1 30); do window=$(swift "$finder" 2>/dev/null || true); [ -n "$window" ] && break; sleep 1; done
rm -f "$finder"
[ -n "$window" ] || { STATUS="ERROR: CUPS Admin window didn't appear on this Space"; exit 1; }
sleep 3 # let the sidebar, header and tab content load
# Capture it as the active window (colored traffic lights, accent-colored selection).
osascript -e "tell application id \"$APP_DOMAIN\" to activate" >/dev/null 2>&1 || true
open "$APP"
sleep 2
mkdir -p "$(dirname "$file")"
screencapture -x -o -l "$window" "$file" \
|| { STATUS="ERROR: screencapture failed (Terminal needs Screen Recording permission)"; exit 1; }
echo "captured $file ($(sips -g pixelWidth -g pixelHeight "$file" | awk '/pixel/ {print $2}' | paste -sd x -))"
}
take_screenshots() {
step "screenshots"
[ "$(defaults read -g AppleInterfaceStyle 2>/dev/null)" != "Dark" ] \
|| { STATUS="ERROR: switch macOS to Light appearance first (the README screenshot is light mode)"; exit 1; }
|| { STATUS="ERROR: switch macOS to Light appearance first (the README screenshots are light mode)"; exit 1; }
[ -f "$RICOH_DEMO_PPD" ] \
|| { STATUS="ERROR: the Options screenshot needs Ricoh's driver ($RICOH_DEMO_PPD not found)"; exit 1; }
cleanup_screenshots
# Save the user's app settings (window frame, selection, tab); restored in cleanup.
SAVED_PREFS=$(mktemp -t cupsadmin-prefs).plist
defaults export "$APP_DOMAIN" "$SAVED_PREFS" 2>/dev/null || defaults export "$APP_DOMAIN" - >/dev/null 2>&1 || true
defaults export "$APP_DOMAIN" "$SAVED_PREFS" 2>/dev/null || true
[ -s "$SAVED_PREFS" ] || printf '{}' | plutil -convert xml1 -o "$SAVED_PREFS" -
for queue in $DEMO_QUEUES; do
for queue in Front_Office Library_Color Lab_Mono; do
lpadmin -p "$queue" -v "lpd://127.0.0.1/$queue" -m drv:///sample.drv/generic.ppd \
-D "${queue//_/ }" -L "Building A" -o printer-is-shared=false -E 2>&1 | grep -v deprecated || true
done
# A Ricoh queue for the Options tab, set to Letter + fit to nearest size (what a US site uses).
lpadmin -p Copy_Room -v lpd://127.0.0.1/Copy_Room -P "$RICOH_DEMO_PPD" -D "Copy Room" -L "Building A" \
-o printer-is-shared=false -o PageSize=Letter -o RIPaperPolicy=NearestSizeAdjust -E 2>&1 | grep -v deprecated || true
# Held jobs never leave the Mac; owners are generic names, not the person running the build.
lp -d Front_Office -U alex -H hold -t "Quarterly budget.pdf" /etc/hosts >/dev/null
lp -d Front_Office -U jordan -H hold -t "Staff meeting agenda" /etc/hosts >/dev/null
@@ -83,37 +124,13 @@ take_screenshots() {
lp -d Library_Color -U jordan -H hold -t "Event poster" /etc/hosts >/dev/null
SCREEN=$(osascript -e 'tell application "Finder" to get bounds of window of desktop' | tr -d ' ' | tr ',' ' ')
defaults write "$APP_DOMAIN" selectedQueue Front_Office
defaults write "$APP_DOMAIN" detailTab jobs
defaults write "$APP_DOMAIN" "NSWindow Frame main" "120 120 1400 860 $SCREEN "
open "$APP" --args --only-queues "${DEMO_QUEUES// /,}"
FINDER=$(mktemp -t cupsadmin-window).swift
cat > "$FINDER" <<'SWIFT'
import CoreGraphics
let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []
if let w = windows.first(where: { ($0[kCGWindowOwnerName as String] as? String) == "CUPS Admin" && ($0[kCGWindowLayer as String] as? Int) == 0 }),
let id = w[kCGWindowNumber as String] as? Int { print(id) }
SWIFT
WINDOW=""
for _ in $(seq 1 30); do WINDOW=$(swift "$FINDER" 2>/dev/null || true); [ -n "$WINDOW" ] && break; sleep 1; done
rm -f "$FINDER"
[ -n "$WINDOW" ] || { STATUS="ERROR: CUPS Admin window didn't appear on this Space"; exit 1; }
sleep 3 # let the sidebar, header and jobs load
# Capture it as the active window (colored traffic lights, accent-colored selection).
osascript -e "tell application id \"$APP_DOMAIN\" to activate" >/dev/null 2>&1 || true
open "$APP"
sleep 2
mkdir -p "$(dirname "$SHOT")"
screencapture -x -o -l "$WINDOW" "$SHOT" \
|| { STATUS="ERROR: screencapture failed (Terminal needs Screen Recording permission)"; exit 1; }
SHOT_SIZE="$(sips -g pixelWidth -g pixelHeight "$SHOT" | awk '/pixel/ {print $2}' | paste -sd x -)"
echo "captured $SHOT ($SHOT_SIZE)"
capture_app Front_Office jobs "$SHOT_JOBS"
capture_app Copy_Room options "$SHOT_OPTIONS"
}
# --- configuration -------------------------------------------------------------
cd "$(dirname "$0")"
VERSION="${VERSION:-1.1.0}"
VERSION="${VERSION:-1.1.1}"
IDENTIFIER="edu.wku.cupsadmin"
# build.env fills in only what the environment doesn't already set.
@@ -259,7 +276,7 @@ echo "app architectures: $(lipo -archs "$APP/Contents/MacOS/$APP_EXECUTABLE")"
if [ $SCREENSHOTS -eq 1 ]; then
take_screenshots
STATUS="OK: regenerated $SHOT (${SHOT_SIZE})"
STATUS="OK: regenerated $SHOT_JOBS and $SHOT_OPTIONS"
exit 0
fi
@@ -306,42 +323,83 @@ xcrun stapler validate "$APP"
spctl_check execute "$APP" "$DIST/spctl-app.txt"
# --- build + sign pkg -----------------------------------------------------------
# Payload-free for the app too: an /Applications payload's "." entry is recorded as root:wheel, but
# /Applications is root:admin 775 — installing it could take away admins' drag-install rights.
# pkg/postinstall copies both the CLI and the stapled app instead.
# App: a normal payload with a receipt. The staging root mirrors the system — "/" root:wheel 755,
# Applications root:admin 775, the app root:wheel — and pkgbuild --ownership preserve records exactly
# that, so installing never changes /Applications' ownership. (--ownership recommended would record
# Applications as root:wheel.) Making files root-owned needs admin rights, once per build.
# CLI: stays payload-free — the binary rides in Scripts and pkg/postinstall copies it, because a
# /usr/local/bin payload entry would reset an existing Homebrew-owned /usr/local/bin.
# as_root "<command>": sudo when there's a terminal (or cached credentials), else the standard
# administrator password dialog. Never stores a password.
as_root() {
if sudo -n true 2>/dev/null || [ -t 0 ]; then
sudo /bin/sh -c "$1"
else
local escaped="${1//\\/\\\\}"
escaped="${escaped//\"/\\\"}"
osascript -e "do shell script \"$escaped\" with administrator privileges" >/dev/null
fi
}
step "build and sign pkg"
rm -f "$PKG"
rm -rf "$SCRIPTS/CUPS Admin.app"
cp pkg/postinstall "$SCRIPTS/postinstall"
chmod 755 "$SCRIPTS/postinstall"
rm -rf "$SCRIPTS/CUPS Admin.app"
ditto "$APP" "$SCRIPTS/CUPS Admin.app"
pkgbuild --nopayload --scripts "$SCRIPTS" --identifier "$IDENTIFIER" --version "$VERSION" \
PKGROOT="$PWD/$DIST/pkgroot-$$"
mkdir -p "$PKGROOT/Applications"
ditto "$APP" "$PKGROOT/Applications/CUPS Admin.app"
echo "setting staging ownership (admin rights needed once)"
# Also removes root-owned staging folders left by earlier builds.
as_root "find '$PWD/$DIST' -maxdepth 1 -name 'pkgroot-*' ! -name 'pkgroot-$$' -exec rm -rf {} + ; \
chown root:wheel '$PKGROOT' && chmod 755 '$PKGROOT' && \
chown root:admin '$PKGROOT/Applications' && chmod 775 '$PKGROOT/Applications' && \
chown -R root:wheel '$PKGROOT/Applications/CUPS Admin.app' && chmod -R go-w '$PKGROOT/Applications/CUPS Admin.app'" \
|| { STATUS="ERROR: couldn't set staging ownership (admin rights are needed for the app payload)"; exit 1; }
COMPONENTS="$DIST/components.plist"
pkgbuild --analyze --root "$PKGROOT" "$COMPONENTS" >/dev/null
# Install exactly at /Applications/CUPS Admin.app, never "relocated" to another copy LaunchServices knows about.
plutil -replace 0.BundleIsRelocatable -bool NO "$COMPONENTS"
plutil -replace 0.BundleOverwriteAction -string upgrade "$COMPONENTS"
pkgbuild --root "$PKGROOT" --install-location / --ownership preserve --component-plist "$COMPONENTS" \
--scripts "$SCRIPTS" --identifier "$IDENTIFIER" --version "$VERSION" \
--sign "$INSTALLER_SIGN_ID" --timestamp "$PKG"
pkgutil --check-signature "$PKG"
step "verify pkg contents"
echo "pkgutil --payload-files $PKG:"
PAYLOAD_FILES=$(pkgutil --payload-files "$PKG" 2>&1 || true)
echo "${PAYLOAD_FILES:-(no output)}"
if [ -n "$(printf '%s' "$PAYLOAD_FILES" | grep -v -i 'no payload' || true)" ]; then
STATUS="ERROR: pkg has payload entries; expected none"
exit 1
PAYLOAD_FILES=$(pkgutil --payload-files "$PKG")
echo "payload: $(echo "$PAYLOAD_FILES" | wc -l | tr -d ' ') entries, top level:"
echo "$PAYLOAD_FILES" | awk -F/ 'NF <= 3' | sed 's/^/ /'
if echo "$PAYLOAD_FILES" | grep -v -E '^\.$|^\./Applications$|^\./Applications/CUPS Admin\.app(/|$)' | grep -q .; then
STATUS="ERROR: payload contains something other than /Applications/CUPS Admin.app"; exit 1
fi
EXPANDED="$DIST/expanded"
rm -rf "$EXPANDED"
pkgutil --expand "$PKG" "$EXPANDED"
# pkgutil --expand unpacks the Scripts archive into a directory.
BOM_ROOT=$(lsbom "$EXPANDED/Bom" | awk -F'\t' '$1=="."{print $2, $3}')
BOM_APPS=$(lsbom "$EXPANDED/Bom" | awk -F'\t' '$1=="./Applications"{print $2, $3}')
BOM_APP=$(lsbom "$EXPANDED/Bom" | awk -F'\t' '$1=="./Applications/CUPS Admin.app"{print $2, $3}')
echo "BOM: / $BOM_ROOT · /Applications $BOM_APPS · CUPS Admin.app $BOM_APP"
[ "$BOM_ROOT" = "40755 0/0" ] && [ "$BOM_APPS" = "40775 0/80" ] && [ "$BOM_APP" = "40755 0/0" ] \
|| { STATUS="ERROR: pkg ownership doesn't match the system (expected / 40755 0/0, /Applications 40775 0/80, app 40755 0/0)"; exit 1; }
echo "Scripts: $(ls "$EXPANDED/Scripts" | tr '\n' ' ')"
[ -x "$EXPANDED/Scripts/postinstall" ] && [ -f "$EXPANDED/Scripts/cupsadmin" ] && [ -d "$EXPANDED/Scripts/CUPS Admin.app" ] \
|| { STATUS="ERROR: Scripts missing postinstall, cupsadmin or CUPS Admin.app"; exit 1; }
[ -x "$EXPANDED/Scripts/postinstall" ] && [ -f "$EXPANDED/Scripts/cupsadmin" ] && [ ! -e "$EXPANDED/Scripts/CUPS Admin.app" ] \
|| { STATUS="ERROR: Scripts should hold only postinstall and cupsadmin"; exit 1; }
codesign --verify --strict "$EXPANDED/Scripts/cupsadmin" \
|| { STATUS="ERROR: binary inside pkg Scripts lost its signature"; exit 1; }
codesign --verify --deep --strict "$EXPANDED/Scripts/CUPS Admin.app" \
|| { STATUS="ERROR: app inside pkg Scripts lost its signature"; exit 1; }
xcrun stapler validate "$EXPANDED/Scripts/CUPS Admin.app" >/dev/null \
|| { STATUS="ERROR: app inside pkg Scripts lost its stapled ticket"; exit 1; }
echo "inside pkg: cupsadmin signed ($(lipo -archs "$EXPANDED/Scripts/cupsadmin")), CUPS Admin.app signed and stapled"
rm -rf "$EXPANDED"
FULL="$DIST/expanded-full"
rm -rf "$FULL"
pkgutil --expand-full "$PKG" "$FULL"
codesign --verify --deep --strict "$FULL/Payload/Applications/CUPS Admin.app" \
|| { STATUS="ERROR: app in the payload lost its signature"; exit 1; }
xcrun stapler validate "$FULL/Payload/Applications/CUPS Admin.app" >/dev/null \
|| { STATUS="ERROR: app in the payload lost its stapled ticket"; exit 1; }
echo "inside pkg: CUPS Admin.app signed and stapled (payload), cupsadmin signed (Scripts)"
rm -rf "$FULL"
# --- notarize + staple pkg ------------------------------------------------------
step "notarize pkg (uploads to Apple and waits)"
@@ -357,4 +415,4 @@ spctl_check execute "$APP" "$DIST/spctl-app.txt"
echo
echo "install: sudo installer -pkg $PKG -target /"
STATUS="OK: $PKG (CLI + CUPS Admin.app) signed, app and pkg notarized, stapled, spctl accepted"
STATUS="OK: $PKG (app payload + CLI postinstall) signed, app and pkg notarized, stapled, spctl accepted"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View File

@@ -1,32 +1,18 @@
#!/bin/bash
# cupsadmin postinstall (payload-free pkg): the CLI binary and CUPS Admin.app ship in the Scripts
# archive next to this file. Never changes ownership/permissions of an existing /usr/local/bin
# (Homebrew on Intel owns it) or of /Applications (root:admin 775).
# cupsadmin postinstall: installs the CLI. The binary ships in the Scripts archive next to this file
# (not in the payload), so the package never changes ownership/permissions of an existing
# /usr/local/bin (Homebrew on Intel owns it). CUPS Admin.app is a normal payload with a receipt.
set -euo pipefail
TARGET="${3:-/}"
HERE="$(cd "$(dirname "$0")" && pwd)"
# --- CLI -------------------------------------------------------------------------
DEST_DIR="${TARGET%/}/usr/local/bin"
if [ ! -d "$DEST_DIR" ]; then
mkdir -p -m 755 "$DEST_DIR"
echo "cupsadmin: created $DEST_DIR"
fi
# -S: copy to a temp file and rename, so a running cupsadmin is never half-written.
install -S -m 755 -o root -g wheel "$HERE/cupsadmin" "$DEST_DIR/cupsadmin"
echo "cupsadmin: installed $DEST_DIR/cupsadmin"
# --- app -------------------------------------------------------------------------
if [ -d "$HERE/CUPS Admin.app" ]; then
APPS="${TARGET%/}/Applications"
STAGE="$APPS/.CUPS Admin.app.installing"
rm -rf "$STAGE"
ditto "$HERE/CUPS Admin.app" "$STAGE"
chown -R root:wheel "$STAGE"
chmod -R go-w "$STAGE"
# Swap in place so a half-copied bundle is never at the real path.
rm -rf "$APPS/CUPS Admin.app"
mv "$STAGE" "$APPS/CUPS Admin.app"
echo "cupsadmin: installed $APPS/CUPS Admin.app"
fi