mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-19 13:53:01 +02:00
Etappen 1 and 2 of the Compose migration (43 commits). The Maven build, the single source tree and the JPMS module-info went together. core now holds the domain and app-desktop the user interface, both built by Gradle, packaged through jpackage from Gradle, with SpotBugs and PMD wired in. JavaFX moves to the classpath, which is why the launcher is Main and not the Application subclass. The PR check, the nightly artifacts, the release and the AUR packages all build from Gradle; the macOS DMG points at its output. Verified on Linux, macOS and Windows. core then loses JavaFX entirely. It observes through kst4contest.observe -- ObservableValue, ObservableRoster, and UiDispatcher as the one boundary a worker thread crosses to touch the interface. The controller collections become rosters and the visible station list a pure derivation rather than a FilteredList/SortedList chain, which is also what fixed two focus defects only a load test had exposed: the keyboard focus stays where the operator put it, and the send field keeps it after a row click. The rule that outlives this commit: ObservableRoster is the store and the list a view is bound to is a mirror of it, never the other way round.
349 lines
14 KiB
Bash
Executable File
349 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Local signed (and optionally notarized) macOS build.
|
|
#
|
|
# jpackage cannot sign the app itself: it ad-hoc signs the embedded runtime and
|
|
# then re-runs codesign on the same files without --force, which codesign
|
|
# rejects with "is already signed". So this builds an unsigned app-image, signs
|
|
# it from the inside out ourselves, and only then wraps it into a DMG.
|
|
#
|
|
# Required:
|
|
# SIGNING_IDENTITY The name part of the Developer ID Application certificate,
|
|
# without the "Developer ID Application: " prefix. Example:
|
|
# SIGNING_IDENTITY="Philipp Wagner (ABCDE12345)"
|
|
# List available ones with:
|
|
# security find-identity -v -p codesigning
|
|
#
|
|
# Optional:
|
|
# Notarization, either as three separate values (what CI uses)...
|
|
# NOTARY_KEY Path to the App Store Connect .p8 private key
|
|
# NOTARY_KEY_ID The key's ID, also part of the .p8 filename
|
|
# NOTARY_ISSUER The issuer UUID, shown above the key list in the portal
|
|
# ...or as a keychain profile previously created with
|
|
# NOTARY_PROFILE xcrun notarytool store-credentials <name>
|
|
#
|
|
# With neither, the build is signed but not notarized -- enough to test
|
|
# locally, not enough to distribute.
|
|
#
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "$0")/../.."
|
|
REPO_ROOT="$PWD"
|
|
|
|
BUNDLE_ID="de.x08.KST4Contest"
|
|
ENTITLEMENTS="packaging/macos/kst4contest.entitlements"
|
|
|
|
if [ -z "${SIGNING_IDENTITY:-}" ]; then
|
|
echo "SIGNING_IDENTITY is not set. Available signing identities:" >&2
|
|
security find-identity -v -p codesigning >&2 || true
|
|
exit 1
|
|
fi
|
|
FULL_IDENTITY="Developer ID Application: $SIGNING_IDENTITY"
|
|
|
|
# notarytool takes either an API key triple or a stored keychain profile. The
|
|
# triple needs no keychain at all, which is why CI uses it.
|
|
NOTARY_ARGS=()
|
|
if [ -n "${NOTARY_KEY:-}" ] && [ -n "${NOTARY_KEY_ID:-}" ] && [ -n "${NOTARY_ISSUER:-}" ]; then
|
|
NOTARY_ARGS=(--key "$NOTARY_KEY" --key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER")
|
|
elif [ -n "${NOTARY_PROFILE:-}" ]; then
|
|
NOTARY_ARGS=(--keychain-profile "$NOTARY_PROFILE")
|
|
fi
|
|
|
|
echo "==> Building JAR and collecting runtime dependencies"
|
|
chmod +x gradlew
|
|
./gradlew -S :app-desktop:collectRuntime
|
|
|
|
# jpackage only accepts a numeric major[.minor[.patch]] as the macOS bundle
|
|
# version, so a Maven qualifier like "-nightly" has to be trimmed off.
|
|
POM_VERSION="${JAR##*/praktiKST-}"
|
|
POM_VERSION="${POM_VERSION%.jar}"
|
|
APP_VERSION="$(printf '%s' "$POM_VERSION" | sed -e 's/[^0-9.].*$//' -e 's/\.*$//')"
|
|
[ -n "$APP_VERSION" ] || { echo "Could not derive app version from $JAR" >&2; exit 1; }
|
|
echo "==> Version: $POM_VERSION -> bundle version $APP_VERSION"
|
|
|
|
echo "==> Step 1/4: jpackage app-image (unsigned)"
|
|
rm -rf dist
|
|
mkdir -p dist
|
|
ADD_MODULES="$(grep -m1 '^jpackageAddModules=' gradle.properties | cut -d= -f2)"
|
|
|
|
MACOSX_DEPLOYMENT_TARGET="13.0" jpackage \
|
|
--type app-image \
|
|
--name KST4Contest \
|
|
--app-version "$APP_VERSION" \
|
|
--icon packaging/icons/kst4contest.icns \
|
|
--input app-desktop/build/dist-libs \
|
|
--main-jar app.jar \
|
|
--main-class kst4contest.view.Main \
|
|
--add-modules "$ADD_MODULES" \
|
|
--mac-package-identifier "$BUNDLE_ID" \
|
|
--mac-package-name KST4Contest \
|
|
--dest dist/appimage
|
|
|
|
APP="dist/appimage/KST4Contest.app"
|
|
[ -d "$APP" ] || { echo "jpackage produced no app image" >&2; exit 1; }
|
|
|
|
echo "==> Step 2/4: signing bundle contents (this takes a few minutes)"
|
|
|
|
# Apple's notary service unpacks JARs and checks the native libraries inside
|
|
# them. sqlite-jdbc ships libsqlitejdbc.dylib for both architectures that way,
|
|
# and an unsigned binary in there fails the whole submission. So sign those
|
|
# first: the app bundle's seal covers Contents/app, and rewriting a JAR
|
|
# afterwards would invalidate it.
|
|
echo " scanning jars for native libraries"
|
|
find "$APP/Contents/app" -name '*.jar' -type f | while read -r JARPATH; do
|
|
# Only unpack jars that can plausibly hold a native library. Note the
|
|
# plain grep: "grep -q" exits at the first match, which hands unzip a
|
|
# SIGPIPE, and under "set -o pipefail" that failure becomes the pipeline's
|
|
# status -- inverting this very test.
|
|
if ! unzip -l "$JARPATH" | grep -E '\.(dylib|jnilib|so)$' >/dev/null; then
|
|
continue
|
|
fi
|
|
|
|
JARABS="$(cd "$(dirname "$JARPATH")" && pwd)/$(basename "$JARPATH")"
|
|
JARTMP="$(mktemp -d)"
|
|
unzip -q "$JARABS" -d "$JARTMP"
|
|
|
|
NATIVES="$(mktemp)"
|
|
( cd "$JARTMP" && find . -type f \( -name '*.dylib' -o -name '*.jnilib' -o -name '*.so' \) \
|
|
| while read -r n; do
|
|
if [ "$(file --mime-type -b "$n")" = "application/x-mach-binary" ]; then
|
|
printf '%s\n' "${n#./}"
|
|
fi
|
|
done ) > "$NATIVES"
|
|
|
|
if [ -s "$NATIVES" ]; then
|
|
echo " $(basename "$JARPATH"): $(wc -l < "$NATIVES" | tr -d ' ') native lib(s)"
|
|
( cd "$JARTMP" && xargs -I {} codesign --force --timestamp --options runtime \
|
|
--sign "$FULL_IDENTITY" {} < "$NATIVES" )
|
|
# Update in place rather than repacking, so the rest of the jar --
|
|
# manifest, module descriptor, entry order -- stays byte for byte.
|
|
( cd "$JARTMP" && xargs jar --update --file "$JARABS" < "$NATIVES" )
|
|
fi
|
|
|
|
rm -rf "$JARTMP" "$NATIVES"
|
|
done
|
|
|
|
# Every Mach-O file has to carry its own signature before the enclosing bundle
|
|
# can be sealed, so collect them first. jpackage leaves them ad-hoc signed,
|
|
# hence --force on every call.
|
|
# file(1) pads its output into columns when given several arguments at once,
|
|
# so ask it one file at a time with -b and get an unambiguous answer.
|
|
MACHO_LIST="$(mktemp)"
|
|
find "$APP" -type f -print0 | while IFS= read -r -d '' f; do
|
|
case "$(file --mime-type -b "$f")" in
|
|
application/x-mach-binary) printf '%s\n' "$f" ;;
|
|
esac
|
|
done > "$MACHO_LIST"
|
|
|
|
COUNT="$(wc -l < "$MACHO_LIST" | tr -d ' ')"
|
|
echo " $COUNT Mach-O files to sign"
|
|
|
|
# Serially, deliberately. Running codesign concurrently over several files of
|
|
# the same bundle fails intermittently -- a CI run died with "replacing existing
|
|
# signature" immediately followed by "No such file or directory" for that same
|
|
# path, while the identical script passed locally. Each call contacts Apple's
|
|
# timestamp server, so this costs about a minute for a runtime this size.
|
|
xargs -I {} codesign --force --timestamp --options runtime \
|
|
--sign "$FULL_IDENTITY" {} < "$MACHO_LIST"
|
|
rm -f "$MACHO_LIST"
|
|
|
|
# The embedded JDK is a bundle in its own right and must be sealed before the
|
|
# app that contains it.
|
|
echo " sealing embedded runtime"
|
|
codesign --force --timestamp --options runtime \
|
|
--sign "$FULL_IDENTITY" "$APP/Contents/runtime"
|
|
|
|
# Entitlements go on the outermost bundle: the hardened runtime derives the
|
|
# process's entitlements from the main executable's signature.
|
|
echo " sealing app bundle"
|
|
codesign --force --timestamp --options runtime \
|
|
--entitlements "$ENTITLEMENTS" \
|
|
--sign "$FULL_IDENTITY" "$APP"
|
|
|
|
# Apple rejects the whole submission over a single unsigned native library, and
|
|
# a round trip to the notary service costs minutes. Check its two criteria --
|
|
# a Developer ID authority and a secure timestamp -- locally first.
|
|
echo " preflight: verifying native libraries inside jars"
|
|
PREFLIGHT_ERRORS="$(mktemp)"
|
|
find "$APP/Contents/app" -name '*.jar' -type f | while read -r JARPATH; do
|
|
if ! unzip -l "$JARPATH" | grep -E '\.(dylib|jnilib|so)$' >/dev/null; then
|
|
continue
|
|
fi
|
|
CHECKTMP="$(mktemp -d)"
|
|
unzip -q "$JARPATH" -d "$CHECKTMP"
|
|
find "$CHECKTMP" -type f \( -name '*.dylib' -o -name '*.jnilib' -o -name '*.so' \) \
|
|
| while read -r NATIVE; do
|
|
[ "$(file --mime-type -b "$NATIVE")" = "application/x-mach-binary" ] || continue
|
|
INFO="$(codesign -dv --verbose=2 "$NATIVE" 2>&1 || true)"
|
|
LABEL="$(basename "$JARPATH")/${NATIVE#"$CHECKTMP"/}"
|
|
printf '%s' "$INFO" | grep -q "Authority=Developer ID Application" \
|
|
|| echo "$LABEL: not signed with a Developer ID certificate" >> "$PREFLIGHT_ERRORS"
|
|
printf '%s' "$INFO" | grep -q "Timestamp=" \
|
|
|| echo "$LABEL: signature has no secure timestamp" >> "$PREFLIGHT_ERRORS"
|
|
done
|
|
rm -rf "$CHECKTMP"
|
|
done
|
|
if [ -s "$PREFLIGHT_ERRORS" ]; then
|
|
echo "ERROR: these would fail notarization:" >&2
|
|
sed 's/^/ /' "$PREFLIGHT_ERRORS" >&2
|
|
rm -f "$PREFLIGHT_ERRORS"
|
|
exit 1
|
|
fi
|
|
rm -f "$PREFLIGHT_ERRORS"
|
|
echo " preflight ok"
|
|
|
|
echo "==> Step 3/4: building the dmg"
|
|
|
|
# Not with jpackage: "jpackage --type dmg --app-image" re-signs the app it is
|
|
# handed, replacing our Developer ID signature with an ad-hoc one and dropping
|
|
# the hardened runtime flag. hdiutil copies the bundle verbatim instead.
|
|
DMG="dist/KST4Contest-${APP_VERSION}.dmg"
|
|
STAGE="$(mktemp -d)"
|
|
# ditto rather than cp -R: it preserves the extended attributes the code
|
|
# signature depends on.
|
|
ditto "$APP" "$STAGE/KST4Contest.app"
|
|
ln -s /Applications "$STAGE/Applications"
|
|
|
|
# The installer window shows a background with an arrow from the app to the
|
|
# Applications folder. Finder only stores that layout (.DS_Store) when it has
|
|
# arranged a mounted, writable image itself, so this drives Finder through
|
|
# AppleScript. That can fail, e.g. without automation permission for Finder or
|
|
# on a headless CI runner; the DMG is then built without the layout and only a
|
|
# warning is printed. Window size and icon positions must match
|
|
# packaging/macos/dmg/render-background.swift.
|
|
DMG_BACKGROUND="packaging/macos/dmg/background.tiff"
|
|
VOLUME_NAME="KST4Contest"
|
|
|
|
build_dmg_with_layout() {
|
|
local mount_point="/Volumes/$VOLUME_NAME"
|
|
local rw_dir rw_dmg size_mb
|
|
|
|
if [ ! -f "$DMG_BACKGROUND" ]; then
|
|
echo " $DMG_BACKGROUND is missing" >&2
|
|
return 1
|
|
fi
|
|
# Finder addresses the disk by its name; a second mounted volume of the same
|
|
# name would get another name and break the stored background reference.
|
|
if [ -e "$mount_point" ]; then
|
|
echo " $mount_point is already mounted, eject it first" >&2
|
|
return 1
|
|
fi
|
|
|
|
rw_dir="$(mktemp -d)"
|
|
rw_dmg="$rw_dir/KST4Contest-rw.dmg"
|
|
# Some headroom for the background picture and the .DS_Store Finder writes.
|
|
size_mb=$(( $(du -sm "$STAGE" | cut -f1) + 20 ))
|
|
|
|
hdiutil create -volname "$VOLUME_NAME" -srcfolder "$STAGE" -fs HFS+ \
|
|
-format UDRW -size "${size_mb}m" -ov -quiet "$rw_dmg" || { rm -rf "$rw_dir"; return 1; }
|
|
hdiutil attach "$rw_dmg" -readwrite -noverify -noautoopen -quiet || { rm -rf "$rw_dir"; return 1; }
|
|
|
|
local layout_ok=0
|
|
mkdir -p "$mount_point/.background" \
|
|
&& cp "$DMG_BACKGROUND" "$mount_point/.background/background.tiff" \
|
|
&& osascript <<APPLESCRIPT || layout_ok=1
|
|
tell application "Finder"
|
|
tell disk "$VOLUME_NAME"
|
|
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 the bounds of container window to {200, 120, 840, 520}
|
|
set viewOptions to the 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 background picture of viewOptions to file ".background:background.tiff"
|
|
set position of item "KST4Contest.app" of container window to {170, 180}
|
|
set position of item "Applications" of container window to {470, 180}
|
|
close
|
|
open
|
|
update without registering applications
|
|
delay 2
|
|
close
|
|
end tell
|
|
end tell
|
|
APPLESCRIPT
|
|
|
|
# Give Finder time to write .DS_Store before the image is detached.
|
|
sync
|
|
sleep 2
|
|
rm -rf "$mount_point/.fseventsd"
|
|
hdiutil detach "$mount_point" -quiet \
|
|
|| hdiutil detach "$mount_point" -force -quiet \
|
|
|| { rm -rf "$rw_dir"; return 1; }
|
|
|
|
if [ "$layout_ok" -ne 0 ]; then
|
|
rm -rf "$rw_dir"
|
|
return 1
|
|
fi
|
|
|
|
hdiutil convert "$rw_dmg" -format UDZO -ov -quiet -o "$DMG" || { rm -rf "$rw_dir"; return 1; }
|
|
rm -rf "$rw_dir"
|
|
}
|
|
|
|
if build_dmg_with_layout; then
|
|
echo " installer window layout applied"
|
|
else
|
|
echo "WARNING: could not apply the DMG window layout, building a plain DMG instead" >&2
|
|
rm -f "$DMG"
|
|
hdiutil create -volname "$VOLUME_NAME" -srcfolder "$STAGE" \
|
|
-ov -format UDZO -quiet "$DMG"
|
|
fi
|
|
rm -rf "$STAGE"
|
|
[ -f "$DMG" ] || { echo "hdiutil produced no DMG" >&2; exit 1; }
|
|
|
|
# Signing the DMG itself is not what Gatekeeper judges -- that is the .app
|
|
# inside -- but Apple expects the container to be signed too.
|
|
codesign --force --timestamp --sign "$FULL_IDENTITY" "$DMG"
|
|
echo "==> Built $DMG"
|
|
|
|
echo "==> Step 4/4: verification"
|
|
|
|
if [ ${#NOTARY_ARGS[@]} -gt 0 ]; then
|
|
echo " submitting for notarization (waits for Apple's verdict)"
|
|
# Without a timeout a stalled submission would hang a CI job forever.
|
|
xcrun notarytool submit "$DMG" "${NOTARY_ARGS[@]}" --wait --timeout 30m
|
|
echo " stapling ticket"
|
|
xcrun stapler staple "$DMG"
|
|
else
|
|
echo " no notarization credentials set, skipping notarization"
|
|
fi
|
|
|
|
# Everything below inspects the app as it actually ships, mounted from the DMG,
|
|
# rather than the staging copy on disk.
|
|
MOUNT_POINT="$(mktemp -d)"
|
|
hdiutil attach "$DMG" -nobrowse -quiet -mountpoint "$MOUNT_POINT"
|
|
trap 'hdiutil detach "$MOUNT_POINT" -quiet 2>/dev/null || hdiutil detach "$MOUNT_POINT" -force -quiet 2>/dev/null || true' EXIT
|
|
SHIPPED_APP="$MOUNT_POINT/KST4Contest.app"
|
|
|
|
echo "--- codesign --verify on the app inside the DMG ---"
|
|
codesign --verify --deep --strict --verbose=2 "$SHIPPED_APP"
|
|
|
|
echo "--- app identity ---"
|
|
codesign -dv --verbose=2 "$SHIPPED_APP" 2>&1 | grep -iE "identifier|authority|teamidentifier|flags"
|
|
|
|
# An ad-hoc signature here means something along the way re-signed the bundle.
|
|
if codesign -dv "$SHIPPED_APP" 2>&1 | grep -q "adhoc"; then
|
|
echo "ERROR: the app inside the DMG is ad-hoc signed, not Developer ID signed" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "--- entitlements as signed ---"
|
|
codesign -d --entitlements - --xml "$SHIPPED_APP" 2>/dev/null | plutil -convert xml1 -o - - | grep -E "key|true|false"
|
|
|
|
echo "--- dmg identity ---"
|
|
codesign -dv --verbose=2 "$DMG" 2>&1 | grep -iE "authority|teamidentifier" | head -2
|
|
|
|
echo "--- spctl assessment ---"
|
|
# Without notarization this reports "rejected"; that is expected.
|
|
spctl --assess --type execute --verbose=4 "$SHIPPED_APP" || true
|
|
|
|
if [ ${#NOTARY_ARGS[@]} -gt 0 ]; then
|
|
echo "--- stapler validate ---"
|
|
xcrun stapler validate "$DMG"
|
|
fi
|
|
|
|
echo
|
|
echo "Done: $REPO_ROOT/$DMG"
|