diff --git a/.gitea/workflows/macos.yml b/.gitea/workflows/macos.yml index db350b9..029a94a 100644 --- a/.gitea/workflows/macos.yml +++ b/.gitea/workflows/macos.yml @@ -35,7 +35,8 @@ jobs: # The job still succeeds quickly so `package`'s `needs:` stays satisfied. # Homebrew's xcodegen formula requires Xcode 15.3, which cannot be # installed on macOS 12 (#109). The script installs a pinned - # prebuilt release instead. + # prebuilt release instead. dmgbuild is not installed here — the + # test job does not package (#95). - name: Ensure host tools if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))" run: scripts/ensure-host-tools.sh @@ -193,8 +194,10 @@ jobs: # scripts/package-release.sh runs `xcodegen generate` and dmgbuild; # see build-and-test for why brew is not used on macOS 12 (#109). + # INSTALL_DMGBUILD isolates dmgbuild in build/.venv-dmgbuild so + # the test job never pip-installs it (#95). - name: Ensure host tools - run: scripts/ensure-host-tools.sh + run: INSTALL_DMGBUILD=1 scripts/ensure-host-tools.sh - name: Package release run: scripts/package-release.sh diff --git a/docs/23-assets.md b/docs/23-assets.md index fe336b3..fcc257e 100644 --- a/docs/23-assets.md +++ b/docs/23-assets.md @@ -25,7 +25,7 @@ Cone-only mark for window/taskbar. Raster set: | File | Use | |------|-----| -| `icons/dmg-background.png` (+ `@2x`, `.svg`) | macOS DMG window (ice cream / wordmark scene). Headless `dmgbuild` after #189 | +| `Resources/dmg-background.png` (+ `@2x`; source `brand/dmg-background.svg`) | macOS DMG window. `scripts/package-release.sh` builds a HiDPI TIFF and passes it to `dmgbuild==1.6.7` from `build/.venv-dmgbuild` (created by `INSTALL_DMGBUILD=1 scripts/ensure-host-tools.sh`). Monterey Python 3.9 needs `PIP_IGNORE_REQUIRES_PYTHON=1` or pip will keep 1.6.5. Missing art is a hard fail (#95, #189). | | `icons/wix-banner.bmp`, `wix-dialog.bmp` | MSI | | `icons/nsis-header.bmp`, `nsis-sidebar.bmp` | NSIS | diff --git a/project.yml b/project.yml index 642e5c3..b706f78 100644 --- a/project.yml +++ b/project.yml @@ -21,6 +21,8 @@ targets: - ICCery.entitlements - ICCery.Debug.entitlements - Argyll + - dmg-background.png + - dmg-background@2x.png - path: Resources/Argyll type: folder dependencies: diff --git a/scripts/dmgbuild-settings.py b/scripts/dmgbuild-settings.py index be4e87c..bd72f2f 100755 --- a/scripts/dmgbuild-settings.py +++ b/scripts/dmgbuild-settings.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # scripts/dmgbuild-settings.py # -# dmgbuild settings for ICCery. Set DMG_APP, DMG_FILENAME and DMG_VOLUME_NAME -# in the environment, or accept the defaults. Background art can be supplied -# later by placing a PNG at Resources/dmg-background.png and setting -# DMG_BACKGROUND. +# dmgbuild settings for ICCery. scripts/package-release.sh exports +# DMG_APP, DMG_FILENAME, DMG_VOLUME_NAME, and DMG_BACKGROUND (a +# HiDPI TIFF). A missing background is a hard error — a grey +# Finder window is not an acceptable release artefact (#95). import os import sys @@ -23,15 +23,23 @@ if not app_path or not app_path.endswith('.app') or not os.path.isdir(app_path): files = [app_path] -# Background art is optional. If the referenced PNG does not exist, fall back -# to a plain window. See docs/23-assets.md for the DMG background spec. -background = os.environ.get('DMG_BACKGROUND', 'Resources/dmg-background.png') -if background and not os.path.exists(background): - background = None +# Finder on Sonoma+ is picky about PNG-with-alpha window pictures and +# about classic Alias Manager blobs. package-release.sh always passes +# a flattened HiDPI TIFF as DMG_BACKGROUND. dmgbuild 1.6.7 (bookmark +# .DS_Store) needs Python >= 3.10, which the Monterey runner does not +# have; 1.6.5 + TIFF is what CI can ship (#95). +background = os.environ.get('DMG_BACKGROUND', '') +if not background or not os.path.isfile(background): + sys.stderr.write( + 'error: DMG_BACKGROUND must point at an existing image ' + '(got %r)\n' % background) + sys.exit(1) icon = None # Window size is enough for the app icon and the Applications alias. +# Bitmap is slightly larger than this rect so title-bar chrome on +# 14+ does not crop the wordmark. window_rect = ((100, 100), (660, 400)) # Use icon view without extra chrome. diff --git a/scripts/ensure-host-tools.sh b/scripts/ensure-host-tools.sh index 9c0afef..5924edc 100755 --- a/scripts/ensure-host-tools.sh +++ b/scripts/ensure-host-tools.sh @@ -2,19 +2,79 @@ # scripts/ensure-host-tools.sh # # Bootstrap host tools needed by CI on the macOS 12 runner: -# - xcodegen: pinned prebuilt release from GitHub (Homebrew's current -# formula requires Xcode 15.3, which cannot be installed on macOS 12). -# - dmgbuild: via pip3 (used by scripts/package-release.sh). +# - xcodegen: always. Pinned prebuilt release from GitHub (Homebrew's +# current formula requires Xcode 15.3, which cannot be installed on +# macOS 12). +# - dmgbuild: only when INSTALL_DMGBUILD=1 or --dmgbuild. Isolated in +# build/.venv-dmgbuild so the test job never pip-installs it. # -# Safe to run repeatedly: existing tools are left alone. +# dmgbuild 1.6.6+, ds_store 1.3.2+ and mac_alias 2.2.3 declare +# Requires-Python >= 3.10. The wheels are py3-none-any and run on the +# runner's 3.9; PIP_IGNORE_REQUIRES_PYTHON is required or pip will only +# offer 1.6.5 and keep a cached venv on that version (#95). +# pip itself is capped at <26.1: 26.1+ needs Python 3.10. +# +# Safe to run repeatedly: existing tools are left alone unless the +# dmgbuild pin is not met. set -eu +ROOT="$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)" XCODEGEN_VERSION="2.38.0" INSTALL_ROOT="${XCODEGEN_HOME:-$HOME/.local/xcodegen/$XCODEGEN_VERSION}" +VENV="$ROOT/build/.venv-dmgbuild" +DMGBUILD_PIN="1.6.7" -echo "==> Ensuring dmgbuild" -python3 -c "import dmgbuild" 2>/dev/null || pip3 install dmgbuild +INSTALL_DMGBUILD="${INSTALL_DMGBUILD:-0}" +for arg in "$@"; do + case "$arg" in + --dmgbuild) INSTALL_DMGBUILD=1 ;; + esac +done + +if [ "$INSTALL_DMGBUILD" = "1" ]; then + echo "==> Ensuring dmgbuild==$DMGBUILD_PIN in $VENV" + mkdir -p "$ROOT/build" + # pip 26.1+ requires Python 3.10 (dataclass slots). A leftover + # `pip install --upgrade pip` on this 3.9 venv installed 26.2.1 and + # the next pip invocation crashed. Recreate if pip is already dead. + if [ -x "$VENV/bin/python" ] \ + && ! "$VENV/bin/python" -m pip --version >/dev/null 2>&1; then + echo "==> venv pip is broken; recreating $VENV" + rm -rf "$VENV" + fi + if [ ! -x "$VENV/bin/python" ]; then + python3 -m venv "$VENV" + fi + # Without this, pip on Python 3.9 hides 1.6.6+ and leaves 1.6.5. + PIP_IGNORE_REQUIRES_PYTHON=1 + export PIP_IGNORE_REQUIRES_PYTHON + "$VENV/bin/python" -m pip install --upgrade 'pip>=24.3,<26.1' + "$VENV/bin/python" -m pip install --upgrade --force-reinstall \ + "dmgbuild==$DMGBUILD_PIN" \ + 'ds_store>=1.3.3' \ + 'mac_alias>=2.2.3' + "$VENV/bin/python" -c 'from importlib.metadata import version +print("dmgbuild", version("dmgbuild")) +print("ds_store", version("ds_store")) +print("mac_alias", version("mac_alias")) +parts=[] +for p in version("dmgbuild").split("."): + try: + parts.append(int("".join(c for c in p if c.isdigit()) or "0")) + except ValueError: + parts.append(0) +parts += [0, 0, 0] +raise SystemExit(0 if tuple(parts[:3]) >= (1, 6, 7) else 1) +' + if [ -n "${GITHUB_PATH:-}" ]; then + echo "$VENV/bin" >> "$GITHUB_PATH" + fi + PATH="$VENV/bin:$PATH" + export PATH +else + echo "==> Skipping dmgbuild (set INSTALL_DMGBUILD=1 for the package job)" +fi if command -v xcodegen >/dev/null 2>&1; then echo "==> xcodegen already on PATH: $(xcodegen --version)" diff --git a/scripts/package-release.sh b/scripts/package-release.sh index 88ef010..872688e 100755 --- a/scripts/package-release.sh +++ b/scripts/package-release.sh @@ -103,19 +103,39 @@ EOF scripts/verify-sidecar-signatures.sh "$APP" fi -echo "==> Installing / locating dmgbuild" +echo "==> Locating dmgbuild" +# The package CI job already ran INSTALL_DMGBUILD=1 ensure-host-tools.sh, +# which created build/.venv-dmgbuild and prepended it to PATH. Local +# runs bootstrap the same venv if dmgbuild is missing. +VENV="$ROOT/build/.venv-dmgbuild" if ! command -v dmgbuild >/dev/null 2>&1; then - VENV="$ROOT/build/.venv-dmgbuild" - if [ ! -d "$VENV/bin" ]; then - python3 -m venv "$VENV" - "$VENV/bin/pip" install --upgrade pip - "$VENV/bin/pip" install dmgbuild + if [ ! -x "$VENV/bin/dmgbuild" ]; then + INSTALL_DMGBUILD=1 "$ROOT/scripts/ensure-host-tools.sh" --dmgbuild fi PATH="$VENV/bin:$PATH" export PATH fi if ! command -v dmgbuild >/dev/null 2>&1; then - echo "error: dmgbuild not available. Try 'python3 -m venv .venv && pip install dmgbuild'" >&2 + echo "error: dmgbuild not on PATH; run INSTALL_DMGBUILD=1 scripts/ensure-host-tools.sh" >&2 + exit 1 +fi +echo "dmgbuild $(command -v dmgbuild)" +if [ -x "$VENV/bin/python" ]; then + "$VENV/bin/python" -c 'from importlib.metadata import version; print("dmgbuild", version("dmgbuild"))' +fi + +PNG1X="$ROOT/Resources/dmg-background.png" +PNG2X="$ROOT/Resources/dmg-background@2x.png" +if [ ! -f "$PNG1X" ] || [ ! -f "$PNG2X" ]; then + echo "error: missing $PNG1X or $PNG2X" >&2 + exit 1 +fi +mkdir -p "$ROOT/build" +DMG_BACKGROUND="$ROOT/build/dmg-background.tiff" +echo "==> Building HiDPI DMG background TIFF" +tiffutil -cathidpicheck "$PNG1X" "$PNG2X" -out "$DMG_BACKGROUND" +if [ ! -f "$DMG_BACKGROUND" ]; then + echo "error: tiffutil did not write $DMG_BACKGROUND" >&2 exit 1 fi @@ -128,6 +148,7 @@ VOLUME_NAME="ICCery ${VERSION}" DMG_APP="$APP" \ DMG_FILENAME="$DMG" \ DMG_VOLUME_NAME="$VOLUME_NAME" \ +DMG_BACKGROUND="$DMG_BACKGROUND" \ dmgbuild -s scripts/dmgbuild-settings.py "$VOLUME_NAME" "$DMG" echo "DMG: $PWD/$DMG"