Files
iKeyMon/scripts/build_release.sh
2025-11-26 18:43:01 +01:00

247 lines
7.3 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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="${SPARKLE_DOWNLOAD_BASE_URL:-}"
local version_prefix="${SPARKLE_DOWNLOAD_VERSION_PREFIX:-}"
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 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
rewrite_appcast_urls "$output" "$version_prefix"
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() {
local appcast="$1"
local version_prefix="$2"
local marker="${SPARKLE_DOWNLOAD_VERSION_MARKER:-/releases/download/}"
if [[ -z "$version_prefix" || -z "$marker" ]]; then
return
fi
python3 - "$appcast" "$marker" "$version_prefix" <<'PY'
import sys
import xml.etree.ElementTree as ET
path, marker, prefix = sys.argv[1:]
tree = ET.parse(path)
root = tree.getroot()
ns = {'sparkle': 'http://www.andymatuschak.org/xml-namespaces/sparkle'}
changed = False
for item in root.findall('.//item'):
short = item.find('sparkle:shortVersionString', ns)
enclosure = item.find('enclosure')
if short is None or enclosure is None:
continue
version = (short.text or '').strip()
url = enclosure.get('url')
if not version or not url or marker not in url:
continue
desired = f"{marker}{prefix}{version}/"
if desired in url:
continue
base, rest = url.split(marker, 1)
if rest.startswith(f"{prefix}{version}/"):
continue
new_rest = f"{prefix}{version}/{rest.lstrip('/')}"
enclosure.set('url', f"{base}{marker}{new_rest}")
changed = True
if changed:
ET.indent(tree, space=" ", level=0)
tree.write(path, encoding='utf-8', xml_declaration=True)
PY
}
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 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"
pushd "$(dirname "$APP_PATH")" >/dev/null
zip -r "$ARTIFACTS_DIR/$ZIP_NAME" "$(basename "$APP_PATH")"
popd >/dev/null
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"