#!/usr/bin/env bash set -euo pipefail if [[ $# -lt 2 ]]; then echo "Usage: $0 [output-path]" >&2 exit 1 fi DMG_PATH="$1" TAG="$2" OUT_PATH="${3:-appcast.xml}" if [[ -z "${SPARKLE_PRIVATE_KEY:-}" ]]; then echo "SPARKLE_PRIVATE_KEY is required (exported from Sparkle generate_keys)." >&2 exit 1 fi SPARKLE_VERSION="${SPARKLE_VERSION:-2.8.1}" DOWNLOAD_URL_PREFIX="${DOWNLOAD_URL_PREFIX:-https://github.com/manaflow-ai/cmux/releases/download/$TAG/}" RELEASE_NOTES_URL="${RELEASE_NOTES_URL:-https://github.com/manaflow-ai/cmux/releases/tag/$TAG}" work_dir="$(mktemp -d)" cleanup() { rm -rf "$work_dir" } trap cleanup EXIT echo "Cloning Sparkle ${SPARKLE_VERSION}..." git clone --depth 1 --branch "$SPARKLE_VERSION" https://github.com/sparkle-project/Sparkle "$work_dir/Sparkle" echo "Building Sparkle generate_appcast tool..." xcodebuild \ -project "$work_dir/Sparkle/Sparkle.xcodeproj" \ -scheme generate_appcast \ -configuration Release \ -derivedDataPath "$work_dir/build" \ CODE_SIGNING_ALLOWED=NO \ build >/dev/null echo "Building Sparkle sign_update tool..." xcodebuild \ -project "$work_dir/Sparkle/Sparkle.xcodeproj" \ -scheme sign_update \ -configuration Release \ -derivedDataPath "$work_dir/build" \ CODE_SIGNING_ALLOWED=NO \ build >/dev/null generate_appcast="$work_dir/build/Build/Products/Release/generate_appcast" sign_update="$work_dir/build/Build/Products/Release/sign_update" if [[ ! -x "$generate_appcast" ]]; then echo "generate_appcast binary not found at $generate_appcast" >&2 exit 1 fi if [[ ! -x "$sign_update" ]]; then echo "sign_update binary not found at $sign_update" >&2 exit 1 fi archives_dir="$work_dir/archives" mkdir -p "$archives_dir" cp "$DMG_PATH" "$archives_dir/$(basename "$DMG_PATH")" key_file="$work_dir/sparkle_ed_key" # Ensure base64 padding (keys may be stored without trailing '=') padded_key="$SPARKLE_PRIVATE_KEY" while (( ${#padded_key} % 4 != 0 )); do padded_key="${padded_key}=" done printf "%s" "$padded_key" > "$key_file" generated_appcast_path="$archives_dir/$(basename "$OUT_PATH")" "$generate_appcast" \ --ed-key-file "$key_file" \ --download-url-prefix "$DOWNLOAD_URL_PREFIX" \ --full-release-notes-url "$RELEASE_NOTES_URL" \ "$archives_dir" if [[ ! -f "$generated_appcast_path" ]]; then fallback_generated_appcast="$(find "$archives_dir" -maxdepth 1 -name '*.xml' | head -n 1)" if [[ -n "$fallback_generated_appcast" ]]; then generated_appcast_path="$fallback_generated_appcast" fi fi if [[ ! -f "$generated_appcast_path" ]]; then echo "Expected appcast was not generated." >&2 exit 1 fi # Check if generate_appcast added the edSignature. If not, use sign_update # to sign the DMG and inject the signature. generate_appcast silently skips # signing when the public key derived from the private key doesn't match the # SUPublicEDKey in the app's Info.plist. if ! grep -q 'sparkle:edSignature' "$generated_appcast_path"; then echo "Warning: generate_appcast did not add edSignature. Using sign_update fallback..." SIGNATURE=$("$sign_update" -p --ed-key-file "$key_file" "$DMG_PATH") DMG_LENGTH=$(stat -f%z "$DMG_PATH") echo " EdDSA signature: ${SIGNATURE:0:20}..." echo " DMG length: $DMG_LENGTH" # Inject sparkle:edSignature and correct length into the enclosure element python3 -c " import sys xml = open('$generated_appcast_path').read() sig = '$SIGNATURE' length = '$DMG_LENGTH' # Add edSignature to enclosure xml = xml.replace( 'type=\"application/octet-stream\"', 'sparkle:edSignature=\"' + sig + '\" length=\"' + length + '\" type=\"application/octet-stream\"' ) open('$generated_appcast_path', 'w').write(xml) print(' Injected edSignature into appcast.xml') " fi cp "$generated_appcast_path" "$OUT_PATH" echo "Generated appcast at $OUT_PATH" # Verify the appcast has a signature if grep -q 'sparkle:edSignature' "$OUT_PATH"; then echo "Verified: appcast contains sparkle:edSignature" else echo "ERROR: appcast is missing sparkle:edSignature!" >&2 exit 1 fi