From 9f7301e53c6eda3823e19fbf14919389c54e941c Mon Sep 17 00:00:00 2001 From: Camille Simon <43054281+camsim99@users.noreply.github.com> Date: Mon, 11 Aug 2025 16:18:09 -0700 Subject: [PATCH 1/5] [3.36] Create `release-candidate-branch.version` (#173583) As the title says :) Part of https://github.com/flutter/flutter/issues/173427. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. --- bin/internal/release-candidate-branch.version | 1 + 1 file changed, 1 insertion(+) create mode 100644 bin/internal/release-candidate-branch.version diff --git a/bin/internal/release-candidate-branch.version b/bin/internal/release-candidate-branch.version new file mode 100644 index 0000000000000..ef3df3a8b7408 --- /dev/null +++ b/bin/internal/release-candidate-branch.version @@ -0,0 +1 @@ +flutter-3.36-candidate.0 From 74338730f69c0599efa7636c1bdec2f7715e3d12 Mon Sep 17 00:00:00 2001 From: Victoria Ashworth <15619084+vashworth@users.noreply.github.com> Date: Tue, 12 Aug 2025 17:34:38 -0500 Subject: [PATCH 2/5] [CP beta] Use LLDB as the default debugging method for iOS 17+ and Xcode 26+ (#173443) (#173659) Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request. ### Issue Link: Part 2 of https://github.com/flutter/flutter/issues/144218 ### Changelog Description: Explain this cherry pick in one line that is accessible to most Flutter developers. See [best practices](https://github.com/flutter/flutter/blob/main/docs/releases/Hotfix-Documentation-Best-Practices.md) for examples Use LLDB and devicectl for deploying to physical iOS devices when using Xcode 26. This will allow Xcode 26 to `flutter run` twice in a row. ### Impact Description: What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)? Does it impact development (ex. flutter doctor crashes when Android Studio is installed), or the shipping production app (the app crashes on launch) Flutter developers running Xcode 26 can `flutter run` to a tethered iOS device once. However subsequent `flutter run` attempts are likely to fail. ### Workaround: Is there a workaround for this issue? Quitting and reopening Xcode. ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: What are the steps to validate that this fix works? Create a flutter project and run `flutter run` twice in a row with a physical iOS 17+ device and Xcode 26. --- .ci.yaml | 10 + TESTOWNERS | 1 + .../hot_mode_dev_cycle_ios_xcode_debug.dart | 45 ++ packages/flutter_tools/lib/src/device.dart | 9 +- packages/flutter_tools/lib/src/features.dart | 21 + .../lib/src/flutter_features.dart | 3 + .../lib/src/ios/core_devices.dart | 61 +-- .../flutter_tools/lib/src/ios/devices.dart | 301 ++++++++----- packages/flutter_tools/lib/src/ios/lldb.dart | 8 +- .../flutter_tools/lib/src/macos/xcdevice.dart | 1 + .../test/general.shard/device_test.dart | 16 - .../general.shard/flutter_validator_test.dart | 3 + .../general.shard/ios/core_devices_test.dart | 416 +----------------- .../test/general.shard/ios/devices_test.dart | 23 + .../ios/ios_device_install_test.dart | 4 + .../ios/ios_device_project_test.dart | 4 + .../ios_device_start_nonprebuilt_test.dart | 109 ++++- .../ios/ios_device_start_prebuilt_test.dart | 415 ++++++++++++++++- .../test/general.shard/ios/lldb_test.dart | 6 +- packages/flutter_tools/test/src/fakes.dart | 7 + 20 files changed, 857 insertions(+), 606 deletions(-) create mode 100644 dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_xcode_debug.dart diff --git a/.ci.yaml b/.ci.yaml index 9672d7b07ef26..cf27cb82731e6 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -5481,6 +5481,16 @@ targets: ["devicelab", "hostonly", "mac"] task_name: hot_mode_dev_cycle_ios_simulator + - name: Mac_ios hot_mode_dev_cycle_ios_xcode_debug + recipe: devicelab/devicelab_drone + bringup: true + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "ios", "mac"] + task_name: hot_mode_dev_cycle_ios_xcode_debug + - name: Mac_ios fullscreen_textfield_perf_ios__e2e_summary recipe: devicelab/devicelab_drone presubmit: false diff --git a/TESTOWNERS b/TESTOWNERS index 07827f1e9be41..63beb5f2af2cb 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -267,6 +267,7 @@ /dev/devicelab/bin/tasks/hello_world_macos__compile.dart @cbracken @flutter/desktop /dev/devicelab/bin/tasks/hello_world_win_desktop__compile.dart @yaakovschectman @flutter/desktop /dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_simulator.dart @louisehsu @flutter/tool +/dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_xcode_debug.dart @vashworth @flutter/tool /dev/devicelab/bin/tasks/hot_mode_dev_cycle_macos_target__benchmark.dart @cbracken @flutter/tool /dev/devicelab/bin/tasks/hot_mode_dev_cycle_win_target__benchmark.dart @cbracken @flutter/desktop /dev/devicelab/bin/tasks/integration_ui_test_test_macos.dart @cbracken @flutter/desktop diff --git a/dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_xcode_debug.dart b/dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_xcode_debug.dart new file mode 100644 index 0000000000000..0beaa1d5d8815 --- /dev/null +++ b/dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_xcode_debug.dart @@ -0,0 +1,45 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/task_result.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; +import 'package:flutter_devicelab/tasks/hot_mode_tests.dart'; +import 'package:path/path.dart' as path; + +/// This is a test to validate that Xcode debugging still works now that LLDB is the default. +Future main() async { + await task(() async { + deviceOperatingSystem = DeviceOperatingSystem.ios; + try { + await disableLLDBDebugging(); + // This isn't actually a benchmark test, so do not use the returned `benchmarkScoreKeys` result. + await createHotModeTest()(); + return TaskResult.success(null); + } finally { + await enableLLDBDebugging(); + } + }); +} + +Future disableLLDBDebugging() async { + final int configResult = await exec(path.join(flutterDirectory.path, 'bin', 'flutter'), [ + 'config', + '--no-enable-lldb-debugging', + ]); + if (configResult != 0) { + print('Failed to disable configuration, tasks may not run.'); + } +} + +Future enableLLDBDebugging() async { + final int configResult = await exec(path.join(flutterDirectory.path, 'bin', 'flutter'), [ + 'config', + '--enable-lldb-debugging', + ], canFail: true); + if (configResult != 0) { + print('Failed to enable configuration.'); + } +} diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 19e925a3f03bf..57030638fa05d 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -1198,7 +1198,6 @@ class DebuggingOptions { String? route, Map platformArgs, { DeviceConnectionInterface interfaceType = DeviceConnectionInterface.attached, - bool isCoreDevice = false, }) { return [ if (enableDartProfiling) '--enable-dart-profiling', @@ -1212,13 +1211,7 @@ class DebuggingOptions { if (environmentType == EnvironmentType.simulator && dartFlags.isNotEmpty) '--dart-flags=$dartFlags', if (useTestFonts) '--use-test-fonts', - // Core Devices (iOS 17 devices) are debugged through Xcode so don't - // include these flags, which are used to check if the app was launched - // via Flutter CLI and `ios-deploy`. - if (debuggingEnabled && !isCoreDevice) ...[ - '--enable-checked-mode', - '--verify-entry-points', - ], + if (debuggingEnabled) ...['--enable-checked-mode', '--verify-entry-points'], if (enableSoftwareRendering) '--enable-software-rendering', if (traceSystrace) '--trace-systrace', if (traceToFile != null) '--trace-to-file="$traceToFile"', diff --git a/packages/flutter_tools/lib/src/features.dart b/packages/flutter_tools/lib/src/features.dart index 21733b69e3f16..31d02666a59b0 100644 --- a/packages/flutter_tools/lib/src/features.dart +++ b/packages/flutter_tools/lib/src/features.dart @@ -67,6 +67,9 @@ abstract class FeatureFlags { /// Whether desktop windowing is enabled. bool get isWindowingEnabled; + /// Whether physical iOS devices are debugging with LLDB. + bool get isLLDBDebuggingEnabled; + /// Whether a particular feature is enabled for the current channel. /// /// Prefer using one of the specific getters above instead of this API. @@ -87,6 +90,7 @@ abstract class FeatureFlags { swiftPackageManager, omitLegacyVersionFile, windowingFeature, + lldbDebugging, ]; /// All current Flutter feature flags that can be configured. @@ -218,6 +222,23 @@ const windowingFeature = Feature( master: FeatureChannelSetting(available: true), ); +/// Enable LLDB debugging for physical iOS devices. When LLDB debugging is off, +/// Xcode debugging is used instead. +/// +/// Requires iOS 17+ and Xcode 26+. If those requirements are not met, the previous +/// default debugging method is used instead. +const lldbDebugging = Feature( + name: 'support for debugging with LLDB for physical iOS devices', + extraHelpText: + 'If LLDB debugging is off, Xcode debugging is used instead. ' + 'Only available for iOS 17 or newer devices. Requires Xcode 26 or greater.', + configSetting: 'enable-lldb-debugging', + environmentOverride: 'FLUTTER_LLDB_DEBUGGING', + master: FeatureChannelSetting(available: true, enabledByDefault: true), + beta: FeatureChannelSetting(available: true, enabledByDefault: true), + stable: FeatureChannelSetting(available: true, enabledByDefault: true), +); + /// A [Feature] is a process for conditionally enabling tool features. /// /// All settings are optional, and if not provided will generally default to diff --git a/packages/flutter_tools/lib/src/flutter_features.dart b/packages/flutter_tools/lib/src/flutter_features.dart index fdec727aa5d0e..386fcbc625e4b 100644 --- a/packages/flutter_tools/lib/src/flutter_features.dart +++ b/packages/flutter_tools/lib/src/flutter_features.dart @@ -57,6 +57,9 @@ mixin FlutterFeatureFlagsIsEnabled implements FeatureFlags { @override bool get isWindowingEnabled => isEnabled(windowingFeature); + + @override + bool get isLLDBDebuggingEnabled => isEnabled(lldbDebugging); } interface class FlutterFeatureFlags extends FeatureFlags with FlutterFeatureFlagsIsEnabled { diff --git a/packages/flutter_tools/lib/src/ios/core_devices.dart b/packages/flutter_tools/lib/src/ios/core_devices.dart index 418f0559a5951..bab9e5d27ce1b 100644 --- a/packages/flutter_tools/lib/src/ios/core_devices.dart +++ b/packages/flutter_tools/lib/src/ios/core_devices.dart @@ -66,7 +66,7 @@ class IOSCoreDeviceLauncher { } // Launch app to device - final IOSCoreDeviceLaunchResult? launchResult = await _coreDeviceControl.launchAppInternal( + final IOSCoreDeviceLaunchResult? launchResult = await _coreDeviceControl.launchApp( deviceId: deviceId, bundleId: bundleId, launchArguments: launchArguments, @@ -99,7 +99,7 @@ class IOSCoreDeviceLauncher { } // Launch app on device, but start it stopped so it will wait until the debugger is attached before starting. - final IOSCoreDeviceLaunchResult? launchResult = await _coreDeviceControl.launchAppInternal( + final IOSCoreDeviceLaunchResult? launchResult = await _coreDeviceControl.launchApp( deviceId: deviceId, bundleId: bundleId, launchArguments: launchArguments, @@ -532,66 +532,11 @@ class IOSCoreDeviceControl { } } - Future launchApp({ - required String deviceId, - required String bundleId, - List launchArguments = const [], - }) async { - if (!_xcode.isDevicectlInstalled) { - _logger.printError('devicectl is not installed.'); - return false; - } - - final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.'); - final File output = tempDirectory.childFile('launch_results.json'); - output.createSync(); - - final command = [ - ..._xcode.xcrunCommand(), - 'devicectl', - 'device', - 'process', - 'launch', - '--device', - deviceId, - bundleId, - if (launchArguments.isNotEmpty) ...launchArguments, - '--json-output', - output.path, - ]; - - try { - await _processUtils.run(command, throwOnError: true); - final String stringOutput = output.readAsStringSync(); - - try { - final Object? decodeResult = (json.decode(stringOutput) as Map)['info']; - if (decodeResult is Map && decodeResult['outcome'] == 'success') { - return true; - } - _logger.printError('devicectl returned unexpected JSON response: $stringOutput'); - return false; - } on FormatException { - // We failed to parse the devicectl output, or it returned junk. - _logger.printError('devicectl returned non-JSON response: $stringOutput'); - return false; - } - } on ProcessException catch (err) { - _logger.printError('Error executing devicectl: $err'); - return false; - } finally { - tempDirectory.deleteSync(recursive: true); - } - } - /// Launches the app on the device. /// /// If [startStopped] is true, the app will be launched and paused, waiting /// for a debugger to attach. - // TODO(vashworth): Rename this method to launchApp and replace old version. - // See https://github.com/flutter/flutter/issues/173416. - @visibleForTesting - Future launchAppInternal({ + Future launchApp({ required String deviceId, required String bundleId, List launchArguments = const [], diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 98b4e87d6e645..a6dd75ee5a90d 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; import 'package:process/process.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import 'package:vm_service/vm_service.dart' as vm_service; import '../application_package.dart'; @@ -24,6 +25,7 @@ import '../darwin/darwin.dart'; import '../device.dart'; import '../device_port_forwarder.dart'; import '../device_vm_service_discovery_for_attach.dart'; +import '../features.dart'; import '../globals.dart' as globals; import '../macos/xcdevice.dart'; import '../mdns_discovery.dart'; @@ -59,6 +61,15 @@ In the meantime, we recommend these temporary workarounds: profile mode via --release or --profile flags. ════════════════════════════════════════════════════════════════════════════════'''; +enum IOSDeploymentMethod { + iosDeployLaunch, + iosDeployLaunchAndAttach, + coreDeviceWithoutDebugger, + coreDeviceWithLLDB, + coreDeviceWithXcode, + coreDeviceWithXcodeFallback, +} + class IOSDevices extends PollingDeviceDiscovery { IOSDevices({ required Platform platform, @@ -284,6 +295,7 @@ class IOSDevice extends Device { required XcodeDebug xcodeDebug, required IProxy iProxy, required super.logger, + required Analytics analytics, }) : _sdkVersion = sdkVersion, _iosDeploy = iosDeploy, _iMobileDevice = iMobileDevice, @@ -293,6 +305,7 @@ class IOSDevice extends Device { _iproxy = iProxy, _fileSystem = fileSystem, _logger = logger, + _analytics = analytics, _platform = platform, super(category: Category.mobile, platformType: PlatformType.ios, ephemeral: true) { if (!_platform.isMacOS) { @@ -303,14 +316,12 @@ class IOSDevice extends Device { final String? _sdkVersion; final IOSDeploy _iosDeploy; + final Analytics _analytics; final FileSystem _fileSystem; final Logger _logger; final Platform _platform; final IMobileDevice _iMobileDevice; final IOSCoreDeviceControl _coreDeviceControl; - - // TODO(vashworth): See https://github.com/flutter/flutter/issues/173416. - // ignore: unused_field final IOSCoreDeviceLauncher _coreDeviceLauncher; final XcodeDebug _xcodeDebug; final IProxy _iproxy; @@ -492,7 +503,7 @@ class IOSDevice extends Device { _logger.printError('Could not build the precompiled application for the device.'); await diagnoseXcodeBuildFailure( buildResult, - analytics: globals.analytics, + analytics: _analytics, fileSystem: globals.fs, logger: globals.logger, platform: FlutterDarwinPlatform.ios, @@ -519,9 +530,10 @@ class IOSDevice extends Device { route, platformArgs, interfaceType: connectionInterface, - isCoreDevice: isCoreDevice, ); Status startAppStatus = _logger.startProgress('Installing and launching...'); + + IOSDeploymentMethod? deploymentMethod; try { ProtocolDiscovery? vmServiceDiscovery; var installationResult = 1; @@ -537,18 +549,21 @@ class IOSDevice extends Device { } if (isCoreDevice) { - installationResult = - await _startAppOnCoreDevice( - debuggingOptions: debuggingOptions, - package: package, - launchArguments: launchArguments, - mainPath: mainPath, - discoveryTimeout: discoveryTimeout, - shutdownHooks: shutdownHooks ?? globals.shutdownHooks, - ) - ? 0 - : 1; + final ( + bool result, + IOSDeploymentMethod coreDeviceDeploymentMethod, + ) = await _startAppOnCoreDevice( + debuggingOptions: debuggingOptions, + package: package, + launchArguments: launchArguments, + mainPath: mainPath, + discoveryTimeout: discoveryTimeout, + shutdownHooks: shutdownHooks ?? globals.shutdownHooks, + ); + installationResult = result ? 0 : 1; + deploymentMethod = coreDeviceDeploymentMethod; } else if (iosDeployDebugger == null) { + deploymentMethod = IOSDeploymentMethod.iosDeployLaunch; installationResult = await _iosDeploy.launchApp( deviceId: id, bundlePath: bundle.path, @@ -558,15 +573,30 @@ class IOSDevice extends Device { uninstallFirst: debuggingOptions.uninstallFirst, ); } else { + deploymentMethod = IOSDeploymentMethod.iosDeployLaunchAndAttach; installationResult = await iosDeployDebugger!.launchAndAttach() ? 0 : 1; } if (installationResult != 0) { + _analytics.send( + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: deploymentMethod.name, + result: 'launch failed', + ), + ); _printInstallError(bundle); await dispose(); return LaunchResult.failed(); } if (!debuggingOptions.debuggingEnabled) { + _analytics.send( + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: deploymentMethod.name, + result: 'release success', + ), + ); return LaunchResult.succeeded(); } @@ -587,13 +617,6 @@ class IOSDevice extends Device { _logger.printError( 'The Dart VM Service was not discovered after $defaultTimeout seconds. This is taking much longer than expected...', ); - if (isCoreDevice && debuggingOptions.debuggingEnabled) { - _logger.printError( - 'Open the Xcode window the project is opened in to ensure the app ' - 'is running. If the app is not running, try selecting "Product > Run" ' - 'to fix the problem.', - ); - } // If debugging with a wireless device and the timeout is reached, remind the // user to allow local network permissions. if (isWirelesslyConnected) { @@ -632,6 +655,13 @@ class IOSDevice extends Device { if (serviceURL == null) { await iosDeployDebugger?.stopAndDumpBacktrace(); await dispose(); + _analytics.send( + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: deploymentMethod.name, + result: 'wireless debugging failed', + ), + ); return LaunchResult.failed(); } @@ -688,13 +718,36 @@ class IOSDevice extends Device { if (localUri == null) { await iosDeployDebugger?.stopAndDumpBacktrace(); await dispose(); + _analytics.send( + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: deploymentMethod.name, + result: 'debugging failed', + ), + ); return LaunchResult.failed(); } + _analytics.send( + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: deploymentMethod.name, + result: 'debugging success', + ), + ); return LaunchResult.succeeded(vmServiceUri: localUri); } on ProcessException catch (e) { await iosDeployDebugger?.stopAndDumpBacktrace(); _logger.printError(e.message); await dispose(); + if (deploymentMethod != null) { + _analytics.send( + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: deploymentMethod.name, + result: 'process exception', + ), + ); + } return LaunchResult.failed(); } finally { startAppStatus.stop(); @@ -881,17 +934,25 @@ class IOSDevice extends Device { ); } + /// Uses either `devicectl` or Xcode automation to install, launch, and debug + /// apps on physical iOS devices. + /// /// Starting with Xcode 15 and iOS 17, `ios-deploy` stopped working due to /// the new CoreDevice connectivity stack. Previously, `ios-deploy` was used /// to install the app, launch the app, and start `debugserver`. + /// /// Xcode 15 introduced a new command line tool called `devicectl` that /// includes much of the functionality supplied by `ios-deploy`. However, - /// `devicectl` lacks the ability to start a `debugserver` and therefore `ptrace`, which are needed - /// for debug mode due to using a JIT Dart VM. + /// `devicectl` lacked the ability to start a `debugserver` and therefore `ptrace`, + /// which are needed for debug mode due to using a JIT Dart VM. + /// + /// Xcode 16 introduced a command to lldb that allows you to start a debugserver, which + /// can be used in unison with `devicectl`. /// /// Therefore, when starting an app on a CoreDevice, use `devicectl` when - /// debugging is not enabled. Otherwise, use Xcode automation. - Future _startAppOnCoreDevice({ + /// debugging is not enabled. If using Xcode 16, use `devicectl` and `lldb`. + /// Otherwise use Xcode automation. + Future<(bool, IOSDeploymentMethod)> _startAppOnCoreDevice({ required DebuggingOptions debuggingOptions, required IOSApp package, required List launchArguments, @@ -908,98 +969,140 @@ class IOSDevice extends Device { bundlePath: package.deviceBundlePath, ); if (!installSuccess) { - return installSuccess; + return (installSuccess, IOSDeploymentMethod.coreDeviceWithoutDebugger); } // Launch app to device - final bool launchSuccess = await _coreDeviceControl.launchApp( + final IOSCoreDeviceLaunchResult? launchResult = await _coreDeviceControl.launchApp( deviceId: id, bundleId: package.id, launchArguments: launchArguments, ); + final bool launchSuccess = launchResult != null && launchResult.outcome == 'success'; - return launchSuccess; - } else { - _logger.printStatus( - 'You may be prompted to give access to control Xcode. Flutter uses Xcode ' - 'to run your app. If access is not allowed, you can change this through ' - 'your Settings > Privacy & Security > Automation.', + return (launchSuccess, IOSDeploymentMethod.coreDeviceWithoutDebugger); + } + + IOSDeploymentMethod? deploymentMethod; + + // Xcode 16 introduced a way to start and attach to a debugserver through LLDB. + // However, it doesn't work reliably until Xcode 26. + // Use LLDB if Xcode version is greater than 26 and the feature is enabled. + final Version? xcodeVersion = globals.xcode?.currentVersion; + final bool lldbFeatureEnabled = featureFlags.isLLDBDebuggingEnabled; + if (xcodeVersion != null && xcodeVersion.major >= 26 && lldbFeatureEnabled) { + final bool launchSuccess = await _coreDeviceLauncher.launchAppWithLLDBDebugger( + deviceId: id, + bundlePath: package.deviceBundlePath, + bundleId: package.id, + launchArguments: launchArguments, ); - final launchTimeout = isWirelesslyConnected ? 45 : 30; - final timer = Timer(discoveryTimeout ?? Duration(seconds: launchTimeout), () { - _logger.printError( - 'Xcode is taking longer than expected to start debugging the app. ' - 'Ensure the project is opened in Xcode.', + + // If it succeeds to launch with LLDB, return, otherwise continue on to + // try launching with Xcode. + if (launchSuccess) { + return (launchSuccess, IOSDeploymentMethod.coreDeviceWithLLDB); + } else { + deploymentMethod = IOSDeploymentMethod.coreDeviceWithXcodeFallback; + _analytics.send( + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithLLDB.name, + result: 'launch failed', + ), ); - }); + } + } - XcodeDebugProject debugProject; - final FlutterProject flutterProject = FlutterProject.current(); + deploymentMethod ??= IOSDeploymentMethod.coreDeviceWithXcode; - if (package is PrebuiltIOSApp) { - debugProject = await _xcodeDebug.createXcodeProjectWithCustomBundle( - package.deviceBundlePath, - templateRenderer: globals.templateRenderer, - verboseLogging: _logger.isVerbose, - ); - } else if (package is BuildableIOSApp) { - // Before installing/launching/debugging with Xcode, update the build - // settings to use a custom configuration build directory so Xcode - // knows where to find the app bundle to launch. - final Directory bundle = _fileSystem.directory(package.deviceBundlePath); - await updateGeneratedXcodeProperties( - project: flutterProject, - buildInfo: debuggingOptions.buildInfo, - targetOverride: mainPath, - configurationBuildDir: bundle.parent.absolute.path, - ); + // If LLDB is not available or fails, fallback to using Xcode. + _logger.printStatus( + 'You may be prompted to give access to control Xcode. Flutter uses Xcode ' + 'to run your app. If access is not allowed, you can change this through ' + 'your Settings > Privacy & Security > Automation.', + ); + final launchTimeout = isWirelesslyConnected ? 45 : 30; + final timer = Timer(discoveryTimeout ?? Duration(seconds: launchTimeout), () { + _logger.printError( + 'Xcode is taking longer than expected to start debugging the app. ' + 'If the issue persists, try closing Xcode and re-running your Flutter command.', + ); + }); - final IosProject project = package.project; - final XcodeProjectInfo? projectInfo = await project.projectInfo(); - if (projectInfo == null) { - globals.printError('Xcode project not found.'); - return false; - } - if (project.xcodeWorkspace == null) { - globals.printError('Unable to get Xcode workspace.'); - return false; - } - final String? scheme = projectInfo.schemeFor(debuggingOptions.buildInfo); - if (scheme == null) { - projectInfo.reportFlavorNotFoundAndExit(); - } + XcodeDebugProject debugProject; + final FlutterProject flutterProject = FlutterProject.current(); - _xcodeDebug.ensureXcodeDebuggerLaunchAction(project.xcodeProjectSchemeFile(scheme: scheme)); + if (package is PrebuiltIOSApp) { + debugProject = await _xcodeDebug.createXcodeProjectWithCustomBundle( + package.deviceBundlePath, + templateRenderer: globals.templateRenderer, + verboseLogging: _logger.isVerbose, + ); + } else if (package is BuildableIOSApp) { + // Before installing/launching/debugging with Xcode, update the build + // settings to use a custom configuration build directory so Xcode + // knows where to find the app bundle to launch. + final Directory bundle = _fileSystem.directory(package.deviceBundlePath); + await updateGeneratedXcodeProperties( + project: flutterProject, + buildInfo: debuggingOptions.buildInfo, + targetOverride: mainPath, + configurationBuildDir: bundle.parent.absolute.path, + ); - debugProject = XcodeDebugProject( - scheme: scheme, - xcodeProject: project.xcodeProject, - xcodeWorkspace: project.xcodeWorkspace!, - hostAppProjectName: project.hostAppProjectName, - expectedConfigurationBuildDir: bundle.parent.absolute.path, - verboseLogging: _logger.isVerbose, - ); - } else { - // This should not happen. Currently, only PrebuiltIOSApp and - // BuildableIOSApp extend from IOSApp. - _logger.printError('IOSApp type ${package.runtimeType} is not recognized.'); - return false; + final IosProject project = package.project; + final XcodeProjectInfo? projectInfo = await project.projectInfo(); + if (projectInfo == null) { + globals.printError('Xcode project not found.'); + return (false, deploymentMethod); + } + if (project.xcodeWorkspace == null) { + globals.printError('Unable to get Xcode workspace.'); + return (false, deploymentMethod); + } + final String? scheme = projectInfo.schemeFor(debuggingOptions.buildInfo); + if (scheme == null) { + projectInfo.reportFlavorNotFoundAndExit(); } - final bool debugSuccess = await _xcodeDebug.debugApp( - project: debugProject, - deviceId: id, - launchArguments: launchArguments, + _xcodeDebug.ensureXcodeDebuggerLaunchAction(project.xcodeProjectSchemeFile(scheme: scheme)); + + debugProject = XcodeDebugProject( + scheme: scheme, + xcodeProject: project.xcodeProject, + xcodeWorkspace: project.xcodeWorkspace!, + hostAppProjectName: project.hostAppProjectName, + expectedConfigurationBuildDir: bundle.parent.absolute.path, + verboseLogging: _logger.isVerbose, ); - timer.cancel(); + } else { + // This should not happen. Currently, only PrebuiltIOSApp and + // BuildableIOSApp extend from IOSApp. + _logger.printError('IOSApp type ${package.runtimeType} is not recognized.'); + return (false, deploymentMethod); + } - // Kill Xcode on shutdown when running from CI - if (debuggingOptions.usingCISystem) { - shutdownHooks.addShutdownHook(() => _xcodeDebug.exit(force: true)); - } + // Core Devices (iOS 17 devices) are debugged through Xcode so don't + // include these flags, which are used to check if the app was launched + // via Flutter CLI and `ios-deploy`. + final List filteredLaunchArguments = launchArguments + .where((String arg) => arg != '--enable-checked-mode' && arg != '--verify-entry-points') + .toList(); + + final bool debugSuccess = await _xcodeDebug.debugApp( + project: debugProject, + deviceId: id, + launchArguments: filteredLaunchArguments, + ); + timer.cancel(); - return debugSuccess; + // Kill Xcode on shutdown when running from CI + if (debuggingOptions.usingCISystem) { + shutdownHooks.addShutdownHook(() => _xcodeDebug.exit(force: true)); } + + return (debugSuccess, deploymentMethod); } @override @@ -1012,7 +1115,7 @@ class IOSDevice extends Device { if (_xcodeDebug.debugStarted) { return _xcodeDebug.exit(); } - return false; + return _coreDeviceLauncher.stopApp(deviceId: id); } @override diff --git a/packages/flutter_tools/lib/src/ios/lldb.dart b/packages/flutter_tools/lib/src/ios/lldb.dart index 76912f3d9a1c9..025876ecec4f6 100644 --- a/packages/flutter_tools/lib/src/ios/lldb.dart +++ b/packages/flutter_tools/lib/src/ios/lldb.dart @@ -99,7 +99,7 @@ return False await _attachToAppProcess(appProcessId); await _resumeProcess(); } on _LLDBError catch (e) { - _logger.printError('lldb failed with error: ${e.message}'); + _logger.printTrace('lldb failed with error: ${e.message}'); exit(); return false; } finally { @@ -115,7 +115,7 @@ return False /// to `stderr`, complete with an error and stop the process. Future _startLLDB(int appProcessId) async { if (_lldbProcess != null) { - _logger.printError( + _logger.printTrace( 'An LLDB process is already running. It must be stopped before starting a new one.', ); return false; @@ -139,7 +139,7 @@ return False .transform(utf8.decoder) .transform(const LineSplitter()) .listen((String line) { - _logger.printError('[lldb]: $line'); + _logger.printTrace('[lldb]: $line'); _monitorError(line); }); @@ -155,7 +155,7 @@ return False }), ); } on ProcessException catch (exception) { - _logger.printError('Process exception running lldb:\n$exception'); + _logger.printTrace('Process exception running lldb:\n$exception'); return false; } return true; diff --git a/packages/flutter_tools/lib/src/macos/xcdevice.dart b/packages/flutter_tools/lib/src/macos/xcdevice.dart index 687784cb37f19..9534531b1eb7d 100644 --- a/packages/flutter_tools/lib/src/macos/xcdevice.dart +++ b/packages/flutter_tools/lib/src/macos/xcdevice.dart @@ -602,6 +602,7 @@ class XCDevice { iProxy: _iProxy, fileSystem: globals.fs, logger: _logger, + analytics: globals.analytics, iosDeploy: _iosDeploy, iMobileDevice: _iMobileDevice, coreDeviceControl: _coreDeviceControl, diff --git a/packages/flutter_tools/test/general.shard/device_test.dart b/packages/flutter_tools/test/general.shard/device_test.dart index e18b510f4022d..ad86a756eeef9 100644 --- a/packages/flutter_tools/test/general.shard/device_test.dart +++ b/packages/flutter_tools/test/general.shard/device_test.dart @@ -791,22 +791,6 @@ void main() { }, ); - testWithoutContext( - 'Get launch arguments for physical CoreDevice with debugging enabled with no launch arguments', - () { - final original = DebuggingOptions.enabled(BuildInfo.debug); - - final List launchArguments = original.getIOSLaunchArguments( - EnvironmentType.physical, - null, - {}, - isCoreDevice: true, - ); - - expect(launchArguments.join(' '), ['--enable-dart-profiling'].join(' ')); - }, - ); - testWithoutContext('Get launch arguments for physical device with iPv4 network connection', () { final original = DebuggingOptions.enabled(BuildInfo.debug); diff --git a/packages/flutter_tools/test/general.shard/flutter_validator_test.dart b/packages/flutter_tools/test/general.shard/flutter_validator_test.dart index b9b147c0a7db6..2f96ce4e71fd9 100644 --- a/packages/flutter_tools/test/general.shard/flutter_validator_test.dart +++ b/packages/flutter_tools/test/general.shard/flutter_validator_test.dart @@ -818,6 +818,9 @@ class FakeFlutterFeatures extends FeatureFlags { @override bool get isWindowingEnabled => _enabled; + @override + bool get isLLDBDebuggingEnabled => _enabled; + @override final List allFeatures; diff --git a/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart b/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart index eedfa39685475..80434cac57e6f 100644 --- a/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart @@ -681,13 +681,13 @@ void main() { }); testWithoutContext('fails to launch app', () async { - final bool status = await deviceControl.launchApp( + final IOSCoreDeviceLaunchResult? result = await deviceControl.launchApp( deviceId: 'device-id', bundleId: 'com.example.flutterApp', ); expect(fakeProcessManager, hasNoRemainingExpectations); - expect(logger.errorText, contains('devicectl is not installed.')); - expect(status, isFalse); + expect(logger.traceText, contains('devicectl is not installed.')); + expect(result, isNull); }); testWithoutContext('fails to check if app is installed', () async { @@ -1287,403 +1287,7 @@ invalid JSON ), ); - final bool status = await deviceControl.launchApp(deviceId: deviceId, bundleId: bundleId); - - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(logger.errorText, isEmpty); - expect(tempFile, isNot(exists)); - expect(status, true); - }); - - testWithoutContext('Successful launch with launch args', () async { - const deviceControlOutput = ''' -{ - "info" : { - "arguments" : [ - "devicectl", - "device", - "process", - "launch", - "--device", - "00001234-0001234A3C03401E", - "com.example.flutterApp", - "--arg1", - "--arg2", - "--json-output", - "/var/folders/wq/randompath/T/flutter_tools.rand0/core_devices.rand0/install_results.json" - ], - "commandType" : "devicectl.device.process.launch", - "environment" : { - - }, - "outcome" : "success", - "version" : "341" - }, - "result" : { - "deviceIdentifier" : "123456BB5-AEDE-7A22-B890-1234567890DD", - "launchOptions" : { - "activatedWhenStarted" : true, - "arguments" : [ - - ], - "environmentVariables" : { - "TERM" : "vt100" - }, - "platformSpecificOptions" : { - - }, - "startStopped" : false, - "terminateExistingInstances" : false, - "user" : { - "active" : true - } - }, - "process" : { - "auditToken" : [ - 12345, - 678 - ], - "executable" : "file:///private/var/containers/Bundle/Application/12345E6A-7F89-0C12-345E-F6A7E890CFF1/Runner.app/Runner", - "processIdentifier" : 1234 - } - } -} -'''; - final File tempFile = fileSystem.systemTempDirectory - .childDirectory('core_devices.rand0') - .childFile('launch_results.json'); - fakeProcessManager.addCommand( - FakeCommand( - command: [ - 'xcrun', - 'devicectl', - 'device', - 'process', - 'launch', - '--device', - deviceId, - bundleId, - '--arg1', - '--arg2', - '--json-output', - tempFile.path, - ], - onRun: (_) { - expect(tempFile, exists); - tempFile.writeAsStringSync(deviceControlOutput); - }, - ), - ); - - final bool status = await deviceControl.launchApp( - deviceId: deviceId, - bundleId: bundleId, - launchArguments: ['--arg1', '--arg2'], - ); - - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(logger.errorText, isEmpty); - expect(tempFile, isNot(exists)); - expect(status, true); - }); - - testWithoutContext('devicectl fails install with an error', () async { - const deviceControlOutput = ''' -{ - "error" : { - "code" : -10814, - "domain" : "NSOSStatusErrorDomain", - "userInfo" : { - "_LSFunction" : { - "string" : "runEvaluator" - }, - "_LSLine" : { - "int" : 1608 - } - } - }, - "info" : { - "arguments" : [ - "devicectl", - "device", - "process", - "launch", - "--device", - "00001234-0001234A3C03401E", - "com.example.flutterApp", - "--json-output", - "/var/folders/wq/randompath/T/flutter_tools.rand0/core_devices.rand0/install_results.json" - ], - "commandType" : "devicectl.device.process.launch", - "environment" : { - - }, - "outcome" : "failed", - "version" : "341" - } -} -'''; - final File tempFile = fileSystem.systemTempDirectory - .childDirectory('core_devices.rand0') - .childFile('launch_results.json'); - fakeProcessManager.addCommand( - FakeCommand( - command: [ - 'xcrun', - 'devicectl', - 'device', - 'process', - 'launch', - '--device', - deviceId, - bundleId, - '--json-output', - tempFile.path, - ], - onRun: (_) { - expect(tempFile, exists); - tempFile.writeAsStringSync(deviceControlOutput); - }, - exitCode: 1, - stderr: ''' -ERROR: The operation couldn?t be completed. (OSStatus error -10814.) (NSOSStatusErrorDomain error -10814.) - _LSFunction = runEvaluator - _LSLine = 1608 -''', - ), - ); - - final bool status = await deviceControl.launchApp(deviceId: deviceId, bundleId: bundleId); - - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(logger.errorText, contains('ERROR: The operation couldn?t be completed.')); - expect(tempFile, isNot(exists)); - expect(status, false); - }); - - testWithoutContext('devicectl fails install without an error', () async { - const deviceControlOutput = ''' -{ - "error" : { - "code" : -10814, - "domain" : "NSOSStatusErrorDomain", - "userInfo" : { - "_LSFunction" : { - "string" : "runEvaluator" - }, - "_LSLine" : { - "int" : 1608 - } - } - }, - "info" : { - "arguments" : [ - "devicectl", - "device", - "process", - "launch", - "--device", - "00001234-0001234A3C03401E", - "com.example.flutterApp", - "--json-output", - "/var/folders/wq/randompath/T/flutter_tools.rand0/core_devices.rand0/install_results.json" - ], - "commandType" : "devicectl.device.process.launch", - "environment" : { - - }, - "outcome" : "failed", - "version" : "341" - } -} -'''; - final File tempFile = fileSystem.systemTempDirectory - .childDirectory('core_devices.rand0') - .childFile('launch_results.json'); - fakeProcessManager.addCommand( - FakeCommand( - command: [ - 'xcrun', - 'devicectl', - 'device', - 'process', - 'launch', - '--device', - deviceId, - bundleId, - '--json-output', - tempFile.path, - ], - onRun: (_) { - expect(tempFile, exists); - tempFile.writeAsStringSync(deviceControlOutput); - }, - ), - ); - - final bool status = await deviceControl.launchApp(deviceId: deviceId, bundleId: bundleId); - - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(tempFile, isNot(exists)); - expect(status, false); - }); - - testWithoutContext('fails launch because of unexpected JSON', () async { - const deviceControlOutput = ''' -{ - "valid_unexpected_json": true -} -'''; - final File tempFile = fileSystem.systemTempDirectory - .childDirectory('core_devices.rand0') - .childFile('launch_results.json'); - fakeProcessManager.addCommand( - FakeCommand( - command: [ - 'xcrun', - 'devicectl', - 'device', - 'process', - 'launch', - '--device', - deviceId, - bundleId, - '--json-output', - tempFile.path, - ], - onRun: (_) { - expect(tempFile, exists); - tempFile.writeAsStringSync(deviceControlOutput); - }, - ), - ); - - final bool status = await deviceControl.launchApp(deviceId: deviceId, bundleId: bundleId); - - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(logger.errorText, contains('devicectl returned unexpected JSON response')); - expect(tempFile, isNot(exists)); - expect(status, false); - }); - - testWithoutContext('fails launch because of invalid JSON', () async { - const deviceControlOutput = ''' -invalid JSON -'''; - final File tempFile = fileSystem.systemTempDirectory - .childDirectory('core_devices.rand0') - .childFile('launch_results.json'); - fakeProcessManager.addCommand( - FakeCommand( - command: [ - 'xcrun', - 'devicectl', - 'device', - 'process', - 'launch', - '--device', - deviceId, - bundleId, - '--json-output', - tempFile.path, - ], - onRun: (_) { - expect(tempFile, exists); - tempFile.writeAsStringSync(deviceControlOutput); - }, - ), - ); - - final bool status = await deviceControl.launchApp(deviceId: deviceId, bundleId: bundleId); - - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(logger.errorText, contains('devicectl returned non-JSON response')); - expect(tempFile, isNot(exists)); - expect(status, false); - }); - }); - - group('launchAppInternal', () { - const deviceId = 'device-id'; - const bundleId = 'com.example.flutterApp'; - - testWithoutContext('Successful launch without launch args', () async { - const deviceControlOutput = ''' -{ - "info" : { - "arguments" : [ - "devicectl", - "device", - "process", - "launch", - "--device", - "00001234-0001234A3C03401E", - "com.example.flutterApp", - "--json-output", - "/var/folders/wq/randompath/T/flutter_tools.rand0/core_devices.rand0/install_results.json" - ], - "commandType" : "devicectl.device.process.launch", - "environment" : { - - }, - "outcome" : "success", - "version" : "341" - }, - "result" : { - "deviceIdentifier" : "123456BB5-AEDE-7A22-B890-1234567890DD", - "launchOptions" : { - "activatedWhenStarted" : true, - "arguments" : [ - - ], - "environmentVariables" : { - "TERM" : "vt100" - }, - "platformSpecificOptions" : { - - }, - "startStopped" : false, - "terminateExistingInstances" : false, - "user" : { - "active" : true - } - }, - "process" : { - "auditToken" : [ - 12345, - 678 - ], - "executable" : "file:///private/var/containers/Bundle/Application/12345E6A-7F89-0C12-345E-F6A7E890CFF1/Runner.app/Runner", - "processIdentifier" : 1234 - } - } -} -'''; - final File tempFile = fileSystem.systemTempDirectory - .childDirectory('core_devices.rand0') - .childFile('launch_results.json'); - fakeProcessManager.addCommand( - FakeCommand( - command: [ - 'xcrun', - 'devicectl', - 'device', - 'process', - 'launch', - '--device', - deviceId, - bundleId, - '--json-output', - tempFile.path, - ], - onRun: (_) { - expect(tempFile, exists); - tempFile.writeAsStringSync(deviceControlOutput); - }, - ), - ); - - final IOSCoreDeviceLaunchResult? result = await deviceControl.launchAppInternal( + final IOSCoreDeviceLaunchResult? result = await deviceControl.launchApp( deviceId: deviceId, bundleId: bundleId, ); @@ -1775,7 +1379,7 @@ invalid JSON ), ); - final IOSCoreDeviceLaunchResult? result = await deviceControl.launchAppInternal( + final IOSCoreDeviceLaunchResult? result = await deviceControl.launchApp( deviceId: deviceId, bundleId: bundleId, launchArguments: ['--arg1', '--arg2'], @@ -1854,7 +1458,7 @@ ERROR: The operation couldn?t be completed. (OSStatus error -10814.) (NSOSStatus ), ); - final IOSCoreDeviceLaunchResult? result = await deviceControl.launchAppInternal( + final IOSCoreDeviceLaunchResult? result = await deviceControl.launchApp( deviceId: deviceId, bundleId: bundleId, ); @@ -1925,7 +1529,7 @@ ERROR: The operation couldn?t be completed. (OSStatus error -10814.) (NSOSStatus ), ); - final IOSCoreDeviceLaunchResult? result = await deviceControl.launchAppInternal( + final IOSCoreDeviceLaunchResult? result = await deviceControl.launchApp( deviceId: deviceId, bundleId: bundleId, ); @@ -1966,7 +1570,7 @@ ERROR: The operation couldn?t be completed. (OSStatus error -10814.) (NSOSStatus ), ); - final IOSCoreDeviceLaunchResult? result = await deviceControl.launchAppInternal( + final IOSCoreDeviceLaunchResult? result = await deviceControl.launchApp( deviceId: deviceId, bundleId: bundleId, ); @@ -2005,7 +1609,7 @@ invalid JSON ), ); - final IOSCoreDeviceLaunchResult? result = await deviceControl.launchAppInternal( + final IOSCoreDeviceLaunchResult? result = await deviceControl.launchApp( deviceId: deviceId, bundleId: bundleId, ); @@ -3584,7 +3188,7 @@ class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl { } @override - Future launchAppInternal({ + Future launchApp({ required String deviceId, required String bundleId, List launchArguments = const [], diff --git a/packages/flutter_tools/test/general.shard/ios/devices_test.dart b/packages/flutter_tools/test/general.shard/ios/devices_test.dart index 9b43d4c117663..f09b38040af65 100644 --- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart @@ -28,6 +28,7 @@ import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/xcode_debug.dart'; import 'package:flutter_tools/src/macos/xcdevice.dart'; import 'package:test/fake.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; import '../../src/fake_process_manager.dart'; @@ -79,6 +80,7 @@ void main() { logger: logger, platform: macPlatform, iosDeploy: iosDeploy, + analytics: FakeAnalytics(), iMobileDevice: iMobileDevice, coreDeviceControl: coreDeviceControl, coreDeviceLauncher: coreDeviceLauncher, @@ -101,6 +103,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -125,6 +128,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -148,6 +152,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -171,6 +176,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -194,6 +200,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -217,6 +224,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -242,6 +250,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -267,6 +276,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -292,6 +302,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -317,6 +328,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -342,6 +354,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -363,6 +376,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -387,6 +401,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -412,6 +427,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -444,6 +460,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: platform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -533,6 +550,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -609,6 +627,7 @@ void main() { cpuArchitecture: DarwinArch.arm64, iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), iosDeploy: iosDeploy, + analytics: FakeAnalytics(), iMobileDevice: iMobileDevice, coreDeviceControl: coreDeviceControl, coreDeviceLauncher: coreDeviceLauncher, @@ -630,6 +649,7 @@ void main() { cpuArchitecture: DarwinArch.arm64, iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), iosDeploy: iosDeploy, + analytics: FakeAnalytics(), iMobileDevice: iMobileDevice, coreDeviceControl: coreDeviceControl, coreDeviceLauncher: coreDeviceLauncher, @@ -939,6 +959,7 @@ void main() { cpuArchitecture: DarwinArch.arm64, iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), iosDeploy: iosDeploy, + analytics: FakeAnalytics(), iMobileDevice: iMobileDevice, coreDeviceControl: coreDeviceControl, coreDeviceLauncher: coreDeviceLauncher, @@ -1108,3 +1129,5 @@ class FakeXcodeDebug extends Fake implements XcodeDebug { class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {} class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher {} + +class FakeAnalytics extends Fake implements Analytics {} diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart index 539c14f009773..9d62ad5307eff 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart @@ -19,6 +19,7 @@ import 'package:flutter_tools/src/ios/iproxy.dart'; import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/xcode_debug.dart'; import 'package:test/fake.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; import '../../src/fake_process_manager.dart'; @@ -380,6 +381,7 @@ IOSDevice setUpIOSDevice({ artifacts: artifacts, cache: cache, ), + analytics: FakeAnalytics(), coreDeviceControl: FakeIOSCoreDeviceControl(), coreDeviceLauncher: FakeIOSCoreDeviceLauncher(), xcodeDebug: FakeXcodeDebug(), @@ -412,3 +414,5 @@ class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl { } class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher {} + +class FakeAnalytics extends Fake implements Analytics {} diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart index 5fcb5a71fdde4..d42e12c1e1b80 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart @@ -18,6 +18,7 @@ import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/xcode_debug.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:test/fake.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; import '../../src/context.dart'; @@ -107,6 +108,7 @@ IOSDevice setUpIOSDevice(FileSystem fileSystem) { artifacts: Artifacts.test(), cache: Cache.test(processManager: processManager), ), + analytics: FakeAnalytics(), iMobileDevice: IMobileDevice.test(processManager: processManager), coreDeviceControl: FakeIOSCoreDeviceControl(), coreDeviceLauncher: FakeIOSCoreDeviceLauncher(), @@ -129,3 +131,5 @@ class FakeXcodeDebug extends Fake implements XcodeDebug {} class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {} class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher {} + +class FakeAnalytics extends Fake implements Analytics {} diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart index a681ad09225b2..9e7a498510c1f 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart @@ -219,11 +219,13 @@ void main() { testUsingContext( 'with buildable app', () async { + final fakeExactAnalytics = FakeExactAnalytics(); final IOSDevice iosDevice = setUpIOSDevice( fileSystem: fileSystem, processManager: processManager, logger: logger, artifacts: artifacts, + analytics: fakeExactAnalytics, ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory( @@ -279,6 +281,13 @@ void main() { expect(fileSystem.directory('build/ios/iphoneos'), exists); expect(launchResult.started, true); expect(processManager, hasNoRemainingExpectations); + expect(fakeExactAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunch.name, + result: 'release success', + ), + ]); }, overrides: { ProcessManager: () => processManager, @@ -296,11 +305,13 @@ void main() { 'ONLY_ACTIVE_ARCH is NO if different host and target architectures', () async { // Host architecture is x64, target architecture is arm64. + final fakeExactAnalytics = FakeExactAnalytics(); final IOSDevice iosDevice = setUpIOSDevice( fileSystem: fileSystem, processManager: processManager, logger: logger, artifacts: artifacts, + analytics: fakeExactAnalytics, ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory( @@ -385,6 +396,13 @@ void main() { expect(fileSystem.directory('build/ios/iphoneos'), exists); expect(launchResult.started, true); expect(processManager, hasNoRemainingExpectations); + expect(fakeExactAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunch.name, + result: 'release success', + ), + ]); }, overrides: { ProcessManager: () => processManager, @@ -401,11 +419,13 @@ void main() { testUsingContext( 'with concurrent build failures', () async { + final fakeExactAnalytics = FakeExactAnalytics(); final IOSDevice iosDevice = setUpIOSDevice( fileSystem: fileSystem, processManager: processManager, logger: logger, artifacts: artifacts, + analytics: fakeExactAnalytics, ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory( @@ -464,6 +484,13 @@ void main() { ); expect(launchResult.started, true); expect(processManager, hasNoRemainingExpectations); + expect(fakeExactAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunch.name, + result: 'release success', + ), + ]); }), ), ); @@ -502,7 +529,10 @@ void main() { projectInfo = XcodeProjectInfo(['Runner'], ['Debug', 'Release'], [ 'Runner', ], logger); - fakeXcodeProjectInterpreter = FakeXcodeProjectInterpreter(projectInfo: projectInfo); + fakeXcodeProjectInterpreter = FakeXcodeProjectInterpreter( + projectInfo: projectInfo, + xcodeVersion: Version(15, 0, 0), + ); xcode = Xcode.test( processManager: FakeProcessManager.any(), xcodeProjectInterpreter: fakeXcodeProjectInterpreter, @@ -513,6 +543,7 @@ void main() { testUsingContext( 'succeeds when install and launch succeed', () async { + final fakeExactAnalytics = FakeExactAnalytics(); final IOSDevice iosDevice = setUpIOSDevice( fileSystem: fileSystem, processManager: FakeProcessManager.any(), @@ -520,6 +551,7 @@ void main() { artifacts: artifacts, isCoreDevice: true, coreDeviceControl: FakeIOSCoreDeviceControl(), + analytics: fakeExactAnalytics, ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory( @@ -543,6 +575,13 @@ void main() { expect(fileSystem.directory('build/ios/iphoneos'), exists); expect(launchResult.started, true); expect(processManager, hasNoRemainingExpectations); + expect(fakeExactAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithoutDebugger.name, + result: 'release success', + ), + ]); }, overrides: { ProcessManager: () => FakeProcessManager.any(), @@ -559,6 +598,7 @@ void main() { testUsingContext( 'fails when install fails', () async { + final fakeExactAnalytics = FakeExactAnalytics(); final IOSDevice iosDevice = setUpIOSDevice( fileSystem: fileSystem, processManager: FakeProcessManager.any(), @@ -566,6 +606,8 @@ void main() { artifacts: artifacts, isCoreDevice: true, coreDeviceControl: FakeIOSCoreDeviceControl(installSuccess: false), + coreDeviceLauncher: FakeIOSCoreDeviceLauncher(launchResult: false), + analytics: fakeExactAnalytics, ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory( @@ -589,6 +631,13 @@ void main() { expect(fileSystem.directory('build/ios/iphoneos'), exists); expect(launchResult.started, false); expect(processManager, hasNoRemainingExpectations); + expect(fakeExactAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithoutDebugger.name, + result: 'launch failed', + ), + ]); }, overrides: { ProcessManager: () => FakeProcessManager.any(), @@ -652,6 +701,7 @@ void main() { 'ensure arguments passed to launch', () async { final coreDeviceControl = FakeIOSCoreDeviceControl(); + final fakeExactAnalytics = FakeExactAnalytics(); final IOSDevice iosDevice = setUpIOSDevice( fileSystem: fileSystem, processManager: FakeProcessManager.any(), @@ -659,6 +709,7 @@ void main() { artifacts: artifacts, isCoreDevice: true, coreDeviceControl: coreDeviceControl, + analytics: fakeExactAnalytics, ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory( @@ -684,6 +735,13 @@ void main() { expect(processManager, hasNoRemainingExpectations); expect(coreDeviceControl.argumentsUsedForLaunch, isNotNull); expect(coreDeviceControl.argumentsUsedForLaunch, contains('--enable-dart-profiling')); + expect(fakeExactAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithoutDebugger.name, + result: 'release success', + ), + ]); }, overrides: { ProcessManager: () => FakeProcessManager.any(), @@ -1177,6 +1235,7 @@ IOSDevice setUpIOSDevice({ IOSCoreDeviceLauncher? coreDeviceLauncher, FakeXcodeDebug? xcodeDebug, DarwinArch cpuArchitecture = DarwinArch.arm64, + FakeExactAnalytics? analytics, }) { artifacts ??= Artifacts.test(); final cache = Cache.test( @@ -1200,6 +1259,7 @@ IOSDevice setUpIOSDevice({ artifacts: artifacts, cache: cache, ), + analytics: analytics ?? FakeExactAnalytics(), iMobileDevice: IMobileDevice( logger: logger, processManager: processManager ?? FakeProcessManager.any(), @@ -1226,7 +1286,8 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete 'WRAPPER_NAME': 'My Super Awesome App.app', 'DEVELOPMENT_TEAM': '3333CCCC33', }, - }); + Version? xcodeVersion, + }) : version = xcodeVersion ?? Version(1000, 0, 0); final Map buildSettings; final XcodeProjectInfo? projectInfo; @@ -1235,7 +1296,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete final isInstalled = true; @override - final version = Version(1000, 0, 0); + Version? version; @override String get versionText => version.toString(); @@ -1319,13 +1380,17 @@ class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl { } @override - Future launchApp({ + Future launchApp({ required String deviceId, required String bundleId, List launchArguments = const [], + bool startStopped = false, }) async { _launchArguments = launchArguments; - return launchSuccess; + final outcome = launchSuccess ? 'success' : 'failed'; + return IOSCoreDeviceLaunchResult.fromJson({ + 'info': {'outcome': outcome}, + }); } } @@ -1395,4 +1460,36 @@ const _validScheme = ''' '''; -class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher {} +class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher { + FakeIOSCoreDeviceLauncher({this.launchResult = true}); + bool launchResult; + + @override + Future launchAppWithoutDebugger({ + required String deviceId, + required String bundlePath, + required String bundleId, + required List launchArguments, + }) async { + return launchResult; + } + + @override + Future launchAppWithLLDBDebugger({ + required String deviceId, + required String bundlePath, + required String bundleId, + required List launchArguments, + }) async { + return true; + } +} + +class FakeExactAnalytics extends Fake implements Analytics { + final sentEvents = []; + + @override + void send(Event event) { + sentEvents.add(event); + } +} diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart index 05ed61ae0100a..e294e9441e914 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart @@ -13,6 +13,7 @@ import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/base/template.dart'; +import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/device.dart'; @@ -24,8 +25,10 @@ import 'package:flutter_tools/src/ios/ios_deploy.dart'; import 'package:flutter_tools/src/ios/iproxy.dart'; import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/xcode_debug.dart'; +import 'package:flutter_tools/src/macos/xcode.dart'; import 'package:flutter_tools/src/mdns_discovery.dart'; import 'package:test/fake.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; import '../../src/context.dart'; @@ -131,9 +134,11 @@ void main() { () async { final FileSystem fileSystem = MemoryFileSystem.test(); final processManager = FakeProcessManager.list([attachDebuggerCommand()]); + final fakeAnalytics = FakeAnalytics(); final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -162,6 +167,13 @@ void main() { expect(launchResult.started, true); expect(launchResult.hasVmService, true); expect(await device.stopApp(iosApp), false); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging success', + ), + ]); }, ); @@ -171,6 +183,7 @@ void main() { final logger = BufferLogger.test(); final FileSystem fileSystem = MemoryFileSystem.test(); final completer = Completer(); + final fakeAnalytics = FakeAnalytics(); final processManager = FakeProcessManager.list([ attachDebuggerCommand(stdout: 'PROCESS_EXITED'), attachDebuggerCommand( @@ -183,6 +196,7 @@ void main() { processManager: processManager, fileSystem: fileSystem, logger: logger, + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -214,6 +228,18 @@ void main() { expect(secondLaunchResult.started, true); expect(secondLaunchResult.hasVmService, true); expect(await device.stopApp(iosApp), true); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'launch failed', + ), + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging success', + ), + ]); }, ); @@ -222,10 +248,12 @@ void main() { () async { final FileSystem fileSystem = MemoryFileSystem.test(); final processManager = FakeProcessManager.list([kLaunchDebugCommand]); + final fakeAnalytics = FakeAnalytics(); final IOSDevice device = setUpIOSDevice( sdkVersion: '12.4.4', processManager: processManager, fileSystem: fileSystem, + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -254,6 +282,13 @@ void main() { expect(launchResult.started, true); expect(launchResult.hasVmService, true); expect(await device.stopApp(iosApp), false); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunch.name, + result: 'debugging success', + ), + ]); }, ); @@ -264,12 +299,14 @@ void main() { final logger = BufferLogger.test(); final stdin = CompleterIOSink(); final completer = Completer(); + final fakeAnalytics = FakeAnalytics(); final processManager = FakeProcessManager.list([ attachDebuggerCommand(stdin: stdin, completer: completer), ]); final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, + analytics: fakeAnalytics, logger: logger, ); final IOSApp iosApp = PrebuiltIOSApp( @@ -307,6 +344,13 @@ void main() { expect(utf8.decoder.convert(stdin.writes.first), contains('process interrupt')); completer.complete(); expect(processManager, hasNoRemainingExpectations); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging success', + ), + ]); }, ); @@ -317,12 +361,14 @@ void main() { final logger = BufferLogger.test(); final stdin = CompleterIOSink(); final completer = Completer(); + final fakeAnalytics = FakeAnalytics(); final processManager = FakeProcessManager.list([ attachDebuggerCommand(stdin: stdin, completer: completer, isWirelessDevice: true), ]); final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, + analytics: fakeAnalytics, logger: logger, interfaceType: DeviceConnectionInterface.wireless, ); @@ -371,6 +417,13 @@ void main() { ), ); completer.complete(); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging success', + ), + ]); expect(processManager, hasNoRemainingExpectations); }, overrides: {MDnsVmServiceDiscovery: () => FakeMDnsVmServiceDiscovery()}, @@ -382,6 +435,7 @@ void main() { final logger = BufferLogger.test(); final FileSystem fileSystem = MemoryFileSystem.test(); final completer = Completer(); + final fakeAnalytics = FakeAnalytics(); final processManager = FakeProcessManager.list([ attachDebuggerCommand( stdout: @@ -399,6 +453,7 @@ void main() { processManager: processManager, fileSystem: fileSystem, logger: logger, + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -425,6 +480,13 @@ void main() { expect(launchResult.started, true); expect(launchResult.hasVmService, true); expect(await device.stopApp(iosApp), true); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging success', + ), + ]); }, ); @@ -433,6 +495,7 @@ void main() { () async { final logger = BufferLogger.test(); final FileSystem fileSystem = MemoryFileSystem.test(); + final fakeAnalytics = FakeAnalytics(); final processManager = FakeProcessManager.list([ attachDebuggerCommand( stdout: @@ -442,6 +505,7 @@ void main() { final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, + analytics: fakeAnalytics, logger: logger, ); final IOSApp iosApp = PrebuiltIOSApp( @@ -464,13 +528,25 @@ void main() { expect(launchResult.started, false); expect(launchResult.hasVmService, false); expect(await device.stopApp(iosApp), false); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging failed', + ), + ]); }, ); testWithoutContext('IOSDevice.startApp succeeds in release mode', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final processManager = FakeProcessManager.list([kLaunchReleaseCommand]); - final IOSDevice device = setUpIOSDevice(processManager: processManager, fileSystem: fileSystem); + final fakeAnalytics = FakeAnalytics(); + final IOSDevice device = setUpIOSDevice( + processManager: processManager, + fileSystem: fileSystem, + analytics: fakeAnalytics, + ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', bundleName: 'Runner', @@ -489,10 +565,18 @@ void main() { expect(launchResult.hasVmService, false); expect(await device.stopApp(iosApp), false); expect(processManager, hasNoRemainingExpectations); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunch.name, + result: 'release success', + ), + ]); }); testWithoutContext('IOSDevice.startApp forwards all supported debugging options', () async { final FileSystem fileSystem = MemoryFileSystem.test(); + final fakeAnalytics = FakeAnalytics(); final processManager = FakeProcessManager.list([ FakeCommand( command: [ @@ -546,6 +630,7 @@ void main() { sdkVersion: '13.3', processManager: processManager, fileSystem: fileSystem, + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -594,10 +679,18 @@ void main() { expect(launchResult.started, true); expect(await device.stopApp(iosApp), false); expect(processManager, hasNoRemainingExpectations); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging success', + ), + ]); }); testWithoutContext('startApp using route', () async { final FileSystem fileSystem = MemoryFileSystem.test(); + final fakeAnalytics = FakeAnalytics(); final processManager = FakeProcessManager.list([ FakeCommand( command: [ @@ -633,6 +726,7 @@ void main() { sdkVersion: '13.3', processManager: processManager, fileSystem: fileSystem, + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -661,10 +755,18 @@ void main() { expect(launchResult.started, true); expect(await device.stopApp(iosApp), false); expect(processManager, hasNoRemainingExpectations); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging success', + ), + ]); }); testWithoutContext('startApp using trace-startup', () async { final FileSystem fileSystem = MemoryFileSystem.test(); + final fakeAnalytics = FakeAnalytics(); final processManager = FakeProcessManager.list([ FakeCommand( command: [ @@ -700,6 +802,7 @@ void main() { sdkVersion: '13.3', processManager: processManager, fileSystem: fileSystem, + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -727,14 +830,210 @@ void main() { expect(launchResult.started, true); expect(await device.stopApp(iosApp), false); expect(processManager, hasNoRemainingExpectations); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging success', + ), + ]); }); group('IOSDevice.startApp for CoreDevice', () { group('in debug mode', () { - testUsingContext('succeeds', () async { + testUsingContext( + 'uses LLDB with Xcode 26+', + () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final processManager = FakeProcessManager.empty(); + final Directory bundleLocation = fileSystem.currentDirectory; + final fakeAnalytics = FakeAnalytics(); + final fakeLauncher = FakeIOSCoreDeviceLauncher(); + final IOSDevice device = setUpIOSDevice( + processManager: processManager, + fileSystem: fileSystem, + isCoreDevice: true, + coreDeviceLauncher: fakeLauncher, + analytics: fakeAnalytics, + ); + final IOSApp iosApp = PrebuiltIOSApp( + projectBundleId: 'app', + bundleName: 'Runner', + uncompressedBundle: bundleLocation, + applicationPackage: bundleLocation, + ); + final deviceLogReader = FakeDeviceLogReader(); + + device.portForwarder = const NoOpDevicePortForwarder(); + device.setLogReader(iosApp, deviceLogReader); + + // Start writing messages to the log reader. + Timer.run(() { + deviceLogReader.addLine('Foo'); + deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456'); + }); + + final LaunchResult launchResult = await device.startApp( + iosApp, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + platformArgs: {}, + ); + + expect(launchResult.started, true); + expect(fakeLauncher.launchedWithLLDB, true); + expect(fakeLauncher.launchedWithXcode, false); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithLLDB.name, + result: 'debugging success', + ), + ]); + }, + overrides: { + Xcode: () => FakeXcode(currentVersion: Version(26, 0, 0)), + Analytics: () => FakeAnalytics(), + }, + ); + + testUsingContext('uses Xcode if LLDB fails', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final processManager = FakeProcessManager.empty(); + final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory + .childDirectory('flutter_empty_xcode.rand0'); + final Directory bundleLocation = fileSystem.currentDirectory; + final fakeAnalytics = FakeAnalytics(); + final fakeLauncher = FakeIOSCoreDeviceLauncher(lldbLaunchResult: false); + final IOSDevice device = setUpIOSDevice( + processManager: processManager, + fileSystem: fileSystem, + isCoreDevice: true, + coreDeviceControl: FakeIOSCoreDeviceControl(), + xcodeDebug: FakeXcodeDebug( + expectedProject: XcodeDebugProject( + scheme: 'Runner', + xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'), + xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'), + hostAppProjectName: 'Runner', + ), + expectedDeviceId: '123', + expectedLaunchArguments: ['--enable-dart-profiling'], + expectedBundlePath: bundleLocation.path, + ), + coreDeviceLauncher: fakeLauncher, + analytics: fakeAnalytics, + ); + final IOSApp iosApp = PrebuiltIOSApp( + projectBundleId: 'app', + bundleName: 'Runner', + uncompressedBundle: bundleLocation, + applicationPackage: bundleLocation, + ); + final deviceLogReader = FakeDeviceLogReader(); + + device.portForwarder = const NoOpDevicePortForwarder(); + device.setLogReader(iosApp, deviceLogReader); + + // Start writing messages to the log reader. + Timer.run(() { + deviceLogReader.addLine('Foo'); + deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456'); + }); + + final LaunchResult launchResult = await device.startApp( + iosApp, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + platformArgs: {}, + ); + + expect(launchResult.started, true); + expect(fakeLauncher.launchedWithLLDB, true); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithLLDB.name, + result: 'launch failed', + ), + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithXcodeFallback.name, + result: 'debugging success', + ), + ]); + }, overrides: {Xcode: () => FakeXcode(currentVersion: Version(26, 0, 0))}); + + testUsingContext( + 'uses Xcode if less than Xcode 26', + () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final processManager = FakeProcessManager.empty(); + final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory + .childDirectory('flutter_empty_xcode.rand0'); + final Directory bundleLocation = fileSystem.currentDirectory; + final fakeAnalytics = FakeAnalytics(); + final fakeLauncher = FakeIOSCoreDeviceLauncher(); + final IOSDevice device = setUpIOSDevice( + processManager: processManager, + fileSystem: fileSystem, + isCoreDevice: true, + coreDeviceControl: FakeIOSCoreDeviceControl(), + xcodeDebug: FakeXcodeDebug( + expectedProject: XcodeDebugProject( + scheme: 'Runner', + xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'), + xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'), + hostAppProjectName: 'Runner', + ), + expectedDeviceId: '123', + expectedLaunchArguments: ['--enable-dart-profiling'], + expectedBundlePath: bundleLocation.path, + ), + coreDeviceLauncher: fakeLauncher, + analytics: fakeAnalytics, + ); + final IOSApp iosApp = PrebuiltIOSApp( + projectBundleId: 'app', + bundleName: 'Runner', + uncompressedBundle: bundleLocation, + applicationPackage: bundleLocation, + ); + final deviceLogReader = FakeDeviceLogReader(); + + device.portForwarder = const NoOpDevicePortForwarder(); + device.setLogReader(iosApp, deviceLogReader); + + // Start writing messages to the log reader. + Timer.run(() { + deviceLogReader.addLine('Foo'); + deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456'); + }); + final LaunchResult launchResult = await device.startApp( + iosApp, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + platformArgs: {}, + ); + + expect(launchResult.started, true); + expect(fakeLauncher.launchedWithLLDB, false); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithXcode.name, + result: 'debugging success', + ), + ]); + }, + overrides: {Xcode: () => FakeXcode(currentVersion: Version(16, 0, 0))}, + ); + + testUsingContext('succeeds', () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final processManager = FakeProcessManager.empty(); + final fakeAnalytics = FakeAnalytics(); final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory .childDirectory('flutter_empty_xcode.rand0'); final Directory bundleLocation = fileSystem.currentDirectory; @@ -754,6 +1053,7 @@ void main() { expectedLaunchArguments: ['--enable-dart-profiling'], expectedBundlePath: bundleLocation.path, ), + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -780,6 +1080,13 @@ void main() { ); expect(launchResult.started, true); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithXcode.name, + result: 'debugging success', + ), + ]); }); testUsingContext('prints warning message if it takes too long to start debugging', () async { @@ -839,7 +1146,8 @@ void main() { expect( logger.errorText, contains( - 'Xcode is taking longer than expected to start debugging the app. Ensure the project is opened in Xcode.', + 'Xcode is taking longer than expected to start debugging the app. ' + 'If the issue persists, try closing Xcode and re-running your Flutter command.', ), ); completer.complete(); @@ -853,6 +1161,7 @@ void main() { final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory .childDirectory('flutter_empty_xcode.rand0'); final Directory bundleLocation = fileSystem.currentDirectory; + final fakeAnalytics = FakeAnalytics(); final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, @@ -869,6 +1178,7 @@ void main() { expectedLaunchArguments: ['--enable-dart-profiling'], expectedBundlePath: bundleLocation.path, ), + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -899,6 +1209,13 @@ void main() { expect(launchResult.started, true); expect(shutDownHooks.hooks.length, 1); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithXcode.name, + result: 'debugging success', + ), + ]); }); testUsingContext( @@ -910,10 +1227,12 @@ void main() { final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory .childDirectory('flutter_empty_xcode.rand0'); final Directory bundleLocation = fileSystem.currentDirectory; + final fakeAnalytics = FakeAnalytics(); final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, isCoreDevice: true, + analytics: fakeAnalytics, coreDeviceControl: FakeIOSCoreDeviceControl(), xcodeDebug: FakeXcodeDebug( expectedProject: XcodeDebugProject( @@ -948,6 +1267,13 @@ void main() { expect(launchResult.started, true); expect(launchResult.hasVmService, true); expect(await device.stopApp(iosApp), true); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithXcode.name, + result: 'debugging success', + ), + ]); }, // If mDNS is not the only method of discovery, it shouldn't throw on error. overrides: { @@ -965,7 +1291,7 @@ void main() { testUsingContext('when mDNS fails', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final processManager = FakeProcessManager.empty(); - + final fakeAnalytics = FakeAnalytics(); final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory .childDirectory('flutter_empty_xcode.rand0'); final Directory bundleLocation = fileSystem.currentDirectory; @@ -985,6 +1311,7 @@ void main() { expectedLaunchArguments: ['--enable-dart-profiling'], expectedBundlePath: bundleLocation.path, ), + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -1017,6 +1344,13 @@ void main() { expect(launchResult.started, true); expect(launchResult.hasVmService, true); expect(await device.stopApp(iosApp), true); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithXcode.name, + result: 'debugging success', + ), + ]); }, overrides: {MDnsVmServiceDiscovery: () => mdnsDiscovery}); }); @@ -1032,6 +1366,7 @@ void main() { final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory .childDirectory('flutter_empty_xcode.rand0'); final Directory bundleLocation = fileSystem.currentDirectory; + final fakeAnalytics = FakeAnalytics(); final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, @@ -1052,6 +1387,7 @@ void main() { operatingSystem: 'macos', environment: {'HOME': pathToHome}, ), + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( @@ -1091,6 +1427,13 @@ void main() { .existsSync(), true, ); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithXcode.name, + result: 'debugging failed', + ), + ]); completer.complete(); }); time.elapse(const Duration(minutes: 15)); @@ -1184,6 +1527,8 @@ IOSDevice setUpIOSDevice({ DeviceConnectionInterface interfaceType = DeviceConnectionInterface.attached, bool isCoreDevice = false, IOSCoreDeviceControl? coreDeviceControl, + IOSCoreDeviceLauncher? coreDeviceLauncher, + Analytics? analytics, FakeXcodeDebug? xcodeDebug, FakePlatform? platform, }) { @@ -1214,6 +1559,7 @@ IOSDevice setUpIOSDevice({ artifacts: artifacts, cache: cache, ), + analytics: analytics ?? FakeAnalytics(), iMobileDevice: IMobileDevice( logger: logger, processManager: processManager ?? FakeProcessManager.any(), @@ -1221,7 +1567,7 @@ IOSDevice setUpIOSDevice({ cache: cache, ), coreDeviceControl: coreDeviceControl ?? FakeIOSCoreDeviceControl(), - coreDeviceLauncher: FakeIOSCoreDeviceLauncher(), + coreDeviceLauncher: coreDeviceLauncher ?? FakeIOSCoreDeviceLauncher(), xcodeDebug: xcodeDebug ?? FakeXcodeDebug(), cpuArchitecture: DarwinArch.arm64, connectionInterface: interfaceType, @@ -1349,4 +1695,61 @@ class FakeShutDownHooks extends Fake implements ShutdownHooks { } } -class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher {} +class FakeXcode extends Fake implements Xcode { + FakeXcode({this.currentVersion}); + + @override + Version? currentVersion; +} + +class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher { + FakeIOSCoreDeviceLauncher({this.lldbLaunchResult = true, this.xcodeLaunchResult = true}); + bool lldbLaunchResult; + bool xcodeLaunchResult; + var launchedWithLLDB = false; + var launchedWithXcode = false; + + Completer? xcodeCompleter; + + @override + Future launchAppWithLLDBDebugger({ + required String deviceId, + required String bundlePath, + required String bundleId, + required List launchArguments, + }) async { + launchedWithLLDB = true; + return lldbLaunchResult; + } + + @override + Future launchAppWithXcodeDebugger({ + required String deviceId, + required DebuggingOptions debuggingOptions, + required IOSApp package, + required List launchArguments, + required TemplateRenderer templateRenderer, + String? mainPath, + Duration? discoveryTimeout, + }) async { + if (xcodeCompleter != null) { + await xcodeCompleter!.future; + } + launchedWithXcode = true; + return xcodeLaunchResult; + } + + @override + Future stopApp({required String deviceId, int? processId}) async { + return false; + } +} + +class FakeAnalytics extends Fake implements Analytics { + final sentEvents = []; + + @override + void send(Event event) { + sentEvents.add(event); + } +} diff --git a/packages/flutter_tools/test/general.shard/ios/lldb_test.dart b/packages/flutter_tools/test/general.shard/ios/lldb_test.dart index e94da55b1b8f4..3756f58d6ded6 100644 --- a/packages/flutter_tools/test/general.shard/ios/lldb_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/lldb_test.dart @@ -43,7 +43,7 @@ void main() { expect(lldb.isRunning, isFalse); expect(lldb.appProcessId, isNull); expect(processManager.hasRemainingExpectations, isFalse); - expect(logger.errorText, contains('Process exception running lldb')); + expect(logger.traceText, contains('Process exception running lldb')); }); testWithoutContext('attachAndStart returns true on success', () async { @@ -174,7 +174,7 @@ Target 0: (Runner) stopped. expect(lldb.appProcessId, isNull); expect(expectedInputs, isEmpty); expect(processManager.hasRemainingExpectations, isFalse); - expect(logger.errorText, contains(errorText)); + expect(logger.traceText, contains(errorText)); }); testWithoutContext('attachAndStart returns false when stderr not during log waiter', () async { @@ -220,7 +220,7 @@ Target 0: (Runner) stopped. expect(lldb.appProcessId, isNull); expect(expectedInputs, isEmpty); expect(processManager.hasRemainingExpectations, isFalse); - expect(logger.errorText, contains(errorText)); + expect(logger.traceText, contains(errorText)); }); testWithoutContext('attachAndStart prints warning if takes too long', () async { diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart index 56af2b901a0d1..ba3198b7da87c 100644 --- a/packages/flutter_tools/test/src/fakes.dart +++ b/packages/flutter_tools/test/src/fakes.dart @@ -503,6 +503,7 @@ class TestFeatureFlags implements FeatureFlags { this.isSwiftPackageManagerEnabled = false, this.isOmitLegacyVersionFileEnabled = false, this.isWindowingEnabled = false, + this.isLLDBDebuggingEnabled = false, }); @override @@ -544,6 +545,9 @@ class TestFeatureFlags implements FeatureFlags { @override final bool isWindowingEnabled; + @override + final bool isLLDBDebuggingEnabled; + @override bool isEnabled(Feature feature) { return switch (feature) { @@ -557,8 +561,10 @@ class TestFeatureFlags implements FeatureFlags { flutterCustomDevicesFeature => areCustomDevicesEnabled, cliAnimation => isCliAnimationEnabled, nativeAssets => isNativeAssetsEnabled, + swiftPackageManager => isSwiftPackageManagerEnabled, omitLegacyVersionFile => isOmitLegacyVersionFileEnabled, windowingFeature => isWindowingEnabled, + lldbDebugging => isLLDBDebuggingEnabled, _ => false, }; } @@ -578,6 +584,7 @@ class TestFeatureFlags implements FeatureFlags { swiftPackageManager, omitLegacyVersionFile, windowingFeature, + lldbDebugging, ]; @override From 780ac30ee8b969c646d3c05039162612ee9301b7 Mon Sep 17 00:00:00 2001 From: Robert Ancell Date: Thu, 14 Aug 2025 04:03:23 +1200 Subject: [PATCH 3/5] [beta] Cherry pick fix GTK redraw call being called from non-GTK thread (#173669) Cherry pick of https://github.com/flutter/flutter/pull/173602 Impacted users: All Linux users of Flutter Impact Description: Due to calling gtk_window_redraw on a Flutter thread a lock up may occur. The Flutter app will then become unresponsive. Workaround: No workaround Risk: Low - fix is to run the GTK call on the GTK thread which is what the correct behaviour should be. Test coverage: Rendering covered by existing tests, use of thread not explicitly tested, but https://github.com/flutter/flutter/issues/173660 opened to add this in future. Validation Steps: Run test program in https://github.com/flutter/flutter/issues/173447 which generates many frames and maximizes the chance of a lock up. --- .../flutter/shell/platform/linux/fl_view.cc | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index 6d7443449a9ba..f770b93e1cb65 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc @@ -99,11 +99,16 @@ G_DEFINE_TYPE_WITH_CODE( G_IMPLEMENT_INTERFACE(fl_plugin_registry_get_type(), fl_view_plugin_registry_iface_init)) -// Emit the first frame signal in the main thread. -static gboolean first_frame_idle_cb(gpointer user_data) { +// Redraw the view from the GTK thread. +static gboolean redraw_cb(gpointer user_data) { FlView* self = FL_VIEW(user_data); - g_signal_emit(self, fl_view_signals[SIGNAL_FIRST_FRAME], 0); + gtk_widget_queue_draw(GTK_WIDGET(self->render_area)); + + if (!self->have_first_frame) { + self->have_first_frame = TRUE; + g_signal_emit(self, fl_view_signals[SIGNAL_FIRST_FRAME], 0); + } return FALSE; } @@ -247,14 +252,8 @@ static void fl_view_present_layers(FlRenderable* renderable, fl_compositor_present_layers(self->compositor, layers, layers_count); - gtk_widget_queue_draw(GTK_WIDGET(self->render_area)); - - if (!self->have_first_frame) { - self->have_first_frame = TRUE; - // This is not the main thread, so the signal needs to be done via an idle - // callback. - g_idle_add(first_frame_idle_cb, self); - } + // Perform the redraw in the GTK thead. + g_idle_add(redraw_cb, self); } // Implements FlPluginRegistry::get_registrar_for_plugin. From 877970cbc98718dbee8f0a34374fa2b1a1db2248 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Wed, 13 Aug 2025 16:03:23 -0700 Subject: [PATCH 4/5] [3.36] Do not include `:unittests` unless `enable_unittests` (#173737) Same as https://github.com/flutter/flutter/pull/173734, but for the 3.36 branch. --- engine/src/flutter/BUILD.gn | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/engine/src/flutter/BUILD.gn b/engine/src/flutter/BUILD.gn index 1258d615fd6a4..f5ace7c353167 100644 --- a/engine/src/flutter/BUILD.gn +++ b/engine/src/flutter/BUILD.gn @@ -78,10 +78,12 @@ group("flutter") { if (!is_qnx) { public_deps = [ - ":unittests", "//flutter/shell/platform/embedder:flutter_engine", "//flutter/sky", ] + if (enable_unittests) { + public_deps += [ ":unittests" ] + } } # Ensure the example for a sample embedder compiles. From c84cdd0d0b582d1b65028a05bb39ba5f0a1599c2 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Wed, 13 Aug 2025 17:21:27 -0700 Subject: [PATCH 5/5] [3.36] Create `engine.version` (#173749) Pointing at 877970cbc98718dbee8f0a34374fa2b1a1db2248. --- bin/internal/engine.version | 1 + 1 file changed, 1 insertion(+) create mode 100644 bin/internal/engine.version diff --git a/bin/internal/engine.version b/bin/internal/engine.version new file mode 100644 index 0000000000000..21fab17782acc --- /dev/null +++ b/bin/internal/engine.version @@ -0,0 +1 @@ +877970cbc98718dbee8f0a34374fa2b1a1db2248