#!/usr/bin/env bash # based on scripts in /engine/src/flutter/testing/scenario_app # and xcode_backend.sh script for integration in xcode # Exit on error set -e debug_sim="" if [[ $(uname -m) == "arm64" ]]; then TARGET_POSTFIX='_arm64' CLANG_POSTFIX='_arm64' SIM_ARCH='arm64' else TARGET_POSTFIX='' CLANG_POSTFIX='_X86' SIM_ARCH='x86_64' fi ReadPubspecVersion() { local fallback_build_name="${FLUTTER_BUILD_NAME:-}" local fallback_build_number="${FLUTTER_BUILD_NUMBER:-}" local app_path="${FLUTTER_APPLICATION_PATH:-}" if [[ -z "$app_path" ]]; then if [[ -n "${PROJECT_DIR:-}" ]]; then app_path="$(cd "$(dirname "$PROJECT_DIR")" && pwd)" else app_path="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" fi fi local pubspec="$app_path/pubspec.yaml" local pub_version="" if [[ -f "$pubspec" ]]; then local line while IFS= read -r line; do case "$line" in version:*) pub_version="${line#version:}" pub_version="${pub_version%%#*}" pub_version="${pub_version#"${pub_version%%[![:space:]]*}"}" pub_version="${pub_version%"${pub_version##*[![:space:]]}"}" pub_version="${pub_version%\"}" pub_version="${pub_version#\"}" pub_version="${pub_version%\'}" pub_version="${pub_version#\'}" break ;; esac done < "$pubspec" fi if [[ -n "$pub_version" ]]; then FLUTTER_BUILD_NAME="${pub_version%+*}" if [[ "$pub_version" == *+* ]]; then FLUTTER_BUILD_NUMBER="${pub_version#*+}" else FLUTTER_BUILD_NUMBER="1" fi else FLUTTER_BUILD_NAME="${fallback_build_name:-1.0.0}" FLUTTER_BUILD_NUMBER="${fallback_build_number:-1}" fi export FLUTTER_BUILD_NAME export FLUTTER_BUILD_NUMBER } SetPlistString() { local plist="$1" local key="$2" local value="$3" local current="" if current="$(/usr/libexec/PlistBuddy -c "Print :$key" "$plist" 2>/dev/null)"; then if [[ "$current" != "$value" ]]; then /usr/libexec/PlistBuddy -c "Set :$key $value" "$plist" fi else /usr/libexec/PlistBuddy -c "Add :$key string $value" "$plist" fi } ValidatePlistVersion() { local plist="$1" local label="$2" local build_name="" local build_number="" build_name="$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$plist" 2>/dev/null || true)" build_number="$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$plist" 2>/dev/null || true)" if [[ "$build_name" != "$FLUTTER_BUILD_NAME" || "$build_number" != "$FLUTTER_BUILD_NUMBER" ]]; then echo " └─ERROR: $label Info.plist version is $build_name ($build_number), expected $FLUTTER_BUILD_NAME ($FLUTTER_BUILD_NUMBER)" return 1 fi } CopyAppFrameworkInfoPlist() { local plist="$1" local minimum_os_version="${2:-${TVOS_DEPLOYMENT_TARGET:-}}" if [[ -z "$minimum_os_version" ]]; then echo " └─ERROR: TVOS_DEPLOYMENT_TARGET is not set for App.framework Info.plist" return 1 fi cp "$PROJECT_DIR/scripts/Info.plist" "$plist" SetPlistString "$plist" MinimumOSVersion "$minimum_os_version" } ValidateMinimumOSVersion() { local plist="$1" local label="$2" local expected="$3" local current="" current="$(/usr/libexec/PlistBuddy -c "Print :MinimumOSVersion" "$plist" 2>/dev/null || true)" if [[ "$current" != "$expected" ]]; then echo " └─ERROR: $label MinimumOSVersion is $current, expected $expected" return 1 fi } VersionLessThan() { awk -v lhs="$1" -v rhs="$2" ' BEGIN { split(lhs, left, ".") split(rhs, right, ".") for (i = 1; i <= 3; i++) { l = left[i] == "" ? 0 : left[i] + 0 r = right[i] == "" ? 0 : right[i] + 0 if (l < r) exit 0 if (l > r) exit 1 } exit 1 } ' } FrameworkExecutable() { local framework="$1" local plist="$framework/Info.plist" local executable_name="" executable_name="$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$plist" 2>/dev/null || true)" if [[ -z "$executable_name" ]]; then executable_name="$(basename "$framework" .framework)" fi printf "%s/%s\n" "$framework" "$executable_name" } MachOMinimumOSVersion() { local executable="$1" otool -l "$executable" 2>/dev/null | awk ' $1 == "cmd" && ($2 == "LC_BUILD_VERSION" || $2 == "LC_VERSION_MIN_APPLETVOS" || $2 == "LC_VERSION_MIN_IPHONEOS") { in_version_command = 1 next } in_version_command && ($1 == "minos" || $1 == "version") { print $2 exit } $1 == "cmd" { in_version_command = 0 } ' } NormalizeFrameworkMinimumOSVersion() { local framework="$1" local app_minimum_os_version="$2" local executable="" local macho_minimum_os_version="" local plist_minimum_os_version="$app_minimum_os_version" executable="$(FrameworkExecutable "$framework")" if [[ ! -f "$executable" ]]; then echo " └─ERROR: framework executable missing: $executable" return 1 fi macho_minimum_os_version="$(MachOMinimumOSVersion "$executable")" if [[ -n "$macho_minimum_os_version" ]] && VersionLessThan "$plist_minimum_os_version" "$macho_minimum_os_version"; then plist_minimum_os_version="$macho_minimum_os_version" fi SetPlistString "$framework/Info.plist" MinimumOSVersion "$plist_minimum_os_version" ValidateMinimumOSVersion "$framework/Info.plist" "$(basename "$framework")" "$plist_minimum_os_version" } CodeSignFramework() { local framework="$1" local executable="" executable="$(FrameworkExecutable "$framework")" if [[ ! -f "$executable" ]]; then echo " └─ERROR: framework executable missing: $executable" return 1 fi if [[ "${PLATFORM_NAME:-}" != "appletvsimulator" && -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" && "${CODE_SIGNING_ALLOWED:-YES}" != "NO" ]]; then codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "$framework" fi } EmbedFlutterFrameworks() { if [[ "${TARGET_NAME:-}" != "Runner" ]]; then return 0 fi local minimum_os_version="${TVOS_DEPLOYMENT_TARGET:-}" local runner_plist="$TARGET_BUILD_DIR/$WRAPPER_NAME/Info.plist" if [[ -z "$minimum_os_version" && -f "$runner_plist" ]]; then minimum_os_version="$(/usr/libexec/PlistBuddy -c "Print :MinimumOSVersion" "$runner_plist" 2>/dev/null || true)" fi if [[ -z "$minimum_os_version" ]]; then echo " └─ERROR: could not resolve tvOS MinimumOSVersion for embedded Flutter frameworks" return 1 fi local app_frameworks_dir="$TARGET_BUILD_DIR/$WRAPPER_NAME/Frameworks" mkdir -p "$app_frameworks_dir" local framework="" for framework in App.framework Flutter.framework; do local source="$BUILT_PRODUCTS_DIR/$framework" local destination="$app_frameworks_dir/$framework" if [[ ! -d "$source" ]]; then echo " └─ERROR: built framework missing: $source" return 1 fi NormalizeFrameworkMinimumOSVersion "$source" "$minimum_os_version" rm -rf "$destination" cp -R "$source" "$app_frameworks_dir" NormalizeFrameworkMinimumOSVersion "$destination" "$minimum_os_version" done for framework in "$app_frameworks_dir"/*.framework; do if [[ ! -d "$framework" ]]; then continue fi NormalizeFrameworkMinimumOSVersion "$framework" "$minimum_os_version" CodeSignFramework "$framework" done } SyncRunnerVersion() { ReadPubspecVersion local plist="" if [[ -n "${TARGET_BUILD_DIR:-}" && -n "${INFOPLIST_PATH:-}" ]]; then plist="$TARGET_BUILD_DIR/$INFOPLIST_PATH" fi if [[ -z "$plist" || ! -f "$plist" ]]; then plist="$TARGET_BUILD_DIR/$WRAPPER_NAME/Info.plist" fi if [[ ! -f "$plist" ]]; then echo " └─ERROR: Runner Info.plist not found for version sync" return 1 fi local bundle_label="${TARGET_NAME:-Runner}" echo " └─Syncing $bundle_label version $FLUTTER_BUILD_NAME ($FLUTTER_BUILD_NUMBER)" SetPlistString "$plist" CFBundleShortVersionString "$FLUTTER_BUILD_NAME" SetPlistString "$plist" CFBundleVersion "$FLUTTER_BUILD_NUMBER" ValidatePlistVersion "$plist" "$bundle_label" local top_shelf_plist="$TARGET_BUILD_DIR/$WRAPPER_NAME/PlugIns/TopShelfExtension.appex/Info.plist" if [[ -f "$top_shelf_plist" ]]; then echo " └─Syncing TopShelfExtension version $FLUTTER_BUILD_NAME ($FLUTTER_BUILD_NUMBER)" SetPlistString "$top_shelf_plist" CFBundleShortVersionString "$FLUTTER_BUILD_NAME" SetPlistString "$top_shelf_plist" CFBundleVersion "$FLUTTER_BUILD_NUMBER" ValidatePlistVersion "$top_shelf_plist" "TopShelfExtension" fi EmbedFlutterFrameworks } EngineOutputExists() { local variant="$1" [[ -d "$FLUTTER_LOCAL_ENGINE/out/$variant" ]] } ResolveEngineOutput() { local variant for variant in "$@"; do local candidate="$FLUTTER_LOCAL_ENGINE/out/$variant" if [[ -d "$candidate" ]]; then printf '%s\n' "$candidate" return 0 fi done echo " └─ERROR: none of these Flutter engine outputs exist: $*" >&2 return 1 } ResolveDartAotRuntime() { local host_tools="$1" local packaged_runtime="$host_tools/dart-sdk/bin/dartaotruntime" if [[ -x "$packaged_runtime" ]] && "$packaged_runtime" --version >/dev/null 2>&1; then printf '%s\n' "$packaged_runtime" return 0 fi local flutter_runtime="${FLUTTER_ROOT:-}/bin/cache/dart-sdk/bin/dartaotruntime" local packaged_version_file="$host_tools/dart-sdk/version" local flutter_version_file="${FLUTTER_ROOT:-}/bin/cache/dart-sdk/version" if [[ ! -x "$flutter_runtime" || ! -f "$packaged_version_file" || ! -f "$flutter_version_file" ]]; then echo " └─ERROR: the packaged Dart runtime cannot execute on this host and no compatible Flutter runtime was found" >&2 return 1 fi local packaged_version local flutter_version packaged_version="$(tr -d '[:space:]' < "$packaged_version_file")" flutter_version="$(tr -d '[:space:]' < "$flutter_version_file")" if [[ "$packaged_version" != "$flutter_version" ]]; then echo " └─ERROR: the packaged Dart runtime cannot execute on this host and Flutter's Dart version does not match ($flutter_version != $packaged_version)" >&2 return 1 fi if ! "$flutter_runtime" --version >/dev/null 2>&1; then echo " └─ERROR: Flutter's Dart runtime cannot execute on this host" >&2 return 1 fi printf '%s\n' "$flutter_runtime" } ResolveFrontendServer() { local host_tools="$1" local dart_aot_runtime="$2" local frontend_server="$host_tools/dart-sdk/bin/snapshots/frontend_server_aot.dart.snapshot" if [[ "$dart_aot_runtime" != "$host_tools/dart-sdk/bin/dartaotruntime" ]]; then frontend_server="$FLUTTER_ROOT/bin/cache/dart-sdk/bin/snapshots/frontend_server_aot.dart.snapshot" elif [[ ! -f "$frontend_server" ]]; then frontend_server="$host_tools/gen/frontend_server_aot.dart.snapshot" fi if [[ ! -f "$frontend_server" ]]; then echo " └─ERROR: compatible frontend_server snapshot was not found" >&2 return 1 fi printf '%s\n' "$frontend_server" } BuildAppDebug() { # Host tools (frontend_server, patched SDK, dartaotruntime) ship in # host_release for both debug and release consumers — the frontend_server # compiles debug kernels regardless of the host build flavor. HOST_TOOLS=$FLUTTER_LOCAL_ENGINE/out/host_release if [[ "$debug_sim" == "true" ]]; then DEVICE_TOOLS=$(ResolveEngineOutput "tvos_debug_sim_unopt$TARGET_POSTFIX" "tvos_debug_sim_unopt_arm64" "tvos_debug_sim_unopt") || return 1 else # Device build is always arm64; gn outputs `tvos_debug_unopt` without suffix. DEVICE_TOOLS=$(ResolveEngineOutput "tvos_debug_unopt") || return 1 fi ROOTDIR=$(dirname "$PROJECT_DIR") OUTDIR=$ROOTDIR/build/ios/Release-iphoneos mkdir -p $OUTDIR echo " └─OUTDIR: $OUTDIR" echo " └─BUILT_PRODUCTS_DIR: $BUILT_PRODUCTS_DIR" echo " └─Copying Flutter.framework" rm -rf "$OUTDIR/Flutter.framework" cp -R "$DEVICE_TOOLS/Flutter.framework" "$OUTDIR" # The engine tarball's Flutter.framework/Info.plist declares MinimumOSVersion # 13.0 but the binary is linked with the resolved deployment target. Apple's # validator compares both against the host app, so keep the plist aligned. plutil -replace MinimumOSVersion -string "$TVOS_DEPLOYMENT_TARGET" "$OUTDIR/Flutter.framework/Info.plist" tvos_deployment_target="$TVOS_DEPLOYMENT_TARGET" echo " └─Generate flutter_assets via flutter build bundle" mkdir -p "$OUTDIR/App.framework/flutter_assets" # Resolve the flutter CLI: prefer FLUTTER_ROOT from Generated.xcconfig, else # fall back to the `flutter` on PATH. FLUTTER_BIN="" if [ -n "$FLUTTER_ROOT" ] && [ -x "$FLUTTER_ROOT/bin/flutter" ]; then FLUTTER_BIN="$FLUTTER_ROOT/bin/flutter" elif command -v flutter >/dev/null 2>&1; then FLUTTER_BIN="$(command -v flutter)" fi if [ -z "$FLUTTER_BIN" ]; then echo " └─ERROR: flutter CLI not found (set FLUTTER_ROOT or add flutter to PATH)" return 1 fi DART_AOT_RUNTIME=$(ResolveDartAotRuntime "$HOST_TOOLS") || return 1 # flutter build bundle produces: AssetManifest, FontManifest, NOTICES, # shaders, fonts, assets, packages, plus a kernel_blob.bin and # isolate_snapshot_data compiled against the stock flutter engine. We # overwrite kernel_blob.bin and both snapshot blobs below with versions # from our tvOS engine so the tvOS VM can load them. ( cd "$FLUTTER_APPLICATION_PATH" && \ "$FLUTTER_BIN" build bundle \ --asset-dir="$OUTDIR/App.framework/flutter_assets" \ --no-tree-shake-icons \ --suppress-analytics ) || { echo " └─ERROR: flutter build bundle failed" return 1 } echo " └─Compiling tvOS kernel via compatible frontend_server" FRONTEND_SERVER=$(ResolveFrontendServer "$HOST_TOOLS" "$DART_AOT_RUNTIME") || return 1 "$DART_AOT_RUNTIME" \ "$FRONTEND_SERVER" \ --sdk-root "$HOST_TOOLS/flutter_patched_sdk" \ --tfa --target=flutter \ -DTVOS_BUILD=true \ --output-dill "$OUTDIR/App.framework/flutter_assets/kernel_blob.bin" \ "$FLUTTER_APPLICATION_PATH/lib/main.dart" echo " └─Copying tvOS VM + isolate snapshots (pure JIT, no gen_snapshot)" cp "$DEVICE_TOOLS/gen/flutter/lib/snapshot/vm_isolate_snapshot.bin" \ "$OUTDIR/App.framework/flutter_assets/vm_snapshot_data" cp "$DEVICE_TOOLS/gen/flutter/lib/snapshot/isolate_snapshot.bin" \ "$OUTDIR/App.framework/flutter_assets/isolate_snapshot_data" if [[ "$debug_sim" == "true" ]]; then SYSROOT=$(xcrun --sdk appletvsimulator --show-sdk-path) else SYSROOT=$(xcrun --sdk appletvos --show-sdk-path) fi echo " └─Creating stub App using $SYSROOT" if [[ "$debug_sim" == "true" ]]; then echo "static const int Moo = 88;" | xcrun clang -x c \ -arch $SIM_ARCH \ -L"$SYSROOT/usr/lib" \ -isysroot "$SYSROOT" \ -mappletvsimulator-version-min=$tvos_deployment_target \ -dynamiclib \ -Xlinker -rpath -Xlinker '@executable_path/Frameworks' \ -Xlinker -rpath -Xlinker '@loader_path/Frameworks' \ -install_name '@rpath/App.framework/App' \ -o "$OUTDIR/App.framework/App" - else echo "static const int Moo = 88;" | xcrun clang -x c \ -arch arm64 \ -isysroot "$SYSROOT" \ -mtvos-version-min=$tvos_deployment_target \ -dynamiclib \ -Xlinker -rpath -Xlinker '@executable_path/Frameworks' \ -Xlinker -rpath -Xlinker '@loader_path/Frameworks' \ -install_name '@rpath/App.framework/App' \ -o "$OUTDIR/App.framework/App" - fi strip "$OUTDIR/App.framework/App" echo " └─copy frameworks" CopyAppFrameworkInfoPlist "$OUTDIR/App.framework/Info.plist" "$tvos_deployment_target" # Two destinations: # 1. BUILT_PRODUCTS_DIR — Swift/linker search path. For plain builds this # equals TARGET_BUILD_DIR; for Archive it differs. # 2. $TARGET_BUILD_DIR/$WRAPPER_NAME/Frameworks — embedded into Runner.app # so @rpath resolves at runtime. # # DO NOT drop frameworks flat into TARGET_BUILD_DIR. During archive that's # Products/Applications/ — sibling to Runner.app — and extra framework # bundles there make Organizer classify the archive as "Other Items". mkdir -p "$BUILT_PRODUCTS_DIR" rm -rf "$BUILT_PRODUCTS_DIR/App.framework" "$BUILT_PRODUCTS_DIR/Flutter.framework" cp -R "${OUTDIR}/"{App.framework,Flutter.framework} "$BUILT_PRODUCTS_DIR" APP_FRAMEWORKS_DIR="$TARGET_BUILD_DIR/$WRAPPER_NAME/Frameworks" mkdir -p "$APP_FRAMEWORKS_DIR" rm -rf "$APP_FRAMEWORKS_DIR/App.framework" "$APP_FRAMEWORKS_DIR/Flutter.framework" cp -R "${OUTDIR}/"{App.framework,Flutter.framework} "$APP_FRAMEWORKS_DIR" # Sign the embedded frameworks. Skip when signing is disabled. Xcode's # later CodeSign phase re-signs the full app anyway. echo " └─Sign" if [[ "$debug_sim" != "true" && -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" && "${CODE_SIGNING_ALLOWED:-YES}" != "NO" ]]; then codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${BUILT_PRODUCTS_DIR}/App.framework/App" codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${BUILT_PRODUCTS_DIR}/Flutter.framework/Flutter" codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${APP_FRAMEWORKS_DIR}/App.framework/App" codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${APP_FRAMEWORKS_DIR}/Flutter.framework/Flutter" else echo " (skipped — no code sign identity or signing disabled)" fi echo " └─Done" return 0 } BuildAppRelease() { HOST_TOOLS=$FLUTTER_LOCAL_ENGINE/out/host_release DEVICE_TOOLS=$FLUTTER_LOCAL_ENGINE/out/tvos_release # Locate gen_snapshot. Use the cross-compile variant that targets iOS/tvOS # arm64 (emits arm64-ios assembly). The plain host gen_snapshot targets # macOS and its snapshots are rejected at runtime by the iOS/tvOS VM. GEN_SNAPSHOT="" for cand in \ "$DEVICE_TOOLS/artifacts_arm64/gen_snapshot_arm64" \ "$DEVICE_TOOLS/universal/gen_snapshot_arm64" \ "$DEVICE_TOOLS/artifacts_x64/gen_snapshot_arm64" \ "$DEVICE_TOOLS/gen_snapshot_arm64"; do if [ -x "$cand" ]; then GEN_SNAPSHOT="$cand" break fi done if [ -z "$GEN_SNAPSHOT" ]; then echo " └─ERROR: gen_snapshot not found under $DEVICE_TOOLS or $HOST_TOOLS" return 1 fi ROOTDIR=$(dirname "$PROJECT_DIR") OUTDIR=$ROOTDIR/build/ios/Release-iphoneos mkdir -p "$OUTDIR" echo " └─OUTDIR: $OUTDIR" echo " └─gen_snapshot: $GEN_SNAPSHOT" echo " └─Copying Flutter.framework" rm -rf "$OUTDIR/Flutter.framework" cp -R "$DEVICE_TOOLS/Flutter.framework" "$OUTDIR" # The engine tarball's Flutter.framework/Info.plist declares MinimumOSVersion # 13.0 but the binary is linked with the resolved deployment target. Apple's # validator compares both against the host app, so keep the plist aligned. plutil -replace MinimumOSVersion -string "$TVOS_DEPLOYMENT_TARGET" "$OUTDIR/Flutter.framework/Info.plist" tvos_deployment_target="$TVOS_DEPLOYMENT_TARGET" # Resolve flutter CLI. FLUTTER_BIN="" if [ -n "$FLUTTER_ROOT" ] && [ -x "$FLUTTER_ROOT/bin/flutter" ]; then FLUTTER_BIN="$FLUTTER_ROOT/bin/flutter" elif command -v flutter >/dev/null 2>&1; then FLUTTER_BIN="$(command -v flutter)" fi if [ -z "$FLUTTER_BIN" ]; then echo " └─ERROR: flutter CLI not found (set FLUTTER_ROOT or add flutter to PATH)" return 1 fi DART_AOT_RUNTIME=$(ResolveDartAotRuntime "$HOST_TOOLS") || return 1 echo " └─Generate flutter_assets via flutter build bundle (release)" mkdir -p "$OUTDIR/App.framework/flutter_assets" ( cd "$FLUTTER_APPLICATION_PATH" && \ "$FLUTTER_BIN" build bundle \ --release \ --asset-dir="$OUTDIR/App.framework/flutter_assets" \ --no-tree-shake-icons \ --suppress-analytics ) || { echo " └─ERROR: flutter build bundle failed" return 1 } # AOT builds don't need kernel_blob.bin in flutter_assets — the compiled # arm64 code lives in App.framework/App itself. rm -f "$OUTDIR/App.framework/flutter_assets/kernel_blob.bin" echo " └─Compiling AOT kernel via local engine frontend_server" # The snapshot under dart-sdk/bin/snapshots/ is the actual AOT-compiled one; # the one under gen/ is a stale/placeholder kernel. FRONTEND_SERVER=$(ResolveFrontendServer "$HOST_TOOLS" "$DART_AOT_RUNTIME") || return 1 "$DART_AOT_RUNTIME" \ "$FRONTEND_SERVER" \ --sdk-root "$HOST_TOOLS/flutter_patched_sdk" \ --aot --tfa --target=flutter \ -Ddart.vm.product=true \ -Ddart.vm.profile=false \ -DFLUTTER_BUILD_MODE=release \ -DTARGET_PLATFORM=TVOS \ -DTVOS_BUILD=true \ --output-dill "$OUTDIR/app.dill" \ "$FLUTTER_APPLICATION_PATH/lib/main.dart" echo " └─Compiling AOT Assembly" "$GEN_SNAPSHOT" \ --deterministic \ --snapshot_kind=app-aot-assembly \ --assembly="$OUTDIR/snapshot_assembly.S" \ --strip \ "$OUTDIR/app.dill" echo " └─Compiling Assembly" SYSROOT=$(xcrun --sdk appletvos --show-sdk-path) cc -arch arm64 \ -isysroot "$SYSROOT" \ -mtvos-version-min=$tvos_deployment_target \ -c "$OUTDIR/snapshot_assembly.S" \ -o "$OUTDIR/snapshot_assembly.o" echo " └─Linking app" clang -arch arm64 \ -isysroot "$SYSROOT" \ -mtvos-version-min=$tvos_deployment_target \ -dynamiclib \ -Xlinker -rpath -Xlinker @executable_path/Frameworks \ -Xlinker -rpath -Xlinker @loader_path/Frameworks \ -install_name @rpath/App.framework/App \ -o "$OUTDIR/App.framework/App" \ "$OUTDIR/snapshot_assembly.o" strip "$OUTDIR/App.framework/App" CopyAppFrameworkInfoPlist "$OUTDIR/App.framework/Info.plist" "$tvos_deployment_target" echo " └─copy frameworks" # BUILT_PRODUCTS_DIR = linker search path. DO NOT use TARGET_BUILD_DIR # flat — during archive that's Products/Applications/ and loose framework # bundles next to Runner.app make Xcode classify the archive as "Other". mkdir -p "$BUILT_PRODUCTS_DIR" rm -rf "$BUILT_PRODUCTS_DIR/App.framework" "$BUILT_PRODUCTS_DIR/Flutter.framework" cp -R "${OUTDIR}/"{App.framework,Flutter.framework} "$BUILT_PRODUCTS_DIR" APP_FRAMEWORKS_DIR="$TARGET_BUILD_DIR/$WRAPPER_NAME/Frameworks" mkdir -p "$APP_FRAMEWORKS_DIR" rm -rf "$APP_FRAMEWORKS_DIR/App.framework" "$APP_FRAMEWORKS_DIR/Flutter.framework" cp -R "${OUTDIR}/"{App.framework,Flutter.framework} "$APP_FRAMEWORKS_DIR" echo " └─Sign" if [[ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" && "${CODE_SIGNING_ALLOWED:-YES}" != "NO" ]]; then codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${BUILT_PRODUCTS_DIR}/App.framework/App" codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${BUILT_PRODUCTS_DIR}/Flutter.framework/Flutter" codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${APP_FRAMEWORKS_DIR}/App.framework/App" codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${APP_FRAMEWORKS_DIR}/Flutter.framework/Flutter" else echo " (skipped — no code sign identity or signing disabled)" fi echo " └─Done" return 0 } BuildApp() { ReadPubspecVersion local build_mode="$(echo "${FLUTTER_BUILD_MODE:-${CONFIGURATION}}" | tr "[:upper:]" "[:lower:]")" echo "Compiling Flutter/App.Framework" echo " └─version $FLUTTER_BUILD_NAME ($FLUTTER_BUILD_NUMBER)" if [ -z "$FLUTTER_LOCAL_ENGINE" ]; then echo " └─ERROR: FLUTTER_LOCAL_ENGINE not set!" return 1; fi echo " └─engine $FLUTTER_LOCAL_ENGINE" if [[ "$PLATFORM_NAME" == "appletvsimulator" ]]; then debug_sim="true" if [[ ! "$build_mode" =~ "debug" ]]; then echo " └─simulator builds use the debug simulator engine" fi BuildAppDebug elif [[ "$build_mode" =~ "debug" ]]; then if EngineOutputExists "tvos_debug_unopt"; then BuildAppDebug else echo " └─debug tvOS device engine not found; building release Flutter app" BuildAppRelease fi elif [[ "$build_mode" =~ "release" ]]; then # release/archive (archive: build mode == "release" && ${ACTION} == "install") BuildAppRelease else echo " └─ERROR: unknown target: ${build_mode}" return 1; fi return 0 } # Main entry point. if [[ $# == 0 ]]; then # Backwards-compatibility: if no args are provided, build and embed. BuildApp EmbedFlutterFrameworks else case $1 in "build") BuildApp ;; "sync_version") SyncRunnerVersion ;; # "embed_and_thin") # "Not needed, used from flutter xcode_backend.sh script" esac fi