Files
voice-cat/scripts/dist-ios-adhoc.sh

286 lines
13 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
#
# dist-ios-adhoc.sh — build an ad-hoc IPA of the iOS client and the files needed to
# install it on registered devices from an HTTPS web page (itms-services OTA install).
#
# This is for handing the app to a handful of friends BEFORE TestFlight. Ad-hoc builds
# only run on devices whose UDID is registered in your Apple Developer account, so the
# script registers UDIDs (via the App Store Connect API) before it signs.
#
# Pipeline:
# 1. Register each --udid with App Store Connect (idempotent; skip with --skip-register).
# 2. Build VoiceCatCore.xcframework (iOS device slice).
# 3. xcodebuild archive (Release, generic/platform=iOS), letting Xcode auto-create the
# ad-hoc Distribution cert + profile via -allowProvisioningUpdates + the API key.
# 4. xcodebuild -exportArchive with method=release-testing (Xcode's modern name for
# "ad-hoc") -> VoiceCatiOS.ipa.
# 5. Generate manifest.plist (OTA install manifest) + index.html (install page).
# 6. Stage VoiceCatiOS.ipa + manifest.plist + index.html into dist/ios-adhoc/.
#
# You then upload those three files to your HTTPS host and open index.html on the iPhone.
#
# Prerequisites (one-time, not scriptable):
# - Paid Apple Developer Program membership (Team ID is already set in the project).
# - An App Store Connect API "Team Key" (.p8) with Admin or App Manager access:
# App Store Connect -> Users and Access -> Integrations -> App Store Connect API.
# Note its Key ID and Issuer ID. Keep the .p8 out of the repo.
# - Each friend's device UDID (read it in Finder with the iPhone connected to a Mac).
#
# Auth — supply via env vars (or the matching flags):
# ASC_KEY_ID App Store Connect API Key ID (--key-id)
# ASC_ISSUER_ID App Store Connect API Issuer ID (--issuer-id)
# ASC_KEY_PATH path to AuthKey_XXXXXXXXXX.p8 (--key)
#
# Usage:
# scripts/dist-ios-adhoc.sh --udid <UDID> --name "Friend iPhone" \
# --base-url https://example.com/voicecat
# scripts/dist-ios-adhoc.sh --udids-file friends.csv --base-url https://example.com/vc
# scripts/dist-ios-adhoc.sh --skip-register --base-url https://example.com/vc # rebuild only
# scripts/dist-ios-adhoc.sh --dist /out --no-configure
# scripts/dist-ios-adhoc.sh -h|--help
#
# Flags:
# --udid <UDID> device to register (repeatable). Pair with an optional --name.
# --name <label> label for the immediately-preceding --udid (default: the UDID).
# --udids-file <file> CSV of "udid,name" lines (one device per line; blank/# lines skipped).
# --base-url <url> public HTTPS folder you'll upload the output to. Used to fill
# manifest.plist + index.html. If omitted, a __BASE_URL__ placeholder
# is written and you must edit both files before they work.
# --skip-register don't touch App Store Connect; just rebuild/export/stage.
# --key-id / --issuer-id / --key override the ASC_* env vars.
# --dist <path> output root (default <repo>/dist). Files land in <dist>/ios-adhoc/.
# --no-configure skip the cmake configure step in the xcframework build.
# -h, --help this help.
#
# macOS only. Requires Xcode, VCPKG_ROOT (or a configured build/apple-dev cache), and the
# Python `cryptography` package (already present in a standard macOS Python 3).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VC_SCRIPT_NAME="dist-ios-adhoc"
source "$SCRIPT_DIR/common.sh"
# ── Project constants (see clients/apple/iOS/VoiceCatiOS.xcodeproj) ───────────────
TEAM_ID="FJV8L966W4"
APP_BUNDLE_ID="cat.voice.VoiceCatiOS"
SCHEME="VoiceCatiOS"
APP_TITLE="VoiceCat"
XCODEPROJ="$VC_REPO_ROOT/clients/apple/iOS/VoiceCatiOS.xcodeproj"
# ── Args ──────────────────────────────────────────────────────────────────────────
KEY_ID="${ASC_KEY_ID:-}"
ISSUER_ID="${ASC_ISSUER_ID:-}"
KEY_PATH="${ASC_KEY_PATH:-}"
BASE_URL=""
SKIP_REGISTER=false
DO_CONFIGURE=true
UDIDS=() # parallel arrays: UDIDS[i] / UDID_NAMES[i]
UDID_NAMES=()
while [[ $# -gt 0 ]]; do
case "$1" in
--udid) UDIDS+=("$2"); UDID_NAMES+=("$2"); shift 2 ;;
--name) [[ ${#UDID_NAMES[@]} -gt 0 ]] || vc_die "--name must follow a --udid"
UDID_NAMES[${#UDID_NAMES[@]}-1]="$2"; shift 2 ;;
--udids-file) [[ -f "$2" ]] || vc_die "udids file not found: $2"
while IFS=',' read -r u n _; do
u="${u// /}"
[[ -z "$u" || "$u" == \#* ]] && continue
UDIDS+=("$u"); UDID_NAMES+=("${n:-$u}")
done < "$2"
shift 2 ;;
--base-url) BASE_URL="${2%/}"; shift 2 ;;
--skip-register) SKIP_REGISTER=true; shift ;;
--key-id) KEY_ID="$2"; shift 2 ;;
--issuer-id) ISSUER_ID="$2"; shift 2 ;;
--key) KEY_PATH="$2"; shift 2 ;;
--dist) vc_set_dist "$2"; shift 2 ;;
--no-configure) DO_CONFIGURE=false; shift ;;
-h|--help) vc_print_help "$0"; exit 0 ;;
*) vc_die "unknown arg: $1 (try --help)" ;;
esac
done
vc_require_macos
# Auth is needed for device registration AND for -allowProvisioningUpdates (Xcode uses the
# same key to mint the ad-hoc Distribution cert/profile).
[[ -n "$KEY_ID" ]] || vc_die "missing API Key ID (set ASC_KEY_ID or pass --key-id)"
[[ -n "$ISSUER_ID" ]] || vc_die "missing Issuer ID (set ASC_ISSUER_ID or pass --issuer-id)"
[[ -n "$KEY_PATH" ]] || vc_die "missing .p8 path (set ASC_KEY_PATH or pass --key)"
[[ -f "$KEY_PATH" ]] || vc_die "API key file not found: $KEY_PATH"
python3 -c "import cryptography" 2>/dev/null \
|| vc_die "Python 'cryptography' package not available: pip3 install cryptography"
if [[ -z "$BASE_URL" ]]; then
BASE_URL="__BASE_URL__"
vc_log "no --base-url given: manifest.plist + index.html will use the __BASE_URL__"
vc_log "placeholder — edit both files to your real HTTPS folder before uploading."
fi
OUT="$(vc_dist_subdir ios-adhoc)"
ARCHIVE="$OUT/VoiceCatiOS.xcarchive"
EXPORT_DIR="$OUT/export"
DERIVED="$OUT/DerivedData"
IPA="$OUT/VoiceCatiOS.ipa"
# ── Step 1: register devices ───────────────────────────────────────────────────────
if $SKIP_REGISTER; then
vc_step "skipping device registration (--skip-register)"
elif [[ ${#UDIDS[@]} -eq 0 ]]; then
vc_step "no --udid/--udids-file given — skipping device registration"
vc_log "(devices must already be registered, or this IPA won't install for them)"
else
vc_step "register ${#UDIDS[@]} device(s) with App Store Connect"
for i in "${!UDIDS[@]}"; do
python3 "$SCRIPT_DIR/asc_api.py" register \
--key-id "$KEY_ID" --issuer-id "$ISSUER_ID" --key "$KEY_PATH" \
--udid "${UDIDS[$i]}" --name "${UDID_NAMES[$i]}"
done
vc_ok "device registration complete"
fi
# ── Step 2: xcframework (iOS device slice) ──────────────────────────────────────────
vc_step "build VoiceCatCore.xcframework (iOS device slice)"
vc_resolve_vcpkg_root "apple-dev"
XCFW_ARGS=( --preset apple-ios )
$DO_CONFIGURE || XCFW_ARGS+=( --no-configure )
"$VC_REPO_ROOT/clients/apple/scripts/build-xcframework.sh" "${XCFW_ARGS[@]}"
XCFW="$VC_REPO_ROOT/clients/apple/VoiceCatCore.xcframework"
[[ -d "$XCFW" ]] || vc_die "xcframework not found at $XCFW"
vc_ok "xcframework ready -> $XCFW"
# ── Step 3: archive ─────────────────────────────────────────────────────────────────
vc_step "xcodebuild archive ($SCHEME / Release / generic iOS)"
xcodebuild archive \
-project "$XCODEPROJ" \
-scheme "$SCHEME" \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$ARCHIVE" \
-derivedDataPath "$DERIVED" \
-allowProvisioningUpdates \
-authenticationKeyPath "$KEY_PATH" \
-authenticationKeyID "$KEY_ID" \
-authenticationKeyIssuerID "$ISSUER_ID"
[[ -d "$ARCHIVE" ]] || vc_die "archive not produced at $ARCHIVE"
vc_ok "archived -> $ARCHIVE"
# ── Step 4: export ad-hoc IPA ───────────────────────────────────────────────────────
# method "release-testing" is Xcode 15.3+/26's name for the old "ad-hoc" method.
# manageAppGroups lets automatic signing carry the group.cat.voice.VoiceCat capability
# (used by the ReplayKit broadcast extension) into the generated profiles.
EXPORT_PLIST="$OUT/ExportOptions.plist"
cat > "$EXPORT_PLIST" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key> <string>release-testing</string>
<key>teamID</key> <string>$TEAM_ID</string>
<key>signingStyle</key> <string>automatic</string>
<key>manageAppGroups</key> <true/>
<key>stripSwiftSymbols</key> <true/>
</dict>
</plist>
PLIST
vc_step "xcodebuild -exportArchive (method=release-testing / ad-hoc)"
xcodebuild -exportArchive \
-archivePath "$ARCHIVE" \
-exportPath "$EXPORT_DIR" \
-exportOptionsPlist "$EXPORT_PLIST" \
-allowProvisioningUpdates \
-authenticationKeyPath "$KEY_PATH" \
-authenticationKeyID "$KEY_ID" \
-authenticationKeyIssuerID "$ISSUER_ID"
EXPORTED_IPA="$(find "$EXPORT_DIR" -maxdepth 1 -name '*.ipa' | head -n1)"
[[ -n "$EXPORTED_IPA" ]] || vc_die "no .ipa found in $EXPORT_DIR"
mv "$EXPORTED_IPA" "$IPA"
vc_ok "exported -> $IPA ($(du -h "$IPA" | cut -f1))"
# ── Step 5: read version + generate manifest.plist & index.html ─────────────────────
APP_IN_ARCHIVE="$(find "$ARCHIVE/Products/Applications" -maxdepth 1 -name '*.app' | head -n1)"
SHORT_VER="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' \
"$APP_IN_ARCHIVE/Info.plist" 2>/dev/null || echo "1.0")"
MANIFEST="$OUT/manifest.plist"
cat > "$MANIFEST" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key> <string>software-package</string>
<key>url</key> <string>$BASE_URL/VoiceCatiOS.ipa</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>bundle-identifier</key> <string>$APP_BUNDLE_ID</string>
<key>bundle-version</key> <string>$SHORT_VER</string>
<key>kind</key> <string>software</string>
<key>title</key> <string>$APP_TITLE</string>
</dict>
</dict>
</array>
</dict>
</plist>
PLIST
INDEX="$OUT/index.html"
cat > "$INDEX" <<HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Install $APP_TITLE</title>
<style>
body { font: 17px -apple-system, system-ui, sans-serif; margin: 0; padding: 2.5rem 1.5rem;
color: #111; background: #f5f5f7; -webkit-text-size-adjust: 100%; }
main { max-width: 30rem; margin: 0 auto; }
h1 { font-size: 1.6rem; }
.btn { display: block; text-align: center; text-decoration: none; margin: 1.5rem 0;
padding: 0.9rem 1rem; border-radius: 0.8rem; background: #0a84ff; color: #fff;
font-weight: 600; }
.note { font-size: 0.9rem; color: #555; line-height: 1.5; }
code { background: #e5e5ea; padding: 0.1rem 0.3rem; border-radius: 0.3rem; }
</style>
</head>
<body>
<main>
<h1>$APP_TITLEtest build</h1>
<p>Tap below on your <strong>iPhone</strong> to install (version $SHORT_VER).</p>
<a class="btn" href="itms-services://?action=download-manifest&amp;url=$BASE_URL/manifest.plist">
Install $APP_TITLE
</a>
<p class="note">
Only works on devices whose UDID was registered for this build, on iOS 18 or later.
If nothing happens, make sure you opened this page in <strong>Safari</strong> and that
it is served over <strong>HTTPS</strong>. After installing, the app launches normally —
no "trust developer" step is needed.
</p>
</main>
</body>
</html>
HTML
# ── Done ────────────────────────────────────────────────────────────────────────────
vc_ok "staged install files in $OUT:"
printf ' %s\n' "VoiceCatiOS.ipa" "manifest.plist" "index.html"
echo
if [[ "$BASE_URL" == "__BASE_URL__" ]]; then
vc_log "NEXT: replace __BASE_URL__ in manifest.plist and index.html with your HTTPS"
vc_log " folder URL, then upload all three files there."
else
vc_log "NEXT: upload the three files above to $BASE_URL/"
vc_log " then open $BASE_URL/index.html in Safari on a registered iPhone."
fi