Sign the Sparkle framework before signing the whole app to ensure proper code signature chain for sandboxed installation.
225 lines
6.7 KiB
Bash
Executable File
225 lines
6.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
BUILD_DIR="$ROOT_DIR/build"
|
||
ARTIFACTS_DIR="$ROOT_DIR/dist"
|
||
SCHEME="iKeyMon"
|
||
PROJECT="iKeyMon.xcodeproj"
|
||
CREDENTIALS_FILE="$ROOT_DIR/.signing.env"
|
||
VERSION_FILE="$ROOT_DIR/version.json"
|
||
DERIVED_DATA_ROOT="${DERIVED_DATA_ROOT:-$HOME/Library/Developer/Xcode/DerivedData}"
|
||
|
||
find_generate_appcast() {
|
||
if [[ -n "${SPARKLE_GENERATE_APPCAST:-}" && -x "${SPARKLE_GENERATE_APPCAST}" ]]; then
|
||
echo "$SPARKLE_GENERATE_APPCAST"
|
||
return
|
||
fi
|
||
|
||
if [[ -d "$DERIVED_DATA_ROOT" ]]; then
|
||
local candidate
|
||
candidate="$(find "$DERIVED_DATA_ROOT" -path "*/SourcePackages/artifacts/sparkle/Sparkle/bin/generate_appcast" -type f 2>/dev/null | head -n 1 || true)"
|
||
if [[ -n "$candidate" ]]; then
|
||
echo "$candidate"
|
||
return
|
||
fi
|
||
fi
|
||
}
|
||
|
||
generate_appcast() {
|
||
local generator
|
||
generator="$(find_generate_appcast)"
|
||
local download_prefix=""
|
||
if [[ -n "${SPARKLE_DOWNLOAD_BASE_TEMPLATE:-}" ]]; then
|
||
download_prefix="${SPARKLE_DOWNLOAD_BASE_TEMPLATE//\{\{VERSION\}\}/$VERSION}"
|
||
elif [[ -n "${SPARKLE_DOWNLOAD_BASE_URL:-}" ]]; then
|
||
download_prefix="${SPARKLE_DOWNLOAD_BASE_URL%/}/v${VERSION}"
|
||
fi
|
||
|
||
# Ensure the version segment is present to match Gitea's /download/vX.Y.Z/ layout.
|
||
if [[ -n "$download_prefix" ]] && [[ "$download_prefix" != *"/$VERSION"* ]]; then
|
||
download_prefix="${download_prefix%/}/v${VERSION}"
|
||
fi
|
||
|
||
if [[ -z "$generator" || -z "${SPARKLE_EDDSA_KEY_FILE:-}" || -z "$download_prefix" ]]; then
|
||
echo "ℹ️ Skipping Sparkle appcast generation (generator/key/download prefix not configured)."
|
||
return
|
||
fi
|
||
|
||
local sparkle_cache="$HOME/Library/Caches/Sparkle_generate_appcast"
|
||
rm -rf "$sparkle_cache"
|
||
|
||
local output="$SPARKLE_APPCAST_OUTPUT"
|
||
mkdir -p "$(dirname "$output")"
|
||
local staging_dir
|
||
staging_dir="$(mktemp -d)"
|
||
cp "$ARTIFACTS_DIR"/*.zip "$staging_dir"/ 2>/dev/null || true
|
||
|
||
if ! ls "$staging_dir"/*.zip >/dev/null 2>&1; then
|
||
echo "ℹ️ Skipping Sparkle appcast generation (no ZIP archives found)."
|
||
rm -rf "$staging_dir"
|
||
return
|
||
fi
|
||
|
||
echo "🧾 Generating Sparkle appcast at $output"
|
||
if ! "$generator" \
|
||
--download-url-prefix "$download_prefix" \
|
||
--ed-key-file "$SPARKLE_EDDSA_KEY_FILE" \
|
||
-o "$output" \
|
||
"$staging_dir"; then
|
||
echo "⚠️ Sparkle appcast generation failed."
|
||
fi
|
||
rm -rf "$staging_dir"
|
||
}
|
||
|
||
sign_update_artifacts() {
|
||
local signer
|
||
signer="$(find "$DERIVED_DATA_ROOT" -path "*/SourcePackages/artifacts/sparkle/Sparkle/bin/sign_update" -type f 2>/dev/null | head -n 1 || true)"
|
||
if [[ -z "$signer" || -z "${SPARKLE_EDDSA_KEY_FILE:-}" ]]; then
|
||
echo "ℹ️ Skipping Sparkle signing (sign_update or SPARKLE_EDDSA_KEY_FILE missing)."
|
||
return
|
||
fi
|
||
echo "🔑 Signing ${ZIP_NAME} for Sparkle feed"
|
||
if ! "$signer" "${ARTIFACTS_DIR}/${ZIP_NAME}" --ed-key-file "${SPARKLE_EDDSA_KEY_FILE}"; then
|
||
echo "⚠️ sign_update failed (continuing without signature)"
|
||
fi
|
||
}
|
||
|
||
rewrite_appcast_urls() {
|
||
: # no-op (old helper removed)
|
||
}
|
||
|
||
submit_for_notarization() {
|
||
local target="$1"
|
||
local label="$2"
|
||
echo "📝 Submitting ${label} for notarization..."
|
||
xcrun notarytool submit "$target" \
|
||
--apple-id "$NOTARY_APPLE_ID" \
|
||
--team-id "$NOTARY_TEAM_ID" \
|
||
--password "$NOTARY_PASSWORD" \
|
||
--wait
|
||
}
|
||
|
||
notarize_app_bundle() {
|
||
local bundle="$1"
|
||
local label="$2"
|
||
if [[ -z "${NOTARY_APPLE_ID:-}" || -z "${NOTARY_TEAM_ID:-}" || -z "${NOTARY_PASSWORD:-}" ]]; then
|
||
echo "ℹ️ Skipping notarization for ${label} (NOTARY_* variables not set)."
|
||
return 1
|
||
fi
|
||
|
||
local tmp_dir
|
||
tmp_dir="$(mktemp -d)"
|
||
local archive="$tmp_dir/$(basename "$bundle").zip"
|
||
ditto -c -k --keepParent "$bundle" "$archive"
|
||
|
||
submit_for_notarization "$archive" "$label"
|
||
xcrun stapler staple "$bundle"
|
||
rm -rf "$tmp_dir"
|
||
}
|
||
|
||
notarize_artifact() {
|
||
local artifact="$1"
|
||
local label="$2"
|
||
if [[ -z "${NOTARY_APPLE_ID:-}" || -z "${NOTARY_TEAM_ID:-}" || -z "${NOTARY_PASSWORD:-}" ]]; then
|
||
echo "ℹ️ Skipping notarization for ${label} (NOTARY_* variables not set)."
|
||
return 1
|
||
fi
|
||
submit_for_notarization "$artifact" "$label"
|
||
xcrun stapler staple "$artifact"
|
||
}
|
||
|
||
if [[ -f "$CREDENTIALS_FILE" ]]; then
|
||
set -a
|
||
# shellcheck disable=SC1090
|
||
source "$CREDENTIALS_FILE"
|
||
set +a
|
||
fi
|
||
|
||
: "${SPARKLE_APPCAST_OUTPUT:=$ROOT_DIR/Sparkle/appcast.xml}"
|
||
export SPARKLE_APPCAST_OUTPUT
|
||
|
||
"$ROOT_DIR/scripts/sync_version.sh"
|
||
|
||
rm -rf "$BUILD_DIR" "$ARTIFACTS_DIR"
|
||
mkdir -p "$ARTIFACTS_DIR"
|
||
|
||
xcodebuild \
|
||
-project "$ROOT_DIR/$PROJECT" \
|
||
-scheme "$SCHEME" \
|
||
-configuration Release \
|
||
-derivedDataPath "$BUILD_DIR" \
|
||
CODE_SIGNING_ALLOWED=NO \
|
||
clean build
|
||
|
||
APP_PATH="$BUILD_DIR/Build/Products/Release/iKeyMon.app"
|
||
if [[ ! -d "$APP_PATH" ]]; then
|
||
echo "❌ Failed to find built app at $APP_PATH"
|
||
exit 1
|
||
fi
|
||
|
||
if [[ -n "${CODESIGN_IDENTITY:-}" ]]; then
|
||
echo "🔏 Codesigning Sparkle framework..."
|
||
codesign \
|
||
--force \
|
||
--options runtime \
|
||
--timestamp \
|
||
--sign "$CODESIGN_IDENTITY" \
|
||
"$APP_PATH/Contents/Frameworks/Sparkle.framework"
|
||
|
||
echo "🔏 Codesigning app with identity: $CODESIGN_IDENTITY"
|
||
codesign \
|
||
--deep \
|
||
--force \
|
||
--options runtime \
|
||
--timestamp \
|
||
--entitlements "$ROOT_DIR/iKeyMon.entitlements" \
|
||
--sign "$CODESIGN_IDENTITY" \
|
||
"$APP_PATH"
|
||
else
|
||
echo "⚠️ Skipping codesign (CODESIGN_IDENTITY not set)."
|
||
fi
|
||
|
||
notarize_app_bundle "$APP_PATH" "iKeyMon.app"
|
||
|
||
STAGING_DIR=$(mktemp -d)
|
||
mkdir -p "$STAGING_DIR"
|
||
cp -R "$APP_PATH" "$STAGING_DIR/"
|
||
ln -s /Applications "$STAGING_DIR/Applications"
|
||
mkdir -p "$STAGING_DIR/.background"
|
||
cp "$ROOT_DIR/Assets/dmg_background.png" "$STAGING_DIR/.background/background.png"
|
||
|
||
VERSION="$(python3 - <<'PY' "$VERSION_FILE"
|
||
import json, sys
|
||
with open(sys.argv[1], "r", encoding="utf-8") as handle:
|
||
data = json.load(handle)
|
||
print(data.get("marketing_version", "dev"))
|
||
PY
|
||
)"
|
||
ZIP_NAME="iKeyMon-${VERSION}.zip"
|
||
ditto -c -k --keepParent "$APP_PATH" "$ARTIFACTS_DIR/$ZIP_NAME"
|
||
|
||
DMG_NAME="iKeyMon-${VERSION}.dmg"
|
||
hdiutil create -volname "iKeyMon" -srcfolder "$STAGING_DIR" -ov -format UDZO "$ARTIFACTS_DIR/$DMG_NAME"
|
||
|
||
sign_update_artifacts
|
||
|
||
if [[ -n "${NOTARY_APPLE_ID:-}" && -n "${NOTARY_TEAM_ID:-}" && -n "${NOTARY_PASSWORD:-}" ]]; then
|
||
notarize_artifact "$ARTIFACTS_DIR/$DMG_NAME" "$DMG_NAME"
|
||
else
|
||
echo "⚠️ Skipping DMG notarization (NOTARY_* variables not set)."
|
||
fi
|
||
rm -rf "$STAGING_DIR"
|
||
|
||
generate_appcast
|
||
|
||
if [[ -n "${GITEA_TOKEN:-}" && -n "${GITEA_OWNER:-}" && -n "${GITEA_REPO:-}" ]]; then
|
||
"$ROOT_DIR/scripts/publish_release.sh" "$VERSION" "$ARTIFACTS_DIR/$ZIP_NAME" "$ARTIFACTS_DIR/$DMG_NAME"
|
||
else
|
||
echo "ℹ️ Skipping Gitea release publishing (GITEA_* variables not fully set)."
|
||
fi
|
||
|
||
echo "✅ Build complete. Artifacts:"
|
||
echo " - $ARTIFACTS_DIR/$ZIP_NAME"
|
||
echo " - $ARTIFACTS_DIR/$DMG_NAME"
|