Skip to content

Commit

Permalink
Ensure Xcode project is setup to start debugger (#136977)
Browse files Browse the repository at this point in the history
Some users have their Xcode settings set to not debug (see example here flutter/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/flutter#136197.
  • Loading branch information
vashworth authored Oct 25, 2023
1 parent 9366170 commit 5dd2a4e
Show file tree
Hide file tree
Showing 11 changed files with 385 additions and 9 deletions.
10 changes: 10 additions & 0 deletions .ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions TESTOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions dev/devicelab/bin/tasks/flavors_test_ios_xcode_debug.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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;
});
}
10 changes: 7 additions & 3 deletions dev/devicelab/lib/tasks/integration_tests.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -219,6 +221,7 @@ class IntegrationTest {
this.extraOptions = const <String>[],
this.createPlatforms = const <String>[],
this.withTalkBack = false,
this.environment,
}
);

Expand All @@ -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 {
Expand Down Expand Up @@ -258,7 +262,7 @@ class IntegrationTest {
testTarget,
...extraOptions,
];
await flutter('test', options: options);
await flutter('test', options: options, environment: environment);

if (withTalkBack) {
await disableTalkBack();
Expand Down
2 changes: 2 additions & 0 deletions packages/flutter_tools/lib/src/ios/devices.dart
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,8 @@ class IOSDevice extends Device {
projectInfo.reportFlavorNotFoundAndExit();
}

_xcodeDebug.ensureXcodeDebuggerLaunchAction(project.xcodeProjectSchemeFile(scheme: scheme));

debugProject = XcodeDebugProject(
scheme: scheme,
xcodeProject: project.xcodeProject,
Expand Down
47 changes: 46 additions & 1 deletion packages/flutter_tools/lib/src/ios/xcode_debug.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions packages/flutter_tools/lib/src/xcode_project.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>();
Expand Down Expand Up @@ -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,
});
Expand All @@ -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({
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () {
Expand Down Expand Up @@ -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');
Expand Down
Loading

0 comments on commit 5dd2a4e

Please sign in to comment.