1#!/bin/bash 2# Copyright 2016 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6RunCommand() { 7 if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then 8 echo "♦ $*" 9 fi 10 "$@" 11 return $? 12} 13 14# When provided with a pipe by the host Flutter build process, output to the 15# pipe goes to stdout of the Flutter build process directly. 16StreamOutput() { 17 if [[ -n "$SCRIPT_OUTPUT_STREAM_FILE" ]]; then 18 echo "$1" > $SCRIPT_OUTPUT_STREAM_FILE 19 fi 20} 21 22EchoError() { 23 echo "$@" 1>&2 24} 25 26AssertExists() { 27 if [[ ! -e "$1" ]]; then 28 if [[ -h "$1" ]]; then 29 EchoError "The path $1 is a symlink to a path that does not exist" 30 else 31 EchoError "The path $1 does not exist" 32 fi 33 exit -1 34 fi 35 return 0 36} 37 38BuildApp() { 39 local project_path="${SOURCE_ROOT}/.." 40 if [[ -n "$FLUTTER_APPLICATION_PATH" ]]; then 41 project_path="${FLUTTER_APPLICATION_PATH}" 42 fi 43 44 local target_path="lib/main.dart" 45 if [[ -n "$FLUTTER_TARGET" ]]; then 46 target_path="${FLUTTER_TARGET}" 47 fi 48 49 local derived_dir="${SOURCE_ROOT}/Flutter" 50 if [[ -e "${project_path}/.ios" ]]; then 51 derived_dir="${project_path}/.ios/Flutter" 52 fi 53 54 # Default value of assets_path is flutter_assets 55 local assets_path="flutter_assets" 56 # The value of assets_path can set by add FLTAssetsPath to AppFrameworkInfo.plist 57 FLTAssetsPath=$(/usr/libexec/PlistBuddy -c "Print :FLTAssetsPath" "${derived_dir}/AppFrameworkInfo.plist" 2>/dev/null) 58 if [[ -n "$FLTAssetsPath" ]]; then 59 assets_path="${FLTAssetsPath}" 60 fi 61 62 # Use FLUTTER_BUILD_MODE if it's set, otherwise use the Xcode build configuration name 63 # This means that if someone wants to use an Xcode build config other than Debug/Profile/Release, 64 # they _must_ set FLUTTER_BUILD_MODE so we know what type of artifact to build. 65 local build_mode="$(echo "${FLUTTER_BUILD_MODE:-${CONFIGURATION}}" | tr "[:upper:]" "[:lower:]")" 66 local artifact_variant="unknown" 67 case "$build_mode" in 68 *release*) build_mode="release"; artifact_variant="ios-release";; 69 *profile*) build_mode="profile"; artifact_variant="ios-profile";; 70 *debug*) build_mode="debug"; artifact_variant="ios";; 71 *) 72 EchoError "========================================================================" 73 EchoError "ERROR: Unknown FLUTTER_BUILD_MODE: ${build_mode}." 74 EchoError "Valid values are 'Debug', 'Profile', or 'Release' (case insensitive)." 75 EchoError "This is controlled by the FLUTTER_BUILD_MODE environment variable." 76 EchoError "If that is not set, the CONFIGURATION environment variable is used." 77 EchoError "" 78 EchoError "You can fix this by either adding an appropriately named build" 79 EchoError "configuration, or adding an appropriate value for FLUTTER_BUILD_MODE to the" 80 EchoError ".xcconfig file for the current build configuration (${CONFIGURATION})." 81 EchoError "========================================================================" 82 exit -1;; 83 esac 84 85 # Archive builds (ACTION=install) should always run in release mode. 86 if [[ "$ACTION" == "install" && "$build_mode" != "release" ]]; then 87 EchoError "========================================================================" 88 EchoError "ERROR: Flutter archive builds must be run in Release mode." 89 EchoError "" 90 EchoError "To correct, ensure FLUTTER_BUILD_MODE is set to release or run:" 91 EchoError "flutter build ios --release" 92 EchoError "" 93 EchoError "then re-run Archive from Xcode." 94 EchoError "========================================================================" 95 exit -1 96 fi 97 98 local framework_path="${FLUTTER_ROOT}/bin/cache/artifacts/engine/${artifact_variant}" 99 100 AssertExists "${framework_path}" 101 AssertExists "${project_path}" 102 103 RunCommand mkdir -p -- "$derived_dir" 104 AssertExists "$derived_dir" 105 106 RunCommand rm -rf -- "${derived_dir}/App.framework" 107 108 local flutter_engine_flag="" 109 local local_engine_flag="" 110 local flutter_framework="${framework_path}/Flutter.framework" 111 local flutter_podspec="${framework_path}/Flutter.podspec" 112 113 if [[ -n "$FLUTTER_ENGINE" ]]; then 114 flutter_engine_flag="--local-engine-src-path=${FLUTTER_ENGINE}" 115 fi 116 117 local bitcode_flag="" 118 if [[ -n "$LOCAL_ENGINE" ]]; then 119 if [[ $(echo "$LOCAL_ENGINE" | tr "[:upper:]" "[:lower:]") != *"$build_mode"* ]]; then 120 EchoError "========================================================================" 121 EchoError "ERROR: Requested build with Flutter local engine at '${LOCAL_ENGINE}'" 122 EchoError "This engine is not compatible with FLUTTER_BUILD_MODE: '${build_mode}'." 123 EchoError "You can fix this by updating the LOCAL_ENGINE environment variable, or" 124 EchoError "by running:" 125 EchoError " flutter build ios --local-engine=ios_${build_mode}" 126 EchoError "or" 127 EchoError " flutter build ios --local-engine=ios_${build_mode}_unopt" 128 EchoError "========================================================================" 129 exit -1 130 fi 131 local_engine_flag="--local-engine=${LOCAL_ENGINE}" 132 flutter_framework="${FLUTTER_ENGINE}/out/${LOCAL_ENGINE}/Flutter.framework" 133 flutter_podspec="${FLUTTER_ENGINE}/out/${LOCAL_ENGINE}/Flutter.podspec" 134 if [[ $ENABLE_BITCODE == "YES" ]]; then 135 bitcode_flag="--bitcode" 136 fi 137 fi 138 139 if [[ -e "${project_path}/.ios" ]]; then 140 RunCommand rm -rf -- "${derived_dir}/engine" 141 mkdir "${derived_dir}/engine" 142 RunCommand cp -r -- "${flutter_podspec}" "${derived_dir}/engine" 143 RunCommand cp -r -- "${flutter_framework}" "${derived_dir}/engine" 144 RunCommand find "${derived_dir}/engine/Flutter.framework" -type f -exec chmod a-w "{}" \; 145 else 146 RunCommand rm -rf -- "${derived_dir}/Flutter.framework" 147 RunCommand cp -r -- "${flutter_framework}" "${derived_dir}" 148 RunCommand find "${derived_dir}/Flutter.framework" -type f -exec chmod a-w "{}" \; 149 fi 150 151 RunCommand pushd "${project_path}" > /dev/null 152 153 AssertExists "${target_path}" 154 155 local verbose_flag="" 156 if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then 157 verbose_flag="--verbose" 158 fi 159 160 local build_dir="${FLUTTER_BUILD_DIR:-build}" 161 162 local track_widget_creation_flag="" 163 if [[ -n "$TRACK_WIDGET_CREATION" ]]; then 164 track_widget_creation_flag="--track-widget-creation" 165 fi 166 167 if [[ "${build_mode}" != "debug" ]]; then 168 StreamOutput " ├─Building Dart code..." 169 # Transform ARCHS to comma-separated list of target architectures. 170 local archs="${ARCHS// /,}" 171 if [[ $archs =~ .*i386.* || $archs =~ .*x86_64.* ]]; then 172 EchoError "========================================================================" 173 EchoError "ERROR: Flutter does not support running in profile or release mode on" 174 EchoError "the Simulator (this build was: '$build_mode')." 175 EchoError "You can ensure Flutter runs in Debug mode with your host app in release" 176 EchoError "mode by setting FLUTTER_BUILD_MODE=debug in the .xcconfig associated" 177 EchoError "with the ${CONFIGURATION} build configuration." 178 EchoError "========================================================================" 179 exit -1 180 fi 181 182 RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics \ 183 ${verbose_flag} \ 184 build aot \ 185 --output-dir="${build_dir}/aot" \ 186 --target-platform=ios \ 187 --target="${target_path}" \ 188 --${build_mode} \ 189 --ios-arch="${archs}" \ 190 ${flutter_engine_flag} \ 191 ${local_engine_flag} \ 192 ${bitcode_flag} 193 194 if [[ $? -ne 0 ]]; then 195 EchoError "Failed to build ${project_path}." 196 exit -1 197 fi 198 StreamOutput "done" 199 200 local app_framework="${build_dir}/aot/App.framework" 201 202 RunCommand cp -r -- "${app_framework}" "${derived_dir}" 203 204 if [[ "${build_mode}" == "release" ]]; then 205 StreamOutput " ├─Generating dSYM file..." 206 # Xcode calls `symbols` during app store upload, which uses Spotlight to 207 # find dSYM files for embedded frameworks. When it finds the dSYM file for 208 # `App.framework` it throws an error, which aborts the app store upload. 209 # To avoid this, we place the dSYM files in a folder ending with ".noindex", 210 # which hides it from Spotlight, https://github.com/flutter/flutter/issues/22560. 211 RunCommand mkdir -p -- "${build_dir}/dSYMs.noindex" 212 RunCommand xcrun dsymutil -o "${build_dir}/dSYMs.noindex/App.framework.dSYM" "${app_framework}/App" 213 if [[ $? -ne 0 ]]; then 214 EchoError "Failed to generate debug symbols (dSYM) file for ${app_framework}/App." 215 exit -1 216 fi 217 StreamOutput "done" 218 219 StreamOutput " ├─Stripping debug symbols..." 220 RunCommand xcrun strip -x -S "${derived_dir}/App.framework/App" 221 if [[ $? -ne 0 ]]; then 222 EchoError "Failed to strip ${derived_dir}/App.framework/App." 223 exit -1 224 fi 225 StreamOutput "done" 226 fi 227 228 else 229 RunCommand mkdir -p -- "${derived_dir}/App.framework" 230 231 # Build stub for all requested architectures. 232 local arch_flags="" 233 read -r -a archs <<< "$ARCHS" 234 for arch in "${archs[@]}"; do 235 arch_flags="${arch_flags}-arch $arch " 236 done 237 238 RunCommand eval "$(echo "static const int Moo = 88;" | xcrun clang -x c \ 239 ${arch_flags} \ 240 -dynamiclib \ 241 -Xlinker -rpath -Xlinker '@executable_path/Frameworks' \ 242 -Xlinker -rpath -Xlinker '@loader_path/Frameworks' \ 243 -install_name '@rpath/App.framework/App' \ 244 -o "${derived_dir}/App.framework/App" -)" 245 fi 246 247 local plistPath="${project_path}/ios/Flutter/AppFrameworkInfo.plist" 248 if [[ -e "${project_path}/.ios" ]]; then 249 plistPath="${project_path}/.ios/Flutter/AppFrameworkInfo.plist" 250 fi 251 252 RunCommand cp -- "$plistPath" "${derived_dir}/App.framework/Info.plist" 253 254 local precompilation_flag="" 255 if [[ "$CURRENT_ARCH" != "x86_64" ]] && [[ "$build_mode" != "debug" ]]; then 256 precompilation_flag="--precompiled" 257 fi 258 259 StreamOutput " ├─Assembling Flutter resources..." 260 RunCommand "${FLUTTER_ROOT}/bin/flutter" \ 261 ${verbose_flag} \ 262 build bundle \ 263 --target-platform=ios \ 264 --target="${target_path}" \ 265 --${build_mode} \ 266 --depfile="${build_dir}/snapshot_blob.bin.d" \ 267 --asset-dir="${derived_dir}/App.framework/${assets_path}" \ 268 ${precompilation_flag} \ 269 ${flutter_engine_flag} \ 270 ${local_engine_flag} \ 271 ${track_widget_creation_flag} 272 273 if [[ $? -ne 0 ]]; then 274 EchoError "Failed to package ${project_path}." 275 exit -1 276 fi 277 StreamOutput "done" 278 StreamOutput " └─Compiling, linking and signing..." 279 280 RunCommand popd > /dev/null 281 282 echo "Project ${project_path} built and packaged successfully." 283 return 0 284} 285 286# Returns the CFBundleExecutable for the specified framework directory. 287GetFrameworkExecutablePath() { 288 local framework_dir="$1" 289 290 local plist_path="${framework_dir}/Info.plist" 291 local executable="$(defaults read "${plist_path}" CFBundleExecutable)" 292 echo "${framework_dir}/${executable}" 293} 294 295# Destructively thins the specified executable file to include only the 296# specified architectures. 297LipoExecutable() { 298 local executable="$1" 299 shift 300 # Split $@ into an array. 301 read -r -a archs <<< "$@" 302 303 # Extract architecture-specific framework executables. 304 local all_executables=() 305 for arch in "${archs[@]}"; do 306 local output="${executable}_${arch}" 307 local lipo_info="$(lipo -info "${executable}")" 308 if [[ "${lipo_info}" == "Non-fat file:"* ]]; then 309 if [[ "${lipo_info}" != *"${arch}" ]]; then 310 echo "Non-fat binary ${executable} is not ${arch}. Running lipo -info:" 311 echo "${lipo_info}" 312 exit 1 313 fi 314 else 315 lipo -output "${output}" -extract "${arch}" "${executable}" 316 if [[ $? == 0 ]]; then 317 all_executables+=("${output}") 318 else 319 echo "Failed to extract ${arch} for ${executable}. Running lipo -info:" 320 lipo -info "${executable}" 321 exit 1 322 fi 323 fi 324 done 325 326 # Generate a merged binary from the architecture-specific executables. 327 # Skip this step for non-fat executables. 328 if [[ ${#all_executables[@]} > 0 ]]; then 329 local merged="${executable}_merged" 330 lipo -output "${merged}" -create "${all_executables[@]}" 331 332 cp -f -- "${merged}" "${executable}" > /dev/null 333 rm -f -- "${merged}" "${all_executables[@]}" 334 fi 335} 336 337# Destructively thins the specified framework to include only the specified 338# architectures. 339ThinFramework() { 340 local framework_dir="$1" 341 shift 342 343 local plist_path="${framework_dir}/Info.plist" 344 local executable="$(GetFrameworkExecutablePath "${framework_dir}")" 345 LipoExecutable "${executable}" "$@" 346} 347 348ThinAppFrameworks() { 349 local app_path="${TARGET_BUILD_DIR}/${WRAPPER_NAME}" 350 local frameworks_dir="${app_path}/Frameworks" 351 352 [[ -d "$frameworks_dir" ]] || return 0 353 find "${app_path}" -type d -name "*.framework" | while read framework_dir; do 354 ThinFramework "$framework_dir" "$ARCHS" 355 done 356} 357 358# Adds the App.framework as an embedded binary and the flutter_assets as 359# resources. 360EmbedFlutterFrameworks() { 361 AssertExists "${FLUTTER_APPLICATION_PATH}" 362 363 # Prefer the hidden .ios folder, but fallback to a visible ios folder if .ios 364 # doesn't exist. 365 local flutter_ios_out_folder="${FLUTTER_APPLICATION_PATH}/.ios/Flutter" 366 local flutter_ios_engine_folder="${FLUTTER_APPLICATION_PATH}/.ios/Flutter/engine" 367 if [[ ! -d ${flutter_ios_out_folder} ]]; then 368 flutter_ios_out_folder="${FLUTTER_APPLICATION_PATH}/ios/Flutter" 369 flutter_ios_engine_folder="${FLUTTER_APPLICATION_PATH}/ios/Flutter" 370 fi 371 372 AssertExists "${flutter_ios_out_folder}" 373 374 # Embed App.framework from Flutter into the app (after creating the Frameworks directory 375 # if it doesn't already exist). 376 local xcode_frameworks_dir=${BUILT_PRODUCTS_DIR}"/"${PRODUCT_NAME}".app/Frameworks" 377 RunCommand mkdir -p -- "${xcode_frameworks_dir}" 378 RunCommand cp -Rv -- "${flutter_ios_out_folder}/App.framework" "${xcode_frameworks_dir}" 379 380 # Embed the actual Flutter.framework that the Flutter app expects to run against, 381 # which could be a local build or an arch/type specific build. 382 # Remove it first since Xcode might be trying to hold some of these files - this way we're 383 # sure to get a clean copy. 384 RunCommand rm -rf -- "${xcode_frameworks_dir}/Flutter.framework" 385 RunCommand cp -Rv -- "${flutter_ios_engine_folder}/Flutter.framework" "${xcode_frameworks_dir}/" 386 387 # Sign the binaries we moved. 388 local identity="${EXPANDED_CODE_SIGN_IDENTITY_NAME:-$CODE_SIGN_IDENTITY}" 389 if [[ -n "$identity" && "$identity" != "\"\"" ]]; then 390 RunCommand codesign --force --verbose --sign "${identity}" -- "${xcode_frameworks_dir}/App.framework/App" 391 RunCommand codesign --force --verbose --sign "${identity}" -- "${xcode_frameworks_dir}/Flutter.framework/Flutter" 392 fi 393} 394 395# Main entry point. 396 397# TODO(cbracken): improve error handling, then enable set -e 398 399if [[ $# == 0 ]]; then 400 # Backwards-compatibility: if no args are provided, build. 401 BuildApp 402else 403 case $1 in 404 "build") 405 BuildApp ;; 406 "thin") 407 ThinAppFrameworks ;; 408 "embed") 409 EmbedFlutterFrameworks ;; 410 esac 411fi 412