Browse / github.com/storytold/filmcraft / patch
macOS DMG: hdiutil create, so the app in it passes codesign --strict (#882)
Release dmg: bundled .app fails `codesign --verify --deep --strict` (FinderInfo xattrs) Same packaging issue as storytold/photocraft#3034: the FilmCraft.app inside the universal release dmg fails `codesign --verify --deep --strict` with `resource fork, Finder information, or similar detritus not allowed` (empty `com.apple.FinderInfo` xattrs on bundle resources). `spctl` and `stapler validate` pass. Details, workaround and suggested fix are in the linked issue.
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- ZJHeepfixer (imported from pull request #3254, open)
- Base
fa550ce2d07bdfee6b1d7b6835e8abc8c6913d00- Size
- +195 −10 in 6 files
- Review
- pending
- Shared
- 2026-10-28
- Id
78f1b695d31611487cfdb280
How it went
No other operator has reported using it yet.
Diff
raw · .github/workflows/packaging-lint.yml docs/releasing.md packaging/macos/dmg-test.sh packaging/macos/dmg.sh packaging/macos/dmg/README.md packaging/macos/package.sh
@@ -21,3 +21,13 @@ jobs: lint: # Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos. uses: storytold/craft-common-actions/.github/workflows/packaging-lint.yml@main++ macos-dmg:+ timeout-minutes: 15+ name: macOS DMG app passes codesign --strict+ runs-on: macos-15+ steps:+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1+ with:+ persist-credentials: false+ - run: bash packaging/macos/dmg-test.sh@@ -76,8 +76,11 @@ deliberately, in every job at once. `entitlements.plist`. The Developer ID certificate is imported into a temporary keychain by `packaging/macos/import-cert.sh`. - **Notarization:** the app is sent with `xcrun notarytool submit`, then the ticket is stapled- and checked with `stapler validate`. The app goes on a DMG (`hdiutil makehybrid`), which is- signed and notarized too. The universal `filmcraft-cli` is signed the same way, zipped, and the zip is notarized.+ and checked with `stapler validate`. The app goes on a DMG (`hdiutil create`), which is+ signed and notarized too. Before notarizing it, the script mounts the DMG and checks the app in+ it, and a copy installed from it, with `codesign --verify --strict --deep` (an image made with+ `makehybrid` failed that, #882; `packaging/macos/dmg-test.sh` tests it).+ The universal `filmcraft-cli` is signed the same way, zipped, and the zip is notarized. Its Finder window (background, icon size and positions) comes from [`packaging/macos/dmg/`](../packaging/macos/dmg/README.md), and its volume is named `FilmCraft` without the version, which the window's background needs; the DMG file name keeps the version.new file mode 100755@@ -0,0 +1,79 @@+#!/usr/bin/env bash+# The app in the macOS DMG, and a copy installed from it, pass `codesign --verify --strict --deep`+# (#882). Runs package.sh end to end, signed ad hoc, around a stand-in executable (no Rust build,+# no secrets), with a font licence that carries Finder info as the input. Then shows the check+# rejects the image `hdiutil makehybrid -hfs` made of the same files. macOS only;+# CI: .github/workflows/packaging-lint.yml.+set -euo pipefail+HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"+fail() { echo "FAIL: $*" >&2; exit 1; }+if [[ "$(uname -s)" != Darwin ]]; then+ echo "skip: needs macOS (hdiutil, codesign)"+ exit 0+fi+case "$(uname -m)" in+ arm64) ARCH=aarch64 TRIPLE=aarch64-apple-darwin CC_ARCH=arm64 ;;+ x86_64) ARCH=x86_64 TRIPLE=x86_64-apple-darwin CC_ARCH=x86_64 ;;+ *) fail "unknown machine $(uname -m)" ;;+esac++TMP="$(mktemp -d)"+trap 'rm -rf "$TMP"' EXIT+# Ad hoc, not notarized, and no Actions annotations from the script under test.+unset MACOS_SIGN_IDENTITY MACOS_KEYCHAIN APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID GITHUB_ACTIONS+export CARGO_TARGET_DIR="$TMP/target" DIST="$TMP/dist" CRAFT_FONTS_DIR="$TMP/craft-fonts"+# shellcheck source=../env.sh+. "$HERE/../env.sh"+# shellcheck source=dmg.sh+. "$HERE/dmg.sh"++# package.sh only joins, signs and runs (`filmcraft-cli --version`) the binaries, so any+# executable stands in for them.+bin="$CARGO_TARGET_DIR/$TRIPLE/release"+mkdir -p "$bin"+printf 'int main(void) { return 0; }\n' | cc -arch "$CC_ARCH" -x c -o "$bin/filmcraft" -+cp "$bin/filmcraft" "$bin/filmcraft-cli"+# A licence with Finder info (what makehybrid gave every file): `cp` brings it into the bundle, so+# the strip before signing is tested too.+mkdir -p "$CRAFT_FONTS_DIR/fonts/test-sans"+echo "test licence" >"$CRAFT_FONTS_DIR/fonts/test-sans/OFL.txt"+/usr/bin/xattr -wx com.apple.FinderInfo 00000000000000000000FFFFFFFF000000000000000000000000000000000000 \+ "$CRAFT_FONTS_DIR/fonts/test-sans/OFL.txt"++"$HERE/package.sh" --arch "$ARCH" --skip-build || fail "package.sh failed"+DMG="$DIST/filmcraft-$VERSION-macos-$ARCH.dmg"+check_dmg_app "$DMG" FilmCraft.app || fail "the app in $(basename "$DMG") fails codesign --strict"++# Interrupted while the image is mounted (SIGTERM, as a cancelled job gets it: a stand-in codesign+# sends it to the check), the check still detaches the image and removes its scratch directory.+mkdir "$TMP/stub" "$TMP/interrupted"+cat >"$TMP/stub/codesign" <<'EOF'+#!/bin/sh+kill -TERM "$PPID"+EOF+chmod +x "$TMP/stub/codesign"+if (+ PATH="$TMP/stub:$PATH"+ export RUNNER_TEMP="$TMP/interrupted"+ check_dmg_app "$DMG" FilmCraft.app+) >/dev/null 2>&1; then+ fail "check_dmg_app passed though it was interrupted"+fi+left="$(ls -A "$TMP/interrupted")"+if [[ -n "$left" ]]; then+ hdiutil detach -force "$TMP/interrupted/$left/mnt" >/dev/null 2>&1 || true+ fail "an interrupted check_dmg_app left $left behind, its image still attached"+fi++# The check must catch the defect it guards against: the same staged files, imaged the old way.+OLD="$TMP/makehybrid.dmg"+hdiutil makehybrid -quiet -hfs -hfs-volume-name FilmCraft -o "$OLD" "$CARGO_TARGET_DIR/macos-package/dmg"+if out="$(check_dmg_app "$OLD" FilmCraft.app 2>&1)"; then+ fail "check_dmg_app accepted a makehybrid image"+fi+case "$out" in+ *"Finder information, or similar detritus"*) ;;+ *) fail "check_dmg_app rejected the makehybrid image for another reason: $out" ;;+esac++echo "ok: the app in the DMG passes codesign --strict; an interrupted check detaches; a makehybrid image of it is rejected"new file mode 100644@@ -0,0 +1,70 @@+# shellcheck shell=bash+# DMG helpers shared by package.sh and dmg-test.sh. Source it after ../env.sh (`warn`).++# `hdiutil create` and `attach` attach a device, which fails now and then on CI runners ("Resource+# busy"), so they get a few tries. A retry can't end a hang: where a sandbox blocks the disk-image+# helper (some agent sandboxes do), hdiutil can wait forever, so build and check DMGs outside one.+# Usage: hdiutil_retry <what> <hdiutil args…>; tries five times with growing pauses.+hdiutil_retry() {+ local what="$1" attempt+ shift+ for attempt in 1 2 3 4 5; do+ if hdiutil "$@"; then+ return 0+ fi+ if [[ "$attempt" = 5 ]]; then+ echo "error: hdiutil $what failed 5 times" >&2+ return 1+ fi+ warn "hdiutil $what failed (attempt $attempt of 5); retrying in $((attempt * 10))s"+ sleep $((attempt * 10))+ done+}++# The app must pass `codesign --verify --strict --deep` as users get it: in the mounted image, and+# copied out of it with ditto, which keeps extended attributes the way a Finder drag to Applications+# does. Checking the bundle before it goes on the image is not enough: `hdiutil makehybrid -hfs`+# gave every file in the image a com.apple.FinderInfo, which --strict rejects as "resource fork,+# Finder information, or similar detritus not allowed" (#882), while the DMG's own signature and+# Gatekeeper still passed.+# Usage: check_dmg_app <dmg> <App.app>+#+# The check runs in a subshell whose EXIT trap detaches the image and removes the scratch directory+# however it ends: a failed step, `set -e`, Ctrl-C, or the SIGTERM a cancelled job gets (bash skips+# the EXIT trap when a signal kills it, so HUP, INT and TERM are turned into exits). The traps are+# the subshell's own, so a caller's EXIT trap is left alone.+check_dmg_app() (+ dmg="$1" app="$2" rc=0+ dir="$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/dmg-check.XXXXXX")"+ trap 'exit 129' HUP+ trap 'exit 130' INT+ trap 'exit 143' TERM+ trap 'dmg_check_cleanup "$dir" "$?"' EXIT+ mkdir "$dir/mnt" "$dir/copy"+ hdiutil_retry attach attach -readonly -nobrowse -noautoopen -mountpoint "$dir/mnt" "$dmg" >/dev/null || exit 1+ echo "==> codesign --verify --strict --deep: $app in $(basename "$dmg"), then a copy installed from it"+ if ! codesign --verify --strict --deep --verbose=2 "$dir/mnt/$app"; then+ rc=1+ elif ! ditto "$dir/mnt/$app" "$dir/copy/$app" || ! codesign --verify --strict --deep --verbose=2 "$dir/copy/$app"; then+ rc=1+ fi+ if [[ "$rc" != 0 ]]; then+ echo "error: $app in $(basename "$dmg") fails codesign --verify --strict --deep" >&2+ fi+ exit "$rc"+)++# check_dmg_app's EXIT trap. Whatever is mounted on <dir>/mnt is detached (found by its device, not+# a flag, so an image attached just before a signal goes too), then <dir> is removed. Exits with+# <status>, or 1 when the image won't detach (then <dir> stays, with the mount in it).+# Usage: dmg_check_cleanup <dir> <status>+dmg_check_cleanup() {+ local dir="$1" rc="$2" dev+ dev="$(stat -f %d "$dir/mnt" 2>/dev/null)" || dev=""+ if [[ -n "$dev" && "$dev" != "$(stat -f %d "$dir")" ]] && ! hdiutil_retry detach detach "$dir/mnt" >/dev/null; then+ echo "error: could not detach $dir/mnt" >&2+ exit 1+ fi+ rm -rf "$dir"+ exit "$rc"+}@@ -2,8 +2,8 @@ What Finder shows when the DMG opens: a 660 × 400 pt background with the app icon and the `Applications` link side by side. `package.sh` copies these files into the image; nothing here is-generated at build time, so the DMG still builds with `hdiutil makehybrid` (no mounted device, no-Finder scripting on CI, nothing extra installed in the signing job).+generated at build time, so the DMG builds with a plain `hdiutil create -srcfolder` (no Finder+scripting on CI, nothing extra installed in the signing job). | File | What | |---|---|@@ -19,6 +19,20 @@ through an alias that includes the volume name, and Finder resolves it by that n versioned name the window keeps its size and icon positions but shows no background (tested). The DMG file name (`filmcraft-<version>-macos-<arch>.dmg`) still carries the version. +## `hdiutil create`, not `makehybrid`++`hdiutil makehybrid -hfs` gives every file and folder on the volume a `com.apple.FinderInfo` (a+Finder location of -1,-1), even when the staged files have none. `codesign --verify --strict`+rejects it on the app in the image and on every copy installed from it ("resource fork, Finder+information, or similar detritus not allowed", #882), though Gatekeeper accepts the app.+`hdiutil create -srcfolder` writes the files as they are. `package.sh` checks the app in the+finished image (`check_dmg_app` in `../dmg.sh`), and `../dmg-test.sh` tests both ways.++The one thing `create` can't do is `makehybrid -hfs-openfolder`, which listed the volume's root as+its first open folder. Opening the window doesn't depend on it: double-clicking a read-only disk+image opens its volume in the Finder (hdiutil(1), `auto-open-ro-root`). The window's background,+icon size and positions come from `.DS_Store`, as before.+ ## Rules - **Finder draws the icon labels in black in light and dark mode** when a window has a background,@@ -16,6 +16,8 @@ set -euo pipefail # shellcheck source=../env.sh . "$(dirname "${BASH_SOURCE[0]}")/../env.sh" HERE="$ROOT/packaging/macos"+# shellcheck source=dmg.sh+. "$HERE/dmg.sh" ARCH=universal SKIP_BUILD=0@@ -110,6 +112,10 @@ plutil -lint "$APP/Contents/Info.plist" printf 'APPL????' >"$APP/Contents/PkgInfo" # licences of the embedded craft-fonts fonts (builds with CRAFT_FONTS_DIR) copy_font_licences "$APP/Contents/Resources"+# `codesign --verify --strict` rejects Finder info and resource forks in a bundle (#882), and `cp`+# copies extended attributes, so a local checkout can bring them in on the icon or a licence.+# Apple's xattr: a Homebrew `xattr` earlier on PATH has no -r.+/usr/bin/xattr -cr "$APP" # Sign inside-out: nested code first, then the bundle itself (no --deep on the final signature). # Today the only nested code is the main executable; frameworks/helpers would be signed here too.@@ -135,15 +141,18 @@ ln -s /Applications "$STAGE/Applications" mkdir -p "$STAGE/.background" cp "$HERE/dmg/background.tiff" "$STAGE/.background/background.tiff" cp "$HERE/dmg/dmg-layout.DS_Store" "$STAGE/.DS_Store"-rm -f "$DMG" "$WORK/raw.dmg"-# makehybrid + convert builds the image without attaching a device, unlike `create -srcfolder`,-# which is flaky on CI runners ("Resource busy") and hangs in sandboxed sessions.+rm -f "$DMG"+# `hdiutil create -srcfolder` writes the files as they are. `makehybrid -hfs` gave every file a+# com.apple.FinderInfo (a Finder location of -1,-1), which `codesign --verify --strict` rejects on+# the app in the image and on a copy installed from it (#882; the sibling apps made the same+# change in storytold/pdfcraft#823 and storytold/vectorcraft#1121). `create` attaches a device+# while it copies: see hdiutil_retry in dmg.sh for CI and sandboxes. # The volume name has no version: .DS_Store finds the background through an alias that includes it.-hdiutil makehybrid -hfs -hfs-volume-name "FilmCraft" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE"-hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG"-rm -f "$WORK/raw.dmg"+hdiutil_retry create create -srcfolder "$STAGE" -volname "FilmCraft" -fs HFS+ -format UDZO -imagekey zlib-level=9 -ov "$DMG" sign "$DMG" codesign --verify --strict --verbose=2 "$DMG"+# Before notarizing: the app in the image must still pass --strict, so this can't regress unseen.+check_dmg_app "$DMG" FilmCraft.app if [[ "$NOTARIZE" = 1 ]]; then notarize "$DMG" xcrun stapler staple "$DMG"