#!/usr/bin/env bash # ============================================================================= # NetDesk by Akronlogic -- iOS build (Flutter + Rust core). # # Runs on macOS ONLY (needs Xcode + the iOS SDKs). It: # 1. adds the Rust iOS targets (aarch64-apple-ios + aarch64-apple-ios-sim) # 2. builds the Rust core static lib for device AND simulator # 3. assembles NetDeskCore.xcframework (xcodebuild -create-xcframework) # 4. ensures ui/ios/ exists (flutter create --platforms=ios .) # 5. wires the xcframework into the Flutter iOS project (CocoaPods podspec) # 6. flutter build ipa (signed with your Apple team) # # The Rust FFI static lib comes from the workspace `core` crate # (package netdesk-core, lib netdesk_core -> libnetdesk_core.a). When the # Phase-3 flutter_rust_bridge wrapper crate lands under a different name # (e.g. "netdesk-frb"), point the script at it with: # CORE_CRATE=netdesk-frb LIB_NAME=netdesk_frb ./build-ios.sh ... # # See docs/build-ios.md for prerequisites (Apple Developer Program, signing # cert + provisioning profile) and the full walkthrough. # # Usage: # packaging/build-ios.sh --team ABCDE12345 # packaging/build-ios.sh --team ABCDE12345 --method ad-hoc --org com.akronlogic # packaging/build-ios.sh --skip-ipa # just build the xcframework # packaging/build-ios.sh --help # # Env overrides: CORE_CRATE LIB_NAME DEVICE_TARGET SIM_TARGET IOS_MIN # FLUTTER TEAM_ID EXPORT_METHOD FLUTTER_ORG # ============================================================================= set -euo pipefail # ---- configuration (env-overridable) --------------------------------------- CORE_CRATE="${CORE_CRATE:-netdesk-core}" # cargo package to build LIB_NAME="${LIB_NAME:-netdesk_core}" # -> lib.a DEVICE_TARGET="${DEVICE_TARGET:-aarch64-apple-ios}" SIM_TARGET="${SIM_TARGET:-aarch64-apple-ios-sim}" # Intel Macs: x86_64-apple-ios IOS_MIN="${IOS_MIN:-13.0}" FLUTTER="${FLUTTER:-flutter}" FLUTTER_ORG="${FLUTTER_ORG:-space.netdesk}" # -> bundle id .netdesk EXPORT_METHOD="${EXPORT_METHOD:-app-store}" # app-store | ad-hoc | development | enterprise TEAM_ID="${TEAM_ID:-${DEVELOPMENT_TEAM:-}}" # 10-char Apple Team ID XCFRAMEWORK="NetDeskCore.xcframework" SKIP_IPA=0 # ---- args ------------------------------------------------------------------ usage() { sed -n '3,29p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-0}"; } while [ $# -gt 0 ]; do case "$1" in --team) TEAM_ID="${2:?--team needs an Apple Team ID}"; shift 2 ;; --method) EXPORT_METHOD="${2:?--method needs a value}"; shift 2 ;; --org) FLUTTER_ORG="${2:?--org needs a value}"; shift 2 ;; --skip-ipa|--xcframework-only) SKIP_IPA=1; shift ;; -h|--help) usage 0 ;; *) echo "unknown argument: $1" >&2; usage 1 ;; esac done # ---- paths ----------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" UI="$ROOT/ui" IOSDIR="$UI/ios" TEMPLATES="$SCRIPT_DIR/ios" DIST="$SCRIPT_DIR/dist/ios" DEV_LIB="$ROOT/target/$DEVICE_TARGET/release/lib$LIB_NAME.a" SIM_LIB="$ROOT/target/$SIM_TARGET/release/lib$LIB_NAME.a" say() { printf '\033[36m==> %s\033[0m\n' "$*"; } ok() { printf '\033[32m %s\033[0m\n' "$*"; } die() { printf '\033[31mERROR: %s\033[0m\n' "$*" >&2; exit 1; } cat <}") BANNER # ---- 0. preflight ---------------------------------------------------------- say "0/6 preflight" [ "$(uname -s)" = "Darwin" ] || die "iOS builds require macOS. Run this on the Mac." command -v rustup >/dev/null || die "rustup not found. Install Rust: https://rustup.rs" command -v cargo >/dev/null || die "cargo not found on PATH (try: source \$HOME/.cargo/env)" command -v xcrun >/dev/null || die "xcrun not found. Install Xcode + command line tools (xcode-select --install)." xcode-select -p >/dev/null 2>&1 || die "no active Xcode. Run: sudo xcode-select -s /Applications/Xcode.app" command -v "$FLUTTER" >/dev/null || die "flutter not found. Install the Flutter SDK and add it to PATH (or set FLUTTER=/path/to/flutter)." if [ "$SKIP_IPA" = 0 ] && [ -z "$TEAM_ID" ]; then die "no Apple Team ID. Pass --team ABCDE12345 (or TEAM_ID=...), or use --skip-ipa to only build the xcframework." fi ok "macOS $(sw_vers -productVersion 2>/dev/null || true), Xcode $(xcodebuild -version 2>/dev/null | head -1 || true)" # ---- 1. rust ios targets --------------------------------------------------- say "1/6 rustup targets" rustup target add "$DEVICE_TARGET" "$SIM_TARGET" ok "$DEVICE_TARGET, $SIM_TARGET installed" # ---- 2. build the rust core static lib for both targets -------------------- say "2/6 cargo build (release, staticlib) for device + simulator" # Match Flutter's deployment target so the slices are link-compatible. export IPHONEOS_DEPLOYMENT_TARGET="$IOS_MIN" ( cd "$ROOT" && cargo build --release -p "$CORE_CRATE" --target "$DEVICE_TARGET" ) ( cd "$ROOT" && cargo build --release -p "$CORE_CRATE" --target "$SIM_TARGET" ) [ -f "$DEV_LIB" ] || die "expected device lib not found: $DEV_LIB" [ -f "$SIM_LIB" ] || die "expected simulator lib not found: $SIM_LIB" ok "device: $DEV_LIB" ok "sim : $SIM_LIB" # ---- 3. assemble the xcframework ------------------------------------------- say "3/6 assemble $XCFRAMEWORK" mkdir -p "$DIST" rm -rf "$DIST/$XCFRAMEWORK" # xcodebuild refuses to overwrite # No -headers yet: the C ABI header (cbindgen over core/src/ffi.rs) is Phase 3. # When it exists, add: -headers "$ROOT/target/include" to each -library pair. xcodebuild -create-xcframework \ -library "$DEV_LIB" \ -library "$SIM_LIB" \ -output "$DIST/$XCFRAMEWORK" ok "built $DIST/$XCFRAMEWORK" if [ "$SKIP_IPA" = 1 ]; then # Still stage it into ui/ios if that project already exists, so a later # `flutter build ipa` picks up the fresh binary. if [ -d "$IOSDIR" ]; then cp -R "$DIST/$XCFRAMEWORK" "$IOSDIR/" ok "staged into $IOSDIR/$XCFRAMEWORK" fi say "done (--skip-ipa). xcframework at: $DIST/$XCFRAMEWORK" exit 0 fi # ---- 4. ensure the Flutter iOS project exists ------------------------------ say "4/6 ensure ui/ios Flutter project" if [ ! -d "$IOSDIR" ]; then ok "ui/ios missing — generating with flutter create" ( cd "$UI" && "$FLUTTER" create --org "$FLUTTER_ORG" --project-name netdesk --platforms=ios . ) else ok "ui/ios already present — leaving it in place" fi ( cd "$UI" && "$FLUTTER" pub get ) # ---- 5. wire the xcframework into ui/ios (CocoaPods) ----------------------- say "5/6 wire xcframework into the iOS project" cp -R "$DIST/$XCFRAMEWORK" "$IOSDIR/" ok "copied $XCFRAMEWORK into ui/ios" cp "$TEMPLATES/netdesk_core.podspec" "$IOSDIR/netdesk_core.podspec" ok "installed ui/ios/netdesk_core.podspec" # Podfile: install the template if there is none, otherwise inject the pod line. if [ ! -f "$IOSDIR/Podfile" ]; then cp "$TEMPLATES/Podfile" "$IOSDIR/Podfile" ok "installed ui/ios/Podfile (from template)" else # Uncomment any commented `platform :ios` line so it matches the xcframework. if grep -qE '^\s*#\s*platform :ios' "$IOSDIR/Podfile"; then sed -i '' -E "s/^\s*#\s*platform :ios.*/platform :ios, '$IOS_MIN'/" "$IOSDIR/Podfile" fi # Inject the netdesk_core pod into the Runner target if it is not there yet. if ! grep -q "pod 'netdesk_core'" "$IOSDIR/Podfile"; then awk ' /target '\''Runner'\'' do/ && !done { print print " # NetDesk Rust core (added by packaging/build-ios.sh)." print " pod '\''netdesk_core'\'', :path => '\''.'\''" done = 1 next } { print } ' "$IOSDIR/Podfile" > "$IOSDIR/Podfile.tmp" && mv "$IOSDIR/Podfile.tmp" "$IOSDIR/Podfile" ok "injected pod 'netdesk_core' into existing Podfile" else ok "Podfile already references netdesk_core" fi fi # Signing for the archive step: set the team + automatic signing via the # Flutter xcconfig layer (applies because a fresh Runner target has no explicit # DEVELOPMENT_TEAM in project.pbxproj). Idempotent. for cfg in Release Debug; do XC="$IOSDIR/Flutter/$cfg.xcconfig" [ -f "$XC" ] || continue if ! grep -q '^DEVELOPMENT_TEAM' "$XC"; then { echo "" echo "// NetDesk signing (added by packaging/build-ios.sh)" echo "DEVELOPMENT_TEAM = $TEAM_ID" echo "CODE_SIGN_STYLE = Automatic" } >> "$XC" ok "set DEVELOPMENT_TEAM in Flutter/$cfg.xcconfig" else sed -i '' -E "s/^DEVELOPMENT_TEAM.*/DEVELOPMENT_TEAM = $TEAM_ID/" "$XC" ok "updated DEVELOPMENT_TEAM in Flutter/$cfg.xcconfig" fi done # ExportOptions.plist consumed by `flutter build ipa` for the export phase. cat > "$IOSDIR/ExportOptions.plist" < method$EXPORT_METHOD teamID$TEAM_ID signingStyleautomatic uploadSymbols stripSwiftSymbols destinationexport PLIST ok "wrote ui/ios/ExportOptions.plist (method=$EXPORT_METHOD)" # Resolve pods now so any CocoaPods error surfaces before the long archive. if command -v pod >/dev/null; then ( cd "$IOSDIR" && pod install ) ok "pod install complete" else ok "CocoaPods 'pod' not on PATH — flutter will run it during the build (sudo gem install cocoapods to install)" fi # ---- 6. flutter build ipa -------------------------------------------------- say "6/6 flutter build ipa" ( cd "$UI" && "$FLUTTER" build ipa --release --export-options-plist=ios/ExportOptions.plist ) IPA="$(ls -t "$UI/build/ios/ipa/"*.ipa 2>/dev/null | head -1 || true)" echo "" echo "=== DONE ===" echo "xcframework : $DIST/$XCFRAMEWORK (also staged in ui/ios/)" if [ -n "$IPA" ]; then echo "ipa : $IPA" echo "" echo "Upload to App Store Connect / TestFlight with:" echo " xcrun altool --upload-app -f \"$IPA\" -t ios --apiKey --apiIssuer " echo "or open ui/build/ios/archive/Runner.xcarchive in Xcode > Organizer > Distribute App." else echo "ipa : (not found under ui/build/ios/ipa — check the flutter output above)" fi