xcode_backend.sh 15.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1 2
#!/usr/bin/env bash
# Copyright 2014 The Flutter Authors. All rights reserved.
3 4 5 6
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

RunCommand() {
xster's avatar
xster committed
7 8 9
  if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
    echo "♦ $*"
  fi
10
  "$@"
11 12 13
  return $?
}

14 15 16 17 18 19 20 21
# When provided with a pipe by the host Flutter build process, output to the
# pipe goes to stdout of the Flutter build process directly.
StreamOutput() {
  if [[ -n "$SCRIPT_OUTPUT_STREAM_FILE" ]]; then
    echo "$1" > $SCRIPT_OUTPUT_STREAM_FILE
  fi
}

22 23 24 25 26
EchoError() {
  echo "$@" 1>&2
}

AssertExists() {
27 28 29 30 31 32
  if [[ ! -e "$1" ]]; then
    if [[ -h "$1" ]]; then
      EchoError "The path $1 is a symlink to a path that does not exist"
    else
      EchoError "The path $1 does not exist"
    fi
33 34 35 36 37 38
    exit -1
  fi
  return 0
}

BuildApp() {
39 40
  local project_path="${SOURCE_ROOT}/.."
  if [[ -n "$FLUTTER_APPLICATION_PATH" ]]; then
41
    project_path="${FLUTTER_APPLICATION_PATH}"
42 43 44 45
  fi

  local target_path="lib/main.dart"
  if [[ -n "$FLUTTER_TARGET" ]]; then
46
    target_path="${FLUTTER_TARGET}"
47 48
  fi

49 50 51 52 53 54 55 56 57 58 59 60 61
  local derived_dir="${SOURCE_ROOT}/Flutter"
  if [[ -e "${project_path}/.ios" ]]; then
    derived_dir="${project_path}/.ios/Flutter"
  fi

  # Default value of assets_path is flutter_assets
  local assets_path="flutter_assets"
  # The value of assets_path can set by add FLTAssetsPath to AppFrameworkInfo.plist
  FLTAssetsPath=$(/usr/libexec/PlistBuddy -c "Print :FLTAssetsPath" "${derived_dir}/AppFrameworkInfo.plist" 2>/dev/null)
  if [[ -n "$FLTAssetsPath" ]]; then
    assets_path="${FLTAssetsPath}"
  fi

62 63 64 65 66 67
  # Use FLUTTER_BUILD_MODE if it's set, otherwise use the Xcode build configuration name
  # This means that if someone wants to use an Xcode build config other than Debug/Profile/Release,
  # they _must_ set FLUTTER_BUILD_MODE so we know what type of artifact to build.
  local build_mode="$(echo "${FLUTTER_BUILD_MODE:-${CONFIGURATION}}" | tr "[:upper:]" "[:lower:]")"
  local artifact_variant="unknown"
  case "$build_mode" in
68 69 70
    *release*) build_mode="release"; artifact_variant="ios-release";;
    *profile*) build_mode="profile"; artifact_variant="ios-profile";;
    *debug*) build_mode="debug"; artifact_variant="ios";;
71
    *)
72 73 74
      EchoError "========================================================================"
      EchoError "ERROR: Unknown FLUTTER_BUILD_MODE: ${build_mode}."
      EchoError "Valid values are 'Debug', 'Profile', or 'Release' (case insensitive)."
Chris Bracken's avatar
Chris Bracken committed
75
      EchoError "This is controlled by the FLUTTER_BUILD_MODE environment variable."
76 77 78
      EchoError "If that is not set, the CONFIGURATION environment variable is used."
      EchoError ""
      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 81 82 83 84
      EchoError ".xcconfig file for the current build configuration (${CONFIGURATION})."
      EchoError "========================================================================"
      exit -1;;
  esac

85
  # Archive builds (ACTION=install) should always run in release mode.
86
  if [[ "$ACTION" == "install" && "$build_mode" != "release" ]]; then
87 88 89
    EchoError "========================================================================"
    EchoError "ERROR: Flutter archive builds must be run in Release mode."
    EchoError ""
90
    EchoError "To correct, ensure FLUTTER_BUILD_MODE is set to release or run:"
91 92 93 94 95 96 97
    EchoError "flutter build ios --release"
    EchoError ""
    EchoError "then re-run Archive from Xcode."
    EchoError "========================================================================"
    exit -1
  fi

98
  local framework_path="${FLUTTER_ROOT}/bin/cache/artifacts/engine/${artifact_variant}"
99

100
  AssertExists "${framework_path}"
101
  AssertExists "${project_path}"
102

103 104
  RunCommand mkdir -p -- "$derived_dir"
  AssertExists "$derived_dir"
105

106
  RunCommand rm -rf -- "${derived_dir}/App.framework"
