mirror of
https://github.com/kfattic/cupsadmin.git
synced 2026-11-13 19:06:43 -05:00
Package 1.1.0: one PKG installs the CLI and the notarized app
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -53,8 +53,10 @@ 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` copies the binary), because
|
||||
any payload's "." entry would reset an existing `/usr/local/bin`'s ownership.
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
|
||||
## Install
|
||||
|
||||
> Status: the CLI is stable (1.0.x). The app is under active development — see [Releases](../../releases) for what's shipped.
|
||||
> 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).
|
||||
|
||||
Download the signed, notarized PKG from the latest [release](../../releases). It installs:
|
||||
|
||||
- `/usr/local/bin/cupsadmin`
|
||||
- `/Applications/CUPS Admin.app` (once the app ships)
|
||||
- `/Applications/CUPS Admin.app`
|
||||
|
||||
The package is payload-free and installs via a postinstall script, so it never changes the ownership of an existing `/usr/local/bin` (Homebrew on Intel Macs is safe). For Munki, use an `installs` array pointing at `/usr/local/bin/cupsadmin`; there is no payload file list to key off.
|
||||
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.
|
||||
|
||||
Requires macOS 14 or later. Universal (Apple silicon and Intel).
|
||||
|
||||
@@ -98,7 +98,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 with the hardened runtime, builds the payload-free PKG, notarizes, staples and verifies 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 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).
|
||||
|
||||
## Notes for admins
|
||||
|
||||
|
||||
86
build.sh
86
build.sh
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Universal release build -> signed CLI binary + signed "CUPS Admin.app" -> signed payload-free PKG
|
||||
# (postinstall installs /usr/local/bin/cupsadmin) -> notarize -> staple -> spctl.
|
||||
# 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.
|
||||
# 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
|
||||
@@ -113,7 +113,7 @@ SWIFT
|
||||
|
||||
# --- configuration -------------------------------------------------------------
|
||||
cd "$(dirname "$0")"
|
||||
VERSION="${VERSION:-1.0.1}"
|
||||
VERSION="${VERSION:-1.1.0}"
|
||||
IDENTIFIER="edu.wku.cupsadmin"
|
||||
|
||||
# build.env fills in only what the environment doesn't already set.
|
||||
@@ -268,11 +268,53 @@ if [ $BUILD_ONLY -eq 1 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- notarize helper --------------------------------------------------------------
|
||||
# notarize <file> <label>: submits, waits, saves the log; fails the build unless Accepted.
|
||||
notarize() {
|
||||
local file="$1" label="$2"
|
||||
local json="$DIST/notary-$label.json"
|
||||
xcrun notarytool submit "$file" --keychain-profile "$NOTARY_PROFILE" --wait --output-format json > "$json" || true
|
||||
local status id
|
||||
status=$(plutil -extract status raw "$json" 2>/dev/null || echo "unknown")
|
||||
id=$(plutil -extract id raw "$json" 2>/dev/null || echo "")
|
||||
echo "$label submission $id: $status"
|
||||
if [ -n "$id" ]; then
|
||||
xcrun notarytool log "$id" --keychain-profile "$NOTARY_PROFILE" "$DIST/notary-$label-log.json" >/dev/null 2>&1 || true
|
||||
fi
|
||||
[ "$status" = "Accepted" ] \
|
||||
|| { STATUS="ERROR: $label notarization $status (see $json and $DIST/notary-$label-log.json)"; exit 1; }
|
||||
}
|
||||
|
||||
# spctl_check <type> <path> <log>: must be accepted as Notarized Developer ID.
|
||||
spctl_check() {
|
||||
local type="$1" path="$2" log="$3" out
|
||||
out=$(spctl -a -vv -t "$type" "$path" 2>&1 || true)
|
||||
echo "$out" | tee "$log"
|
||||
echo "$out" | grep -q ': accepted' && echo "$out" | grep -q 'source=Notarized Developer ID' \
|
||||
|| { STATUS="ERROR: spctl did not accept $path as Notarized Developer ID"; exit 1; }
|
||||
}
|
||||
|
||||
# --- notarize + staple app --------------------------------------------------------
|
||||
step "notarize app (uploads to Apple and waits)"
|
||||
APP_ZIP="$DIST/CUPS-Admin-$VERSION.zip"
|
||||
rm -f "$APP_ZIP"
|
||||
ditto -c -k --keepParent "$APP" "$APP_ZIP"
|
||||
notarize "$APP_ZIP" app
|
||||
rm -f "$APP_ZIP"
|
||||
xcrun stapler staple "$APP"
|
||||
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.
|
||||
step "build and sign pkg"
|
||||
rm -f "$PKG"
|
||||
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" \
|
||||
--sign "$INSTALLER_SIGN_ID" --timestamp "$PKG"
|
||||
pkgutil --check-signature "$PKG"
|
||||
@@ -286,41 +328,33 @@ if [ -n "$(printf '%s' "$PAYLOAD_FILES" | grep -v -i 'no payload' || true)" ]; t
|
||||
exit 1
|
||||
fi
|
||||
EXPANDED="$DIST/expanded"
|
||||
rm -rf "$EXPANDED"
|
||||
pkgutil --expand "$PKG" "$EXPANDED"
|
||||
# pkgutil --expand unpacks the Scripts archive into a directory.
|
||||
echo "Scripts: $(ls "$EXPANDED/Scripts" | tr '\n' ' ')"
|
||||
[ -x "$EXPANDED/Scripts/postinstall" ] && [ -f "$EXPANDED/Scripts/cupsadmin" ] \
|
||||
|| { STATUS="ERROR: Scripts missing postinstall or cupsadmin"; exit 1; }
|
||||
[ -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; }
|
||||
codesign --verify --strict "$EXPANDED/Scripts/cupsadmin" \
|
||||
|| { STATUS="ERROR: binary inside pkg Scripts lost its signature"; exit 1; }
|
||||
echo "binary inside pkg: signature intact, $(lipo -archs "$EXPANDED/Scripts/cupsadmin")"
|
||||
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"
|
||||
|
||||
# --- notarize -------------------------------------------------------------------
|
||||
step "notarize (this uploads the pkg to Apple and waits)"
|
||||
SUBMIT_JSON="$DIST/notary-submit.json"
|
||||
xcrun notarytool submit "$PKG" --keychain-profile "$NOTARY_PROFILE" --wait --output-format json > "$SUBMIT_JSON" || true
|
||||
NOTARY_STATUS=$(plutil -extract status raw "$SUBMIT_JSON" 2>/dev/null || echo "unknown")
|
||||
SUBMISSION_ID=$(plutil -extract id raw "$SUBMIT_JSON" 2>/dev/null || echo "")
|
||||
echo "submission $SUBMISSION_ID: $NOTARY_STATUS"
|
||||
if [ -n "$SUBMISSION_ID" ]; then
|
||||
xcrun notarytool log "$SUBMISSION_ID" --keychain-profile "$NOTARY_PROFILE" "$DIST/notary-log.json" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ "$NOTARY_STATUS" != "Accepted" ]; then
|
||||
STATUS="ERROR: notarization $NOTARY_STATUS (see $SUBMIT_JSON and $DIST/notary-log.json)"
|
||||
exit 1
|
||||
fi
|
||||
# --- notarize + staple pkg ------------------------------------------------------
|
||||
step "notarize pkg (uploads to Apple and waits)"
|
||||
notarize "$PKG" pkg
|
||||
|
||||
# --- staple + verify ------------------------------------------------------------
|
||||
step "staple"
|
||||
step "staple pkg"
|
||||
xcrun stapler staple "$PKG"
|
||||
xcrun stapler validate "$PKG"
|
||||
|
||||
step "spctl assessment"
|
||||
spctl -a -vv -t install "$PKG" 2>&1 | tee "$DIST/spctl.txt"
|
||||
grep -q ': accepted' "$DIST/spctl.txt" && grep -q 'source=Notarized Developer ID' "$DIST/spctl.txt" \
|
||||
|| { STATUS="ERROR: spctl did not accept $PKG as Notarized Developer ID"; exit 1; }
|
||||
spctl_check install "$PKG" "$DIST/spctl.txt"
|
||||
spctl_check execute "$APP" "$DIST/spctl-app.txt"
|
||||
|
||||
echo
|
||||
echo "install: sudo installer -pkg $PKG -target /"
|
||||
STATUS="OK: $PKG signed, notarized ($SUBMISSION_ID), stapled, spctl accepted"
|
||||
STATUS="OK: $PKG (CLI + CUPS Admin.app) signed, app and pkg notarized, stapled, spctl accepted"
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
#!/bin/bash
|
||||
# cupsadmin postinstall (payload-free pkg): the binary ships in the Scripts archive next to this file.
|
||||
# Never changes ownership/permissions of an existing /usr/local/bin (Homebrew on Intel owns it).
|
||||
# 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).
|
||||
set -euo pipefail
|
||||
|
||||
TARGET="${3:-/}"
|
||||
DEST_DIR="${TARGET%/}/usr/local/bin"
|
||||
SOURCE="$(cd "$(dirname "$0")" && pwd)/cupsadmin"
|
||||
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 "$SOURCE" "$DEST_DIR/cupsadmin"
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user