Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*.gif binary
*.ico binary
*.icns binary
*.DS_Store binary
*.pdf binary
# Fonts
*.ttf binary
Expand Down
2 changes: 2 additions & 0 deletions apple/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ scripts/release/build.sh plugins --unsigned && scripts/release/package.sh plugin

Signed: drop `--unsigned`, with the Developer ID identity and the provisioning profile `scripts/release/ExportOptions.plist` names. `scripts/release/notarize.sh <path>...` needs the profile `xcrun notarytool store-credentials TablePro --key <p8> --key-id <id> --issuer <issuer>` creates.

The disk image opens to the window `scripts/release/dmg.DS_Store` describes, a file Finder wrote. To change it, edit the Finder settings in `scripts/release/dmg.sh`, run `scripts/release/dmg.sh layout build/release/TablePro.app` on a Mac with a logged-in session, and open a disk image built with the new file before committing it.

### Clean builds

| Reach for | When |
Expand Down
1 change: 1 addition & 0 deletions apple/changelog.d/3990.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The disk image opens to a window with TablePro beside the Applications folder, ready to drag.
Binary file added apple/scripts/release/dmg.DS_Store
Binary file not shown.
155 changes: 155 additions & 0 deletions apple/scripts/release/dmg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#!/usr/bin/env bash
set -euo pipefail

# Usage: dmg.sh build <app> <dmg> | dmg.sh layout <app>
# build: the disk image a release ships. A read-only HFS+ volume named TablePro holds the app, a link to
# /Applications and the app's icon as the volume icon, and opens to the window layout in dmg.DS_Store.
# layout: on a Mac with a logged-in session, Finder lays out the same volume and its .DS_Store replaces
# dmg.DS_Store. macOS asks once to let the terminal control Finder. CI never runs it.

VOLUME="TablePro"
LAYOUT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/dmg.DS_Store"
# The root folder's Finder flags with kHasCustomIcon (0x0400) set, which makes Finder draw .VolumeIcon.icns.
# Apple DTS names setxattr on com.apple.FinderInfo as the supported way to set it.
CUSTOM_ICON_FINDER_INFO="0000000000000000040000000000000000000000000000000000000000000000"

usage() {
echo "Usage: dmg.sh build <app> <dmg> | dmg.sh layout <app>" >&2
exit 2
}

die() {
echo "dmg.sh: $*" >&2
exit 1
}

WORK="$(mktemp -d)"
DEVICE=""
DEVICES=()
cleanup() {
local device
for device in ${DEVICES[@]+"${DEVICES[@]}"}; do
diskutil eject "$device" > /dev/null 2>&1 || true
done
rm -rf "$WORK"
}
trap cleanup EXIT

# Attaches the image $1 with the diskutil image attach options after it, and sets DEVICE.
attach() {
local image="$1"
shift
DEVICE="$(diskutil image attach --plist "$@" "$image" | plutil -extract 'system-entities.0.dev-entry' raw -o - -)"
DEVICES+=("$DEVICE")
}

# Writes the read/write image $1 holding the volume for the app $2, with the .DS_Store $3 when given, unmounted.
create_volume() {
local image="$1" app="$2" layout="$3" kilobytes mountpoint="$WORK/volume"
# Room for the app and the HFS+ catalog. Free space compresses to almost nothing in the read-only image.
kilobytes=$(($(du -sk "$app" | cut -f1) * 11 / 10 + 20480))
diskutil image create blank --fs None --size $((kilobytes * 1024)) "$image" > /dev/null
# Apple DTS recommends HFS+ for a laid-out volume, and diskutil image formats no blank image as HFS+.
attach "$image" --noMount
diskutil eraseDisk HFS+ "$VOLUME" GPT "$DEVICE" > /dev/null
diskutil eject "$DEVICE" > /dev/null

mkdir "$mountpoint"
attach "$image" --nobrowse --mountPoint "$mountpoint"
ditto "$app" "$mountpoint/TablePro.app"
ln -s /Applications "$mountpoint/Applications"
iconutil -c icns -o "$mountpoint/.VolumeIcon.icns" "$app/Contents/Resources/Assets.car" AppIcon
xattr -wx com.apple.FinderInfo "$CUSTOM_ICON_FINDER_INFO" "$mountpoint"
[ -z "$layout" ] || cp "$layout" "$mountpoint/.DS_Store"
# fseventsd writes .fseventsd onto a writable HFS+ volume, and it would ship as a hidden folder.
rm -rf "$mountpoint/.fseventsd"
diskutil eject "$DEVICE" > /dev/null
}