107

108
  local flutter_engine_flag=""
109 110 111
  local local_engine_flag=""
  local flutter_framework="${framework_path}/Flutter.framework"
  local flutter_podspec="${framework_path}/Flutter.podspec"
112

113 114 115 116
  if [[ -n "$FLUTTER_ENGINE" ]]; then
    flutter_engine_flag="--local-engine-src-path=${FLUTTER_ENGINE}"
  fi

117
  if [[ -n "$LOCAL_ENGINE" ]]; then
118 119 120 121 122 123 124 125 126 127 128 129
    if [[ $(echo "$LOCAL_ENGINE" | tr "[:upper:]" "[:lower:]") != *"$build_mode"* ]]; then
      EchoError "========================================================================"
      EchoError "ERROR: Requested build with Flutter local engine at '${LOCAL_ENGINE}'"
      EchoError "This engine is not compatible with FLUTTER_BUILD_MODE: '${build_mode}'."
      EchoError "You can fix this by updating the LOCAL_ENGINE environment variable, or"
      EchoError "by running:"
      EchoError "  flutter build ios --local-engine=ios_${build_mode}"
      EchoError "or"
      EchoError "  flutter build ios --local-engine=ios_${build_mode}_unopt"
      EchoError "========================================================================"
      exit -1
    fi
130
    local_engine_flag="--local-engine=${LOCAL_ENGINE}"
131 132
    flutter_framework="${FLUTTER_ENGINE}/out/${LOCAL_ENGINE}/Flutter.framework"
    flutter_podspec="${FLUTTER_ENGINE}/out/${LOCAL_ENGINE}/Flutter.podspec"
133 134 135 136 137
  fi

  local bitcode_flag=""
  if [[ $ENABLE_BITCODE == "YES" ]]; then
    bitcode_flag="--bitcode"
138 139
  fi

140 141 142
  if [[ -e "${project_path}/.ios" ]]; then
    RunCommand rm -rf -- "${derived_dir}/engine"
    mkdir "${derived_dir}/engine"
143 144
    RunCommand cp -r -- "${flutter_podspec}" "${derived_dir}/engine"
    RunCommand cp -r -- "${flutter_framework}" "${derived_dir}/engine"
145 146
  else
    RunCommand rm -rf -- "${derived_dir}/Flutter.framework"
147
    RunCommand cp -- "${flutter_podspec}" "${derived_dir}"
148
    RunCommand cp -r -- "${flutter_framework}" "${derived_dir}"
149 150
  fi

151
  RunCommand pushd "${project_path}" > /dev/null
152

153
  AssertExists "${target_path}"
154

155 156 157 158 159
  local verbose_flag=""
  if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
    verbose_flag="--verbose"
  fi

160
  local build_dir="${FLUTTER_BUILD_DIR:-build}"
161

162 163 164 165 166
  local track_widget_creation_flag=""
  if [[ -n "$TRACK_WIDGET_CREATION" ]]; then
    track_widget_creation_flag="--track-widget-creation"
  fi

167
  if [[ "${build_mode}" != "debug" ]]; then
168
    StreamOutput " ├─Building Dart code..."
169 170
    # Transform ARCHS to comma-separated list of target architectures.
    local archs="${ARCHS// /,}"
171 172 173 174 175 176 177 178 179 180
    if [[ $archs =~ .*i386.* || $archs =~ .*x86_64.* ]]; then
      EchoError "========================================================================"
      EchoError "ERROR: Flutter does not support running in profile or release mode on"
      EchoError "the Simulator (this build was: '$build_mode')."
      EchoError "You can ensure Flutter runs in Debug mode with your host app in release"
      EchoError "mode by setting FLUTTER_BUILD_MODE=debug in the .xcconfig associated"
      EchoError "with the ${CONFIGURATION} build configuration."
      EchoError "========================================================================"
      exit -1
    fi
181

182 183 184
    RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics           \
      ${verbose_flag}                                                       \
      build aot                                                             \
185 186 187
      --output-dir="${build_dir}/aot"                                       \
      --target-platform=ios                                                 \
      --target="${target_path}"                                             \
188
      --${build_mode}                                                       \
189
      --ios-arch="${archs}"                                                 \
190
      ${flutter_engine_flag}                                                \
191 192
      ${local_engine_flag}                                                  \
      ${bitcode_flag}
193 194 195 196 197

    if [[ $? -ne 0 ]]; then
      EchoError "Failed to build ${project_path}."
      exit -1
    fi
198
    StreamOutput "done"
199

200 201 202 203
    local app_framework="${build_dir}/aot/App.framework"

    RunCommand cp -r -- "${app_framework}" "${derived_dir}"

