From 36f2f2fba9c8cc57b13c21271c0ec2cd55e66f3a Mon Sep 17 00:00:00 2001 From: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:18:02 -0700 Subject: [PATCH] fix(release): rebuild DMG from scratch via bundle_dmg.sh (supersedes #952/#955) (#957) --- scripts/release/repackage-dmg.sh | 180 +++++++++++++++++-------------- 1 file changed, 98 insertions(+), 82 deletions(-) diff --git a/scripts/release/repackage-dmg.sh b/scripts/release/repackage-dmg.sh index f26572241..4014e944e 100755 --- a/scripts/release/repackage-dmg.sh +++ b/scripts/release/repackage-dmg.sh @@ -8,107 +8,124 @@ # APPLE_ID # APPLE_PASSWORD (app-specific password) # APPLE_TEAM_ID -# Re-packaging involves: -# 1. Converting the original DMG (with correct layout/background/DS_Store) to writable. -# 2. Resizing it to ensure enough space. -# 3. Replacing the original .app with the notarized one using ditto. -# 4. Converting back to compressed format and notarizing. # -# Assets used: -# - app/src-tauri/images/background-dmg.png (baked into the original DMG) -# - /Applications symlink (baked into the original DMG) +# Why a full rebuild instead of mount-and-replace: +# +# The previous implementation converted the original Tauri-built UDZO DMG to +# UDRW, mounted it, replaced the .app with the notarized one, unmounted, +# and converted back to UDZO. That round-trip fails consistently on macOS +# 26.x runners with `hdiutil: convert failed - internal error` immediately +# after "Preparing imaging engine…". The failure is structural — modifying a +# UDZO→UDRW image and re-compressing it is broken in current hdiutil. +# Tauri's own bundle_dmg.sh builds a fresh UDRW from a source folder via +# `hdiutil create -srcfolder` and then converts to UDZO; that path works. +# +# So instead of round-tripping, we reuse Tauri's vendored bundle_dmg.sh +# (which is already on disk in `/dmg/bundle_dmg.sh` from the +# original tauri-build step) and rebuild the DMG from scratch around the +# now-notarized .app. The output DMG has the same layout (background, +# /Applications symlink, icon positions) as the original. set -euo pipefail APP_PATH="${1:?Usage: repackage-dmg.sh }" BUNDLE_DIR="${2:?}" -DMG_PATH="$(find "$BUNDLE_DIR/dmg" -name '*.dmg' -maxdepth 1 2>/dev/null | head -1)" -if [ -z "$DMG_PATH" ]; then - echo "[dmg] No DMG found — skipping DMG re-package" +# Resolve all bundle paths to absolute form — we cd into $MACOS_DIR below +# to invoke bundle_dmg.sh, and relative paths would break after the cd. +BUNDLE_DIR_ABS="$(cd "$BUNDLE_DIR" && pwd)" +DMG_DIR="$BUNDLE_DIR_ABS/dmg" +MACOS_DIR="$BUNDLE_DIR_ABS/macos" +BUNDLE_SCRIPT="$DMG_DIR/bundle_dmg.sh" +SUPPORT_DIR="$BUNDLE_DIR_ABS/share/create-dmg/support" + +if [ ! -x "$BUNDLE_SCRIPT" ]; then + echo "[dmg] ERROR: bundle_dmg.sh not found at $BUNDLE_SCRIPT" >&2 + echo "[dmg] Did the original tauri-build step run successfully?" >&2 + exit 1 +fi +if [ ! -d "$SUPPORT_DIR" ]; then + echo "[dmg] ERROR: support dir not found at $SUPPORT_DIR" >&2 + exit 1 +fi +if [ ! -d "$APP_PATH" ]; then + echo "[dmg] ERROR: app bundle not found at $APP_PATH" >&2 + exit 1 +fi +APP_PATH_ABS="$(cd "$APP_PATH/.." && pwd)/$(basename "$APP_PATH")" +APP_NAME="$(basename "$APP_PATH")" + +# The .app must be inside $MACOS_DIR for the bundle_dmg.sh srcfolder arg. +# If the caller passed an .app from a different location, copy it into +# place so bundle_dmg.sh picks up the right (notarized) bundle. +if [ "$APP_PATH_ABS" != "$MACOS_DIR/$APP_NAME" ]; then + echo "[dmg] Staging $APP_NAME into $MACOS_DIR" + rm -rf "$MACOS_DIR/$APP_NAME" + ditto "$APP_PATH_ABS" "$MACOS_DIR/$APP_NAME" +fi + +# Capture the existing DMG name so the rebuild outputs to the same path. +# tauri-build always produces exactly one .dmg per target. +ORIGINAL_DMG="$(find "$DMG_DIR" -maxdepth 1 -name '*.dmg' ! -name 'rw.*.dmg' -type f 2>/dev/null | head -1 || true)" +if [ -z "$ORIGINAL_DMG" ]; then + echo "[dmg] No DMG found in $DMG_DIR — nothing to repackage" >&2 exit 0 fi +DMG_NAME="$(basename "$ORIGINAL_DMG")" +FINAL_DMG="$DMG_DIR/$DMG_NAME" +echo "[dmg] Rebuilding $DMG_NAME from notarized $APP_NAME" + +# Background image — same one Tauri uses (declared in app/src-tauri/tauri.conf.json). +# Allow override via env so callers (or tests) can point elsewhere. +BACKGROUND_PATH="${DMG_BACKGROUND_PATH:-app/src-tauri/images/background-dmg.png}" +if [ ! -f "$BACKGROUND_PATH" ]; then + echo "[dmg] WARNING: background image not found at $BACKGROUND_PATH — building without background" >&2 + BACKGROUND_PATH="" +fi # ── Cleanup ────────────────────────────────────────────────────────────────── -# Clean up temporary files and unmount images on exit cleanup() { set +e if [ -n "${VERIFY_MOUNT:-}" ] && [ -d "$VERIFY_MOUNT" ]; then - echo "[dmg] Cleaning up verification mount..." hdiutil detach "$VERIFY_MOUNT" -force 2>/dev/null || true rmdir "$VERIFY_MOUNT" 2>/dev/null || true fi - if [ -n "${MOUNT_DIR:-}" ] && [ -d "$MOUNT_DIR" ]; then - echo "[dmg] Cleaning up rebuild mount..." - hdiutil detach "$MOUNT_DIR" -force 2>/dev/null || true - rmdir "$MOUNT_DIR" 2>/dev/null || true - fi - if [ -f "${DMG_RW:-}" ]; then - rm -f "$DMG_RW" - fi + # bundle_dmg.sh writes scratch files alongside the output — clean any leftovers. + find "$DMG_DIR" -maxdepth 1 -name 'rw.*.dmg' -delete 2>/dev/null || true + find "$MACOS_DIR" -maxdepth 1 -name 'rw.*.dmg' -delete 2>/dev/null || true } trap cleanup EXIT -echo "[dmg] Re-packaging DMG to preserve layout (background, icons, symlinks)..." -# 1. Convert the original Tauri-generated DMG to a writable format (UDRW) -# Note: XXXXXX must be at the end of the template for BSD mktemp (macOS). -# We append .dmg to ensure hdiutil doesn't add it implicitly, causing mismatch. -DMG_RW="$(mktemp /tmp/OpenHuman-RW-XXXXXX).dmg" -hdiutil convert "$DMG_PATH" -format UDRW -ov -o "$DMG_RW" +# Pre-clean any leftover scratch DMGs from prior failed runs. +find "$DMG_DIR" -maxdepth 1 -name 'rw.*.dmg' -delete 2>/dev/null || true +find "$MACOS_DIR" -maxdepth 1 -name 'rw.*.dmg' -delete 2>/dev/null || true +rm -f "$FINAL_DMG" -# 2. Resize and replace the app -# Increase size to ensure the notarized bundle fits (may be slightly larger due to stapling) -hdiutil resize -size 1g "$DMG_RW" +BUNDLE_ARGS=( + --volname "OpenHuman" + --icon "$APP_NAME" 180 170 + --app-drop-link 480 170 + --window-size 660 400 + --hide-extension "$APP_NAME" + --skip-jenkins +) +if [ -n "$BACKGROUND_PATH" ]; then + BACKGROUND_ABS="$(cd "$(dirname "$BACKGROUND_PATH")" && pwd)/$(basename "$BACKGROUND_PATH")" + BUNDLE_ARGS+=(--background "$BACKGROUND_ABS") +fi -# Mount the writable image using a temporary directory -MOUNT_DIR="$(mktemp -d /tmp/OpenHuman-Rebuild-XXXXXX)" -hdiutil attach "$DMG_RW" -mountpoint "$MOUNT_DIR" -noautoopen +echo "[dmg] Running bundle_dmg.sh..." +( + cd "$MACOS_DIR" + bash "$BUNDLE_SCRIPT" "${BUNDLE_ARGS[@]}" "$FINAL_DMG" "$APP_NAME" +) -# Replace the non-notarized app with the notarized one -# We use ditto to preserve all metadata and handles the .app bundle correctly -APP_NAME="$(basename "$APP_PATH")" -rm -rf "$MOUNT_DIR/$APP_NAME" -ditto "$APP_PATH" "$MOUNT_DIR/$APP_NAME" +if [ ! -f "$FINAL_DMG" ]; then + echo "[dmg] ERROR: bundle_dmg.sh did not produce $FINAL_DMG" >&2 + exit 1 +fi +echo "[dmg] Built fresh DMG at $FINAL_DMG ($(du -h "$FINAL_DMG" | cut -f1))" -# Unmount. `-force` plus a sync + brief settle window prevents the -# intermittent "hdiutil: convert failed - internal error" that fires when -# the writable image's resource handles are still being released. -hdiutil detach "$MOUNT_DIR" -force -rmdir "$MOUNT_DIR" -MOUNT_DIR="" -sync -sleep 2 - -# 3. Compact the writable image to reclaim the empty space left over from -# the 1GB resize. Without this the UDZO output carries hundreds of MB of -# zero blocks and `hdiutil convert` is more likely to fail. -echo "[dmg] Compacting writable image before recompression..." -hdiutil compact "$DMG_RW" || echo "[dmg] hdiutil compact returned non-zero (continuing)" - -# 4. Convert back to compressed format (UDZO). -# Convert to a TEMPORARY path and `mv` over the original instead of -# `-ov`-overwriting the input. On macOS GitHub runners the in-place -# overwrite fails immediately with "hdiutil: convert failed - internal -# error" — the source DMG handle is still held by the imaging engine when -# the same path is reopened for write. A separate output path sidesteps -# the issue entirely. -DMG_OUT="$(mktemp /tmp/OpenHuman-UDZO-XXXXXX).dmg" -rm -f "$DMG_OUT" # mktemp created an empty file; hdiutil refuses to overwrite without -ov. -convert_attempts=0 -until hdiutil convert "$DMG_RW" -format UDZO -o "$DMG_OUT"; do - convert_attempts=$((convert_attempts + 1)) - if [ "$convert_attempts" -ge 3 ]; then - echo "[dmg] ERROR: hdiutil convert to UDZO failed after $convert_attempts attempts" >&2 - rm -f "$DMG_OUT" - exit 1 - fi - echo "[dmg] hdiutil convert failed (attempt $convert_attempts) — retrying after settle..." >&2 - rm -f "$DMG_OUT" - sync - sleep $((convert_attempts * 5)) -done -mv -f "$DMG_OUT" "$DMG_PATH" -rm -f "$DMG_RW" -DMG_RW="" +DMG_PATH="$FINAL_DMG" echo "[dmg] Notarizing DMG..." DMG_SUBMIT_OUT="$(mktemp /tmp/notarize-dmg-XXXXXX.json)" @@ -143,18 +160,17 @@ fi xcrun stapler staple "$DMG_PATH" echo "[dmg] DMG notarization complete: $DMG_PATH" -# 4. Final verification +# ── Final verification ─────────────────────────────────────────────────────── echo "[dmg] Verifying final DMG layout..." VERIFY_MOUNT="$(mktemp -d /tmp/OpenHuman-Verify-XXXXXX)" hdiutil attach "$DMG_PATH" -mountpoint "$VERIFY_MOUNT" -noautoopen if [ ! -d "$VERIFY_MOUNT/$APP_NAME" ]; then - echo "[dmg] ERROR: .app bundle missing in final DMG" + echo "[dmg] ERROR: $APP_NAME missing in final DMG" >&2 exit 1 fi - if [ ! -L "$VERIFY_MOUNT/Applications" ]; then - echo "[dmg] ERROR: Applications symlink missing in final DMG" + echo "[dmg] ERROR: Applications symlink missing in final DMG" >&2 exit 1 fi