diff --git a/.github/scripts/inject-macos-dmg-installer.sh b/.github/scripts/inject-macos-dmg-installer.sh new file mode 100755 index 0000000..97d2fda --- /dev/null +++ b/.github/scripts/inject-macos-dmg-installer.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Inject Gatekeeper installer files into built macOS DMGs. +# Usage: inject-macos-dmg-installer.sh [dmg-path...] +# With no args, finds DMGs under src-tauri/target/**/bundle/dmg/ +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +INSTALLER_SRC="$ROOT/src-tauri/macos/Install Grimoire.command" +HOWTO_SRC="$ROOT/src-tauri/macos/How to install.txt" + +if [[ ! -f "$INSTALLER_SRC" ]]; then + echo "missing installer: $INSTALLER_SRC" >&2 + exit 1 +fi +if [[ ! -f "$HOWTO_SRC" ]]; then + echo "missing howto: $HOWTO_SRC" >&2 + exit 1 +fi + +if [[ $# -gt 0 ]]; then + DMGS=("$@") +else + # shellcheck disable=SC2207 + DMGS=($(find "$ROOT/src-tauri/target" -type f -path '*/bundle/dmg/*.dmg' ! -name 'rw.*.dmg' 2>/dev/null | sort -u)) +fi + +if [[ ${#DMGS[@]} -eq 0 ]]; then + echo "No DMGs found to patch." >&2 + exit 1 +fi + +inject_one() { + local dmg="$1" + local work rw mount staging out + + echo "Injecting installer into $(basename "$dmg")…" + + work="$(mktemp -d "${TMPDIR:-/tmp}/grimoire-dmg.XXXXXX")" + rw="$work/rw.dmg" + staging="$work/mount" + out="$work/out.dmg" + mkdir -p "$staging" + + # Writable copy with headroom for installer files. + hdiutil convert "$dmg" -format UDRW -o "$rw" >/dev/null + # Resize: current size + 2MB (hdiutil wants sector counts; -size accepts m). + local size_mb + size_mb="$(du -sm "$rw" | awk '{print $1 + 2}')" + hdiutil resize -size "${size_mb}m" "$rw" >/dev/null + + hdiutil attach "$rw" -readwrite -nobrowse -mountpoint "$staging" >/dev/null + + cp "$INSTALLER_SRC" "$staging/Install Grimoire.command" + chmod +x "$staging/Install Grimoire.command" + cp "$HOWTO_SRC" "$staging/How to install.txt" + + # Flush before detach. + sync + hdiutil detach "$staging" >/dev/null + + hdiutil convert "$rw" -format UDZO -imagekey zlib-level=9 -o "$out" >/dev/null + mv -f "$out" "$dmg" + rm -rf "$work" + echo " ok: $dmg" +} + +for dmg in "${DMGS[@]}"; do + if [[ ! -f "$dmg" ]]; then + echo "skip missing: $dmg" >&2 + continue + fi + inject_one "$dmg" +done diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 79db519..d42721c 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -273,6 +273,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Build and upload to draft release + if: matrix.platform != 'macos-latest' uses: tauri-apps/tauri-action@v1 env: GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }} @@ -283,6 +284,43 @@ jobs: prerelease: false args: ${{ matrix.args }} + # macOS: build first, inject Gatekeeper installer into the DMG, then upload. + - name: Build macOS bundles + id: build_mac + if: matrix.platform == 'macos-latest' + uses: tauri-apps/tauri-action@v1 + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }} + with: + args: ${{ matrix.args }} + + - name: Add Gatekeeper installer to macOS DMG + if: matrix.platform == 'macos-latest' + run: | + chmod +x .github/scripts/inject-macos-dmg-installer.sh \ + "src-tauri/macos/Install Grimoire.command" + .github/scripts/inject-macos-dmg-installer.sh + + - name: Upload macOS release assets + if: matrix.platform == 'macos-latest' + env: + GH_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + TAG: ${{ needs.version-and-release.outputs.tag }} + run: | + set -euo pipefail + mapfile -t ASSETS < <(find src-tauri/target -type f \( \ + -path '*/bundle/dmg/*.dmg' -o \ + -path '*/bundle/macos/*.tar.gz' \ + \) ! -name 'rw.*.dmg' | sort -u) + if [[ ${#ASSETS[@]} -eq 0 ]]; then + echo "No macOS bundle artifacts found to upload" >&2 + exit 1 + fi + printf 'Uploading:\n' + printf ' %s\n' "${ASSETS[@]}" + gh release upload "${TAG}" "${ASSETS[@]}" --clobber + publish-release: name: Publish release needs: [version-and-release, build] diff --git a/Makefile b/Makefile index c49bead..2ccab02 100644 --- a/Makefile +++ b/Makefile @@ -111,12 +111,16 @@ endif # Native production build for the machine you are on. build: ensure-frontend clean-dmg-mounts $(TAURI) build $(TAURI_BUILD_FLAGS) +ifeq ($(HOST_OS),Darwin) + .github/scripts/inject-macos-dmg-installer.sh +endif # --- macOS ------------------------------------------------------------------- build-mac: ensure-frontend ensure-mac-targets clean-dmg-mounts ifeq ($(HOST_OS),Darwin) $(TAURI) build $(TAURI_BUILD_FLAGS) --target $(MAC_TARGET) + .github/scripts/inject-macos-dmg-installer.sh else $(error macOS builds require a Darwin host (got $(HOST_OS))) endif @@ -125,6 +129,7 @@ build-mac-arm: ensure-frontend clean-dmg-mounts ifeq ($(HOST_OS),Darwin) @$(RUSTUP) target add $(MAC_ARM_TARGET) $(TAURI) build $(TAURI_BUILD_FLAGS) --target $(MAC_ARM_TARGET) + .github/scripts/inject-macos-dmg-installer.sh else $(error macOS builds require a Darwin host (got $(HOST_OS))) endif @@ -133,6 +138,7 @@ build-mac-intel: ensure-frontend clean-dmg-mounts ifeq ($(HOST_OS),Darwin) @$(RUSTUP) target add $(MAC_INTEL_TARGET) $(TAURI) build $(TAURI_BUILD_FLAGS) --target $(MAC_INTEL_TARGET) + .github/scripts/inject-macos-dmg-installer.sh else $(error macOS builds require a Darwin host (got $(HOST_OS))) endif diff --git a/README.md b/README.md index 43ad747..461cf2a 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,9 @@ Windows releases include both: Both write `%APPDATA%\grimoire\config.json` only when that file does not already exist, so upgrades keep an existing library path. +### macOS install (unsigned builds) + +Release DMGs are not Apple-notarized yet. Gatekeeper may say Grimoire is “damaged.” Open the DMG and double-click **Install Grimoire.command** (right-click → Open if needed). That clears quarantine, installs to Applications, and launches the app. ## Library layout When you choose a library folder, Grimoire writes: diff --git a/src-tauri/macos/How to install.txt b/src-tauri/macos/How to install.txt new file mode 100644 index 0000000..4baa058 --- /dev/null +++ b/src-tauri/macos/How to install.txt @@ -0,0 +1,18 @@ +How to install Grimoire (macOS) +================================ + +If macOS says Grimoire is “damaged and can’t be opened”, that is Gatekeeper +blocking an unsigned download. It is not a corrupt file. + +Recommended: + 1. Open this disk image + 2. Double-click “Install Grimoire.command” + (If macOS blocks the script: right-click it → Open → Open) + 3. The installer clears Gatekeeper quarantine, copies Grimoire to + Applications, and launches it + +Manual alternative (Terminal): + xattr -cr /Applications/Grimoire.app + +After you have an Apple Developer account, signed + notarized builds will not +need this step. diff --git a/src-tauri/macos/Install Grimoire.command b/src-tauri/macos/Install Grimoire.command new file mode 100755 index 0000000..cc7ff6c --- /dev/null +++ b/src-tauri/macos/Install Grimoire.command @@ -0,0 +1,53 @@ +#!/bin/bash +# Install Grimoire for macOS without an Apple Developer certificate. +# Clears Gatekeeper quarantine (the "damaged and can't be opened" message), +# copies the app to /Applications, and launches it. +set -euo pipefail + +clear_quarantine() { + local path="$1" + if [[ -e "$path" ]]; then + xattr -cr "$path" 2>/dev/null || true + fi +} + +here="$(cd "$(dirname "$0")" && pwd)" +# Allow Terminal to run this .command even when the DMG is quarantined. +clear_quarantine "$0" +clear_quarantine "$here" + +app_src="" +if [[ -d "$here/Grimoire.app" ]]; then + app_src="$here/Grimoire.app" +else + echo "Could not find Grimoire.app next to this installer." >&2 + echo "Open the Grimoire .dmg, then double-click \"Install Grimoire.command\" inside it." >&2 + read -r -p "Press Return to close…" _ + exit 1 +fi + +dest="/Applications/Grimoire.app" + +echo "Installing Grimoire…" +echo " from: $app_src" +echo " to: $dest" +echo + +clear_quarantine "$app_src" + +if [[ -d "$dest" ]]; then + echo "Removing previous install…" + rm -rf "$dest" +fi + +# ditto preserves bundle metadata better than cp -R on macOS. +ditto "$app_src" "$dest" +clear_quarantine "$dest" + +echo +echo "Installed. Opening Grimoire…" +open "$dest" + +echo +echo "Done. You can eject the disk image." +read -r -p "Press Return to close…" _