updates
This commit is contained in:
+19
-1
@@ -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
@@ -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"
|
||||
|
||||
Executable
+6
@@ -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
|
||||
Executable
+73
@@ -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
@@ -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"
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user