#!/usr/bin/env bash set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" echo "==> [1/6] Building universal helper binaries (device_helper & airtraffic_host)..." make clean make all APP_NAME="AirCard" APP_DIR="build/${APP_NAME}.app" CONTENTS_DIR="${APP_DIR}/Contents" MACOS_DIR="${CONTENTS_DIR}/MacOS" RESOURCES_DIR="${CONTENTS_DIR}/Resources" BIN_DIR="${RESOURCES_DIR}/bin" LIB_DIR="${RESOURCES_DIR}/lib" echo "==> [2/6] Scaffolding ${APP_NAME}.app bundle structure..." rm -rf "$APP_DIR" mkdir -p "$MACOS_DIR" "$BIN_DIR" "$LIB_DIR" # Write Info.plist cat << 'EOF' > "${CONTENTS_DIR}/Info.plist" CFBundleDevelopmentRegion en CFBundleExecutable AirCard CFBundleIdentifier com.mak5er.aircard CFBundleInfoDictionaryVersion 6.0 CFBundleName AirCard CFBundleDisplayName AirCard CFBundleIconFile AppIcon CFBundlePackageType APPL CFBundleShortVersionString 1.2.6 CFBundleVersion 11 LSMinimumSystemVersion 14.0 NSHighResolutionCapable NSPrincipalClass NSApplication EOF echo "==> [3/6] Bundling universal tools & libraries..." # Copy App Icon if [ -f "dmg_assets/AppIcon.icns" ]; then cp "dmg_assets/AppIcon.icns" "${RESOURCES_DIR}/AppIcon.icns" fi # Copy universal device_helper and airtraffic_host. Device discovery and log # streaming both run through device_helper, which talks to MobileDevice.framework # directly, so the bundle needs no libimobiledevice tooling. cp build/device_helper "$BIN_DIR/" cp build/airtraffic_host "$BIN_DIR/" # Copy python backend scripts cp apply_card_skin.py "$RESOURCES_DIR/" cp aircard.py "$RESOURCES_DIR/" cp aircard_backend.py "$RESOURCES_DIR/" cp card_assets.py "$RESOURCES_DIR/" cp wallet_catalog.py "$RESOURCES_DIR/" # A bundle without these cannot talk to a device at all, so fail here instead # of shipping an app that reports "No iPhone found" for every user. for tool in device_helper airtraffic_host; do if [ ! -x "${BIN_DIR}/${tool}" ]; then echo "ERROR: ${BIN_DIR}/${tool} is missing from the bundle." >&2 exit 1 fi done echo "==> [4/6] Compiling universal Swift binary (arm64 + x86_64)..." if [ -z "${SWIFT_SDK:-}" ]; then SWIFT_SDK="$(xcrun --sdk macosx --show-sdk-path)" CLT_SWIFTUI_SDK="/Library/Developer/CommandLineTools/SDKs/MacOSX26.sdk" if [ "$(xcode-select -p)" = "/Library/Developer/CommandLineTools" ] && [ -d "$CLT_SWIFTUI_SDK" ]; then SWIFT_SDK="$CLT_SWIFTUI_SDK" fi fi swiftc -sdk "$SWIFT_SDK" -O -parse-as-library -target arm64-apple-macosx14.0 AirCardApp.swift Sources/WalletDiscovery.swift Sources/WalletDiagnosticsView.swift -o build/AirCard_arm64 swiftc -sdk "$SWIFT_SDK" -O -parse-as-library -target x86_64-apple-macosx14.0 AirCardApp.swift Sources/WalletDiscovery.swift Sources/WalletDiagnosticsView.swift -o build/AirCard_x86_64 lipo -create -output "${MACOS_DIR}/AirCard" build/AirCard_arm64 build/AirCard_x86_64 chmod +x "${MACOS_DIR}/AirCard" echo "==> [5/6] Setting permissions and signing ${APP_NAME}.app bundle..." chmod -R 755 "$APP_DIR" xattr -cr "$APP_DIR" 2>/dev/null || true # Ad-hoc by default so anyone can build this. Set CODESIGN_IDENTITY to a # Developer ID to ship a build that Gatekeeper will accept after notarising. CODESIGN_IDENTITY="${CODESIGN_IDENTITY:--}" if [ "$CODESIGN_IDENTITY" = "-" ]; then codesign --force --deep --sign - "$APP_DIR" else # Sign inside out. --deep only walks Frameworks and PlugIns, so the helpers # in Resources/bin keep whatever signature they arrived with, and the notary # service rejects the whole bundle over one ad-hoc binary inside it. # Hardened runtime is required before anything can be notarised. for tool in "${BIN_DIR}"/*; do [ -f "$tool" ] || continue codesign --force --options runtime --timestamp \ --sign "$CODESIGN_IDENTITY" "$tool" done codesign --force --options runtime --timestamp \ --sign "$CODESIGN_IDENTITY" "$APP_DIR" codesign --verify --strict --verbose=1 "$APP_DIR" # Catch an ad-hoc straggler here rather than in a notary rejection. Checking # the bundle alone is not enough: codesign reports the top-level signature # only, and the binaries this is guarding are nested inside Resources. for nested in "$APP_DIR/Contents/MacOS"/* "$BIN_DIR"/*; do [ -f "$nested" ] || continue if codesign -dv "$nested" 2>&1 | grep -q "adhoc"; then echo "ERROR: $nested is still ad-hoc signed" >&2 exit 1 fi done fi # The plist promise is only worth anything if the binaries agree with it. A # helper built without a minimum silently inherits the build machine's macOS. for binary in "${MACOS_DIR}/${APP_NAME}" "${BIN_DIR}"/*; do [ -f "$binary" ] || continue actual="$(otool -l "$binary" | awk '/minos/ {print $2; exit}')" if [ -n "$actual" ] && [ "$actual" != "14.0" ]; then echo "ERROR: $(basename "$binary") targets macOS $actual, but the app claims 14.0" >&2 exit 1 fi done echo "==> [6/6] Generating styled DMG (${APP_NAME}.dmg)..." DMG_STAGING="/tmp/aircard_dmg_staging" rm -rf "$DMG_STAGING" mkdir -p "$DMG_STAGING" cp -R "$APP_DIR" "$DMG_STAGING/" rm -f "build/${APP_NAME}.dmg" if command -v create-dmg >/dev/null 2>&1; then create-dmg \ --volname "AirCard" \ --background "dmg_assets/background_700.png" \ --window-pos 200 120 \ --window-size 700 460 \ --icon-size 110 \ --icon "AirCard.app" 175 220 \ --hide-extension "AirCard.app" \ --app-drop-link 525 220 \ --add-file "README.txt" "dmg_assets/README.txt" 350 360 \ --filesystem APFS \ --overwrite \ "build/${APP_NAME}.dmg" \ "$DMG_STAGING" else ln -s /Applications "$DMG_STAGING/Applications" hdiutil create -volname "AirCard" -srcfolder "$DMG_STAGING" -ov -format UDZO "build/${APP_NAME}.dmg" fi # Sign the disk image too, otherwise the signature stops at the app inside it. if [ "$CODESIGN_IDENTITY" != "-" ]; then codesign --force --sign "$CODESIGN_IDENTITY" --timestamp "build/${APP_NAME}.dmg" fi echo "============================================================" echo "🎉 SUCCESS: build/${APP_NAME}.dmg is ready!" if [ "$CODESIGN_IDENTITY" != "-" ]; then echo " Signed with: $CODESIGN_IDENTITY" echo " Notarise with: xcrun notarytool submit build/${APP_NAME}.dmg \\" echo " --keychain-profile --wait" echo " Then staple: xcrun stapler staple build/${APP_NAME}.dmg" fi echo "============================================================"