mirror of
https://github.com/kfattic/cupsadmin.git
synced 2026-11-13 19:06:43 -05:00
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>
This commit is contained in:
10
CLAUDE.md
10
CLAUDE.md
@@ -53,10 +53,12 @@ swift build # debug build of everything
|
|||||||
- Package: tools-version 6.0, Swift 5 language mode, macOS 14 minimum.
|
- 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
|
- `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.
|
`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
|
- PKG layout: the app is a payload (`pkgbuild --root` with a staging `Applications/` that is really
|
||||||
`/usr/local/bin` and the notarized, stapled app to `/Applications`. A payload's "." entry is recorded as
|
root:admin 775 and `--ownership preserve`, `BundleIsRelocatable` false), so it gets a receipt and the
|
||||||
root:wheel for its install location, which would reset an existing `/usr/local/bin` (Homebrew) or
|
package records the system's own ownership. `--ownership recommended` would record Applications as
|
||||||
`/Applications` (root:admin 775). The app is notarized and stapled before packaging; the PKG after.
|
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.
|
- 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>`,
|
- 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
|
`CUPSKIT_LIVE_QUICK=<ricoh>,<generic>`. Use throwaway queues (e.g. `lpd://127.0.0.1/…`, paused) and
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Download the signed, notarized PKG from the latest [release](../../releases). It
|
|||||||
- `/usr/local/bin/cupsadmin`
|
- `/usr/local/bin/cupsadmin`
|
||||||
- `/Applications/CUPS Admin.app`
|
- `/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).
|
Requires macOS 14 or later. Universal (Apple silicon and Intel).
|
||||||
|
|
||||||
@@ -102,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.
|
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
|
## Notes for admins
|
||||||
|
|
||||||
|
|||||||
91
build.sh
91
build.sh
@@ -1,6 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Universal release build -> signed CLI + signed, notarized, stapled "CUPS Admin.app" -> signed payload-free PKG
|
# Universal release build -> signed CLI + signed, notarized, stapled "CUPS Admin.app" -> signed PKG
|
||||||
# (postinstall installs /usr/local/bin/cupsadmin and /Applications/CUPS Admin.app) -> notarize -> staple -> spctl.
|
# (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)
|
# 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 --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 and
|
# ./build.sh --screenshots build like --build-only, then regenerate docs/screenshots/app-jobs.png and
|
||||||
@@ -128,7 +130,7 @@ take_screenshots() {
|
|||||||
|
|
||||||
# --- configuration -------------------------------------------------------------
|
# --- configuration -------------------------------------------------------------
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
VERSION="${VERSION:-1.1.0}"
|
VERSION="${VERSION:-1.1.1}"
|
||||||
IDENTIFIER="edu.wku.cupsadmin"
|
IDENTIFIER="edu.wku.cupsadmin"
|
||||||
|
|
||||||
# build.env fills in only what the environment doesn't already set.
|
# build.env fills in only what the environment doesn't already set.
|
||||||
@@ -321,42 +323,83 @@ xcrun stapler validate "$APP"
|
|||||||
spctl_check execute "$APP" "$DIST/spctl-app.txt"
|
spctl_check execute "$APP" "$DIST/spctl-app.txt"
|
||||||
|
|
||||||
# --- build + sign pkg -----------------------------------------------------------
|
# --- build + sign pkg -----------------------------------------------------------
|
||||||
# Payload-free for the app too: an /Applications payload's "." entry is recorded as root:wheel, but
|
# App: a normal payload with a receipt. The staging root mirrors the system — "/" root:wheel 755,
|
||||||
# /Applications is root:admin 775 — installing it could take away admins' drag-install rights.
|
# Applications root:admin 775, the app root:wheel — and pkgbuild --ownership preserve records exactly
|
||||||
# pkg/postinstall copies both the CLI and the stapled app instead.
|
# 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"
|
step "build and sign pkg"
|
||||||
rm -f "$PKG"
|
rm -f "$PKG"
|
||||||
|
rm -rf "$SCRIPTS/CUPS Admin.app"
|
||||||
cp pkg/postinstall "$SCRIPTS/postinstall"
|
cp pkg/postinstall "$SCRIPTS/postinstall"
|
||||||
chmod 755 "$SCRIPTS/postinstall"
|
chmod 755 "$SCRIPTS/postinstall"
|
||||||
rm -rf "$SCRIPTS/CUPS Admin.app"
|
|
||||||
ditto "$APP" "$SCRIPTS/CUPS Admin.app"
|
PKGROOT="$PWD/$DIST/pkgroot-$$"
|
||||||
pkgbuild --nopayload --scripts "$SCRIPTS" --identifier "$IDENTIFIER" --version "$VERSION" \
|
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"
|
--sign "$INSTALLER_SIGN_ID" --timestamp "$PKG"
|
||||||
pkgutil --check-signature "$PKG"
|
pkgutil --check-signature "$PKG"
|
||||||
|
|
||||||
step "verify pkg contents"
|
step "verify pkg contents"
|
||||||
echo "pkgutil --payload-files $PKG:"
|
PAYLOAD_FILES=$(pkgutil --payload-files "$PKG")
|
||||||
PAYLOAD_FILES=$(pkgutil --payload-files "$PKG" 2>&1 || true)
|
echo "payload: $(echo "$PAYLOAD_FILES" | wc -l | tr -d ' ') entries, top level:"
|
||||||
echo "${PAYLOAD_FILES:-(no output)}"
|
echo "$PAYLOAD_FILES" | awk -F/ 'NF <= 3' | sed 's/^/ /'
|
||||||
if [ -n "$(printf '%s' "$PAYLOAD_FILES" | grep -v -i 'no payload' || true)" ]; then
|
if echo "$PAYLOAD_FILES" | grep -v -E '^\.$|^\./Applications$|^\./Applications/CUPS Admin\.app(/|$)' | grep -q .; then
|
||||||
STATUS="ERROR: pkg has payload entries; expected none"
|
STATUS="ERROR: payload contains something other than /Applications/CUPS Admin.app"; exit 1
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
EXPANDED="$DIST/expanded"
|
EXPANDED="$DIST/expanded"
|
||||||
rm -rf "$EXPANDED"
|
rm -rf "$EXPANDED"
|
||||||
pkgutil --expand "$PKG" "$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' ' ')"
|
echo "Scripts: $(ls "$EXPANDED/Scripts" | tr '\n' ' ')"
|
||||||
[ -x "$EXPANDED/Scripts/postinstall" ] && [ -f "$EXPANDED/Scripts/cupsadmin" ] && [ -d "$EXPANDED/Scripts/CUPS Admin.app" ] \
|
[ -x "$EXPANDED/Scripts/postinstall" ] && [ -f "$EXPANDED/Scripts/cupsadmin" ] && [ ! -e "$EXPANDED/Scripts/CUPS Admin.app" ] \
|
||||||
|| { STATUS="ERROR: Scripts missing postinstall, cupsadmin or CUPS Admin.app"; exit 1; }
|
|| { STATUS="ERROR: Scripts should hold only postinstall and cupsadmin"; exit 1; }
|
||||||
codesign --verify --strict "$EXPANDED/Scripts/cupsadmin" \
|
codesign --verify --strict "$EXPANDED/Scripts/cupsadmin" \
|
||||||
|| { STATUS="ERROR: binary inside pkg Scripts lost its signature"; exit 1; }
|
|| { 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"
|
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 ------------------------------------------------------
|
# --- notarize + staple pkg ------------------------------------------------------
|
||||||
step "notarize pkg (uploads to Apple and waits)"
|
step "notarize pkg (uploads to Apple and waits)"
|
||||||
@@ -372,4 +415,4 @@ spctl_check execute "$APP" "$DIST/spctl-app.txt"
|
|||||||
|
|
||||||
echo
|
echo
|
||||||
echo "install: sudo installer -pkg $PKG -target /"
|
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"
|
||||||
|
|||||||
@@ -1,32 +1,18 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# cupsadmin postinstall (payload-free pkg): the CLI binary and CUPS Admin.app ship in the Scripts
|
# cupsadmin postinstall: installs the CLI. The binary ships in the Scripts archive next to this file
|
||||||
# archive next to this file. Never changes ownership/permissions of an existing /usr/local/bin
|
# (not in the payload), so the package never changes ownership/permissions of an existing
|
||||||
# (Homebrew on Intel owns it) or of /Applications (root:admin 775).
|
# /usr/local/bin (Homebrew on Intel owns it). CUPS Admin.app is a normal payload with a receipt.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
TARGET="${3:-/}"
|
TARGET="${3:-/}"
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
# --- CLI -------------------------------------------------------------------------
|
|
||||||
DEST_DIR="${TARGET%/}/usr/local/bin"
|
DEST_DIR="${TARGET%/}/usr/local/bin"
|
||||||
|
|
||||||
if [ ! -d "$DEST_DIR" ]; then
|
if [ ! -d "$DEST_DIR" ]; then
|
||||||
mkdir -p -m 755 "$DEST_DIR"
|
mkdir -p -m 755 "$DEST_DIR"
|
||||||
echo "cupsadmin: created $DEST_DIR"
|
echo "cupsadmin: created $DEST_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# -S: copy to a temp file and rename, so a running cupsadmin is never half-written.
|
# -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"
|
install -S -m 755 -o root -g wheel "$HERE/cupsadmin" "$DEST_DIR/cupsadmin"
|
||||||
echo "cupsadmin: installed $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