diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 833e7bba..65bf5207 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -203,6 +203,14 @@ jobs: rm -f $CERT_PATH $KEY_PATH $PEM_PATH $P12_PATH + - name: Set up Python for the DMG layout + if: matrix.platform == 'macos-latest' + id: python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 #v7.0.0 + with: + python-version: "3.13" + update-environment: false + - name: Build Tauri app uses: tauri-apps/tauri-action@1deb371b0cd8bd54025b384f1cd735e725c4060f #v1.0.0 env: @@ -228,6 +236,29 @@ jobs: prerelease: false args: ${{ matrix.args }} + # Tauri's DMG has no layout in CI: its Finder script needs a desktop + # session. Rebuild the DMG from the notarized app with a layout written + # directly, sign it, and replace the asset that tauri-action uploaded. + - name: Rebuild macOS DMG with the install layout + if: matrix.platform == 'macos-latest' + shell: bash + env: + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PYTHON: ${{ steps.python.outputs.python-path }} + TAG: ${{ github.ref_name }} + run: | + BUNDLE="src-tauri/target/${{ matrix.target }}/release/bundle" + shopt -s nullglob + DMGS=("$BUNDLE"/dmg/*.dmg) + if [ "${#DMGS[@]}" -ne 1 ]; then + echo "Expected one DMG in $BUNDLE/dmg, found ${#DMGS[@]}" + exit 1 + fi + xcrun stapler validate "$BUNDLE/macos/Donut.app" + scripts/build-macos-dmg.sh "$BUNDLE/macos/Donut.app" "${DMGS[0]}" + gh release upload "$TAG" "${DMGS[0]}" --clobber --repo "$GITHUB_REPOSITORY" + - name: Create portable Windows ZIP if: matrix.platform == 'windows-latest' shell: bash diff --git a/.github/workflows/rolling-release.yml b/.github/workflows/rolling-release.yml index 4e5a7ac1..21160044 100644 --- a/.github/workflows/rolling-release.yml +++ b/.github/workflows/rolling-release.yml @@ -232,6 +232,14 @@ jobs: rm -f "$CERT_PATH" "$KEY_PATH" "$PEM_PATH" "$P12_PATH" + - name: Set up Python for the DMG layout + if: matrix.platform == 'macos-latest' + id: python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 #v7.0.0 + with: + python-version: "3.13" + update-environment: false + - name: Build Tauri app uses: tauri-apps/tauri-action@1deb371b0cd8bd54025b384f1cd735e725c4060f #v1.0.0 env: @@ -256,6 +264,29 @@ jobs: prerelease: true args: ${{ matrix.args }} + # Tauri's DMG has no layout in CI: its Finder script needs a desktop + # session. Rebuild the DMG from the notarized app with a layout written + # directly, sign it, and replace the asset that tauri-action uploaded. + - name: Rebuild macOS DMG with the install layout + if: matrix.platform == 'macos-latest' + shell: bash + env: + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PYTHON: ${{ steps.python.outputs.python-path }} + TAG: "nightly-${{ steps.timestamp.outputs.timestamp }}" + run: | + BUNDLE="src-tauri/target/${{ matrix.target }}/release/bundle" + shopt -s nullglob + DMGS=("$BUNDLE"/dmg/*.dmg) + if [ "${#DMGS[@]}" -ne 1 ]; then + echo "Expected one DMG in $BUNDLE/dmg, found ${#DMGS[@]}" + exit 1 + fi + xcrun stapler validate "$BUNDLE/macos/Donut.app" + scripts/build-macos-dmg.sh "$BUNDLE/macos/Donut.app" "${DMGS[0]}" + gh release upload "$TAG" "${DMGS[0]}" --clobber --repo "$GITHUB_REPOSITORY" + - name: Create portable Windows ZIP if: matrix.platform == 'windows-latest' shell: bash diff --git a/packaging/macos/dmg-background.png b/packaging/macos/dmg-background.png new file mode 100644 index 00000000..e65607d0 Binary files /dev/null and b/packaging/macos/dmg-background.png differ diff --git a/packaging/macos/dmg-background.svg b/packaging/macos/dmg-background.svg new file mode 100644 index 00000000..9a738b2d --- /dev/null +++ b/packaging/macos/dmg-background.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/macos/dmg-background@2x.png b/packaging/macos/dmg-background@2x.png new file mode 100644 index 00000000..69d51268 Binary files /dev/null and b/packaging/macos/dmg-background@2x.png differ diff --git a/packaging/macos/dmg-requirements.txt b/packaging/macos/dmg-requirements.txt new file mode 100644 index 00000000..f9f36423 --- /dev/null +++ b/packaging/macos/dmg-requirements.txt @@ -0,0 +1,3 @@ +dmgbuild==1.6.7 --hash=sha256:37ee5771c377beb3203d9164aae8046ffed8531c06edf9227f5788b3c599b1bf +ds-store==1.3.3 --hash=sha256:b92a371efbf1b4ccce2a04d1ed13fceacc4736c81ba09cf5aefb74c088160a35 +mac-alias==2.2.3 --hash=sha256:7362b521d2132ef92f606a37abfed5fcd849ceb2f28b6f9743e014b02af92f0d diff --git a/packaging/macos/dmg-settings.py b/packaging/macos/dmg-settings.py new file mode 100644 index 00000000..f3eada48 --- /dev/null +++ b/packaging/macos/dmg-settings.py @@ -0,0 +1,52 @@ +# dmgbuild settings for the macOS disk image. scripts/build-macos-dmg.sh runs +# dmgbuild with them. dmgbuild writes the .DS_Store directly, so the layout +# needs no Finder session and is the same on every build. +# +# Icon locations are icon centres, in points from the top-left of the window +# content. The window origin is measured from the bottom-left of the screen, +# and its size includes the title bar. Finder on macOS 26 also shows a tab +# bar, which leaves about 362 points of content height. The background is +# drawn for this layout: change dmg-background.svg with it and render it +# again at 1x and 2x. + +import os.path + +app = defines["app"] +app_name = os.path.basename(app.rstrip("/")) + +format = "UDZO" +compression_level = 9 +filesystem = "HFS+" + +files = [app] +symlinks = {"Applications": "/Applications"} +# No hide_extensions: its Finder flag on the bundle fails codesign --strict. + +icon = defines["volume_icon"] +# dmgbuild joins dmg-background@2x.png with it into one HiDPI TIFF. +background = defines["background"] + +window_rect = ((400, 400), (660, 430)) +default_view = "icon-view" +show_toolbar = False +show_tab_view = False +show_status_bar = False +show_pathbar = False +show_sidebar = False + +arrange_by = None +icon_size = 128 +text_size = 13 +label_pos = "bottom" + +icon_locations = { + app_name: (170, 222), + "Applications": (490, 222), +} + +# Items that Finder shows when hidden files are visible. They sit to the right +# of the window, so they stay out of view. +for i, name in enumerate( + (".background.tiff", ".VolumeIcon.icns", ".fseventsd", ".Trashes") +): + icon_locations[name] = (900 + 160 * i, 222) diff --git a/scripts/build-macos-dmg.sh b/scripts/build-macos-dmg.sh new file mode 100755 index 00000000..27eda58c --- /dev/null +++ b/scripts/build-macos-dmg.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Builds the macOS disk image from a built (and, in CI, notarized) Donut.app, +# with the background and the Finder layout in packaging/macos. +# +# scripts/build-macos-dmg.sh +# +# When APPLE_SIGNING_IDENTITY is set, the image is signed with it. +# PYTHON selects the interpreter (3.10 or newer); the default is python3. +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "usage: $0 " >&2 + exit 2 +fi + +app="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" +out="$(cd "$(dirname "$2")" && pwd)/$(basename "$2")" +root="$(cd "$(dirname "$0")/.." && pwd)" +assets="$root/packaging/macos" +python="${PYTHON:-python3}" + +[[ -d "$app" ]] || { echo "app bundle not found: $app" >&2; exit 1; } +"$python" -c 'import sys; sys.exit(sys.version_info < (3, 10))' || + { echo "dmgbuild needs Python 3.10 or newer; set PYTHON" >&2; exit 1; } + +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +dmg="$work/$(basename "$out")" + +"$python" -m venv "$work/venv" +PIP_DISABLE_PIP_VERSION_CHECK=1 "$work/venv/bin/pip" install --quiet \ + --require-hashes --no-deps --only-binary :all: -r "$assets/dmg-requirements.txt" + +"$work/venv/bin/dmgbuild" \ + -s "$assets/dmg-settings.py" \ + -D app="$app" \ + -D background="$assets/dmg-background.png" \ + -D volume_icon="$root/src-tauri/icons/icon.icns" \ + "Donut" "$dmg" + +if [[ -n "${APPLE_SIGNING_IDENTITY:-}" ]]; then + codesign --force --sign "$APPLE_SIGNING_IDENTITY" --timestamp "$dmg" + codesign --verify --strict "$dmg" +fi +hdiutil verify -quiet "$dmg" + +mv -f "$dmg" "$out" +echo "Built $out"