196 lines
7.7 KiB
Bash
Executable file
196 lines
7.7 KiB
Bash
Executable file
#!/bin/sh
|
|
# Wraps a built macOS binary into "Midi Harbor.app" and a disk image holding it.
|
|
#
|
|
# packaging/macos/bundle.sh [--helper <headless binary>] <binary> <version> <output directory>
|
|
# [<disk image directory>]
|
|
#
|
|
# The app is built in the output directory, and the disk image goes beside it unless a directory
|
|
# is given for it; GoReleaser puts the image in dist/ with the other artifacts.
|
|
#
|
|
# The binary inside the bundle is the whole program: opened from Finder it shows the graphical
|
|
# interface, and `service install` run from it registers the bundled executable, so the daemon
|
|
# launchd starts carries the bundle's identity and its Bluetooth permission.
|
|
#
|
|
# On a Mac it signs with codesign and makes the image with hdiutil. It signs with the identity
|
|
# MIDI_HARBOR_SIGNING_IDENTITY names, or else the first Developer ID Application identity in the
|
|
# keychain. Elsewhere, which is how GoReleaser runs it, it signs with rcodesign and makes the image
|
|
# with xorriso and libdmg-hfsplus's dmg, signing with .signing/developer-id.p12 when it exists.
|
|
# Without a Developer ID certificate it signs ad hoc. With one, and an App Store Connect API key in
|
|
# .signing/notary-api-key.json, it notarizes the app and the disk image and staples both.
|
|
#
|
|
# --helper makes the App Store variant instead, on a Mac only: the sandboxed app, with the helper
|
|
# as the daemon it starts (Contents/MacOS/midi-harbor-daemon), requiring macOS 13 and starting
|
|
# without a Dock icon. It makes no disk image, since the App Store takes an installer package
|
|
# built with the owner's certificates (specs/014-mac-app-store-mode/contracts/bundle.md).
|
|
set -eu
|
|
|
|
helper=
|
|
if [ "${1:-}" = --helper ]; then
|
|
helper=$2
|
|
shift 2
|
|
fi
|
|
binary=$1
|
|
version=$2
|
|
out=$3
|
|
images=${4:-$out}
|
|
cd "$(dirname "$0")/../.."
|
|
app="$out/Midi Harbor.app"
|
|
dmg="$images/Midi-Harbor-$version.dmg"
|
|
staging="$out/dmg-staging"
|
|
p12=.signing/developer-id.p12
|
|
p12_password=.signing/developer-id.p12.password
|
|
api_key=.signing/notary-api-key.json
|
|
|
|
# Assemble the bundle. The bundle's version must be plain numbers, so a snapshot's suffix is
|
|
# dropped there and kept in the image's name.
|
|
rm -rf "$app" "$dmg" "$staging"
|
|
mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" "$images"
|
|
cp "$binary" "$app/Contents/MacOS/midi-harbor"
|
|
chmod 0755 "$app/Contents/MacOS/midi-harbor"
|
|
cp packaging/macos/AppIcon.icns "$app/Contents/Resources/"
|
|
sed "s/@VERSION@/${version%%-*}/g" packaging/macos/Info.plist > "$app/Contents/Info.plist"
|
|
|
|
# The App Store variant: the helper signed first, since signing the bundle seals it, then the
|
|
# app with the sandbox.
|
|
if [ -n "$helper" ]; then
|
|
identity=${MIDI_HARBOR_SIGNING_IDENTITY:--}
|
|
cp "$helper" "$app/Contents/MacOS/midi-harbor-daemon"
|
|
chmod 0755 "$app/Contents/MacOS/midi-harbor-daemon"
|
|
plutil -replace LSMinimumSystemVersion -string 13.0 "$app/Contents/Info.plist"
|
|
plutil -replace LSUIElement -bool true "$app/Contents/Info.plist"
|
|
plutil -lint "$app/Contents/Info.plist" >/dev/null
|
|
codesign --force --options runtime --sign "$identity" \
|
|
--entitlements packaging/macos/helper.entitlements \
|
|
"$app/Contents/MacOS/midi-harbor-daemon"
|
|
codesign --force --options runtime --sign "$identity" \
|
|
--entitlements packaging/macos/app-store.entitlements "$app"
|
|
codesign --verify --strict "$app"
|
|
echo "$app"
|
|
exit 0
|
|
fi
|
|
|
|
# Find the Developer ID certificate. The App Store variant above takes its identity only from
|
|
# MIDI_HARBOR_SIGNING_IDENTITY, since a Developer ID one is the wrong kind for it.
|
|
developer_id=
|
|
if [ "$(uname -s)" = Darwin ]; then
|
|
identity=${MIDI_HARBOR_SIGNING_IDENTITY:-}
|
|
if [ -z "$identity" ]; then
|
|
identity=$(security find-identity -v -p codesigning \
|
|
| sed -n 's/^.*"\(Developer ID Application: .*\)"$/\1/p' | head -n 1)
|
|
fi
|
|
if [ -n "$identity" ] && [ "$identity" != - ]; then
|
|
developer_id=yes
|
|
fi
|
|
elif [ -f "$p12" ]; then
|
|
if [ ! -f "$p12_password" ]; then
|
|
echo "$p12 needs its password in $p12_password" >&2
|
|
exit 1
|
|
fi
|
|
developer_id=yes
|
|
fi
|
|
notarize=
|
|
if [ -n "$developer_id" ] && [ -f "$api_key" ]; then
|
|
notarize=yes
|
|
elif [ -f "$api_key" ]; then
|
|
echo "not notarizing: $api_key needs a Developer ID certificate to sign with" >&2
|
|
fi
|
|
if [ -n "$developer_id" ]; then
|
|
echo "signing with a Developer ID certificate" >&2
|
|
else
|
|
echo "no Developer ID certificate found; signing ad hoc" >&2
|
|
fi
|
|
|
|
# sign <path> signs the app bundle, with the hardened runtime notarization requires, or the disk
|
|
# image, which carries no runtime flag. Without a Developer ID certificate the image is left
|
|
# unsigned, since an ad hoc signature on it proves nothing.
|
|
sign() {
|
|
case "$1" in
|
|
*.app) runtime=yes ;;
|
|
*) runtime= ;;
|
|
esac
|
|
if [ -z "$developer_id" ]; then
|
|
if [ -z "$runtime" ]; then
|
|
return
|
|
fi
|
|
if [ "$(uname -s)" = Darwin ]; then
|
|
codesign --force --options runtime --sign - "$1"
|
|
codesign --verify --strict "$1"
|
|
else
|
|
rcodesign sign --code-signature-flags runtime "$1" >/dev/null
|
|
fi
|
|
elif [ "$(uname -s)" = Darwin ]; then
|
|
codesign --force --timestamp ${runtime:+--options runtime} --sign "$identity" "$1"
|
|
codesign --verify --strict "$1"
|
|
else
|
|
rcodesign sign --p12-file "$p12" --p12-password-file "$p12_password" \
|
|
${runtime:+--code-signature-flags runtime --for-notarization} "$1" >/dev/null
|
|
fi
|
|
}
|
|
|
|
# notarize <path> submits the app bundle or disk image to Apple's notary service, waits for its
|
|
# verdict, and staples the ticket to it so it opens without a network connection.
|
|
notarize() {
|
|
if [ "$(uname -s)" = Darwin ]; then
|
|
# notarytool takes the key as a .p8 file and an app bundle only inside a zip.
|
|
work=$(mktemp -d)
|
|
{
|
|
echo "-----BEGIN PRIVATE KEY-----"
|
|
plutil -extract private_key raw "$api_key" | fold -w 64
|
|
echo "-----END PRIVATE KEY-----"
|
|
} > "$work/key.p8"
|
|
submission=$1
|
|
case "$1" in
|
|
*.app)
|
|
submission="$work/$(basename "$1").zip"
|
|
ditto -c -k --keepParent "$1" "$submission"
|
|
;;
|
|
esac
|
|
status=0
|
|
xcrun notarytool submit "$submission" --wait \
|
|
--key "$work/key.p8" \
|
|
--key-id "$(plutil -extract key_id raw "$api_key")" \
|
|
--issuer "$(plutil -extract issuer_id raw "$api_key")" >&2 || status=$?
|
|
rm -rf "$work"
|
|
if [ "$status" -ne 0 ]; then
|
|
return "$status"
|
|
fi
|
|
xcrun stapler staple "$1" >&2
|
|
else
|
|
rcodesign notary-submit --api-key-file "$api_key" --staple "$1" >&2
|
|
fi
|
|
}
|
|
|
|
# Sign the app, and notarize it so the copy dragged out of the disk image carries its own ticket.
|
|
# Bluetooth permission is granted to the signed bundle, not to a loose binary.
|
|
if [ "$(uname -s)" = Darwin ]; then
|
|
plutil -lint "$app/Contents/Info.plist" >/dev/null
|
|
fi
|
|
sign "$app"
|
|
if [ -n "$notarize" ]; then
|
|
notarize "$app"
|
|
fi
|
|
|
|
# Put it on a disk image beside a link to Applications, the usual way to install one, with the
|
|
# license and documentation the archives carry.
|
|
mkdir -p "$staging/docs"
|
|
cp -R "$app" "$staging/"
|
|
ln -s /Applications "$staging/Applications"
|
|
cp LICENSE.txt "$staging/"
|
|
cp docs/*.md "$staging/docs/"
|
|
if [ "$(uname -s)" = Darwin ]; then
|
|
hdiutil create -quiet -volname "Midi Harbor" -srcfolder "$staging" -ov -format UDZO "$dmg"
|
|
else
|
|
xorriso -as mkisofs -quiet -V "Midi Harbor" -r -D -no-pad -o "$out/uncompressed.iso" "$staging"
|
|
dmg dmg "$out/uncompressed.iso" "$dmg" >/dev/null
|
|
rm -f "$out/uncompressed.iso"
|
|
fi
|
|
rm -rf "$staging"
|
|
|
|
# Sign and notarize the disk image itself, which is what Gatekeeper checks first on download.
|
|
sign "$dmg"
|
|
if [ -n "$notarize" ]; then
|
|
notarize "$dmg"
|
|
fi
|
|
|
|
echo "$app"
|
|
echo "$dmg"
|