check() {
local dmg="$1" app="$2" mountpoint="$WORK/check" items flags
mkdir "$mountpoint"
attach "$dmg" --readOnly --nobrowse --mountPoint "$mountpoint"
diskutil info -plist "$mountpoint" > "$WORK/info.plist"
[ "$(plutil -extract FilesystemType raw -o - "$WORK/info.plist")" = hfs ] || die "$dmg is not HFS+"
[ "$(plutil -extract VolumeName raw -o - "$WORK/info.plist")" = "$VOLUME" ] || die "$dmg is not named $VOLUME"
items="$(cd "$mountpoint" && find . -mindepth 1 -maxdepth 1 | LC_ALL=C sort | tr '\n' ' ')"
[ "$items" = "./.DS_Store ./.VolumeIcon.icns ./Applications ./TablePro.app " ] || die "$dmg holds $items"
[ "$(readlink "$mountpoint/Applications")" = /Applications ] || die "Applications in $dmg is not a link to /Applications"
cmp -s "$mountpoint/.DS_Store" "$LAYOUT" || die "the .DS_Store in $dmg is not $LAYOUT"
flags="$(xattr -px com.apple.FinderInfo "$mountpoint" | tr -d ' \n')"
[ $((16#${flags:16:4} & 16#0400)) -ne 0 ] || die "the volume in $dmg has no custom icon"
# HFS+ stores names decomposed, which would break the seal of a bundle that has a precomposed name.
diff -rq --no-dereference "$app" "$mountpoint/TablePro.app" || die "the app in $dmg differs from $app"
diskutil eject "$DEVICE" > /dev/null
}

build() {
local app="${1%/}" dmg="$2" image="$WORK/volume.dmg"
[ -s "$LAYOUT" ] || die "no $LAYOUT; run dmg.sh layout first"
create_volume "$image" "$app" "$LAYOUT"
# Converted image to image: diskutil image create from a folder leaves .DS_Store out.
rm -f "$dmg"
diskutil image create from "$image" "$dmg" --format ULFO > /dev/null
check "$dmg" "$app"
}

has_records() {
local file="$1" record
shift
[ -f "$file" ] || return 1
for record in "$@"; do
grep -aq "$record" "$file" || return 1
done
}

layout() {
local app="${1%/}" image="$WORK/volume.dmg" mountpoint="/Volumes/$VOLUME" waited=0
[ ! -e "$mountpoint" ] || die "eject $mountpoint first; Finder finds the volume by its name"
create_volume "$image" "$app" ""
attach "$image"
[ -d "$mountpoint" ] || die "the volume did not mount at $mountpoint"
rm -f "$mountpoint/.DS_Store"

osascript <<APPLESCRIPT
tell application "Finder"
tell disk "$VOLUME"
open
set current view of container window to icon view
set toolbar visible of container window to false
set statusbar visible of container window to false
set bounds of container window to {200, 120, 800, 480}
set viewOptions to icon view options of container window
set arrangement of viewOptions to not arranged
set icon size of viewOptions to 128
set text size of viewOptions to 13
set label position of viewOptions to bottom
set shows item info of viewOptions to false
set shows icon preview of viewOptions to false
set position of item "TablePro.app" of container window to {160, 150}
set position of item "Applications" of container window to {440, 150}
close
open
update without registering applications
delay 1
close
end tell
end tell
APPLESCRIPT

# Finder creates .DS_Store when the window opens, and saves the window (bwsp), view (icvp) and icon
# position (Iloc) records only once the window has been closed, opened and closed again.
until has_records "$mountpoint/.DS_Store" bwsp icvp Iloc; do
[ "$waited" -lt 60 ] || die "Finder did not save the layout to $mountpoint/.DS_Store within 60 seconds"
sleep 1
waited=$((waited + 1))
done
cp "$mountpoint/.DS_Store" "$LAYOUT"
echo "Wrote $LAYOUT. Build a disk image with it and open that in Finder before committing."
}

case "${1:-}" in
build) [ $# -eq 3 ] || usage; [ -d "$2" ] || die "no app at $2"; build "$2" "$3" ;;
layout) [ $# -eq 2 ] || usage; [ -d "$2" ] || die "no app at $2"; layout "$2" ;;
*) usage ;;
esac
8 changes: 3 additions & 5 deletions apple/scripts/release/package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,9 @@ package_app() {
mkdir -p "$DIST"
rm -f "$dmg" "$update" "$symbols"

mkdir "$WORK/dmg"
ditto "$app" "$WORK/dmg/TablePro.app"
ln -s /Applications "$WORK/dmg/Applications"
diskutil image create from "$WORK/dmg" "$dmg" --volumeName "TablePro $VERSION"
[ "$SIGNED" = false ] || codesign --sign "Developer ID Application" --timestamp "$dmg"
scripts/release/dmg.sh build "$app" "$dmg"
# Apple asks for an identifier of the image's own, distinct from the app's bundle identifier.
[ "$SIGNED" = false ] || codesign --sign "Developer ID Application" --timestamp --identifier com.TablePro.dmg "$dmg"

# tar keeps the framework symlinks codesign seals; a signed bundle needs no extended attributes.
/usr/bin/tar --no-mac-metadata --no-xattrs --uid 0 --gid 0 \
Expand Down
4 changes: 2 additions & 2 deletions scripts/ci/detect-changed-paths.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ case "$SUITE" in
'apple/TablePro\.xcodeproj/project\.xcworkspace/xcshareddata/swiftpm/Package\.resolved$'
'\.github/plugin-registry\.json$'
'apple/scripts/(generate-project|download-libs|create-openssl-dylibs|build-dameng|build-hana)\.sh$'
'apple/scripts/lib/' 'apple/scripts/release/(build|package)\.sh$'
'apple/scripts/release/(notes|registry|version)\.py$'
'apple/scripts/lib/' 'apple/scripts/release/(build|package|dmg)\.sh$'
'apple/scripts/release/dmg\.DS_Store$' 'apple/scripts/release/(notes|registry|version)\.py$'
'apple/scripts/ci/verify-plugin-loads\.sh$' 'scripts/ci/detect-changed-paths\.sh$'
'\.github/actions/(setup-xcode(gen)?|prepare-build)/' '\.github/workflows/(ci|release-build)\.yml$')
SKIP='apple/Plugins/[^/]+Tests/|apple/Packages/[^/]+/Tests/'
Expand Down
2 changes: 2 additions & 0 deletions scripts/ci/test_detect_changed_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ def test_release_runs_on_what_the_release_build_reads(self):
".github/plugin-registry.json",
"apple/scripts/release/build.sh",
"apple/scripts/release/package.sh",
"apple/scripts/release/dmg.sh",
"apple/scripts/release/dmg.DS_Store",
"apple/scripts/release/registry.py",
"apple/scripts/release/notes.py",
"apple/scripts/release/version.py",
Expand Down
Loading