Skip to content

Commit 5dd2a4e

Browse files
authored
Ensure Xcode project is setup to start debugger (flutter#136977)
Some users have their Xcode settings set to not debug (see example here flutter#136197 (comment)). This will cause the [engine check for a debugger](https://github.com/flutter/engine/blob/22ce5c6a45e2898b4ce348c514b5fa42ca25bc88/runtime/ptrace_check.cc#L56-L71) to fail, which will cause an error and cause the app to crash. This PR parses the scheme file to ensure the scheme is set to start a debugger and warn the user if it's not. Fixes flutter#136197.
1 parent 9366170 commit 5dd2a4e

File tree

11 files changed

+385
-9
lines changed

11 files changed

+385
-9
lines changed

.ci.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3926,6 +3926,16 @@ targets:
39263926
["devicelab", "ios", "mac"]
39273927
task_name: flavors_test_ios
39283928

3929+
- name: Mac_arm64_ios flavors_test_ios_xcode_debug
3930+
recipe: devicelab/devicelab_drone
3931+
presubmit: false
3932+
timeout: 60
3933+
properties:
3934+
tags: >
3935+
["devicelab", "ios", "mac"]
3936+
task_name: flavors_test_ios_xcode_debug
3937+
bringup: true
3938+
39293939
- name: Mac_ios flutter_gallery_ios__compile
39303940
recipe: devicelab/devicelab_drone
39313941
presubmit: false

TESTOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@
174174
/dev/devicelab/bin/tasks/cubic_bezier_perf_ios_sksl_warmup__timeline_summary.dart @zanderso @flutter/engine
175175
/dev/devicelab/bin/tasks/external_ui_integration_test_ios.dart @zanderso @flutter/engine
176176
/dev/devicelab/bin/tasks/flavors_test_ios.dart @vashworth @flutter/tool
177+
/dev/devicelab/bin/tasks/flavors_test_ios_xcode_debug.dart @vashworth @flutter/tool
177178
/dev/devicelab/bin/tasks/flutter_gallery__transition_perf_e2e_ios.dart @zanderso @flutter/engine
178179
/dev/devicelab/bin/tasks/flutter_gallery_ios__compile.dart @vashworth @flutter/engine
179180
/dev/devicelab/bin/tasks/flutter_gallery_ios__start_up.dart @vashworth @flutter/engine
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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/integration_tests.dart';
10+
11+
Future<void> main() async {
12+
deviceOperatingSystem = DeviceOperatingSystem.ios;
13+
await task(() async {
14+
await createFlavorsTest(environment: <String, String>{
15+
'FORCE_XCODE_DEBUG': 'true',
16+
}).call();
17+
await createIntegrationTestFlavorsTest(environment: <String, String>{
18+
'FORCE_XCODE_DEBUG': 'true',
19+
}).call();
20+
// test install and uninstall of flavors app
21+
final TaskResult installTestsResult = await inDirectory(
22+
'${flutterDirectory.path}/dev/integration_tests/flavors',
23+
() async {
24+
await flutter(
25+
'install',
26+
options: <String>['--flavor', 'paid'],
27+
);
28+
await flutter(
29+
'install',
30+
options: <String>['--flavor', 'paid', '--uninstall-only'],
31+
);
32+
final StringBuffer stderr = StringBuffer();
33+
await evalFlutter(
34+
'install',
35+
canFail: true,
36+
stderr: stderr,
37+
options: <String>['--flavor', 'bogus'],
38+
);
39+
40+
final String stderrString = stderr.toString();
41+
if (!stderrString.contains('The Xcode project defines schemes: free, paid')) {
42+
print(stderrString);
43+
return TaskResult.failure('Should not succeed with bogus flavor');
44+
}
45+
46+
return TaskResult.success(null);
47+
},
48+
);
49+
50+
return installTestsResult;
51+
});
52+
}

dev/devicelab/lib/tasks/integration_tests.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,21 @@ TaskFunction createPlatformInteractionTest() {
2222
).call;
2323
}
2424

25-
TaskFunction createFlavorsTest() {
25+
TaskFunction createFlavorsTest({Map<String, String>? environment}) {
2626
return DriverTest(
2727
'${flutterDirectory.path}/dev/integration_tests/flavors',
2828
'lib/main.dart',
2929
extraOptions: <String>['--flavor', 'paid'],
30+
environment: environment,
3031
).call;
3132
}
3233

33-
TaskFunction createIntegrationTestFlavorsTest() {
34+
TaskFunction createIntegrationTestFlavorsTest({Map<String, String>? environment}) {
3435
return IntegrationTest(
3536
'${flutterDirectory.path}/dev/integration_tests/flavors',
3637
'integration_test/integration_test.dart',
3738
extraOptions: <String>['--flavor', 'paid'],
39+
environment: environment,
3840
).call;
3941
}
4042

@@ -219,6 +221,7 @@ class IntegrationTest {
219221
this.extraOptions = const <String>[],
220222
this.createPlatforms = const <String>[],
221223
this.withTalkBack = false,
224+
this.environment,
222225
}
223226
);
224227

@@ -227,6 +230,7 @@ class IntegrationTest {
227230
final List<String> extraOptions;
228231
final List<String> createPlatforms;
229232
final bool withTalkBack;
233+
final Map<String, String>? environment;
230234

231235
Future<TaskResult> call() {
232236
return inDirectory<TaskResult>(testDirectory, () async {
@@ -258,7 +262,7 @@ class IntegrationTest {
258262
testTarget,
259263
...extraOptions,
260264
];
261-
await flutter('test', options: options);
265+
await flutter('test', options: options, environment: environment);
262266

263267
if (withTalkBack) {
264268
await disableTalkBack();

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,8 @@ class IOSDevice extends Device {
877877
projectInfo.reportFlavorNotFoundAndExit();
878878
}
879879

880+
_xcodeDebug.ensureXcodeDebuggerLaunchAction(project.xcodeProjectSchemeFile(scheme: scheme));
881+
880882
debugProject = XcodeDebugProject(
881883
scheme: scheme,
882884
xcodeProject: project.xcodeProject,

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

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import 'dart:async';
66

77
import 'package:meta/meta.dart';
88
import 'package:process/process.dart';
9+
import 'package:xml/xml.dart';
10+
import 'package:xml/xpath.dart';
911

12+
import '../base/common.dart';
1013
import '../base/error_handling_io.dart';
1114
import '../base/file_system.dart';
1215
import '../base/io.dart';
@@ -58,7 +61,6 @@ class XcodeDebug {
5861
required String deviceId,
5962
required List<String> launchArguments,
6063
}) async {
61-
6264
// If project is not already opened in Xcode, open it.
6365
if (!await _isProjectOpenInXcode(project: project)) {
6466
final bool openResult = await _openProjectInXcode(xcodeWorkspace: project.xcodeWorkspace);
@@ -411,6 +413,49 @@ class XcodeDebug {
411413
verboseLogging: verboseLogging,
412414
);
413415
}
416+
417+
/// Ensure the Xcode project is set up to launch an LLDB debugger. If these
418+
/// settings are not set, the launch will fail with a "Cannot create a
419+
/// FlutterEngine instance in debug mode without Flutter tooling or Xcode."
420+
/// error message. These settings should be correct by default, but some users
421+
/// reported them not being so after upgrading to Xcode 15.
422+
void ensureXcodeDebuggerLaunchAction(File schemeFile) {
423+
if (!schemeFile.existsSync()) {
424+
_logger.printError('Failed to find ${schemeFile.path}');
425+
return;
426+
}
427+
428+
final String schemeXml = schemeFile.readAsStringSync();
429+
try {
430+
final XmlDocument document = XmlDocument.parse(schemeXml);
431+
final Iterable<XmlNode> nodes = document.xpath('/Scheme/LaunchAction');
432+
if (nodes.isEmpty) {
433+
_logger.printError('Failed to find LaunchAction for the Scheme in ${schemeFile.path}.');
434+
return;
435+
}
436+
final XmlNode launchAction = nodes.first;
437+
final XmlAttribute? debuggerIdentifer = launchAction.attributes
438+
.where((XmlAttribute attribute) =>
439+
attribute.localName == 'selectedDebuggerIdentifier')
440+
.firstOrNull;
441+
final XmlAttribute? launcherIdentifer = launchAction.attributes
442+
.where((XmlAttribute attribute) =>
443+
attribute.localName == 'selectedLauncherIdentifier')
444+
.firstOrNull;
445+
if (debuggerIdentifer == null ||
446+
launcherIdentifer == null ||
447+
!debuggerIdentifer.value.contains('LLDB') ||
448+
!launcherIdentifer.value.contains('LLDB')) {
449+
throwToolExit('''
450+
Your Xcode project is not setup to start a debugger. To fix this, launch Xcode
451+
and select "Product > Scheme > Edit Scheme", select "Run" in the sidebar,
452+
and ensure "Debug executable" is checked in the "Info" tab.
453+
''');
454+
}
455+
} on XmlException catch (exception) {
456+
_logger.printError('Failed to parse ${schemeFile.path}: $exception');
457+
}
458+
}
414459
}
415460

416461
@visibleForTesting

packages/flutter_tools/lib/src/migrations/xcode_project_object_version_migration.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class XcodeProjectObjectVersionMigration extends ProjectMigrator {
1212
XcodeBasedProject project,
1313
super.logger,
1414
) : _xcodeProjectInfoFile = project.xcodeProjectInfoFile,
15-
_xcodeProjectSchemeFile = project.xcodeProjectSchemeFile;
15+
_xcodeProjectSchemeFile = project.xcodeProjectSchemeFile();
1616

1717
final File _xcodeProjectInfoFile;
1818
final File _xcodeProjectSchemeFile;

packages/flutter_tools/lib/src/xcode_project.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,10 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform {
6868
File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');
6969

7070
/// The 'Runner.xcscheme' file of [xcodeProject].
71-
File get xcodeProjectSchemeFile =>
72-
xcodeProject.childDirectory('xcshareddata').childDirectory('xcschemes').childFile('Runner.xcscheme');
71+
File xcodeProjectSchemeFile({String? scheme}) {
72+
final String schemeName = scheme ?? 'Runner';
73+
return xcodeProject.childDirectory('xcshareddata').childDirectory('xcschemes').childFile('$schemeName.xcscheme');
74+
}
7375

7476
File get xcodeProjectWorkspaceData =>
7577
xcodeProject

packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,81 @@ void main() {
520520
Xcode: () => xcode,
521521
});
522522

523+
group('with flavor', () {
524+
setUp(() {
525+
projectInfo = XcodeProjectInfo(
526+
<String>['Runner'],
527+
<String>['Debug', 'Release', 'Debug-free', 'Release-free'],
528+
<String>['Runner', 'free'],
529+
logger,
530+
);
531+
fakeXcodeProjectInterpreter = FakeXcodeProjectInterpreter(projectInfo: projectInfo);
532+
xcode = Xcode.test(processManager: FakeProcessManager.any(), xcodeProjectInterpreter: fakeXcodeProjectInterpreter);
533+
});
534+
535+
testUsingContext('succeeds', () async {
536+
final IOSDevice iosDevice = setUpIOSDevice(
537+
fileSystem: fileSystem,
538+
processManager: FakeProcessManager.any(),
539+
logger: logger,
540+
artifacts: artifacts,
541+
isCoreDevice: true,
542+
coreDeviceControl: FakeIOSCoreDeviceControl(),
543+
xcodeDebug: FakeXcodeDebug(
544+
expectedProject: XcodeDebugProject(
545+
scheme: 'free',
546+
xcodeWorkspace: fileSystem.directory('/ios/Runner.xcworkspace'),
547+
xcodeProject: fileSystem.directory('/ios/Runner.xcodeproj'),
548+
hostAppProjectName: 'Runner',
549+
),
550+
expectedDeviceId: '123',
551+
expectedLaunchArguments: <String>['--enable-dart-profiling'],
552+
expectedSchemeFilePath: '/ios/Runner.xcodeproj/xcshareddata/xcschemes/free.xcscheme',
553+
),
554+
);
555+
556+
setUpIOSProject(fileSystem);
557+
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
558+
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
559+
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
560+
561+
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
562+
563+
iosDevice.portForwarder = const NoOpDevicePortForwarder();
564+
iosDevice.setLogReader(buildableIOSApp, deviceLogReader);
565+
566+
// Start writing messages to the log reader.
567+
Timer.run(() {
568+
deviceLogReader.addLine('Foo');
569+
deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
570+
});
571+
572+
final LaunchResult launchResult = await iosDevice.startApp(
573+
buildableIOSApp,
574+
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
575+
BuildMode.debug,
576+
'free',
577+
buildName: '1.2.3',
578+
buildNumber: '4',
579+
treeShakeIcons: false,
580+
)),
581+
platformArgs: <String, Object>{},
582+
);
583+
584+
expect(logger.errorText, isEmpty);
585+
expect(fileSystem.directory('build/ios/iphoneos'), exists);
586+
expect(launchResult.started, true);
587+
expect(processManager, hasNoRemainingExpectations);
588+
}, overrides: <Type, Generator>{
589+
ProcessManager: () => FakeProcessManager.any(),
590+
FileSystem: () => fileSystem,
591+
Logger: () => logger,
592+
Platform: () => macPlatform,
593+
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
594+
Xcode: () => xcode,
595+
});
596+
});
597+
523598
testUsingContext('updates Generated.xcconfig before and after launch', () async {
524599
final Completer<void> debugStartedCompleter = Completer<void>();
525600
final Completer<void> debugEndedCompleter = Completer<void>();
@@ -829,6 +904,7 @@ class FakeXcodeDebug extends Fake implements XcodeDebug {
829904
this.expectedProject,
830905
this.expectedDeviceId,
831906
this.expectedLaunchArguments,
907+
this.expectedSchemeFilePath = '/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme',
832908
this.debugStartedCompleter,
833909
this.debugEndedCompleter,
834910
});
@@ -840,6 +916,7 @@ class FakeXcodeDebug extends Fake implements XcodeDebug {
840916
final List<String>? expectedLaunchArguments;
841917
final Completer<void>? debugStartedCompleter;
842918
final Completer<void>? debugEndedCompleter;
919+
final String expectedSchemeFilePath;
843920

844921
@override
845922
Future<bool> debugApp({
@@ -863,6 +940,11 @@ class FakeXcodeDebug extends Fake implements XcodeDebug {
863940
await debugEndedCompleter?.future;
864941
return debugSuccess;
865942
}
943+
944+
@override
945+
void ensureXcodeDebuggerLaunchAction(File schemeFile) {
946+
expect(schemeFile.path, expectedSchemeFilePath);
947+
}
866948
}
867949

868950
class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {

packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@ platform :ios, '11.0'
662662
project.xcodeProjectInfoFile = xcodeProjectInfoFile;
663663

664664
xcodeProjectSchemeFile = memoryFileSystem.file('Runner.xcscheme');
665-
project.xcodeProjectSchemeFile = xcodeProjectSchemeFile;
665+
project.schemeFile = xcodeProjectSchemeFile;
666666
});
667667

668668
testWithoutContext('skipped if files are missing', () {
@@ -1370,8 +1370,10 @@ class FakeIosProject extends Fake implements IosProject {
13701370
@override
13711371
File xcodeProjectInfoFile = MemoryFileSystem.test().file('xcodeProjectInfoFile');
13721372

1373+
File? schemeFile;
1374+
13731375
@override
1374-
File xcodeProjectSchemeFile = MemoryFileSystem.test().file('xcodeProjectSchemeFile');
1376+
File xcodeProjectSchemeFile({String? scheme}) => schemeFile ?? MemoryFileSystem.test().file('xcodeProjectSchemeFile');
13751377

13761378
@override
13771379
File appFrameworkInfoPlist = MemoryFileSystem.test().file('appFrameworkInfoPlist');

0 commit comments

Comments
 (0)