From 0449d8a1533d59785ec8d1dad2353e6d81c0ce70 Mon Sep 17 00:00:00 2001 From: BeaCox Date: Sat, 29 Aug 2026 02:12:40 +0800 Subject: [PATCH] Fix macOS DMG presentation in release builds --- .github/workflows/release.yml | 14 ++++ CHANGELOG.md | 1 + assets/branding/dmg-background.png | Bin 2037 -> 35431 bytes assets/branding/dmg-background.svg | 37 +++++++++ docs/releases/v0.1.0.md | 1 + docs/update-strategy.md | 2 + scripts/verify-macos-dmg-layout.mjs | 102 ++++++++++++++++++++++++ scripts/verify-macos-dmg-layout.test.js | 40 ++++++++++ scripts/verify-macos-dmg.sh | 39 +++++++++ 9 files changed, 236 insertions(+) create mode 100644 assets/branding/dmg-background.svg create mode 100644 scripts/verify-macos-dmg-layout.mjs create mode 100644 scripts/verify-macos-dmg-layout.test.js create mode 100755 scripts/verify-macos-dmg.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f81d050..e59d735 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -156,9 +156,23 @@ jobs: if: matrix.platform == 'macos' env: APPLE_SIGNING_IDENTITY: "-" + TAURI_BUNDLER_DMG_IGNORE_CI: "true" TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: "" run: make bundles TARGET=${{ matrix.target }} CONFIG=src-tauri/tauri.updater.conf.json + - name: Verify macOS DMG layout + if: matrix.platform == 'macos' + env: + RELEASE_TARGET: ${{ matrix.target }} + run: | + set -euo pipefail + shopt -s nullglob + dmg_paths=(target/"$RELEASE_TARGET"/release/bundle/dmg/*.dmg) + if [[ ${#dmg_paths[@]} -ne 1 ]]; then + echo "Expected exactly one macOS DMG for layout verification." >&2 + exit 1 + fi + ./scripts/verify-macos-dmg.sh "${dmg_paths[0]}" - name: Build Linux bundles if: matrix.platform == 'linux' env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c90526..a7bef3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Initial public release for bounded cross-platform and Agent compatibility. - Optional age-encrypted backups for complete plans with a supported restore route. - Atomic operation journal, startup recovery, post-mutation rescans, and all-or-nothing restore preflight and rollback. - Unsigned macOS, Linux, and Windows packaging with SHA-256 checksums and build metadata. +- A guided macOS DMG install layout with CI checks for its Finder metadata and tracked background. - User-initiated Tauri updates with a pinned public key and signed updater artifacts. ### Safety boundary diff --git a/assets/branding/dmg-background.png b/assets/branding/dmg-background.png index 38fb187cc83d9ff8c9a92bbe56cb8052e3e85b44..fa12fcff0e8b9615547fbfe1a5f411bc57c108a6 100644 GIT binary patch literal 35431 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 Bauomo literal 2037 zcmeAS@N?(olHy`uVBq!ia0y~yVEVzpz_^2h2`IAv1B)w=;wFSZ?7HXJfOgE-~iX%df)v`+!n1I5A&LAjK9s2XK=Xq--?0Zf!JbZh7?Iz z28Giv7#Jem*cnb3Sur?FyU56}sfUkYLTU*^gU+au(Qp|}45OK3v}7194o9mAD%OpB Zb38a#Oizni2dujpJYD@<);T3K0RY-ZMArZS diff --git a/assets/branding/dmg-background.svg b/assets/branding/dmg-background.svg new file mode 100644 index 0000000..095d90d --- /dev/null +++ b/assets/branding/dmg-background.svg @@ -0,0 +1,37 @@ + + + + + + CLEANERX / MACOS INSTALLER + LOCAL-ONLY UTILITY + + + + Install CleanerX + Drag the app into Applications. Nothing else is installed. + + + + + + 01 / CLEANERX.APP + 02 / APPLICATIONS + + + + + + + DRAG TO INSTALL + + + + + + + + UNSIGNED BUILD / macOS MAY ASK FOR APPROVAL ON FIRST LAUNCH + FINDER → OPEN + + diff --git a/docs/releases/v0.1.0.md b/docs/releases/v0.1.0.md index 2d1d6bc..dd288a2 100644 --- a/docs/releases/v0.1.0.md +++ b/docs/releases/v0.1.0.md @@ -37,6 +37,7 @@ This checklist is the evidence record for the first CleanerX release. Do not pub - [ ] The release workflow accepts the SemVer tag and confirms every version surface matches. - [ ] Product CI reruns successfully from the exact tag. - [ ] macOS arm64, macOS x86_64, Linux x86_64, and Windows x86_64 release builds succeed. +- [ ] Both macOS DMGs retain the tracked guided-install background, configured Finder window, large icon positions, and exact `/Applications` link. - [ ] Every expected manual installer, updater payload, `.sig`, `latest.json`, lockfile archive, build-metadata file, and checksum file appears exactly once in the draft. - [ ] `latest.json` contains version `0.1.0`, the four intended updater targets, HTTPS URLs for the exact tag, and non-empty signatures. - [ ] Every staged asset matches `CleanerX_0.1.0_SHA256SUMS.txt`. diff --git a/docs/update-strategy.md b/docs/update-strategy.md index 4bc8908..4ca2367 100644 --- a/docs/update-strategy.md +++ b/docs/update-strategy.md @@ -44,6 +44,8 @@ The fixed stable endpoint intentionally excludes GitHub prereleases. A future be Normal developer bundle commands remain unsigned and do not require the updater private key. Tagged releases add `src-tauri/tauri.updater.conf.json`, set `TAURI_SIGNING_PRIVATE_KEY` from GitHub Actions secrets, and ask Tauri to create update artifacts and `.sig` files. The release-draft job gathers every architecture, runs `scripts/generate-update-manifest.mjs`, and fails closed on a missing/empty signature before staging `latest.json` and checksums in a draft GitHub Release. A maintainer verifies the complete draft before publishing it. +[Tauri normally skips Finder cosmetic metadata when it detects `CI=true`](https://github.com/tauri-apps/tauri/blob/tauri-cli-v2.11.4/crates/tauri-bundler/src/bundle/macos/dmg/mod.rs#L160-L168), which would silently reduce a configured DMG to Finder's default icon view. The macOS release jobs explicitly set Tauri's supported `TAURI_BUNDLER_DMG_IGNORE_CI=true` override, then mount each completed DMG read-only and require the tracked field-manual background, a non-empty `.DS_Store` selecting that background and the configured window size, the application bundle, and the exact `/Applications` link. A missing or stale layout fails the release before assets are staged. + WiX/MSI does not accept alphanumeric Semantic Versioning prerelease identifiers. The Windows release job therefore uses `scripts/generate-windows-release-config.mjs` to derive a numeric MSI-only version that sorts below the eventual stable version (`0.1.0-alpha.1` becomes `0.0.65535.10001`). The application, tag, updater manifest, NSIS installer, and release asset names retain the public Semantic Versioning value. Unsupported prerelease shapes fail during tag validation before platform builds begin. Tauri's signature establishes continuity with the public key embedded in an installed CleanerX build. It does not establish operating-system publisher identity. Artifact names and release warnings therefore continue to say that current binaries are unsigned and not notarized. diff --git a/scripts/verify-macos-dmg-layout.mjs b/scripts/verify-macos-dmg-layout.mjs new file mode 100644 index 0000000..54562cd --- /dev/null +++ b/scripts/verify-macos-dmg-layout.mjs @@ -0,0 +1,102 @@ +import { createHash } from "node:crypto"; +import { lstat, readFile, readlink } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const expectedWindowSizeMarker = "760, 440"; + +function digest(contents) { + return createHash("sha256").update(contents).digest("hex"); +} + +async function pathKind(target) { + try { + const stat = await lstat(target); + if (stat.isDirectory()) return "directory"; + if (stat.isSymbolicLink()) return "symbolic-link"; + if (stat.isFile()) return "file"; + return "other"; + } catch (error) { + if (error?.code === "ENOENT") return "missing"; + throw error; + } +} + +export function validateDmgLayout(snapshot) { + const problems = []; + + if (snapshot.appKind !== "directory") { + problems.push("CleanerX.app must be a real directory"); + } + if (snapshot.applicationsKind !== "symbolic-link") { + problems.push("Applications must be a symbolic link"); + } else if (snapshot.applicationsTarget !== "/Applications") { + problems.push("Applications must link exactly to /Applications"); + } + if (snapshot.dsStoreKind !== "file" || snapshot.dsStoreBytes < 512) { + problems.push(".DS_Store must be a non-empty Finder layout file"); + } + if (!snapshot.dsStoreContainsBackground) { + problems.push(".DS_Store must select dmg-background.png"); + } + if (!snapshot.dsStoreContainsWindowSize) { + problems.push(".DS_Store must retain the configured 760 x 440 window size"); + } + if (snapshot.backgroundKind !== "file") { + problems.push("the DMG background must be a real file"); + } else if (snapshot.backgroundDigest !== snapshot.expectedBackgroundDigest) { + problems.push("the staged DMG background must match the tracked background"); + } + + if (problems.length > 0) { + throw new Error(`Invalid macOS DMG layout:\n- ${problems.join("\n- ")}`); + } + + return snapshot; +} + +export async function inspectMountedDmg(mountRoot, expectedBackgroundPath) { + const dsStorePath = path.join(mountRoot, ".DS_Store"); + const backgroundPath = path.join(mountRoot, ".background", "dmg-background.png"); + const applicationsPath = path.join(mountRoot, "Applications"); + const dsStoreKind = await pathKind(dsStorePath); + const backgroundKind = await pathKind(backgroundPath); + const dsStore = dsStoreKind === "file" ? await readFile(dsStorePath) : Buffer.alloc(0); + const background = backgroundKind === "file" ? await readFile(backgroundPath) : Buffer.alloc(0); + const expectedBackground = await readFile(expectedBackgroundPath); + const dsStoreText = dsStore.toString("latin1"); + const applicationsKind = await pathKind(applicationsPath); + + return validateDmgLayout({ + appKind: await pathKind(path.join(mountRoot, "CleanerX.app")), + applicationsKind, + applicationsTarget: + applicationsKind === "symbolic-link" ? await readlink(applicationsPath) : null, + dsStoreKind, + dsStoreBytes: dsStore.byteLength, + dsStoreContainsBackground: dsStoreText.includes("dmg-background.png"), + dsStoreContainsWindowSize: dsStoreText.includes(expectedWindowSizeMarker), + backgroundKind, + backgroundDigest: digest(background), + expectedBackgroundDigest: digest(expectedBackground), + }); +} + +const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]); + +if (isMain) { + if (process.argv.length !== 4) { + console.error("Usage: node scripts/verify-macos-dmg-layout.mjs "); + process.exit(2); + } + + try { + const snapshot = await inspectMountedDmg(process.argv[2], process.argv[3]); + console.log( + `Verified Finder layout (${snapshot.dsStoreBytes} byte .DS_Store, background ${snapshot.backgroundDigest}).`, + ); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} diff --git a/scripts/verify-macos-dmg-layout.test.js b/scripts/verify-macos-dmg-layout.test.js new file mode 100644 index 0000000..df095c4 --- /dev/null +++ b/scripts/verify-macos-dmg-layout.test.js @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { validateDmgLayout } from "./verify-macos-dmg-layout.mjs"; + +const validSnapshot = { + appKind: "directory", + applicationsKind: "symbolic-link", + applicationsTarget: "/Applications", + dsStoreKind: "file", + dsStoreBytes: 10_240, + dsStoreContainsBackground: true, + dsStoreContainsWindowSize: true, + backgroundKind: "file", + backgroundDigest: "expected-digest", + expectedBackgroundDigest: "expected-digest", +}; + +describe("macOS DMG layout validation", () => { + it("accepts the intended Finder layout snapshot", () => { + expect(validateDmgLayout(validSnapshot)).toEqual(validSnapshot); + }); + + it("rejects the unstyled CI layout", () => { + expect(() => validateDmgLayout({ + ...validSnapshot, + dsStoreKind: "missing", + dsStoreBytes: 0, + dsStoreContainsBackground: false, + dsStoreContainsWindowSize: false, + })).toThrow(/Finder layout file[\s\S]*select dmg-background[\s\S]*760 x 440/); + }); + + it("rejects a changed background or unsafe Applications entry", () => { + expect(() => validateDmgLayout({ + ...validSnapshot, + applicationsKind: "directory", + applicationsTarget: null, + backgroundDigest: "different-digest", + })).toThrow(/symbolic link[\s\S]*match the tracked background/); + }); +}); diff --git a/scripts/verify-macos-dmg.sh b/scripts/verify-macos-dmg.sh new file mode 100755 index 0000000..fd30a34 --- /dev/null +++ b/scripts/verify-macos-dmg.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $(uname -s) != "Darwin" ]]; then + echo "verify-macos-dmg requires macOS." >&2 + exit 2 +fi +if [[ $# -ne 1 || ! -f $1 ]]; then + echo "Usage: scripts/verify-macos-dmg.sh " >&2 + exit 2 +fi + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repository_root="$(cd "$script_dir/.." && pwd)" +dmg_path="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" +mount_dir="$(mktemp -d -t cleanerx-dmg-verify)" +mounted=false + +cleanup() { + if [[ $mounted == true ]]; then + hdiutil detach "$mount_dir" >/dev/null 2>&1 || true + fi + rmdir "$mount_dir" >/dev/null 2>&1 || true +} +trap cleanup EXIT + +hdiutil verify "$dmg_path" >/dev/null +hdiutil attach -readonly -nobrowse -mountpoint "$mount_dir" "$dmg_path" >/dev/null +mounted=true + +node "$repository_root/scripts/verify-macos-dmg-layout.mjs" \ + "$mount_dir" \ + "$repository_root/assets/branding/dmg-background.png" + +hdiutil detach "$mount_dir" >/dev/null +mounted=false +rmdir "$mount_dir" +trap - EXIT