This commit is contained in:
Mark Gregory
2026-11-04 15:16:11 +00:00
parent 110840cd38
commit 5bebd30041
13 changed files with 315 additions and 28 deletions
+19 -1
View File
@@ -20,10 +20,28 @@ Provisioning updates are disabled by default. If you explicitly want Xcode to co
Outputs:
- Builds: `build/Debug/` or `build/Release/`, including `build.log` and DerivedData.
- Builds: `build/Debug/` or `build/Release/`, including `build.log` and `Tarnax.app`.
- Releases: a unique folder under `build/releases/`, containing `Tarnax.xcarchive`, `ExportOptions.plist`, logs, and `export/Tarnax.ipa` with a SHA-256 checksum.
- Override roots with `BUILD_OUTPUT_DIR` or `RELEASE_OUTPUT_DIR`.
Release export fails on any archive/export/signature/profile verification error. No unsigned ZIP is passed off as an installable IPA. Profiles include device identifiers: share only the intended IPA with testers and keep the remaining release diagnostics private.
The current contract harness exercises mocked transport and model behavior. It does not sign in to a live server or exercise the physical-device UI. This project has no XCTest target; the script compiles and runs the existing executable contract harness.
## Install on your iPhone
Connect and unlock your iPhone, trust this Mac and enable Developer Mode when required. Pair the device in Xcode's Devices and Simulators first.
```sh
./scripts/deploy.sh --list
./scripts/deploy.sh # Build signed Release, install and launch
./scripts/deploy.sh --device "iPhone name" # Select when multiple phones are paired
./scripts/deploy.sh --ipa /path/Tarnax.ipa # Install an already exported IPA
./scripts/deploy.sh --app /path/Tarnax.app --no-launch
```
The script selects only physical, paired iPhones; it never installs on every device. It checks the signature, app identifier, profile expiry and that the chosen phone is provisioned before installing. `TARNAX_DEVICE` can supply a default device identifier. It does not automatically modify Apple account provisioning. A locked or unreachable phone causes a clear device-tool failure; unlock/reconnect and rerun. Updating Tarnax keeps existing app data; launch replaces an already-running Tarnax process.
## Temporary files
All script-created temporary files and compiler intermediates use `~/Developer/Scratch/Tarnax/`, including DerivedData, test binaries, caches, IPA verification and deployment extraction. `TMPDIR` is also set there for child tools. Override with `TARNAX_SCRATCH_ROOT` if needed. You can delete this directory when no build, test or deploy script is running. Final apps, archives, IPAs and logs remain in the project `build/` directory. Apple services may maintain their own system caches outside this location.
+6 -2
View File
@@ -1,6 +1,7 @@
#!/bin/bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$ROOT/scripts/common.sh"
CONFIGURATION=Debug
SIGNED=NO
while (($#)); do
@@ -14,8 +15,11 @@ while (($#)); do
done
OUTPUT="${BUILD_OUTPUT_DIR:-$ROOT/build/$CONFIGURATION}"
mkdir -p "$OUTPUT"
DERIVED_DATA="$(mktemp -d "$TARNAX_SCRATCH_ROOT/build-$CONFIGURATION.XXXXXX")"
xcodebuild -project "$ROOT/Tarnax.xcodeproj" -scheme Tarnax \
-configuration "$CONFIGURATION" -destination 'generic/platform=iOS' \
-derivedDataPath "$OUTPUT/DerivedData" CODE_SIGNING_ALLOWED="$SIGNED" \
-derivedDataPath "$DERIVED_DATA" CODE_SIGNING_ALLOWED="$SIGNED" \
build 2>&1 | tee "$OUTPUT/build.log"
echo "App: $OUTPUT/DerivedData/Build/Products/$CONFIGURATION-iphoneos/Tarnax.app"
ditto "$DERIVED_DATA/Build/Products/$CONFIGURATION-iphoneos/Tarnax.app" "$OUTPUT/Tarnax.app"
echo "App: $OUTPUT/Tarnax.app"
echo "Temporary build files: $DERIVED_DATA"
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
# Shared scratch location; final products remain in the project build directory.
TARNAX_SCRATCH_ROOT="${TARNAX_SCRATCH_ROOT:-$HOME/Developer/Scratch/Tarnax}"
mkdir -p "$TARNAX_SCRATCH_ROOT/tmp"
export TMPDIR="$TARNAX_SCRATCH_ROOT/tmp/"
export TARNAX_SCRATCH_ROOT
+73
View File
@@ -0,0 +1,73 @@
#!/bin/bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$ROOT/scripts/common.sh"
DEVICE="${TARNAX_DEVICE:-}"
IPA=''
APP=''
LAUNCH=YES
LIST=NO
while (($#)); do
case "$1" in
--device|--ipa|--app)
[[ $# -ge 2 ]] || { echo "$1 needs a value" >&2; exit 2; }
case "$1" in --device) DEVICE="$2" ;; --ipa) IPA="$2" ;; --app) APP="$2" ;; esac
shift ;;
--no-launch) LAUNCH=NO ;;
--list) LIST=YES ;;
-h|--help)
echo 'Usage: scripts/deploy.sh [--device ID_OR_NAME] [--ipa FILE | --app BUNDLE] [--no-launch]'
echo ' scripts/deploy.sh --list'
echo 'Default: build signed Release, install on the single paired physical iPhone and launch.'
exit 0 ;;
*) echo "Unknown option: $1" >&2; exit 2 ;;
esac
shift
done
[[ -z "$IPA" || -z "$APP" ]] || { echo 'Choose --ipa or --app, not both.' >&2; exit 2; }
if [[ "$LIST" == YES ]]; then exec xcrun devicectl list devices; fi
SCRATCH="$(mktemp -d "$TARNAX_SCRATCH_ROOT/deploy.XXXXXX")"
trap 'rm -rf "$SCRATCH"' EXIT
xcrun devicectl list devices --json-output "$SCRATCH/devices.json" >/dev/null
SELECTION="$(python3 - "$SCRATCH/devices.json" "$DEVICE" <<'PY'
import json,sys
with open(sys.argv[1]) as f:devices=json.load(f)['result']['devices']
requested=sys.argv[2]
candidates=[]
for d in devices:
p=d.get('properties',{})
h=p.get('hardware',d.get('hardwareProperties',{}))
c=p.get('connection',d.get('connectionProperties',{}))
state=p.get('state',d.get('deviceProperties',{}))
if h.get('reality')!='physical' or h.get('deviceType')!='iPhone':continue
if c.get('pairingState')!='paired':continue
identifiers=[d.get('identifier'),h.get('udid'),state.get('name')]
if requested and requested not in identifiers:continue
candidates.append((d['identifier'],h['udid']))
if len(candidates)!=1:
sys.exit('Select one paired physical iPhone with --device. Run scripts/deploy.sh --list; connect, unlock and trust your iPhone in Xcode if needed.')
print('\t'.join(candidates[0]))
PY
)"
IFS=$'\t' read -r DEVICE_ID DEVICE_UDID <<< "$SELECTION"
if [[ -n "$IPA" ]]; then
[[ -f "$IPA" ]] || { echo "IPA not found: $IPA" >&2; exit 1; }
unzip -q "$IPA" -d "$SCRATCH/unpacked"
APP="$SCRATCH/unpacked/Payload/Tarnax.app"
elif [[ -z "$APP" ]]; then
BUILD_OUTPUT_DIR="$ROOT/build/Deploy" bash "$ROOT/scripts/build.sh" --release --signed
APP="$ROOT/build/Deploy/Tarnax.app"
fi
[[ -d "$APP" ]] || { echo "App bundle not found: $APP" >&2; exit 1; }
codesign --verify --deep --strict "$APP"
security cms -D -i "$APP/embedded.mobileprovision" > "$SCRATCH/profile.plist"
python3 "$ROOT/scripts/verify_profile.py" "$APP/Info.plist" "$SCRATCH/profile.plist" --device "$DEVICE_UDID"
xcrun devicectl device install app --device "$DEVICE_ID" "$APP" --timeout 120
echo 'Tarnax installed successfully.'
if [[ "$LAUNCH" == YES ]]; then
if ! xcrun devicectl device process launch --device "$DEVICE_ID" --terminate-existing net.absdev.Tarnax --timeout 60; then
echo 'Installation succeeded, but launch failed. Unlock your iPhone and open Tarnax, or rerun with --no-launch.' >&2
exit 1
fi
fi
+8 -16
View File
@@ -1,6 +1,7 @@
#!/bin/bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$ROOT/scripts/common.sh"
METHOD=release-testing
PROVISIONING=()
while (($#)); do
@@ -22,6 +23,7 @@ TEAM="${DEVELOPMENT_TEAM:-39R6U3437D}"
BASE="${RELEASE_OUTPUT_DIR:-$ROOT/build/releases}"
mkdir -p "$BASE"
OUTPUT="$(mktemp -d "$BASE/$(date +%Y%m%d-%H%M%S)-$METHOD.XXXXXX")"
DERIVED_DATA="$(mktemp -d "$TARNAX_SCRATCH_ROOT/release-derived.XXXXXX")"
ARCHIVE="$OUTPUT/Tarnax.xcarchive"
EXPORT="$OUTPUT/export"
echo "Release output: $OUTPUT"
@@ -34,7 +36,7 @@ with open(sys.argv[1], 'wb') as f:
PY
xcodebuild -project "$ROOT/Tarnax.xcodeproj" -scheme Tarnax \
-configuration Release -destination 'generic/platform=iOS' \
-derivedDataPath "$OUTPUT/DerivedData" -archivePath "$ARCHIVE" \
-derivedDataPath "$DERIVED_DATA" -archivePath "$ARCHIVE" \
DEVELOPMENT_TEAM="$TEAM" CODE_SIGN_STYLE=Automatic \
${PROVISIONING[@]+"${PROVISIONING[@]}"} archive 2>&1 | tee "$OUTPUT/archive.log"
xcodebuild -exportArchive -archivePath "$ARCHIVE" -exportPath "$EXPORT" \
@@ -42,27 +44,17 @@ xcodebuild -exportArchive -archivePath "$ARCHIVE" -exportPath "$EXPORT" \
${PROVISIONING[@]+"${PROVISIONING[@]}"} 2>&1 | tee "$OUTPUT/export.log"
IPA="$EXPORT/Tarnax.ipa"
[[ -f "$IPA" ]] || { echo 'Export finished without Tarnax.ipa' >&2; exit 1; }
VERIFY="$(mktemp -d "$OUTPUT/verify.XXXXXX")"
VERIFY="$(mktemp -d "$TARNAX_SCRATCH_ROOT/verify.XXXXXX")"
trap 'rm -rf "$VERIFY"' EXIT
unzip -q "$IPA" -d "$VERIFY"
APP="$VERIFY/Payload/Tarnax.app"
codesign --verify --deep --strict "$APP"
[[ -f "$APP/embedded.mobileprovision" ]] || { echo 'No embedded provisioning profile' >&2; exit 1; }
security cms -D -i "$APP/embedded.mobileprovision" > "$VERIFY/profile.plist"
python3 - "$APP/Info.plist" "$VERIFY/profile.plist" "$METHOD" <<'PY'
import plistlib, sys, datetime
with open(sys.argv[1],'rb') as f: app=plistlib.load(f)
with open(sys.argv[2],'rb') as f: profile=plistlib.load(f)
assert app['CFBundleIdentifier']=='net.absdev.Tarnax', 'Unexpected app identifier'
assert profile.get('ProvisionedDevices'), 'Profile does not include registered devices'
assert profile['ExpirationDate']>datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), 'Profile expired'
assert profile['Entitlements']['application-identifier'].endswith('.'+app['CFBundleIdentifier']), 'Profile/app mismatch'
if sys.argv[3]=='release-testing':
assert not profile['Entitlements'].get('get-task-allow',False), 'Distribution IPA has debugging enabled'
print('Verified:', app['CFBundleDisplayName'], app['CFBundleShortVersionString'], 'build', app['CFBundleVersion'])
print('Registered devices:',len(profile['ProvisionedDevices']))
print('Profile expiry:',profile['ExpirationDate'].isoformat()+'Z')
PY
PROFILE_FLAGS=()
if [[ "$METHOD" == release-testing ]]; then PROFILE_FLAGS=(--distribution); fi
python3 "$ROOT/scripts/verify_profile.py" "$APP/Info.plist" "$VERIFY/profile.plist" ${PROFILE_FLAGS[@]+"${PROFILE_FLAGS[@]}"}
(cd "$EXPORT" && shasum -a 256 Tarnax.ipa > Tarnax.ipa.sha256)
echo "Verified IPA: $IPA"
echo "SHA-256: $EXPORT/Tarnax.ipa.sha256"
+2
View File
@@ -1,6 +1,7 @@
#!/bin/bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$ROOT/scripts/common.sh"
CONTRACTS_ONLY=NO
case "${1:-}" in
--contracts-only) CONTRACTS_ONLY=YES ;;
@@ -12,6 +13,7 @@ if (($# > 1)); then echo 'Too many arguments' >&2; exit 2; fi
cd "$ROOT"
plutil -lint Tarnax/Info.plist Tarnax.xcodeproj/project.pbxproj
for script in scripts/*.sh Tests/run.sh; do bash -n "$script"; done
PYTHONDONTWRITEBYTECODE=1 python3 Tests/ProfileTests.py
bash Tests/run.sh
if [[ "$CONTRACTS_ONLY" == NO ]]; then bash scripts/build.sh; fi
echo 'All requested checks passed.'
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Validate Tarnax's embedded profile, including Apple's wildcard App IDs."""
import argparse
import datetime
import fnmatch
import plistlib
import sys
def validate(app, profile, device=None, distribution=False):
bundle = app.get('CFBundleIdentifier')
if bundle != 'net.absdev.Tarnax':
raise ValueError('This script installs only net.absdev.Tarnax.')
entitlements = profile.get('Entitlements', {})
pattern = entitlements.get('application-identifier', '')
# The App ID prefix can differ from the Team ID on older Apple accounts.
prefixes = profile.get('ApplicationIdentifierPrefix', [])
if not pattern or not any(fnmatch.fnmatchcase(prefix + '.' + bundle, pattern) for prefix in prefixes):
raise ValueError('Profile does not match Tarnax.')
expiry = profile.get('ExpirationDate')
if expiry is None:
raise ValueError('Profile has no expiration date.')
if expiry.tzinfo is None:
expiry = expiry.replace(tzinfo=datetime.timezone.utc)
if expiry <= datetime.datetime.now(datetime.timezone.utc):
raise ValueError('The app provisioning profile has expired.')
devices = profile.get('ProvisionedDevices', [])
if not devices:
raise ValueError('Profile does not include registered devices.')
if device and device not in devices:
raise ValueError('This iPhone is not included in the app provisioning profile. Register it and export again.')
if distribution and entitlements.get('get-task-allow', False):
raise ValueError('Distribution IPA has debugging enabled.')
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('app_plist')
parser.add_argument('profile_plist')
parser.add_argument('--device')
parser.add_argument('--distribution', action='store_true')
args = parser.parse_args()
try:
with open(args.app_plist, 'rb') as f:
app = plistlib.load(f)
with open(args.profile_plist, 'rb') as f:
profile = plistlib.load(f)
validate(app, profile, args.device, args.distribution)
except (ValueError, OSError, plistlib.InvalidFileException) as error:
sys.exit(str(error))
print('Verified Tarnax', app['CFBundleShortVersionString'], 'build', app['CFBundleVersion'])
print('Registered devices:', len(profile['ProvisionedDevices']))
print('Profile expiry:', profile['ExpirationDate'].isoformat())
if __name__ == '__main__':
main()