Unverified Commit 5dd2a4e0 authored by Victoria Ashworth's avatar Victoria Ashworth Committed by GitHub

Ensure Xcode project is setup to start debugger (#136977)

Some users have their Xcode settings set to not debug (see example here https://github.com/flutter/flutter/issues/136197#issuecomment-1766834195). 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 https://github.com/flutter/flutter/issues/136197.
parent 9366170e
......@@ -3926,6 +3926,16 @@ targets:
["devicelab", "ios", "mac"]
task_name: flavors_test_ios
- name: Mac_arm64_ios flavors_test_ios_xcode_debug
recipe: devicelab/devicelab_drone
presubmit: false
timeout: 60
properties:
tags: >
["devicelab", "ios", "mac"]
task_name: flavors_test_ios_xcode_debug
bringup: true
- name: Mac_ios flutter_gallery_ios__compile
recipe: devicelab/devicelab_drone
presubmit: false
......
......@@ -174,6 +174,7 @@
/dev/devicelab/bin/tasks/cubic_bezier_perf_ios_sksl_warmup__timeline_summary.dart @zanderso @flutter/engine
/dev/devicelab/bin/tasks/external_ui_integration_test_ios.dart @zanderso @flutter/engine
/dev/devicelab/bin/tasks/flavors_test_ios.dart @vashworth @flutter/tool
/dev/devicelab/bin/tasks/flavors_test_ios_xcode_debug.dart @vashworth @flutter/tool
/dev/devicelab/bin/tasks/flutter_gallery__transition_perf_e2e_ios.dart @zanderso @flutter/engine
/dev/devicelab/bin/tasks/flutter_gallery_ios__compile.dart @vashworth @flutter/engine
/dev/devicelab/bin/tasks/flutter_gallery_ios__start_up.dart @vashworth @flutter/engine
......
// 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/integration_tests.dart';
Future<void> main() async {
deviceOperatingSystem = DeviceOperatingSystem.ios;
await task(() async {
await createFlavorsTest(environment: <String, String>{
'FORCE_XCODE_DEBUG': 'true',
}).call();
await createIntegrationTestFlavorsTest(environment: <String, String>{
'FORCE_XCODE_DEBUG': 'true',
}).call();
// test install and uninstall of flavors app
final TaskResult installTestsResult = await inDirectory(
'${flutterDirectory.path}/dev/integration_tests/flavors',
() async {
await flutter(
'install',
options: <String>['--flavor', 'paid'],
);
await flutter(
'install',
options: <String>['--flavor', 'paid', '--uninstall-only'],
);
final StringBuffer stderr = StringBuffer();
await evalFlutter(
'install',
canFail: true,
stderr: stderr,
options: <String>['--flavor', 'bogus'],
);
final String stderrString = stderr.toString();
if (!stderrString.contains('The Xcode project defines schemes: free, paid')) {
print(stderrString);
return TaskResult.failure('Should not succeed with bogus flavor');
}
return TaskResult.success(null);
},
);
return installTestsResult;
});
}
......@@ -22,19 +22,21 @@ TaskFunction createPlatformInteractionTest() {
).call;
}
TaskFunction createFlavorsTest() {
TaskFunction createFlavorsTest({Map<String, String>? environment}) {
return DriverTest(
'${flutterDirectory.path}/dev/integration_tests/flavors',
'lib/main.dart',
extraOptions: <String>['--flavor', 'paid'],
environment: environment,
).call;
}
TaskFunction createIntegrationTestFlavorsTest() {
TaskFunction createIntegrationTestFlavorsTest({Map<String, String>? environment}) {
return IntegrationTest(
'${flutterDirectory.path}/dev/integration_tests/flavors',
'integration_test/integration_test.dart',
extraOptions: <String>['--flavor', 'paid'],
environment: environment,
).call;
}
......@@ -219,6 +221,7 @@ class IntegrationTest {
this.extraOptions = const <String>[],
this.createPlatforms = const <String>[],
this.withTalkBack = false,
this.environment,
}
);
......@@ -227,6 +230,7 @@ class IntegrationTest {
final List<String> extraOptions;
final List<String> createPlatforms;
final bool withTalkBack;
final Map<String, String>? environment;
Future<TaskResult> call() {
return inDirectory<TaskResult>(testDirectory, () async {
......@@ -258,7 +262,7 @@ class IntegrationTest {
testTarget,
...extraOptions,
];
await flutter('test', options: options);
await flutter('test', options: options, environment: environment);
if (withTalkBack) {
await disableTalkBack();
......
......@@ -877,6 +877,8 @@ class IOSDevice extends Device {
projectInfo.reportFlavorNotFoundAndExit();
}
_xcodeDebug.ensureXcodeDebuggerLaunchAction(project.xcodeProjectSchemeFile(scheme: scheme));
debugProject = XcodeDebugProject(
scheme: scheme,
xcodeProject: project.xcodeProject,
......
......@@ -6,7 +6,10 @@ import 'dart:async';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'package:xml/xml.dart';
import 'package:xml/xpath.dart';
import '../base/common.dart';
import '../base/error_handling_io.dart';
import '../base/file_system.dart';
import '../base/io.dart';
......@@ -58,7 +61,6 @@ class XcodeDebug {
required String deviceId,
required List<String> launchArguments,
}) async {
// If project is not already opened in Xcode, open it.
if (!await _isProjectOpenInXcode(project: project)) {
final bool openResult = await _openProjectInXcode(xcodeWorkspace: project.xcodeWorkspace);
......@@ -411,6 +413,49 @@ class XcodeDebug {
verboseLogging: verboseLogging,
);
}
/// Ensure the Xcode project is set up to launch an LLDB debugger. If these
/// settings are not set, the launch will fail with a "Cannot create a
/// FlutterEngine instance in debug mode without Flutter tooling or Xcode."
/// error message. These settings should be correct by default, but some users
/// reported them not being so after upgrading to Xcode 15.
void ensureXcodeDebuggerLaunchAction(File schemeFile) {
if (!schemeFile.existsSync()) {
_logger.printError('Failed to find ${schemeFile.path}');
return;
}
final String schemeXml = schemeFile.readAsStringSync();
try {
final XmlDocument document = XmlDocument.parse(schemeXml);
final Iterable<XmlNode> nodes = document.xpath('/Scheme/LaunchAction');
if (nodes.isEmpty) {
_logger.printError('Failed to find LaunchAction for the Scheme in ${schemeFile.path}.');
return;
}
final XmlNode launchAction = nodes.first;
final XmlAttribute? debuggerIdentifer = launchAction.attributes
.where((XmlAttribute attribute) =>
attribute.localName == 'selectedDebuggerIdentifier')
.firstOrNull;
final XmlAttribute? launcherIdentifer = launchAction.attributes
.where((XmlAttribute attribute) =>
attribute.localName == 'selectedLauncherIdentifier')
.firstOrNull;
if (debuggerIdentifer == null ||
launcherIdentifer == null ||
!debuggerIdentifer.value.contains('LLDB') ||
!launcherIdentifer.value.contains('LLDB')) {
throwToolExit('''
Your Xcode project is not setup to start a debugger. To fix this, launch Xcode
and select "Product > Scheme > Edit Scheme", select "Run" in the sidebar,
and ensure "Debug executable" is checked in the "Info" tab.
''');
}
} on XmlException catch (exception) {
_logger.printError('Failed to parse ${schemeFile.path}: $exception');
}
}
}
@visibleForTesting
......
......@@ -12,7 +12,7 @@ class XcodeProjectObjectVersionMigration extends ProjectMigrator {
XcodeBasedProject project,
super.logger,
) : _xcodeProjectInfoFile = project.xcodeProjectInfoFile,
_xcodeProjectSchemeFile = project.xcodeProjectSchemeFile;
_xcodeProjectSchemeFile = project.xcodeProjectSchemeFile();
final File _xcodeProjectInfoFile;
final File _xcodeProjectSchemeFile;
......
......@@ -68,8 +68,10 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform {
File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');
/// The 'Runner.xcscheme' file of [xcodeProject].
File get xcodeProjectSchemeFile =>
xcodeProject.childDirectory('xcshareddata').childDirectory('xcschemes').childFile('Runner.xcscheme');
File xcodeProjectSchemeFile({String? scheme}) {
final String schemeName = scheme ?? 'Runner';
return xcodeProject.childDirectory('xcshareddata').childDirectory('xcschemes').childFile('$schemeName.xcscheme');
}
File get xcodeProjectWorkspaceData =>
xcodeProject
......
......@@ -520,6 +520,81 @@ void main() {
Xcode: () => xcode,
});
group('with flavor', () {
setUp(() {
projectInfo = XcodeProjectInfo(
<String>['Runner'],
<String>['Debug', 'Release', 'Debug-free', 'Release-free'],
<String>['Runner', 'free'],
logger,
);
fakeXcodeProjectInterpreter = FakeXcodeProjectInterpreter(projectInfo: projectInfo);
xcode = Xcode.test(processManager: FakeProcessManager.any(), xcodeProjectInterpreter: fakeXcodeProjectInterpreter);
});
testUsingContext('succeeds', () async {
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: FakeProcessManager.any(),
logger: logger,
artifacts: artifacts,
isCoreDevice: true,
coreDeviceControl: FakeIOSCoreDeviceControl(),
xcodeDebug: FakeXcodeDebug(
expectedProject: XcodeDebugProject(
scheme: 'free',
xcodeWorkspace: fileSystem.directory('/ios/Runner.xcworkspace'),
xcodeProject: fileSystem.directory('/ios/Runner.xcodeproj'),
hostAppProjectName: 'Runner',
),
expectedDeviceId: '123',
expectedLaunchArguments: <String>['--enable-dart-profiling'],
expectedSchemeFilePath: '/ios/Runner.xcodeproj/xcshareddata/xcschemes/free.xcscheme',
),
);
setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
iosDevice.portForwarder = const NoOpDevicePortForwarder();
iosDevice.setLogReader(buildableIOSApp, 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 iosDevice.startApp(
buildableIOSApp,
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
BuildMode.debug,
'free',
buildName: '1.2.3',
buildNumber: '4',
treeShakeIcons: false,
)),
platformArgs: <String, Object>{},
);
expect(logger.errorText, isEmpty);
expect(fileSystem.directory('build/ios/iphoneos'), exists);
expect(launchResult.started, true);
expect(processManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FileSystem: () => fileSystem,
Logger: () => logger,
Platform: () => macPlatform,
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
Xcode: () => xcode,
});
});
testUsingContext('updates Generated.xcconfig before and after launch', () async {
final Completer<void> debugStartedCompleter = Completer<void>();
final Completer<void> debugEndedCompleter = Completer<void>();
......@@ -829,6 +904,7 @@ class FakeXcodeDebug extends Fake implements XcodeDebug {
this.expectedProject,
this.expectedDeviceId,
this.expectedLaunchArguments,
this.expectedSchemeFilePath = '/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme',
this.debugStartedCompleter,
this.debugEndedCompleter,
});
......@@ -840,6 +916,7 @@ class FakeXcodeDebug extends Fake implements XcodeDebug {
final List<String>? expectedLaunchArguments;
final Completer<void>? debugStartedCompleter;
final Completer<void>? debugEndedCompleter;
final String expectedSchemeFilePath;
@override
Future<bool> debugApp({
......@@ -863,6 +940,11 @@ class FakeXcodeDebug extends Fake implements XcodeDebug {
await debugEndedCompleter?.future;
return debugSuccess;
}
@override
void ensureXcodeDebuggerLaunchAction(File schemeFile) {
expect(schemeFile.path, expectedSchemeFilePath);
}
}
class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {
......
......@@ -662,7 +662,7 @@ platform :ios, '11.0'
project.xcodeProjectInfoFile = xcodeProjectInfoFile;
xcodeProjectSchemeFile = memoryFileSystem.file('Runner.xcscheme');
project.xcodeProjectSchemeFile = xcodeProjectSchemeFile;
project.schemeFile = xcodeProjectSchemeFile;
});
testWithoutContext('skipped if files are missing', () {
......@@ -1370,8 +1370,10 @@ class FakeIosProject extends Fake implements IosProject {
@override
File xcodeProjectInfoFile = MemoryFileSystem.test().file('xcodeProjectInfoFile');
File? schemeFile;
@override
File xcodeProjectSchemeFile = MemoryFileSystem.test().file('xcodeProjectSchemeFile');
File xcodeProjectSchemeFile({String? scheme}) => schemeFile ?? MemoryFileSystem.test().file('xcodeProjectSchemeFile');
@override
File appFrameworkInfoPlist = MemoryFileSystem.test().file('appFrameworkInfoPlist');
......
......@@ -1064,6 +1064,96 @@ void main() {
expect(status, isFalse);
});
});
group('ensureXcodeDebuggerLaunchAction', () {
late Xcode xcode;
setUp(() {
xcode = setupXcode(
fakeProcessManager: fakeProcessManager,
fileSystem: fileSystem,
flutterRoot: flutterRoot,
);
});
testWithoutContext('succeeds', () async {
final XcodeDebug xcodeDebug = XcodeDebug(
logger: logger,
processManager: fakeProcessManager,
xcode: xcode,
fileSystem: fileSystem,
);
final File schemeFile = fileSystem.file('ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme');
schemeFile.createSync(recursive: true);
schemeFile.writeAsStringSync(validSchemeXml);
xcodeDebug.ensureXcodeDebuggerLaunchAction(schemeFile);
expect(logger.errorText, isEmpty);
});
testWithoutContext('prints error if scheme file not found', () async {
final XcodeDebug xcodeDebug = XcodeDebug(
logger: logger,
processManager: fakeProcessManager,
xcode: xcode,
fileSystem: fileSystem,
);
final File schemeFile = fileSystem.file('ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme');
xcodeDebug.ensureXcodeDebuggerLaunchAction(schemeFile);
expect(logger.errorText.contains('Failed to find'), isTrue);
});
testWithoutContext('throws error if launch action is missing debugger info', () async {
final XcodeDebug xcodeDebug = XcodeDebug(
logger: logger,
processManager: fakeProcessManager,
xcode: xcode,
fileSystem: fileSystem,
);
final File schemeFile = fileSystem.file('ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme');
schemeFile.createSync(recursive: true);
schemeFile.writeAsStringSync(disabledDebugExecutableSchemeXml);
expect(() => xcodeDebug.ensureXcodeDebuggerLaunchAction(schemeFile),
throwsToolExit(message: 'Your Xcode project is not setup to start a debugger.'));
});
testWithoutContext('prints error if unable to find launch action', () async {
final XcodeDebug xcodeDebug = XcodeDebug(
logger: logger,
processManager: fakeProcessManager,
xcode: xcode,
fileSystem: fileSystem,
);
final File schemeFile = fileSystem.file('ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme');
schemeFile.createSync(recursive: true);
schemeFile.writeAsStringSync('<?xml version="1.0" encoding="UTF-8"?><Scheme></Scheme>');
xcodeDebug.ensureXcodeDebuggerLaunchAction(schemeFile);
expect(logger.errorText.contains('Failed to find LaunchAction for the Scheme'), isTrue);
});
testWithoutContext('prints error if invalid xml', () async {
final XcodeDebug xcodeDebug = XcodeDebug(
logger: logger,
processManager: fakeProcessManager,
xcode: xcode,
fileSystem: fileSystem,
);
final File schemeFile = fileSystem.file('ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme');
schemeFile.createSync(recursive: true);
schemeFile.writeAsStringSync('<?xml version="1.0" encoding="UTF-8"?><Scheme>');
xcodeDebug.ensureXcodeDebuggerLaunchAction(schemeFile);
expect(logger.errorText.contains('Failed to parse'), isTrue);
});
});
});
group('Debug project through Xcode with app bundle', () {
......@@ -1161,3 +1251,89 @@ class FakeProcess extends Fake implements Process {
return true;
}
}
const String validSchemeXml = '''
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
''';
const String disabledDebugExecutableSchemeXml = '''
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
''';
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment