fix(release): rebuild DMG from scratch via bundle_dmg.sh (supersedes #952/#955) (#957)

This commit is contained in:
Steven Enamakel
2026-04-26 19:18:02 -07:00
committed by GitHub
parent e3c46d1c3d
commit 36f2f2fba9
+98 -82
View File
@@ -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 `<bundle_dir>/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 <app_path> <bundle_dir>}"
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