204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
    if [[ "${build_mode}" == "release" ]]; then
      StreamOutput " ├─Generating dSYM file..."
      # Xcode calls `symbols` during app store upload, which uses Spotlight to
      # find dSYM files for embedded frameworks. When it finds the dSYM file for
      # `App.framework` it throws an error, which aborts the app store upload.
      # To avoid this, we place the dSYM files in a folder ending with ".noindex",
      # which hides it from Spotlight, https://github.com/flutter/flutter/issues/22560.
      RunCommand mkdir -p -- "${build_dir}/dSYMs.noindex"
      RunCommand xcrun dsymutil -o "${build_dir}/dSYMs.noindex/App.framework.dSYM" "${app_framework}/App"
      if [[ $? -ne 0 ]]; then
        EchoError "Failed to generate debug symbols (dSYM) file for ${app_framework}/App."
        exit -1
      fi
      StreamOutput "done"

      StreamOutput " ├─Stripping debug symbols..."
      RunCommand xcrun strip -x -S "${derived_dir}/App.framework/App"
      if [[ $? -ne 0 ]]; then
        EchoError "Failed to strip ${derived_dir}/App.framework/App."
        exit -1
      fi
      StreamOutput "done"
226 227
    fi

228
  else
229
    RunCommand mkdir -p -- "${derived_dir}/App.framework"
230 231 232 233 234 235 236 237

    # Build stub for all requested architectures.
    local arch_flags=""
    read -r -a archs <<< "$ARCHS"
    for arch in "${archs[@]}"; do
      arch_flags="${arch_flags}-arch $arch "
    done

238
    RunCommand eval "$(echo "static const int Moo = 88;" | xcrun clang -x c \
239
        ${arch_flags} \
240
        -fembed-bitcode-marker \
241 242 243 244 245
        -dynamiclib \
        -Xlinker -rpath -Xlinker '@executable_path/Frameworks' \
        -Xlinker -rpath -Xlinker '@loader_path/Frameworks' \
        -install_name '@rpath/App.framework/App' \
        -o "${derived_dir}/App.framework/App" -)"
246
  fi
247 248 249 250 251 252 253

  local plistPath="${project_path}/ios/Flutter/AppFrameworkInfo.plist"
  if [[ -e "${project_path}/.ios" ]]; then
    plistPath="${project_path}/.ios/Flutter/AppFrameworkInfo.plist"
  fi

  RunCommand cp -- "$plistPath" "${derived_dir}/App.framework/Info.plist"
254 255

  local precompilation_flag=""
256
  if [[ "$CURRENT_ARCH" != "x86_64" ]] && [[ "$build_mode" != "debug" ]]; then
257 258 259
    precompilation_flag="--precompiled"
  fi

260
  StreamOutput " ├─Assembling Flutter resources..."
261
  RunCommand "${FLUTTER_ROOT}/bin/flutter"     \
262 263
    ${verbose_flag}                                                         \
    build bundle                                                            \
264
    --target-platform=ios                                                   \
265
    --target="${target_path}"                                               \
266
    --${build_mode}                                                         \
267
    --depfile="${build_dir}/snapshot_blob.bin.d"                            \
268
    --asset-dir="${derived_dir}/App.framework/${assets_path}"               \
269
    ${precompilation_flag}                                                  \
270
    ${flutter_engine_flag}                                                  \
271
    ${local_engine_flag}                                                    \
272
    ${track_widget_creation_flag}
273 274 275 276 277

  if [[ $? -ne 0 ]]; then
    EchoError "Failed to package ${project_path}."
    exit -1
  fi
278 279
  StreamOutput "done"
  StreamOutput " └─Compiling, linking and signing..."
280

281
  RunCommand popd > /dev/null
282 283 284 285 286

  echo "Project ${project_path} built and packaged successfully."
  return 0
}

287 288 289 290 291 292 293 294 295 296 297 298 299 300
# Returns the CFBundleExecutable for the specified framework directory.
GetFrameworkExecutablePath() {
  local framework_dir="$1"

  local plist_path="${framework_dir}/Info.plist"
  local executable="$(defaults read "${plist_path}" CFBundleExecutable)"
  echo "${framework_dir}/${executable}"
}

