Files
wehub-resource-sync 5b99bf6bca
CI / test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:34:54 +08:00

98 lines
4.6 KiB
Bash
Executable File

#!/bin/bash
# Build Edmund.app — a standalone macOS application bundle.
# Usage: ./scripts/build-app.sh
# Output: build/Edmund.app (ready to drag into /Applications)
set -euo pipefail
cd "$(dirname "$0")/.."
APP_NAME="Edmund"
BUNDLE="build/${APP_NAME}.app"
# The executable target is "edmd" (see Package.swift); the binary keeps that name
# inside the bundle even though the app presents as "Edmund".
EXECUTABLE="edmd"
echo "Building release binary..."
swift build -c release 2>&1 | tail -3
echo "Creating ${APP_NAME}.app bundle..."
rm -rf "$BUNDLE"
mkdir -p "${BUNDLE}/Contents/MacOS"
mkdir -p "${BUNDLE}/Contents/Resources"
cp ".build/release/${EXECUTABLE}" "${BUNDLE}/Contents/MacOS/${EXECUTABLE}"
cp Info.plist "${BUNDLE}/Contents/"
cp Resources/AppIcon.icns "${BUNDLE}/Contents/Resources/AppIcon.icns"
# Compile the asset catalog so the app's AccentColor (our brown) is available.
# macOS uses it only when the user's system accent is "Multicolor"; a specific
# system accent still wins, which is the behavior we want.
# `actool` ships with full Xcode, not the Command Line Tools, so fall back to
# Xcode.app's copy when xcode-select points at the CLT.
echo "Compiling asset catalog..."
ACTOOL="$(xcrun --find actool 2>/dev/null || echo /Applications/Xcode.app/Contents/Developer/usr/bin/actool)"
"$ACTOOL" Resources/Assets.xcassets \
--compile "${BUNDLE}/Contents/Resources" \
--platform macosx \
--minimum-deployment-target 14.0 \
--output-partial-info-plist "$(mktemp)" \
>/dev/null
# Embed Sparkle.framework so the installed bundle is self-contained.
# SwiftPM links Sparkle but doesn't copy the framework (which carries the XPC
# helpers and Autoupdate.app) into the bundle; without this the updater crashes
# on the first check because it can't locate its helper processes.
echo "Embedding Sparkle.framework..."
mkdir -p "${BUNDLE}/Contents/Frameworks"
SPARKLE_FW="$(find .build -type d -name 'Sparkle.framework' | grep -v '\.dSYM' | head -1)"
if [ -z "$SPARKLE_FW" ]; then
echo "Error: Sparkle.framework not found in .build. Run 'swift build -c release' first." >&2
exit 1
fi
cp -R "$SPARKLE_FW" "${BUNDLE}/Contents/Frameworks/"
# Fix the rpath so the binary resolves @rpath/Sparkle.framework at the bundle-
# relative path above rather than the build-artifacts path that won't exist once
# the app is installed.
install_name_tool -add_rpath "@executable_path/../Frameworks" \
"${BUNDLE}/Contents/MacOS/${EXECUTABLE}" 2>/dev/null || true
# Code sign the bundle as a properly *sealed* bundle — not just the binary.
#
# Why this matters: at install time Sparkle re-validates the downloaded update's
# Apple code signature (SUUpdateValidator). Even with a valid EdDSA signature, if
# the bundle reports as code-signed but fails SecStaticCodeCheckValidity, Sparkle
# rejects the update as "improperly signed and could not be validated." Signing
# only the standalone binary (as we used to) produces a bundle with no
# _CodeSignature seal, which fails that check — so every Sparkle update was
# rejected.
#
# Sign inside-out: Sparkle.framework first (its nested XPC helpers must be signed
# before macOS will launch them), then the whole .app. We seal the app while its
# root contains only Contents/, because codesign refuses to seal a bundle that
# has extra items at the .app root ("unsealed contents present in the bundle
# root"). The SwiftMath resource bundle has to live at the .app root at runtime
# (see below), so we copy it in *after* sealing. That leaves one unsealed item at
# the root, which `codesign --verify` (CLI) and --strict flag — but Sparkle's
# actual check is non-strict (SecStaticCodeCheckValidityWithErrors with
# kSecCSCheckAllArchitectures), which tolerates it. Verified end-to-end.
echo "Code signing..."
codesign --force --deep --sign - "${BUNDLE}/Contents/Frameworks/Sparkle.framework"
codesign --force --deep --sign - --identifier "com.i7t5.edmd" "$BUNDLE"
# SwiftPM dependencies that ship resources (SwiftMath's math fonts) emit a
# per-target bundle next to the binary. SwiftMath's generated Bundle.module
# accessor looks for it at Bundle.main.bundleURL — i.e. the .app root — and only
# otherwise at a hardcoded absolute .build path that doesn't exist once the app
# is installed. So it must sit at the .app root; copy it in *after* signing (it
# can't be sealed there — see above) so the bundle's own seal stays valid.
# Without this the app crashes the moment it renders any LaTeX.
echo "Copying SwiftPM resource bundles..."
for bundle in .build/release/*.bundle; do
[ -e "$bundle" ] && cp -R "$bundle" "${BUNDLE}/"
done
echo ""
echo "Done: ${BUNDLE}"
echo "To install: cp -R ${BUNDLE} /Applications/"