Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 50 additions & 23 deletions .github/workflows/ios-build-test-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,42 @@ jobs:
build_number: ${{ steps.set.outputs.build_number }}

build:
name: Build iOS app (${{ matrix.backend }})
name: Build iOS app (${{ matrix.label }})
needs: computed
# https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md
runs-on: macos-15
# macos-26: https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
# xcode-27: https://github.com/actions/runner-images/blob/main/images/macos/xcode-27-arm64-Readme.md
runs-on: ${{ matrix.runner }}
permissions:
contents: read
timeout-minutes: 180
# The xcode-27 image is a GitHub preview: capacity is still being balanced and
# it can queue for a long time. Let that leg fail without failing the workflow,
# so a preview-image hiccup cannot block the BrowserStack jobs that need this
# one. Drop this once xcode-27 reaches GA.
continue-on-error: ${{ matrix.experimental || false }}
strategy:
fail-fast: false
matrix:
include:
- backend: "tflite"
- label: "tflite"
runner: macos-26
with_apple: 0
with_tflite: 1
- backend: "apple"
- label: "apple"
runner: macos-26
with_apple: 1
with_tflite: 0
# Xcode Cloud archives with Xcode 27 and runs only on master, so an
# Xcode 27 break stayed invisible until after merge -- that is how
# swiftc refusing coreml_util without -swift-version reached master.
# This leg builds the same backend on the same Xcode 27 build Xcode
# Cloud uses (27A266a), on pull requests. It builds only: the test
# package and BrowserStack legs stay on the GA image.
- label: "apple-xcode27"
runner: xcode-27
with_apple: 1
with_tflite: 0
experimental: true
env:
PERF_TEST: true
BAZEL_OUTPUT_ROOT_ARG: "--output_user_root=/tmp/bazel_output"
Expand All @@ -52,7 +71,7 @@ jobs:
# do not get, so it falls back to a local disk cache there.
HAS_GCLOUD_CREDS: ${{ secrets.GCLOUD_SERVICE_ACCOUNT_MOBILE_APP_BUILD != '' }}
FLUTTER_BUILD_NUMBER: ${{ needs.computed.outputs.build_number }}
FLUTTER_IOS_TEST_PACKAGE: ios_tests-${{ matrix.backend }}-${{ needs.computed.outputs.build_number }}.zip
FLUTTER_IOS_TEST_PACKAGE: ios_tests-${{ matrix.label }}-${{ needs.computed.outputs.build_number }}.zip
WITH_APPLE: ${{ matrix.with_apple }}
WITH_TFLITE: ${{ matrix.with_tflite }}
WITH_PIXEL: 0
Expand All @@ -72,19 +91,19 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Select Xcode with iOS 26.1 SDK
- name: Select Xcode
# device_info_plus 13.x (since 12.4.0) calls NSProcessInfo.isiOSAppOnVision,
# an iOS 26.1 API. The @available guard is a runtime check; the selector
# must still be *declared* by the SDK at compile time, so the pods only
# build against the iOS 26.1 (or newer) SDK. The macos-15 runner defaults
# to Xcode 16.4 (iOS 18.5 SDK), which fails with "no visible @interface for
# 'NSProcessInfo' declares the selector 'isiOSAppOnVision'". Select the
# newest installed Xcode 26.x, which ships the iOS 26.1+ SDK.
# build against the iOS 26.1 (or newer) SDK. Select the newest Xcode 26.x
# or 27.x on the runner: 26.6 on macos-26, 27.0 on xcode-27. Pinning to a
# major would fail on one of the two images, since macos-26 ships no
# Xcode 27 and xcode-27 ships no Xcode 26.
run: |
set -euo pipefail
XCODE_APP=$(ls -d /Applications/Xcode_26.*.app 2>/dev/null | sort -V | tail -1)
XCODE_APP=$(ls -d /Applications/Xcode_2[67].*.app 2>/dev/null | sort -V | tail -1)
if [ -z "$XCODE_APP" ]; then
echo "No Xcode 26.x found on the runner" >&2
echo "No Xcode 26.x or 27.x found on the runner" >&2
exit 1
fi
echo "Selecting $XCODE_APP"
Expand All @@ -98,14 +117,16 @@ jobs:
# so hashFiles('**/Podfile.lock') returned empty and the key collapsed
# to the constant "macOS-pods-" -- shared by both matrix jobs, which
# then raced for the single write, and never invalidated when the
# Podfile changed. The backend keeps the two legs from colliding.
key: ${{ runner.os }}-pods-${{ matrix.backend }}-${{ hashFiles('flutter/ios/Podfile') }}
restore-keys: ${{ runner.os }}-pods-${{ matrix.backend }}-
# Podfile changed. The label keeps the legs from colliding, and it is
# the label rather than the backend because two legs build "apple" on
# different Xcode versions.
key: ${{ runner.os }}-pods-${{ matrix.label }}-${{ hashFiles('flutter/ios/Podfile') }}
restore-keys: ${{ runner.os }}-pods-${{ matrix.label }}-
# Two cache layers: a local disk cache in front of the GCS remote cache.
#
# The old key was `${{ runner.os }}-bazel_cache-${{ hashFiles(...) }}` over
# /tmp/bazel_cache, declared identically by these two matrix jobs and the
# Android-macOS job, all on macos-15 -- so all three resolved to one key
# /tmp/bazel_cache, declared identically by the iOS matrix jobs and the
# Android-macOS job, all on one image -- so all three resolved to one key
# and raced for the single write actions/cache allows. Caching worked here
# only because an iOS job usually won that race (this job hits 4486 of
# 4486 cacheable actions), while the Android job restored these iOS
Expand All @@ -117,8 +138,8 @@ jobs:
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: /tmp/bazel_cache
key: ${{ runner.os }}-bazel-ios-${{ matrix.backend }}-${{ hashFiles('**/BUILD', '**/WORKSPACE') }}
restore-keys: ${{ runner.os }}-bazel-ios-${{ matrix.backend }}-
key: ${{ runner.os }}-bazel-ios-${{ matrix.label }}-${{ hashFiles('**/BUILD', '**/WORKSPACE') }}
restore-keys: ${{ runner.os }}-bazel-ios-${{ matrix.label }}-
- name: Set up authentication for Google Cloud SDK
if: env.HAS_GCLOUD_CREDS == 'true'
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
Expand All @@ -137,8 +158,10 @@ jobs:
# Disk first, GCS second. Bazel reads the disk cache before going to
# the network and writes both, so a warm disk layer keeps this job as
# fast as it was on disk alone while GCS covers the cold cases.
# Both matrix backends share one GCS prefix on purpose: they compile
# almost the same C++, so each one warms the cache for the other.
# All legs share one GCS prefix on purpose: the two macos-26 legs
# compile almost the same C++, so each warms the cache for the other.
# The Xcode 27 leg cannot collide with them -- its toolchain is part
# of every action key, so it simply keeps its own entries alongside.
# Note that setup-gcloud above puts a per-run random path on PATH.
# That is exactly what broke the Linux cache, and it is harmless
# only because .bazelrc sets --incompatible_strict_action_env.
Expand Down Expand Up @@ -196,6 +219,9 @@ jobs:
run: |
make flutter/test/unit
- name: Build iOS test package
# The Xcode 27 leg is a compile check only. The device tests keep running
# against the GA image, so a preview-image build never reaches BrowserStack.
if: ${{ !matrix.experimental }}
env:
DEVELOPMENT_TEAM: ${{ secrets.APPLE_DEVELOPMENT_TEAM }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
Expand All @@ -209,9 +235,10 @@ jobs:
fi
make flutter/ios/test-package
- name: Upload test package artifact
if: ${{ !matrix.experimental }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ios-test-package-${{ matrix.backend }}-${{ needs.computed.outputs.build_number }}
name: ios-test-package-${{ matrix.label }}-${{ needs.computed.outputs.build_number }}
path: output/ios-test-package/*.zip
retention-days: 28
if-no-files-found: error
Expand Down
10 changes: 8 additions & 2 deletions flutter/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import Flutter
import UIKit

@main
@objc class AppDelegate: FlutterAppDelegate {
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

// Under the UIScene lifecycle the implicit engine is created after
// didFinishLaunchingWithOptions, so registering plugins there would run
// against an engine that does not exist yet.
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
21 changes: 21 additions & 0 deletions flutter/ios/Runner/Info-Debug.plist
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,27 @@
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
Expand Down
21 changes: 21 additions & 0 deletions flutter/ios/Runner/Info-Release.plist
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,27 @@
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
Expand Down
14 changes: 14 additions & 0 deletions flutter/ios/ci_scripts/ci_post_clone.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ echo "$MC_LOG_PREFIX runner is ${runner}"

echo "$MC_LOG_PREFIX ========== Install dependencies =========="

# Xcode Cloud does not always hand out the same machine. The macOS 27 image
# resolves `brew` to the native arm64 install at /opt/homebrew, but a macOS
# 26.5 image resolves it to the x86_64 install at /usr/local and runs it under
# Rosetta 2. Homebrew stopped building Intel bottles in September 2026, so on
# that machine `brew install protobuf` falls back to a source build and dies
# with "clang: error: unsupported argument 'westmere' to option '-march='"
# (Rosetta reports a westmere CPU that the native clang will not accept).
# Prefer the native Homebrew whenever it is there; this is a no-op when it is
# not, and the `brew config` below records which one we actually got.
if [ -x /opt/homebrew/bin/brew ]; then
export PATH="/opt/homebrew/bin:$PATH"
fi
echo "$MC_LOG_PREFIX brew is $(command -v brew)"

brew update --quiet || true
echo "$MC_LOG_PREFIX brew version:" && brew config

Expand Down
6 changes: 6 additions & 0 deletions mobile_back_apple/cpp/backend_coreml/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ swift_library(
srcs = [
"coreml_util.swift",
],
# Xcode 27 no longer defaults the language mode, so swiftc refuses to build
# without one. The source is mode-agnostic; 6 is what Xcode 27 would pick.
copts = [
"-swift-version",
"6",
],
generates_header = True,
deps = [
":apple_frameworks",
Expand Down
Loading