# Destructively thins the specified executable file to include only the
# specified architectures.
LipoExecutable() {
  local executable="$1"
  shift
301 302
  # Split $@ into an array.
  read -r -a archs <<< "$@"
303 304 305

  # Extract architecture-specific framework executables.
  local all_executables=()
306
  for arch in "${archs[@]}"; do
307
    local output="${executable}_${arch}"
308
    local lipo_info="$(lipo -info "${executable}")"
309 310 311 312 313 314
    if [[ "${lipo_info}" == "Non-fat file:"* ]]; then
      if [[ "${lipo_info}" != *"${arch}" ]]; then
        echo "Non-fat binary ${executable} is not ${arch}. Running lipo -info:"
        echo "${lipo_info}"
        exit 1
      fi
315
    else
316 317 318 319 320 321 322 323
      lipo -output "${output}" -extract "${arch}" "${executable}"
      if [[ $? == 0 ]]; then
        all_executables+=("${output}")
      else
        echo "Failed to extract ${arch} for ${executable}. Running lipo -info:"
        lipo -info "${executable}"
        exit 1
      fi
324 325 326
    fi
  done

327 328 329 330 331 332 333 334 335
  # Generate a merged binary from the architecture-specific executables.
  # Skip this step for non-fat executables.
  if [[ ${#all_executables[@]} > 0 ]]; then
    local merged="${executable}_merged"
    lipo -output "${merged}" -create "${all_executables[@]}"

    cp -f -- "${merged}" "${executable}" > /dev/null
    rm -f -- "${merged}" "${all_executables[@]}"
  fi
336 337 338 339 340 341 342 343
}

# Destructively thins the specified framework to include only the specified
# architectures.
ThinFramework() {
  local framework_dir="$1"
  shift

344
  local plist_path="${framework_dir}/Info.plist"
345
  local executable="$(GetFrameworkExecutablePath "${framework_dir}")"
346
  LipoExecutable "${executable}" "$@"
347 348 349 350 351 352 353
}

ThinAppFrameworks() {
  local app_path="${TARGET_BUILD_DIR}/${WRAPPER_NAME}"
  local frameworks_dir="${app_path}/Frameworks"

  [[ -d "$frameworks_dir" ]] || return 0
354
  find "${app_path}" -type d -name "*.framework" | while read framework_dir; do
355 356 357 358
    ThinFramework "$framework_dir" "$ARCHS"
  done
}

359 360 361 362 363 364 365 366
# Adds the App.framework as an embedded binary and the flutter_assets as
# resources.
EmbedFlutterFrameworks() {
  AssertExists "${FLUTTER_APPLICATION_PATH}"

  # Prefer the hidden .ios folder, but fallback to a visible ios folder if .ios
  # doesn't exist.
  local flutter_ios_out_folder="${FLUTTER_APPLICATION_PATH}/.ios/Flutter"
367
  local flutter_ios_engine_folder="${FLUTTER_APPLICATION_PATH}/.ios/Flutter/engine"
368 369
  if [[ ! -d ${flutter_ios_out_folder} ]]; then
    flutter_ios_out_folder="${FLUTTER_APPLICATION_PATH}/ios/Flutter"
370
    flutter_ios_engine_folder="${FLUTTER_APPLICATION_PATH}/ios/Flutter"
371 372 373 374 375 376
  fi

  AssertExists "${flutter_ios_out_folder}"

  # Embed App.framework from Flutter into the app (after creating the Frameworks directory
  # if it doesn't already exist).
377
  local xcode_frameworks_dir=${BUILT_PRODUCTS_DIR}"/"${PRODUCT_NAME}".app/Frameworks"
378 379 380 381 382
  RunCommand mkdir -p -- "${xcode_frameworks_dir}"
  RunCommand cp -Rv -- "${flutter_ios_out_folder}/App.framework" "${xcode_frameworks_dir}"

  # Embed the actual Flutter.framework that the Flutter app expects to run against,
  # which could be a local build or an arch/type specific build.
383
  # Remove it first since Xcode might be trying to hold some of these files - this way we're
384 385
  # sure to get a clean copy.
  RunCommand rm -rf -- "${xcode_frameworks_dir}/Flutter.framework"
386
  RunCommand cp -Rv -- "${flutter_ios_engine_folder}/Flutter.framework" "${xcode_frameworks_dir}/"
387

388 389
  # Sign the binaries we moved.
  local identity="${EXPANDED_CODE_SIGN_IDENTITY_NAME:-$CODE_SIGN_IDENTITY}"
390
  if [[ -n "$identity" && "$identity" != "\"\"" ]]; then
391 392 393
    RunCommand codesign --force --verbose --sign "${identity}" -- "${xcode_frameworks_dir}/App.framework/App"
    RunCommand codesign --force --verbose --sign "${identity}" -- "${xcode_frameworks_dir}/Flutter.framework/Flutter"
  fi
394 395
}

396 397
# Main entry point.

398
# TODO(cbracken): improve error handling, then enable set -e
399

400
if [[ $# == 0 ]]; then
Josh Soref's avatar
Josh Soref committed
401
  # Backwards-compatibility: if no args are provided, build.
402 403 404 405 406 407 408
  BuildApp
else
  case $1 in
    "build")
      BuildApp ;;
    "thin")
      ThinAppFrameworks ;;
409 410
    "embed")
      EmbedFlutterFrameworks ;;
411 412
  esac
fi