Skip to content

Commit f20e5d8

Browse files
vashworthlucaantonelli
authored andcommitted
Use LLDB as the default debugging method for iOS 17+ and Xcode 26+ (flutter#173443)
This PR introduces a new feature flag called `lldb-debugging`, which is turned on by default. When this feature is turned on, if deploying to a physical iOS 17+ device with Xcode 26+, it will use a mixture of `devicectl` and `lldb` to install, launch, and debug the app. If LLDB fails, it will fallback to use Xcode automation. If LLDB times out, it will warn the user they may want to disable it. If LLDB is disabled, it will use Xcode automation. This PR also adds analytics to track what deployment method is used and if it's successful. Part 2 and 3 of flutter#173416. Fixes flutter#144218. Fixes flutter#133465. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **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. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent 41c0fc8 commit f20e5d8

20 files changed

+857
-606
lines changed

.ci.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5481,6 +5481,16 @@ targets:
54815481
["devicelab", "hostonly", "mac"]
54825482
task_name: hot_mode_dev_cycle_ios_simulator
54835483

5484+
- name: Mac_ios hot_mode_dev_cycle_ios_xcode_debug
5485+
recipe: devicelab/devicelab_drone
5486+
bringup: true
5487+
presubmit: false
5488+
timeout: 60
5489+
properties:
5490+
tags: >
5491+
["devicelab", "ios", "mac"]
5492+
task_name: hot_mode_dev_cycle_ios_xcode_debug
5493+
54845494
- name: Mac_ios fullscreen_textfield_perf_ios__e2e_summary
54855495
recipe: devicelab/devicelab_drone
54865496
presubmit: false

TESTOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@
267267
/dev/devicelab/bin/tasks/hello_world_macos__compile.dart @cbracken @flutter/desktop
268268
/dev/devicelab/bin/tasks/hello_world_win_desktop__compile.dart @yaakovschectman @flutter/desktop
269269
/dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_simulator.dart @louisehsu @flutter/tool
270+
/dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_xcode_debug.dart @vashworth @flutter/tool
270271
/dev/devicelab/bin/tasks/hot_mode_dev_cycle_macos_target__benchmark.dart @cbracken @flutter/tool
271272
/dev/devicelab/bin/tasks/hot_mode_dev_cycle_win_target__benchmark.dart @cbracken @flutter/desktop
272273
/dev/devicelab/bin/tasks/integration_ui_test_test_macos.dart @cbracken @flutter/desktop
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter_devicelab/framework/devices.dart';
6+
import 'package:flutter_devicelab/framework/framework.dart';
7+
import 'package:flutter_devicelab/framework/task_result.dart';
8+
import 'package:flutter_devicelab/framework/utils.dart';
9+
import 'package:flutter_devicelab/tasks/hot_mode_tests.dart';
10+
import 'package:path/path.dart' as path;
11+
12+
/// This is a test to validate that Xcode debugging still works now that LLDB is the default.
13+
Future<void> main() async {
14+
await task(() async {
15+
deviceOperatingSystem = DeviceOperatingSystem.ios;
16+
try {
17+
await disableLLDBDebugging();
18+
// This isn't actually a benchmark test, so do not use the returned `benchmarkScoreKeys` result.
19+
await createHotModeTest()();
20+
return TaskResult.success(null);
21+
} finally {
22+
await enableLLDBDebugging();
23+
}
24+
});
25+
}
26+
27+
Future<void> disableLLDBDebugging() async {
28+
final int configResult = await exec(path.join(flutterDirectory.path, 'bin', 'flutter'), <String>[
29+
'config',
30+
'--no-enable-lldb-debugging',
31+
]);
32+
if (configResult != 0) {
33+
print('Failed to disable configuration, tasks may not run.');
34+
}
35+
}
36+
37+
Future<void> enableLLDBDebugging() async {
38+
final int configResult = await exec(path.join(flutterDirectory.path, 'bin', 'flutter'), <String>[
39+
'config',
40+
'--enable-lldb-debugging',
41+
], canFail: true);
42+
if (configResult != 0) {
43+
print('Failed to enable configuration.');
44+
}
45+
}

packages/flutter_tools/lib/src/device.dart

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,7 +1181,6 @@ class DebuggingOptions {
11811181
String? route,
11821182
Map<String, Object?> platformArgs, {
11831183
DeviceConnectionInterface interfaceType = DeviceConnectionInterface.attached,
1184-
bool isCoreDevice = false,
11851184
}) {
11861185
return <String>[
11871186
if (enableDartProfiling) '--enable-dart-profiling',
@@ -1195,13 +1194,7 @@ class DebuggingOptions {
11951194
if (environmentType == EnvironmentType.simulator && dartFlags.isNotEmpty)
11961195
'--dart-flags=$dartFlags',
11971196
if (useTestFonts) '--use-test-fonts',
1198-
// Core Devices (iOS 17 devices) are debugged through Xcode so don't
1199-
// include these flags, which are used to check if the app was launched
1200-
// via Flutter CLI and `ios-deploy`.
1201-
if (debuggingEnabled && !isCoreDevice) ...<String>[
1202-
'--enable-checked-mode',
1203-
'--verify-entry-points',
1204-
],
1197+
if (debuggingEnabled) ...<String>['--enable-checked-mode', '--verify-entry-points'],
12051198
if (enableSoftwareRendering) '--enable-software-rendering',
12061199
if (traceSystrace) '--trace-systrace',
12071200
if (traceToFile != null) '--trace-to-file="$traceToFile"',

packages/flutter_tools/lib/src/features.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ abstract class FeatureFlags {
6767
/// Whether desktop windowing is enabled.
6868
bool get isWindowingEnabled;
6969

70+
/// Whether physical iOS devices are debugging with LLDB.
71+
bool get isLLDBDebuggingEnabled;
72+
7073
/// Whether a particular feature is enabled for the current channel.
7174
///
7275
/// Prefer using one of the specific getters above instead of this API.
@@ -87,6 +90,7 @@ abstract class FeatureFlags {
8790
swiftPackageManager,
8891
omitLegacyVersionFile,
8992
windowingFeature,
93+
lldbDebugging,
9094
];
9195

9296
/// All current Flutter feature flags that can be configured.
@@ -218,6 +222,23 @@ const windowingFeature = Feature(
218222
master: FeatureChannelSetting(available: true),
219223
);
220224

225+
/// Enable LLDB debugging for physical iOS devices. When LLDB debugging is off,
226+
/// Xcode debugging is used instead.
227+
///
228+
/// Requires iOS 17+ and Xcode 26+. If those requirements are not met, the previous
229+
/// default debugging method is used instead.
230+
const lldbDebugging = Feature(
231+
name: 'support for debugging with LLDB for physical iOS devices',
232+
extraHelpText:
233+
'If LLDB debugging is off, Xcode debugging is used instead. '
234+
'Only available for iOS 17 or newer devices. Requires Xcode 26 or greater.',
235+
configSetting: 'enable-lldb-debugging',
236+
environmentOverride: 'FLUTTER_LLDB_DEBUGGING',
237+
master: FeatureChannelSetting(available: true, enabledByDefault: true),
238+
beta: FeatureChannelSetting(available: true, enabledByDefault: true),
239+
stable: FeatureChannelSetting(available: true, enabledByDefault: true),
240+
);
241+
221242
/// A [Feature] is a process for conditionally enabling tool features.
222243
///
223244
/// All settings are optional, and if not provided will generally default to

packages/flutter_tools/lib/src/flutter_features.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ mixin FlutterFeatureFlagsIsEnabled implements FeatureFlags {
5757

5858
@override
5959
bool get isWindowingEnabled => isEnabled(windowingFeature);
60+
61+
@override
62+
bool get isLLDBDebuggingEnabled => isEnabled(lldbDebugging);
6063
}
6164

6265
interface class FlutterFeatureFlags extends FeatureFlags with FlutterFeatureFlagsIsEnabled {

packages/flutter_tools/lib/src/ios/core_devices.dart

Lines changed: 3 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class IOSCoreDeviceLauncher {
6666
}
6767

6868
// Launch app to device
69-
final IOSCoreDeviceLaunchResult? launchResult = await _coreDeviceControl.launchAppInternal(
69+
final IOSCoreDeviceLaunchResult? launchResult = await _coreDeviceControl.launchApp(
7070
deviceId: deviceId,
7171
bundleId: bundleId,
7272
launchArguments: launchArguments,
@@ -99,7 +99,7 @@ class IOSCoreDeviceLauncher {
9999
}
100100

101101
// Launch app on device, but start it stopped so it will wait until the debugger is attached before starting.
102-
final IOSCoreDeviceLaunchResult? launchResult = await _coreDeviceControl.launchAppInternal(
102+
final IOSCoreDeviceLaunchResult? launchResult = await _coreDeviceControl.launchApp(
103103
deviceId: deviceId,
104104
bundleId: bundleId,
105105
launchArguments: launchArguments,
@@ -532,66 +532,11 @@ class IOSCoreDeviceControl {
532532
}
533533
}
534534

535-
Future<bool> launchApp({
536-
required String deviceId,
537-
required String bundleId,
538-
List<String> launchArguments = const <String>[],
539-
}) async {
540-
if (!_xcode.isDevicectlInstalled) {
541-
_logger.printError('devicectl is not installed.');
542-
return false;
543-
}
544-
545-
final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.');
546-
final File output = tempDirectory.childFile('launch_results.json');
547-
output.createSync();
548-
549-
final command = <String>[
550-
..._xcode.xcrunCommand(),
551-
'devicectl',
552-
'device',
553-
'process',
554-
'launch',
555-
'--device',
556-
deviceId,
557-
bundleId,
558-
if (launchArguments.isNotEmpty) ...launchArguments,
559-
'--json-output',
560-
output.path,
561-
];
562-
563-
try {
564-
await _processUtils.run(command, throwOnError: true);
565-
final String stringOutput = output.readAsStringSync();
566-
567-
try {
568-
final Object? decodeResult = (json.decode(stringOutput) as Map<String, Object?>)['info'];
569-
if (decodeResult is Map<String, Object?> && decodeResult['outcome'] == 'success') {
570-
return true;
571-
}
572-
_logger.printError('devicectl returned unexpected JSON response: $stringOutput');
573-
return false;
574-
} on FormatException {
575-
// We failed to parse the devicectl output, or it returned junk.
576-
_logger.printError('devicectl returned non-JSON response: $stringOutput');
577-
return false;
578-
}
579-
} on ProcessException catch (err) {
580-
_logger.printError('Error executing devicectl: $err');
581-
return false;
582-
} finally {
583-
tempDirectory.deleteSync(recursive: true);
584-
}
585-
}
586-
587535
/// Launches the app on the device.
588536
///
589537
/// If [startStopped] is true, the app will be launched and paused, waiting
590538
/// for a debugger to attach.
591-
// TODO(vashworth): Rename this method to launchApp and replace old version.
592-
// See https://github.com/flutter/flutter/issues/173416.
593-
@visibleForTesting
594-
Future<IOSCoreDeviceLaunchResult?> launchAppInternal({
539+
Future<IOSCoreDeviceLaunchResult?> launchApp({
595540
required String deviceId,
596541
required String bundleId,
597542
List<String> launchArguments = const <String>[],

0 commit comments

Comments
 (0)