Files

242 lines
11 KiB
Bash

#!/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<LIB_NAME>.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 <org>.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
=== NetDesk iOS build (Akronlogic) ===
root : $ROOT
crate : $CORE_CRATE (lib$LIB_NAME.a)
targets: $DEVICE_TARGET + $SIM_TARGET
flutter: $FLUTTER org: $FLUTTER_ORG ios-min: $IOS_MIN
ipa : $([ "$SKIP_IPA" = 1 ] && echo "SKIPPED (--skip-ipa)" || echo "method=$EXPORT_METHOD team=${TEAM_ID:-<unset>}")
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" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key><string>$EXPORT_METHOD</string>
<key>teamID</key><string>$TEAM_ID</string>
<key>signingStyle</key><string>automatic</string>
<key>uploadSymbols</key><true/>
<key>stripSwiftSymbols</key><true/>
<key>destination</key><string>export</string>
</dict>
</plist>
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 <KEY_ID> --apiIssuer <ISSUER_ID>"
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