Files
plezy/tvos/scripts/xcode_appletv.sh
T
2026-04-24 14:11:24 +02:00

365 lines
13 KiB
Bash
Executable File

#!/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
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=$FLUTTER_LOCAL_ENGINE/out/tvos_debug_sim_unopt$TARGET_POSTFIX
else
# Device build is always arm64; gn outputs `tvos_debug_unopt` without suffix.
DEVICE_TOOLS=$FLUTTER_LOCAL_ENGINE/out/tvos_debug_unopt
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"
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
# 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 local engine frontend_server"
FRONTEND_SERVER="$HOST_TOOLS/dart-sdk/bin/snapshots/frontend_server_aot.dart.snapshot"
if [ ! -f "$FRONTEND_SERVER" ]; then
FRONTEND_SERVER="$HOST_TOOLS/gen/frontend_server_aot.dart.snapshot"
fi
"$HOST_TOOLS/dart-sdk/bin/dartaotruntime" \
"$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"
cp "$PROJECT_DIR/scripts/Info.plist" "$OUTDIR/App.framework/Info.plist"
# For Archive builds BUILT_PRODUCTS_DIR differs from TARGET_BUILD_DIR:
# Swift compile looks up frameworks under BUILT_PRODUCTS_DIR, but the
# embedded copy needs to end up inside TARGET_BUILD_DIR/Runner.app.
cp -R "${OUTDIR}/"{App.framework,Flutter.framework} "$TARGET_BUILD_DIR"
if [ -n "${BUILT_PRODUCTS_DIR:-}" ] && [ "$BUILT_PRODUCTS_DIR" != "$TARGET_BUILD_DIR" ]; then
rm -rf "$BUILT_PRODUCTS_DIR/App.framework" "$BUILT_PRODUCTS_DIR/Flutter.framework"
cp -R "${OUTDIR}/"{App.framework,Flutter.framework} "$BUILT_PRODUCTS_DIR"
fi
# Also embed into Runner.app/Frameworks so the dylib @rpath resolves at launch.
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 binaries we moved. Both the flat copy (used by linker/embedder)
# and the embedded copy inside Runner.app/Frameworks need signatures.
# Skip when signing is disabled (CODE_SIGNING_ALLOWED=NO, unit-test builds,
# or sim builds where Xcode doesn't sign frameworks). Xcode's own CodeSign
# phase will sign the final app + frameworks in proper archive builds.
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}" -- "${TARGET_BUILD_DIR}/App.framework/App"
codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${TARGET_BUILD_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"
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
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="$HOST_TOOLS/dart-sdk/bin/snapshots/frontend_server_aot.dart.snapshot"
if [ ! -f "$FRONTEND_SERVER" ]; then
FRONTEND_SERVER="$HOST_TOOLS/gen/frontend_server_aot.dart.snapshot"
fi
"$HOST_TOOLS/dart-sdk/bin/dartaotruntime" \
"$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"
cp "$PROJECT_DIR/scripts/Info.plist" "$OUTDIR/App.framework/Info.plist"
echo " └─copy frameworks"
# Archive builds point BUILT_PRODUCTS_DIR at a separate products path that
# Swift compile scans for frameworks — copy there too so downstream
# compilation in the same target can resolve Flutter.h.
cp -R "${OUTDIR}/"{App.framework,Flutter.framework} "$TARGET_BUILD_DIR"
if [ -n "${BUILT_PRODUCTS_DIR:-}" ] && [ "$BUILT_PRODUCTS_DIR" != "$TARGET_BUILD_DIR" ]; then
rm -rf "$BUILT_PRODUCTS_DIR/App.framework" "$BUILT_PRODUCTS_DIR/Flutter.framework"
cp -R "${OUTDIR}/"{App.framework,Flutter.framework} "$BUILT_PRODUCTS_DIR"
fi
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}" -- "${TARGET_BUILD_DIR}/App.framework/App"
codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${TARGET_BUILD_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() {
local build_mode="$(echo "${FLUTTER_BUILD_MODE:-${CONFIGURATION}}" | tr "[:upper:]" "[:lower:]")"
echo "Compiling Flutter/App.Framework"
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" && "$build_mode" =~ "debug" ]]; then
debug_sim="true"
BuildAppDebug
elif [[ "$build_mode" =~ "debug" ]]; then
BuildAppDebug
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 ;;
# "embed_and_thin")
# "Not needed, used from flutter xcode_backend.sh script"
esac
fi