Add scripts/dist-ios-adhoc.sh and scripts/asc_api.py to build an ad-hoc signed IPA and the OTA web-install files (manifest.plist + index.html) for sharing the iOS client with registered devices before TestFlight. - asc_api.py: App Store Connect API helper (ES256 JWT via cryptography, urllib) to register device UDIDs and list registered devices. - dist-ios-adhoc.sh: registers UDIDs, builds the iOS device xcframework slice, archives + exports with method=release-testing using -allowProvisioningUpdates, and stages the install files into dist/ios-adhoc/. - Document the workflow in clients/apple/README.md; ignore __pycache__.
286 lines
13 KiB
Bash
Executable File
286 lines
13 KiB
Bash
Executable File
#!/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_TITLE — test 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&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
|