diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 84fc50d..3eb2480 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -64,3 +64,21 @@ jobs: name: MusicBridge-simulator path: build/MusicBridge-simulator.zip retention-days: 7 + - name: Build iPhone IPA for local signing + shell: bash + run: bash tools/package_ipa.sh + - name: Save iPhone IPA + uses: actions/upload-artifact@v4 + with: + name: MusicBridge-iPhone + path: build/MusicBridge-iPhone.ipa + if-no-files-found: error + retention-days: 7 + - name: Save device build log + if: always() + uses: actions/upload-artifact@v4 + with: + name: ios-device-build-log + path: build/device-build.log + if-no-files-found: ignore + retention-days: 7 diff --git a/tools/package_ipa.sh b/tools/package_ipa.sh new file mode 100644 index 0000000..2721c8a --- /dev/null +++ b/tools/package_ipa.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -euo pipefail +cd "$(dirname "$0")/.." +mkdir -p build +xcodebuild archive \ + -project MusicBridge.xcodeproj \ + -scheme MusicBridge \ + -configuration Release \ + -destination 'generic/platform=iOS' \ + -derivedDataPath build/DeviceDerivedData \ + -archivePath build/MusicBridge-iPhone.xcarchive \ + ARCHS=arm64 \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGNING_REQUIRED=NO \ + | tee build/device-build.log + +# A device archive packaged for re-signing by Sideloadly on the user's PC. +# This is not an App Store export and contains no Apple credentials or provisioning profile. +package_dir=$(mktemp -d "${TMPDIR:-/tmp}/musicbridge-ipa.XXXXXX") +trap 'rm -rf -- "$package_dir"' EXIT +mkdir -p "$package_dir/Payload" +ditto build/MusicBridge-iPhone.xcarchive/Products/Applications/MusicBridge.app "$package_dir/Payload/MusicBridge.app" +ditto -c -k --keepParent "$package_dir/Payload" build/MusicBridge-iPhone.ipa +python3 tools/verify_ipa.py build/MusicBridge-iPhone.ipa diff --git a/tools/verify_ipa.py b/tools/verify_ipa.py new file mode 100644 index 0000000..5cd902a --- /dev/null +++ b/tools/verify_ipa.py @@ -0,0 +1,33 @@ +"""Verify a thin ARM64 iPhone IPA, independently of the macOS build environment.""" +import pathlib +import plistlib +import struct +import sys +import zipfile + +path = pathlib.Path(sys.argv[1]) +with zipfile.ZipFile(path) as archive: + assert archive.testzip() is None, "Corrupt ZIP member" + prefix = "Payload/MusicBridge.app/" + info = plistlib.loads(archive.read(prefix + "Info.plist")) + assert info["CFBundleSupportedPlatforms"] == ["iPhoneOS"], "Not an iPhone device build" + assert info["CFBundleIdentifier"] == "ru.yukinoki.musicbridge", "Unexpected bundle ID" + assert info["MinimumOSVersion"] == "17.0", "Unexpected minimum iOS version" + executable = info["CFBundleExecutable"] + assert executable == "MusicBridge", "Unexpected executable" + binary = archive.read(prefix + executable) + magic, cpu = struct.unpack_from("= 8 and offset + size <= len(binary), "Invalid Mach-O load command" + if command == 0x32: + platforms.append(struct.unpack_from("