Add lldb init file (#164344)

Adds an .lldbinit file to iOS app xcscheme.

Adding to scheme files can be error prone since a developer may be using
custom schemes (flavors). If we can't add it to the scheme, we print an
error without failing.

Since it is part of the scheme, it will be added to the project and will
be used on every run regardless of the device type/version. The Dart
side handles limiting to specific devices. If needed, we can alter the
.lldbinit file during `flutter assemble` to rewrite it since it doesn't
read the file until launch time (therefore it can be changed during
build time).

During `flutter assemble`, if the project doesn't have an LLDB Init File
set for any schemes, it'll throw an error if running in debug mode with
an iOS 18.4+ device.

## 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].
- [ ] 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].

<!-- 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
This commit is contained in:
Victoria Ashworth 2025-03-06 15:21:52 -06:00 committed by GitHub
parent 8876bccf3a
commit eb07c51230
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1935 additions and 8 deletions

View File

@ -5251,6 +5251,22 @@ targets:
["devicelab", "ios", "mac"]
task_name: hot_mode_dev_cycle_ios__benchmark
- name: Mac_arm64_ios hot_mode_dev_cycle_ios_beta__benchmark
recipe: devicelab/devicelab_drone
presubmit: false
bringup: true
timeout: 60
properties:
os: Mac-15
device_os: iOS-18.4
$flutter/osx_sdk : >-
{
"sdk_version": "16e5104o"
}
tags: >
["devicelab", "ios", "mac"]
task_name: hot_mode_dev_cycle_ios__benchmark
- name: Mac_arm64_ios hot_mode_dev_cycle_ios__benchmark
recipe: devicelab/devicelab_drone
presubmit: false

View File

@ -456,6 +456,14 @@ Future<void> _testBuildIosFramework(Directory projectDir, {bool isModule = false
throw TaskResult.failure('Unexpected GeneratedPluginRegistrant.m.');
}
if (File(path.join(outputPath, 'flutter_lldbinit')).existsSync() == isModule) {
throw TaskResult.failure('Unexpected flutter_lldbinit');
}
if (File(path.join(outputPath, 'flutter_lldb_helper.py')).existsSync() == isModule) {
throw TaskResult.failure('Unexpected flutter_lldb_helper.py.');
}
section('Build frameworks without plugins');
await _testBuildFrameworksWithoutPlugins(projectDir, platform: 'ios');

View File

@ -454,6 +454,8 @@ class Context {
'--ExtraGenSnapshotOptions=${environment['EXTRA_GEN_SNAPSHOT_OPTIONS'] ?? ''}',
'--DartDefines=${environment['DART_DEFINES'] ?? ''}',
'--ExtraFrontEndOptions=${environment['EXTRA_FRONT_END_OPTIONS'] ?? ''}',
'-dSrcRoot=${environment['SRCROOT'] ?? ''}',
'-dTargetDeviceOSVersion=${environment['TARGET_DEVICE_OS_VERSION'] ?? ''}',
]);
if (command == 'prepare') {

View File

@ -973,6 +973,15 @@ const String kAppFlavor = 'FLUTTER_APP_FLAVOR';
/// The Xcode configuration used to build the project.
const String kXcodeConfiguration = 'Configuration';
/// The Xcode build setting SRCROOT. Identifies the directory containing the
/// Xcode target's source files.
const String kSrcRoot = 'SrcRoot';
/// The Xcode build setting TARGET_DEVICE_OS_VERSION. The iOS version of the
/// target device. Only available if a specific device is being targeted during
/// the build.
const String kTargetDeviceOSVersion = 'TargetDeviceOSVersion';
/// The define to pass build number
const String kBuildNumber = 'BuildNumber';

View File

@ -11,6 +11,7 @@ import '../../base/common.dart';
import '../../base/file_system.dart';
import '../../base/io.dart';
import '../../base/process.dart';
import '../../base/version.dart';
import '../../build_info.dart';
import '../../devfs.dart';
import '../../globals.dart' as globals;
@ -432,6 +433,122 @@ class DebugUnpackIOS extends UnpackIOS {
BuildMode get buildMode => BuildMode.debug;
}
abstract class IosLLDBInit extends Target {
const IosLLDBInit();
@override
List<Source> get inputs => <Source>[
const Source.pattern(
'{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart',
),
];
@override
List<Source> get outputs {
final FlutterProject flutterProject = FlutterProject.current();
final String lldbInitFilePath = flutterProject.ios.lldbInitFile.path.replaceFirst(
flutterProject.directory.path,
'{PROJECT_DIR}/',
);
return <Source>[Source.pattern(lldbInitFilePath)];
}
@override
List<Target> get dependencies => <Target>[];
@visibleForOverriding
BuildMode get buildMode;
@override
Future<void> build(Environment environment) async {
final String? sdkRoot = environment.defines[kSdkRoot];
if (sdkRoot == null) {
throw MissingDefineException(kSdkRoot, name);
}
final EnvironmentType? environmentType = environmentTypeFromSdkroot(
sdkRoot,
environment.fileSystem,
);
// LLDB Init File is only required for physical devices in debug mode.
if (!buildMode.isJit || environmentType != EnvironmentType.physical) {
return;
}
final String? targetDeviceVersionString = environment.defines[kTargetDeviceOSVersion];
if (targetDeviceVersionString == null) {
// Skip if TARGET_DEVICE_OS_VERSION is not found. TARGET_DEVICE_OS_VERSION
// is not set if "build ios-framework" is called, which builds the
// DebugIosApplicationBundle directly rather than through flutter assemble.
// If may also be null if the build is targeting multiple device types.
return;
}
final Version? targetDeviceVersion = Version.parse(targetDeviceVersionString);
if (targetDeviceVersion == null) {
environment.logger.printError(
'Failed to parse Target Device Version $targetDeviceVersionString',
);
return;
}
// LLDB Init File is only needed for iOS 18.4+.
if (targetDeviceVersion < Version(18, 4, null)) {
return;
}
// The scheme name is not available in Xcode Build Phases Run Scripts.
// Instead, find all xcscheme files in the Xcode project (this may be the
// Flutter Xcode project or an Add to App native Xcode project) and check
// if any of them contain "customLLDBInitFile". If none have it set, throw
// an error.
final String? srcRoot = environment.defines[kSrcRoot];
if (srcRoot == null) {
throw MissingDefineException(kSdkRoot, name);
}
final Directory xcodeProjectDir = environment.fileSystem.directory(srcRoot);
if (!xcodeProjectDir.existsSync()) {
throw Exception('Failed to find ${xcodeProjectDir.path}');
}
bool anyLLDBInitFound = false;
await for (final FileSystemEntity entity in xcodeProjectDir.list(recursive: true)) {
if (environment.fileSystem.path.extension(entity.path) == '.xcscheme' && entity is File) {
if (entity.readAsStringSync().contains('customLLDBInitFile')) {
anyLLDBInitFound = true;
break;
}
}
}
if (!anyLLDBInitFound) {
final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
if (flutterProject.isModule) {
throwToolExit(
'Debugging Flutter on new iOS versions requires an LLDB Init File. To '
'ensure debug mode works, please run "flutter build ios --config-only" '
'in your Flutter project and follow the instructions to add the file.',
);
} else {
throwToolExit(
'Debugging Flutter on new iOS versions requires an LLDB Init File. To '
'ensure debug mode works, please run "flutter build ios --config-only" '
'in your Flutter project and automatically add the files.',
);
}
}
return;
}
}
class DebugIosLLDBInit extends IosLLDBInit {
const DebugIosLLDBInit();
@override
String get name => 'debug_ios_lldb_init';
@override
BuildMode get buildMode => BuildMode.debug;
}
/// The base class for all iOS bundle targets.
///
/// This is responsible for setting up the basic App.framework structure, including:
@ -583,7 +700,11 @@ class DebugIosApplicationBundle extends IosAssetBundle {
];
@override
List<Target> get dependencies => <Target>[const DebugUniversalFramework(), ...super.dependencies];
List<Target> get dependencies => <Target>[
const DebugUniversalFramework(),
const DebugIosLLDBInit(),
...super.dependencies,
];
}
/// IosAssetBundle with debug symbols, used for Profile and Release builds.

View File

@ -372,6 +372,27 @@ class BuildIOSFrameworkCommand extends BuildFrameworkCommand {
);
}
if (!project.isModule && buildInfos.any((BuildInfo info) => info.isDebug)) {
// Add-to-App must manually add the LLDB Init File to their native Xcode
// project, so provide the files and instructions.
final File lldbInitSourceFile = project.ios.lldbInitFile;
final File lldbInitTargetFile = outputDirectory.childFile(lldbInitSourceFile.basename);
final File lldbHelperPythonFile = project.ios.lldbHelperPythonFile;
lldbInitSourceFile.copySync(lldbInitTargetFile.path);
lldbHelperPythonFile.copySync(outputDirectory.childFile(lldbHelperPythonFile.basename).path);
globals.printStatus(
'\nDebugging Flutter on new iOS versions requires an LLDB Init File. To '
'ensure debug mode works, please complete one of the following in your '
'native Xcode project:\n'
' * Open Xcode > Product > Scheme > Edit Scheme. For both the Run and '
'Test actions, set LLDB Init File to: \n\n'
' ${lldbInitTargetFile.path}\n\n'
' * If you are already using an LLDB Init File, please append the '
'following to your LLDB Init File:\n\n'
' command source ${lldbInitTargetFile.path}\n',
);
}
return FlutterCommandResult.success();
}

View File

@ -25,6 +25,7 @@ import '../globals.dart' as globals;
import '../macos/cocoapod_utils.dart';
import '../macos/swift_package_manager.dart';
import '../macos/xcode.dart';
import '../migrations/lldb_init_migration.dart';
import '../migrations/swift_package_manager_gitignore_migration.dart';
import '../migrations/swift_package_manager_integration_migration.dart';
import '../migrations/xcode_project_object_version_migration.dart';
@ -168,6 +169,14 @@ Future<XcodeBuildResult> buildXcodeProject({
),
SwiftPackageManagerGitignoreMigration(project, globals.logger),
MetalAPIValidationMigrator.ios(app.project, globals.logger),
LLDBInitMigration(
app.project,
buildInfo,
globals.logger,
deviceID: deviceID,
fileSystem: globals.fs,
environmentType: environmentType,
),
];
final ProjectMigration migration = ProjectMigration(migrators);

View File

@ -0,0 +1,285 @@
// 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:xml/xml.dart';
import 'package:xml/xpath.dart';
import '../base/file_system.dart';
import '../base/project_migrator.dart';
import '../build_info.dart';
import '../ios/xcodeproj.dart';
import '../project.dart';
class LLDBInitMigration extends ProjectMigrator {
LLDBInitMigration(
IosProject project,
BuildInfo buildInfo,
super.logger, {
required FileSystem fileSystem,
required EnvironmentType environmentType,
String? deviceID,
}) : _xcodeProject = project,
_buildInfo = buildInfo,
_xcodeProjectInfoFile = project.xcodeProjectInfoFile,
_fileSystem = fileSystem,
_environmentType = environmentType,
_deviceID = deviceID;
final IosProject _xcodeProject;
final BuildInfo _buildInfo;
final FileSystem _fileSystem;
final File _xcodeProjectInfoFile;
final EnvironmentType _environmentType;
final String? _deviceID;
String get _initPath =>
_xcodeProject.lldbInitFile.path.replaceFirst(_xcodeProject.hostAppRoot.path, r'$(SRCROOT)');
static const String _launchActionIdentifier = 'LaunchAction';
static const String _testActionIdentifier = 'TestAction';
@override
Future<void> migrate() async {
SchemeInfo? schemeInfo;
try {
if (!_xcodeProjectInfoFile.existsSync()) {
throw Exception('Xcode project not found.');
}
schemeInfo = await _getSchemeInfo();
final bool isSchemeMigrated = await _isSchemeMigrated(schemeInfo);
if (isSchemeMigrated) {
return;
}
_migrateScheme(schemeInfo);
} on Exception catch (e) {
logger.printError(
'An error occurred when adding LLDB Init File:\n'
'$e',
);
}
}
Future<SchemeInfo> _getSchemeInfo() async {
final XcodeProjectInfo? projectInfo = await _xcodeProject.projectInfo();
if (projectInfo == null) {
throw Exception('Unable to get Xcode project info.');
}
if (_xcodeProject.xcodeWorkspace == null) {
throw Exception('Xcode workspace not found.');
}
final String? scheme = projectInfo.schemeFor(_buildInfo);
if (scheme == null) {
projectInfo.reportFlavorNotFoundAndExit();
}
final File schemeFile = _xcodeProject.xcodeProjectSchemeFile(scheme: scheme);
if (!schemeFile.existsSync()) {
throw Exception('Unable to get scheme file for $scheme.');
}
final String schemeContent = schemeFile.readAsStringSync();
return SchemeInfo(schemeName: scheme, schemeFile: schemeFile, schemeContent: schemeContent);
}
Future<bool> _isSchemeMigrated(SchemeInfo schemeInfo) async {
final String? lldbInitFileLaunchPath;
final String? lldbInitFileTestPath;
try {
// Check that both the LaunchAction and TestAction have the customLLDBInitFile set to flutter_lldbinit.
final XmlDocument document = XmlDocument.parse(schemeInfo.schemeContent);
lldbInitFileLaunchPath = _parseLLDBInitFileFromScheme(
action: _launchActionIdentifier,
document: document,
schemeFile: schemeInfo.schemeFile,
);
lldbInitFileTestPath = _parseLLDBInitFileFromScheme(
action: _testActionIdentifier,
document: document,
schemeFile: schemeInfo.schemeFile,
);
final bool launchActionMigrated =
lldbInitFileLaunchPath != null && lldbInitFileLaunchPath.contains(_initPath);
final bool testActionMigrated =
lldbInitFileTestPath != null && lldbInitFileTestPath.contains(_initPath);
if (launchActionMigrated && testActionMigrated) {
return true;
} else if (launchActionMigrated && !testActionMigrated) {
// If LaunchAction has it set, but TestAction doesn't, give an error
// with instructions to add it to the TestAction.
throw _missingActionException('Test', schemeInfo.schemeName);
} else if (testActionMigrated && !launchActionMigrated) {
// If TestAction has it set, but LaunchAction doesn't, give an error
// with instructions to add it to the LaunchAction.
throw _missingActionException('Launch', schemeInfo.schemeName);
}
} on XmlException catch (exception) {
throw Exception(
'Failed to parse ${schemeInfo.schemeFile.basename}: Invalid xml: ${schemeInfo.schemeContent}\n$exception',
);
}
// If the scheme is using a LLDB Init File that is not flutter_lldbinit,
// attempt to read the file and check if it's importing flutter_lldbinit.
// If the file name contains a variable, attempt to substitute the variable
// using the build settings. If it fails to find the file or fails to
// detect it's using flutter_lldbinit, print a warning to either remove
// their LLDB Init file or append flutter_lldbinit to their existing one.
if (schemeInfo.schemeContent.contains('customLLDBInitFile')) {
try {
Map<String, String>? buildSettings;
if ((lldbInitFileLaunchPath != null && lldbInitFileLaunchPath.contains(r'$')) ||
(lldbInitFileTestPath != null && lldbInitFileTestPath.contains(r'$'))) {
buildSettings =
await _xcodeProject.buildSettingsForBuildInfo(
_buildInfo,
environmentType: _environmentType,
deviceId: _deviceID,
) ??
<String, String>{};
}
final File? lldbInitFileLaunchFile = _resolveLLDBInitFile(
lldbInitFileLaunchPath,
buildSettings,
);
final File? lldbInitFileTestFile = _resolveLLDBInitFile(
lldbInitFileTestPath,
buildSettings,
);
if (lldbInitFileLaunchFile != null &&
lldbInitFileLaunchFile.existsSync() &&
lldbInitFileLaunchFile.readAsStringSync().contains(
_xcodeProject.lldbInitFile.basename,
) &&
lldbInitFileTestFile != null &&
lldbInitFileTestFile.existsSync() &&
lldbInitFileTestFile.readAsStringSync().contains(_xcodeProject.lldbInitFile.basename)) {
return true;
}
} on XmlException catch (exception) {
throw Exception(
'Failed to parse ${schemeInfo.schemeFile.basename}: Invalid xml: ${schemeInfo.schemeContent}\n$exception',
);
}
throw Exception(
'Running Flutter in debug mode on new iOS versions requires a LLDB '
'Init File, but the scheme already has one set. To ensure debug '
'mode works, please complete one of the following:\n'
' * Open Xcode > Product > Scheme > Edit Scheme and remove LLDB Init '
'File for both the Run and Test actions.\n'
' * Append the following to your custom LLDB Init File:\n\n'
' command source ${_xcodeProject.lldbInitFile.absolute.path}\n',
);
}
return false;
}
/// Add customLLDBInitFile and set to [_initPath] for both LaunchAction and TestAction.
void _migrateScheme(SchemeInfo schemeInfo) {
final File schemeFile = schemeInfo.schemeFile;
final String schemeContent = schemeInfo.schemeContent;
final String newScheme = schemeContent.replaceAll(
'selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"',
'''
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$_initPath"''',
);
try {
final XmlDocument document = XmlDocument.parse(newScheme);
_validateSchemeAction(
action: _launchActionIdentifier,
document: document,
schemeFile: schemeFile,
);
_validateSchemeAction(
action: _testActionIdentifier,
document: document,
schemeFile: schemeFile,
);
} on XmlException catch (exception) {
throw Exception(
'Failed to parse ${schemeFile.basename}: Invalid xml: $newScheme\n$exception',
);
}
schemeFile.writeAsStringSync(newScheme);
}
/// Parse the customLLDBInitFile from the XML for the [action] and validate
/// it contains [_initPath].
void _validateSchemeAction({
required String action,
required XmlDocument document,
required File schemeFile,
}) {
final String? lldbInitFile = _parseLLDBInitFileFromScheme(
action: action,
document: document,
schemeFile: schemeFile,
);
if (lldbInitFile == null || !lldbInitFile.contains(_initPath)) {
throw Exception(
'Failed to find correct customLLDBInitFile in $action for the Scheme in ${schemeFile.path}.',
);
}
}
/// Parse the customLLDBInitFile from the XML for the [action].
String? _parseLLDBInitFileFromScheme({
required String action,
required XmlDocument document,
required File schemeFile,
}) {
final Iterable<XmlNode> nodes = document.xpath('/Scheme/$action');
if (nodes.isEmpty) {
throw Exception('Failed to find $action for the Scheme in ${schemeFile.path}.');
}
final XmlNode actionNode = nodes.first;
final XmlAttribute? lldbInitFile =
actionNode.attributes
.where((XmlAttribute attribute) => attribute.localName == 'customLLDBInitFile')
.firstOrNull;
return lldbInitFile?.value;
}
/// Replace any Xcode variables in [lldbInitFilePath] from [buildSettings].
File? _resolveLLDBInitFile(String? lldbInitFilePath, Map<String, String>? buildSettings) {
if (lldbInitFilePath == null) {
return null;
}
if (lldbInitFilePath.contains(r'$') && buildSettings != null) {
// If the path to the LLDB Init File contains a $, it may contain a
// variable from build settings.
final String resolvedInitFilePath = substituteXcodeVariables(lldbInitFilePath, buildSettings);
return _fileSystem.file(resolvedInitFilePath);
}
return _fileSystem.file(lldbInitFilePath);
}
Exception _missingActionException(String missingAction, String schemeName) {
return Exception(
'Running Flutter in debug mode on new iOS versions requires a LLDB '
'Init File, but the $missingAction action in the $schemeName scheme '
'does not have it set. To ensure debug mode works, please complete '
'the following:\n'
' * Open Xcode > Product > Scheme > Edit Scheme and for the '
'$missingAction action, set LLDB Init File to:\n\n'
' $_initPath\n',
);
}
}
class SchemeInfo {
SchemeInfo({required this.schemeName, required this.schemeFile, required this.schemeContent});
final String schemeName;
final File schemeFile;
final String schemeContent;
}

View File

@ -4,6 +4,7 @@
import 'base/error_handling_io.dart';
import 'base/file_system.dart';
import 'base/template.dart';
import 'base/utils.dart';
import 'base/version.dart';
import 'build_info.dart';
@ -349,6 +350,51 @@ class IosProject extends XcodeBasedProject {
// The string starts with `applinks:` and ignores the query param which starts with `?`.
static final RegExp _associatedDomainPattern = RegExp(r'^applinks:([^?]+)');
static const String _lldbPythonHelperTemplateName = 'flutter_lldb_helper.py';
static const String _lldbInitTemplate = '''
#
# Generated file, do not edit.
#
command script import --relative-to-command-file $_lldbPythonHelperTemplateName
''';
static const String _lldbPythonHelperTemplate = r'''
#
# Generated file, do not edit.
#
import lldb
def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
"""Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
base = frame.register["x0"].GetValueAsAddress()
page_len = frame.register["x1"].GetValueAsUnsigned()
# Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
# first page to see if handled it correctly. This makes diagnosing
# misconfiguration (e.g. missing breakpoint) easier.
data = bytearray(page_len)
data[0:8] = b'IHELPED!'
error = lldb.SBError()
frame.GetThread().GetProcess().WriteMemory(base, data, error)
if not error.Success():
print(f'Failed to write into {base}[+{page_len}]', error)
return
def __lldb_init_module(debugger: lldb.SBDebugger, _):
target = debugger.GetDummyTarget()
# Caveat: must use BreakpointCreateByRegEx here and not
# BreakpointCreateByName. For some reasons callback function does not
# get carried over from dummy target for the later.
bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
bp.SetAutoContinue(True)
print("-- LLDB integration loaded --")
''';
Directory get ephemeralModuleDirectory => parent.directory.childDirectory('.ios');
Directory get _editableDirectory => parent.directory.childDirectory('ios');
@ -610,7 +656,8 @@ class IosProject extends XcodeBasedProject {
}
Future<void> ensureReadyForPlatformSpecificTooling() async {
await _regenerateFromTemplateIfNeeded();
await _regenerateModuleFromTemplateIfNeeded();
await _updateLLDBIfNeeded();
if (!_flutterLibRoot.existsSync()) {
return;
}
@ -713,7 +760,45 @@ class IosProject extends XcodeBasedProject {
}
}
Future<void> _regenerateFromTemplateIfNeeded() async {
Future<void> _updateLLDBIfNeeded() async {
if (globals.cache.isOlderThanToolsStamp(lldbInitFile) ||
globals.cache.isOlderThanToolsStamp(lldbHelperPythonFile)) {
if (isModule) {
// When building a module project for Add-to-App, provide instructions
// to manually add the LLDB Init File to their native Xcode project.
globals.logger.printWarning(
'Debugging Flutter on new iOS versions requires an LLDB Init File. '
'To ensure debug mode works, please complete one of the following in '
'your native Xcode project:\n'
' * Open Xcode > Product > Scheme > Edit Scheme. For both the Run and Test actions, set LLDB Init File to: \n\n'
' ${lldbInitFile.path}\n\n'
' * If you are already using an LLDB Init File, please append the '
'following to your LLDB Init File:\n\n'
' command source ${lldbInitFile.path}\n',
);
}
await _renderTemplateToFile(_lldbInitTemplate, null, lldbInitFile, globals.templateRenderer);
await _renderTemplateToFile(
_lldbPythonHelperTemplate,
null,
lldbHelperPythonFile,
globals.templateRenderer,
);
}
}
Future<void> _renderTemplateToFile(
String template,
Object? context,
File file,
TemplateRenderer templateRenderer,
) async {
final String renderedTemplate = templateRenderer.renderString(template, context);
await file.create(recursive: true);
await file.writeAsString(renderedTemplate);
}
Future<void> _regenerateModuleFromTemplateIfNeeded() async {
if (!isModule) {
return;
}
@ -783,6 +868,14 @@ class IosProject extends XcodeBasedProject {
return registryDirectory.childFile('GeneratedPluginRegistrant.m');
}
File get lldbInitFile {
return ephemeralDirectory.childFile('flutter_lldbinit');
}
File get lldbHelperPythonFile {
return ephemeralDirectory.childFile(_lldbPythonHelperTemplateName);
}
Future<void> _overwriteFromTemplate(String path, Directory target) async {
final Template template = await Template.fromName(
path,

View File

@ -46,6 +46,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
@ -74,6 +75,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

View File

@ -46,6 +46,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
@ -74,6 +75,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

View File

@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
@ -43,6 +44,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

View File

@ -26,12 +26,14 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

View File

@ -1152,6 +1152,100 @@ void main() {
expect(processManager, hasNoRemainingExpectations);
});
});
group('DebugIosLLDBInit', () {
testUsingContext(
'throws error if missing LLDB Init File in all schemes',
() async {
const String projectPath = 'path/to/project';
fileSystem.directory(projectPath).createSync(recursive: true);
environment.defines[kIosArchs] = 'arm64';
environment.defines[kSdkRoot] = 'path/to/iPhoneOS.sdk';
environment.defines[kBuildMode] = 'debug';
environment.defines[kSrcRoot] = projectPath;
environment.defines[kTargetDeviceOSVersion] = '18.4.1';
expect(
const DebugIosLLDBInit().build(environment),
throwsToolExit(
message: 'Debugging Flutter on new iOS versions requires an LLDB Init File.',
),
);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
Platform: () => macPlatform,
},
);
testUsingContext(
'skips if targetting simulator',
() async {
const String projectPath = 'path/to/project';
fileSystem.directory(projectPath).createSync(recursive: true);
environment.defines[kIosArchs] = 'arm64';
environment.defines[kSdkRoot] = 'path/to/iPhoneSimulator.sdk';
environment.defines[kBuildMode] = 'debug';
environment.defines[kSrcRoot] = projectPath;
environment.defines[kTargetDeviceOSVersion] = '18.4.1';
await const DebugIosLLDBInit().build(environment);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
Platform: () => macPlatform,
},
);
testUsingContext(
'skips if iOS version is less than 18.4',
() async {
const String projectPath = 'path/to/project';
fileSystem.directory(projectPath).createSync(recursive: true);
environment.defines[kIosArchs] = 'arm64';
environment.defines[kSdkRoot] = 'path/to/iPhoneOS.sdk';
environment.defines[kBuildMode] = 'debug';
environment.defines[kSrcRoot] = projectPath;
environment.defines[kTargetDeviceOSVersion] = '18.3.1';
await const DebugIosLLDBInit().build(environment);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
Platform: () => macPlatform,
},
);
testUsingContext(
'does not throw error if there is an LLDB Init File in any scheme',
() async {
const String projectPath = 'path/to/project';
fileSystem.directory(projectPath).createSync(recursive: true);
fileSystem
.directory(projectPath)
.childDirectory('MyProject.xcodeproj')
.childDirectory('xcshareddata')
.childDirectory('xcschemes')
.childFile('MyProject.xcscheme')
..createSync(recursive: true)
..writeAsStringSync(r'customLLDBInitFile = "some/path/.lldbinit"');
environment.defines[kIosArchs] = 'arm64';
environment.defines[kSdkRoot] = 'path/to/iPhoneOS.sdk';
environment.defines[kBuildMode] = 'debug';
environment.defines[kSrcRoot] = projectPath;
await const DebugIosLLDBInit().build(environment);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
Platform: () => macPlatform,
},
);
});
}
class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {

View File

@ -808,6 +808,7 @@ void main() {
testUsingContext(
'succeeds',
() async {
const String flavor = 'free';
final IOSDevice iosDevice = setUpIOSDevice(
fileSystem: fileSystem,
processManager: FakeProcessManager.any(),
@ -817,7 +818,7 @@ void main() {
coreDeviceControl: FakeIOSCoreDeviceControl(),
xcodeDebug: FakeXcodeDebug(
expectedProject: XcodeDebugProject(
scheme: 'free',
scheme: flavor,
xcodeWorkspace: fileSystem.directory('/ios/Runner.xcworkspace'),
xcodeProject: fileSystem.directory('/ios/Runner.xcodeproj'),
hostAppProjectName: 'Runner',
@ -825,11 +826,11 @@ void main() {
expectedDeviceId: '123',
expectedLaunchArguments: <String>['--enable-dart-profiling'],
expectedSchemeFilePath:
'/ios/Runner.xcodeproj/xcshareddata/xcschemes/free.xcscheme',
'/ios/Runner.xcodeproj/xcshareddata/xcschemes/$flavor.xcscheme',
),
);
setUpIOSProject(fileSystem);
setUpIOSProject(fileSystem, scheme: flavor);
final FlutterProject flutterProject = FlutterProject.fromDirectory(
fileSystem.currentDirectory,
);
@ -1156,7 +1157,11 @@ void main() {
});
}
void setUpIOSProject(FileSystem fileSystem, {bool createWorkspace = true}) {
void setUpIOSProject(
FileSystem fileSystem, {
bool createWorkspace = true,
String scheme = 'Runner',
}) {
fileSystem.file('pubspec.yaml').writeAsStringSync('''
name: my_app
''');
@ -1166,6 +1171,10 @@ name: my_app
fileSystem.directory('ios/Runner.xcworkspace').createSync();
}
fileSystem.file('ios/Runner.xcodeproj/project.pbxproj').createSync(recursive: true);
final File schemeFile = fileSystem.file(
'ios/Runner.xcodeproj/xcshareddata/xcschemes/$scheme.xcscheme',
)..createSync(recursive: true);
schemeFile.writeAsStringSync(_validScheme);
// This is the expected output directory.
fileSystem.directory('build/ios/iphoneos/My Super Awesome App.app').createSync(recursive: true);
}
@ -1330,3 +1339,69 @@ class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {
return launchSuccess;
}
}
const String _validScheme = '''
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction>
</ProfileAction>
<AnalyzeAction>
</AnalyzeAction>
<ArchiveAction>
</ArchiveAction>
</Scheme>
''';

View File

@ -0,0 +1,692 @@
// 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:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/migrations/lldb_init_migration.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:test/fake.dart';
import '../../src/common.dart';
void main() {
group('LLDBInitMigration', () {
testWithoutContext('fails if Xcode project is not found', () async {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final FakeXcodeProject project = FakeXcodeProject(
platform: SupportedPlatform.ios.name,
fileSystem: memoryFileSystem,
logger: testLogger,
);
final LLDBInitMigration migration = LLDBInitMigration(
project,
BuildInfo.debug,
testLogger,
environmentType: EnvironmentType.physical,
fileSystem: memoryFileSystem,
);
await migration.migrate();
expect(testLogger.errorText, contains('Xcode project not found'));
});
group('get scheme file', () {
testWithoutContext('fails if Xcode project info not found', () async {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final FakeXcodeProject project = FakeXcodeProject(
platform: SupportedPlatform.ios.name,
fileSystem: memoryFileSystem,
logger: testLogger,
);
_createProjectFiles(project);
project._projectInfo = null;
final LLDBInitMigration migration = LLDBInitMigration(
project,
BuildInfo.debug,
testLogger,
environmentType: EnvironmentType.physical,
fileSystem: memoryFileSystem,
);
await migration.migrate();
expect(testLogger.errorText, contains('Unable to get Xcode project info.'));
});
testWithoutContext('fails if Xcode workspace not found', () async {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final FakeXcodeProject project = FakeXcodeProject(
platform: SupportedPlatform.ios.name,
fileSystem: memoryFileSystem,
logger: testLogger,
);
_createProjectFiles(project);
project.xcodeWorkspace = null;
final LLDBInitMigration migration = LLDBInitMigration(
project,
BuildInfo.debug,
testLogger,
environmentType: EnvironmentType.physical,
fileSystem: memoryFileSystem,
);
await migration.migrate();
expect(testLogger.errorText, contains('Xcode workspace not found.'));
});
testWithoutContext('fails if scheme not found', () async {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final FakeXcodeProject project = FakeXcodeProject(
platform: SupportedPlatform.ios.name,
fileSystem: memoryFileSystem,
logger: testLogger,
);
_createProjectFiles(project);
project._projectInfo = XcodeProjectInfo(<String>[], <String>[], <String>[], testLogger);
final LLDBInitMigration migration = LLDBInitMigration(
project,
BuildInfo.debug,
testLogger,
environmentType: EnvironmentType.physical,
fileSystem: memoryFileSystem,
);
await migration.migrate();
expect(
testLogger.errorText,
contains('You must specify a --flavor option to select one of the available schemes.'),
);
});
testWithoutContext('fails if scheme file not found', () async {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final FakeXcodeProject project = FakeXcodeProject(
platform: SupportedPlatform.ios.name,
fileSystem: memoryFileSystem,
logger: testLogger,
);
_createProjectFiles(project, createSchemeFile: false);
final LLDBInitMigration migration = LLDBInitMigration(
project,
BuildInfo.debug,
testLogger,
environmentType: EnvironmentType.physical,
fileSystem: memoryFileSystem,
);
await migration.migrate();
expect(testLogger.errorText, contains('Unable to get scheme file for Runner.'));
});
});
testWithoutContext('does nothing if both Launch and Test are already migrated', () async {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final FakeXcodeProject project = FakeXcodeProject(
platform: SupportedPlatform.ios.name,
fileSystem: memoryFileSystem,
logger: testLogger,
);
_createProjectFiles(project);
project.xcodeProjectSchemeFile().writeAsStringSync(
_validScheme(
lldbInitFile:
'\n customLLDBInitFile = "\$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"',
),
);
final LLDBInitMigration migration = LLDBInitMigration(
project,
BuildInfo.debug,
testLogger,
environmentType: EnvironmentType.physical,
fileSystem: memoryFileSystem,
);
await migration.migrate();
expect(testLogger.errorText, isEmpty);
});
testWithoutContext('prints error if only Launch action is migrated', () async {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final FakeXcodeProject project = FakeXcodeProject(
platform: SupportedPlatform.ios.name,
fileSystem: memoryFileSystem,
logger: testLogger,
);
_createProjectFiles(project);
project.xcodeProjectSchemeFile().writeAsStringSync(
_validScheme(
lldbInitFile:
'\n customLLDBInitFile = "\$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"',
testLLDBInitFile: '',
),
);
final LLDBInitMigration migration = LLDBInitMigration(
project,
BuildInfo.debug,
testLogger,
environmentType: EnvironmentType.physical,
fileSystem: memoryFileSystem,
);
await migration.migrate();
expect(
testLogger.errorText,
contains(
'Running Flutter in debug mode on new iOS versions requires a LLDB '
'Init File, but the Test action in the Runner scheme does not have it set.',
),
);
});
testWithoutContext('prints error if only Test action is migrated', () async {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final FakeXcodeProject project = FakeXcodeProject(
platform: SupportedPlatform.ios.name,
fileSystem: memoryFileSystem,
logger: testLogger,
);
_createProjectFiles(project);
project.xcodeProjectSchemeFile().writeAsStringSync(
_validScheme(
testLLDBInitFile:
'\n customLLDBInitFile = "\$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"',
),
);
final LLDBInitMigration migration = LLDBInitMigration(
project,
BuildInfo.debug,
testLogger,
environmentType: EnvironmentType.physical,
fileSystem: memoryFileSystem,
);
await migration.migrate();
expect(
testLogger.errorText,
contains(
'Running Flutter in debug mode on new iOS versions requires a LLDB '
'Init File, but the Launch action in the Runner scheme does not have it set.',
),
);
});
testWithoutContext(
'print error if customLLDBInitFile already exists and does not contain flutter lldbinit',
() async {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final FakeXcodeProject project = FakeXcodeProject(
platform: SupportedPlatform.ios.name,
fileSystem: memoryFileSystem,
logger: testLogger,
);
_createProjectFiles(project);
project.xcodeProjectSchemeFile().writeAsStringSync(
_validScheme(lldbInitFile: '\n customLLDBInitFile = "non_flutter/.lldbinit"'),
);
final LLDBInitMigration migration = LLDBInitMigration(
project,
BuildInfo.debug,
testLogger,
environmentType: EnvironmentType.physical,
fileSystem: memoryFileSystem,
);
await migration.migrate();
expect(
testLogger.errorText,
contains('Running Flutter in debug mode on new iOS versions requires a LLDB Init File'),
);
},
);
testWithoutContext(
'skips if customLLDBInitFile already exists and contain flutter lldbinit',
() async {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final FakeXcodeProject project = FakeXcodeProject(
platform: SupportedPlatform.ios.name,
fileSystem: memoryFileSystem,
logger: testLogger,
);
_createProjectFiles(project);
memoryFileSystem.file('non_flutter/.lldbinit')
..createSync(recursive: true)
..writeAsStringSync('command source /path/to/Flutter/ephemeral/flutter_lldbinit');
project.xcodeProjectSchemeFile().writeAsStringSync(
_validScheme(lldbInitFile: '\n customLLDBInitFile = "non_flutter/.lldbinit"'),
);
final LLDBInitMigration migration = LLDBInitMigration(
project,
BuildInfo.debug,
testLogger,
environmentType: EnvironmentType.physical,
fileSystem: memoryFileSystem,
);
await migration.migrate();
expect(testLogger.errorText, isEmpty);
},
);
testWithoutContext(
'prints error if customLLDBInitFile already exists and not both Launch and Test contain flutter lldbinit',
() async {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final FakeXcodeProject project = FakeXcodeProject(
platform: SupportedPlatform.ios.name,
fileSystem: memoryFileSystem,
logger: testLogger,
);
_createProjectFiles(project);
memoryFileSystem.file('non_flutter/.lldbinit')
..createSync(recursive: true)
..writeAsStringSync('command source /path/to/Flutter/ephemeral/flutter_lldbinit');
project.xcodeProjectSchemeFile().writeAsStringSync(
_validScheme(
lldbInitFile: '\n customLLDBInitFile = "non_flutter/.lldbinit"',
testLLDBInitFile: '\n customLLDBInitFile = "non_flutter/.test_lldbinit"',
),
);
final LLDBInitMigration migration = LLDBInitMigration(
project,
BuildInfo.debug,
testLogger,
environmentType: EnvironmentType.physical,
fileSystem: memoryFileSystem,
);
await migration.migrate();
expect(
testLogger.errorText,
contains('Running Flutter in debug mode on new iOS versions requires a LLDB Init File'),
);
},
);
testWithoutContext(
'parses customLLDBInitFile if already exists and replaces Xcode build settings',
() async {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final FakeXcodeProject project = FakeXcodeProject(
platform: SupportedPlatform.ios.name,
fileSystem: memoryFileSystem,
logger: testLogger,
buildSettings: <String, String>{'SRCROOT': 'src_root'},
);
_createProjectFiles(project);
memoryFileSystem.file('src_root/non_flutter/.lldbinit')
..createSync(recursive: true)
..writeAsStringSync('command source /path/to/Flutter/ephemeral/flutter_lldbinit');
project.xcodeProjectSchemeFile().writeAsStringSync(
_validScheme(
lldbInitFile: '\n customLLDBInitFile = "\$(SRCROOT)/non_flutter/.lldbinit"',
),
);
final LLDBInitMigration migration = LLDBInitMigration(
project,
BuildInfo.debug,
testLogger,
environmentType: EnvironmentType.physical,
fileSystem: memoryFileSystem,
);
await migration.migrate();
expect(testLogger.errorText, isEmpty);
},
);
testWithoutContext('prints error if LaunchAction is missing', () async {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final FakeXcodeProject project = FakeXcodeProject(
platform: SupportedPlatform.ios.name,
fileSystem: memoryFileSystem,
logger: testLogger,
);
_createProjectFiles(project);
project.xcodeProjectSchemeFile().writeAsStringSync(_missingLaunchAction);
final LLDBInitMigration migration = LLDBInitMigration(
project,
BuildInfo.debug,
testLogger,
environmentType: EnvironmentType.physical,
fileSystem: memoryFileSystem,
);
await migration.migrate();
expect(testLogger.errorText, contains('Failed to find LaunchAction for the Scheme'));
});
testWithoutContext('prints error if TestAction is missing', () async {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final FakeXcodeProject project = FakeXcodeProject(
platform: SupportedPlatform.ios.name,
fileSystem: memoryFileSystem,
logger: testLogger,
);
_createProjectFiles(project);
project.xcodeProjectSchemeFile().writeAsStringSync(_missingTestAction);
final LLDBInitMigration migration = LLDBInitMigration(
project,
BuildInfo.debug,
testLogger,
environmentType: EnvironmentType.physical,
fileSystem: memoryFileSystem,
);
await migration.migrate();
expect(testLogger.errorText, contains('Failed to find TestAction for the Scheme'));
});
testWithoutContext('prints error if scheme file is invalid XML', () async {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final FakeXcodeProject project = FakeXcodeProject(
platform: SupportedPlatform.ios.name,
fileSystem: memoryFileSystem,
logger: testLogger,
);
_createProjectFiles(project);
project.xcodeProjectSchemeFile().writeAsStringSync(
'${_validScheme()} <an opening without a close>',
);
final LLDBInitMigration migration = LLDBInitMigration(
project,
BuildInfo.debug,
testLogger,
environmentType: EnvironmentType.physical,
fileSystem: memoryFileSystem,
);
await migration.migrate();
expect(testLogger.errorText, contains('Failed to parse'));
});
testWithoutContext('succeeds', () async {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final FakeXcodeProject project = FakeXcodeProject(
platform: SupportedPlatform.ios.name,
fileSystem: memoryFileSystem,
logger: testLogger,
);
_createProjectFiles(project);
project.xcodeProjectSchemeFile().writeAsStringSync(_validScheme());
final LLDBInitMigration migration = LLDBInitMigration(
project,
BuildInfo.debug,
testLogger,
environmentType: EnvironmentType.physical,
fileSystem: memoryFileSystem,
);
await migration.migrate();
expect(testLogger.errorText, isEmpty);
expect(
project.xcodeProjectSchemeFile().readAsStringSync(),
_validScheme(
lldbInitFile:
'\n customLLDBInitFile = "\$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"',
),
);
});
});
}
void _createProjectFiles(FakeXcodeProject project, {bool createSchemeFile = true, String? scheme}) {
project.parent.directory.createSync(recursive: true);
project.hostAppRoot.createSync(recursive: true);
project.xcodeProjectInfoFile.createSync(recursive: true);
if (createSchemeFile) {
project.xcodeProjectSchemeFile(scheme: scheme).createSync(recursive: true);
project.xcodeProjectSchemeFile().writeAsStringSync(_validScheme());
}
}
String _validScheme({String lldbInitFile = '', String? testLLDBInitFile}) {
testLLDBInitFile ??= lldbInitFile;
return '''
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"$testLLDBInitFile
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"$lldbInitFile
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction>
</ProfileAction>
<AnalyzeAction>
</AnalyzeAction>
<ArchiveAction>
</ArchiveAction>
</Scheme>
''';
}
const String _missingTestAction = '''
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction>
</BuildAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction>
</ProfileAction>
<AnalyzeAction>
</AnalyzeAction>
<ArchiveAction>
</ArchiveAction>
</Scheme>
''';
const String _missingLaunchAction = '''
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<ProfileAction>
</ProfileAction>
<AnalyzeAction>
</AnalyzeAction>
<ArchiveAction>
</ArchiveAction>
</Scheme>
''';
class FakeFlutterProject extends Fake implements FlutterProject {
FakeFlutterProject({required MemoryFileSystem fileSystem})
: directory = fileSystem.directory('app_name');
@override
Directory directory;
}
class FakeXcodeProject extends Fake implements IosProject {
FakeXcodeProject({
required MemoryFileSystem fileSystem,
required String platform,
required this.logger,
this.buildSettings,
}) : hostAppRoot = fileSystem.directory('app_name').childDirectory(platform),
parent = FakeFlutterProject(fileSystem: fileSystem);
final Logger logger;
late XcodeProjectInfo? _projectInfo = XcodeProjectInfo(
<String>['Runner'],
<String>['Debug', 'Release', 'Profile'],
<String>['Runner'],
logger,
);
Map<String, String>? buildSettings;
@override
Directory hostAppRoot;
@override
FakeFlutterProject parent;
@override
Directory get xcodeProject => hostAppRoot.childDirectory('$hostAppProjectName.xcodeproj');
@override
File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');
@override
late Directory? xcodeWorkspace = hostAppRoot.childDirectory('$hostAppProjectName.xcworkspace');
@override
String hostAppProjectName = 'Runner';
@override
File get lldbInitFile => hostAppRoot
.childDirectory('Flutter')
.childDirectory('ephemeral')
.childFile('flutter_lldbinit');
@override
Future<XcodeProjectInfo?> projectInfo() async {
return _projectInfo;
}
@override
File xcodeProjectSchemeFile({String? scheme}) {
final String schemeName = scheme ?? 'Runner';
return xcodeProject
.childDirectory('xcshareddata')
.childDirectory('xcschemes')
.childFile('$schemeName.xcscheme');
}
@override
Future<Map<String, String>?> buildSettingsForBuildInfo(
BuildInfo? buildInfo, {
String? scheme,
String? configuration,
String? target,
EnvironmentType environmentType = EnvironmentType.physical,
String? deviceId,
bool isWatch = false,
}) async {
return buildSettings;
}
}

View File

@ -54,6 +54,8 @@ void main() {
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dSrcRoot=',
'-dTargetDeviceOSVersion=',
'debug_ios_bundle_flutter_assets',
],
),
@ -101,6 +103,8 @@ void main() {
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dSrcRoot=',
'-dTargetDeviceOSVersion=',
'debug_ios_bundle_flutter_assets',
],
),
@ -133,6 +137,8 @@ void main() {
const String splitDebugInfo = '/path/to/split/debug/info';
const String trackWidgetCreation = 'true';
const String treeShake = 'true';
const String srcRoot = '/path/to/project';
const String iOSVersion = '18.3.1';
final TestContext context = TestContext(
<String>['build'],
<String, String>{
@ -154,6 +160,8 @@ void main() {
'SPLIT_DEBUG_INFO': splitDebugInfo,
'TRACK_WIDGET_CREATION': trackWidgetCreation,
'TREE_SHAKE_ICONS': treeShake,
'SRCROOT': srcRoot,
'TARGET_DEVICE_OS_VERSION': iOSVersion,
},
commands: <FakeCommand>[
FakeCommand(
@ -178,6 +186,8 @@ void main() {
'--ExtraGenSnapshotOptions=$extraGenSnapshotOptions',
'--DartDefines=$dartDefines',
'--ExtraFrontEndOptions=$extraFrontEndOptions',
'-dSrcRoot=$srcRoot',
'-dTargetDeviceOSVersion=$iOSVersion',
'-dCodesignIdentity=$expandedCodeSignIdentity',
'release_ios_bundle_flutter_assets',
],
@ -251,6 +261,8 @@ void main() {
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dPreBuildAction=PrepareFramework',
'-dSrcRoot=',
'-dTargetDeviceOSVersion=',
'debug_unpack_ios',
],
),
@ -298,6 +310,8 @@ void main() {
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dSrcRoot=',
'-dTargetDeviceOSVersion=',
'-dPreBuildAction=PrepareFramework',
'debug_unpack_ios',
],
@ -326,6 +340,8 @@ void main() {
const String splitDebugInfo = '/path/to/split/debug/info';
const String trackWidgetCreation = 'true';
const String treeShake = 'true';
const String srcRoot = '/path/to/project';
const String iOSVersion = '18.3.1';
final TestContext context = TestContext(
<String>['prepare'],
<String, String>{
@ -347,6 +363,8 @@ void main() {
'SPLIT_DEBUG_INFO': splitDebugInfo,
'TRACK_WIDGET_CREATION': trackWidgetCreation,
'TREE_SHAKE_ICONS': treeShake,
'SRCROOT': srcRoot,
'TARGET_DEVICE_OS_VERSION': iOSVersion,
},
commands: <FakeCommand>[
FakeCommand(
@ -371,6 +389,8 @@ void main() {
'--ExtraGenSnapshotOptions=$extraGenSnapshotOptions',
'--DartDefines=$dartDefines',
'--ExtraFrontEndOptions=$extraFrontEndOptions',
'-dSrcRoot=$srcRoot',
'-dTargetDeviceOSVersion=$iOSVersion',
'-dPreBuildAction=PrepareFramework',
'-dCodesignIdentity=$expandedCodeSignIdentity',
'release_unpack_ios',
@ -422,6 +442,8 @@ void main() {
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dSrcRoot=',
'-dTargetDeviceOSVersion=',
'-dPreBuildAction=PrepareFramework',
'debug_unpack_ios',
],
@ -473,6 +495,8 @@ void main() {
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dSrcRoot=',
'-dTargetDeviceOSVersion=',
'-dPreBuildAction=PrepareFramework',
'debug_unpack_ios',
],
@ -523,6 +547,8 @@ void main() {
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dSrcRoot=',
'-dTargetDeviceOSVersion=',
'-dPreBuildAction=PrepareFramework',
'debug_unpack_ios',
],

View File

@ -2,14 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/flutter_manifest.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
@ -76,6 +77,21 @@ void main() {
expect(project.xcodeConfigFor('Debug').path, 'app_name/ios/Flutter/Debug.xcconfig');
});
testWithoutContext('lldbInitFile', () {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs));
expect(project.lldbInitFile.path, 'app_name/ios/Flutter/ephemeral/flutter_lldbinit');
});
testWithoutContext('lldbHelperPythonFile', () {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs));
expect(
project.lldbHelperPythonFile.path,
'app_name/ios/Flutter/ephemeral/flutter_lldb_helper.py',
);
});
group('projectInfo', () {
testUsingContext(
'is null if XcodeProjectInterpreter is null',
@ -315,6 +331,110 @@ void main() {
},
);
});
group('ensureReadyForPlatformSpecificTooling', () {
group('lldb files are generated', () {
testUsingContext(
'when they are missing',
() async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory projectDirectory = fs.directory('path');
projectDirectory.childDirectory('ios').createSync(recursive: true);
final FlutterManifest manifest = FakeFlutterManifest();
final FlutterProject flutterProject = FlutterProject(
projectDirectory,
manifest,
manifest,
);
final IosProject project = IosProject.fromFlutter(flutterProject);
expect(project.lldbInitFile, isNot(exists));
expect(project.lldbHelperPythonFile, isNot(exists));
await project.ensureReadyForPlatformSpecificTooling();
expect(project.lldbInitFile, exists);
expect(project.lldbHelperPythonFile, exists);
},
overrides: <Type, Generator>{Cache: () => FakeCache(olderThanToolsStamp: true)},
);
testUsingContext(
'when they are older than tool',
() async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory projectDirectory = fs.directory('path');
projectDirectory.childDirectory('ios').createSync(recursive: true);
final FlutterManifest manifest = FakeFlutterManifest();
final FlutterProject flutterProject = FlutterProject(
projectDirectory,
manifest,
manifest,
);
final IosProject project = IosProject.fromFlutter(flutterProject);
project.lldbInitFile.createSync(recursive: true);
project.lldbInitFile.writeAsStringSync('old');
project.lldbHelperPythonFile.createSync(recursive: true);
project.lldbHelperPythonFile.writeAsStringSync('old');
await project.ensureReadyForPlatformSpecificTooling();
expect(
project.lldbInitFile.readAsStringSync(),
contains('Generated file, do not edit.'),
);
expect(
project.lldbHelperPythonFile.readAsStringSync(),
contains('Generated file, do not edit.'),
);
},
overrides: <Type, Generator>{Cache: () => FakeCache(olderThanToolsStamp: true)},
);
group('with a warning', () {
late BufferLogger testLogger;
late MemoryFileSystem fs;
late FakeCache cache;
setUp(() {
testLogger = BufferLogger.test();
fs = MemoryFileSystem.test();
cache = FakeCache();
});
testUsingContext(
'when the project is a module',
() async {
final Directory projectDirectory = fs.directory('path');
projectDirectory.childDirectory('ios').createSync(recursive: true);
final FlutterManifest manifest = FakeFlutterManifest(isModule: true);
final FlutterProject flutterProject = FlutterProject(
projectDirectory,
manifest,
manifest,
);
final IosProject project = IosProject.fromFlutter(flutterProject);
cache.filesOlderThanToolsStamp[project.lldbInitFile.basename] = true;
await project.ensureReadyForPlatformSpecificTooling();
expect(project.lldbInitFile, exists);
expect(project.lldbHelperPythonFile, exists);
expect(
testLogger.warningText,
contains('Debugging Flutter on new iOS versions requires an LLDB Init File'),
);
},
overrides: <Type, Generator>{
Cache: () => cache,
Logger: () => testLogger,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
FileSystemUtils: () => FakeFileSystemUtils(),
},
);
});
});
});
});
group('MacOSProject', () {
@ -461,6 +581,9 @@ class FakeFlutterProject extends Fake implements FlutterProject {
@override
bool isModule = false;
@override
FlutterManifest get manifest => FakeFlutterManifest();
}
class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
@ -492,4 +615,42 @@ class FakeFlutterManifest extends Fake implements FlutterManifest {
@override
bool isModule;
@override
String? buildName;
@override
String? buildNumber;
@override
String? get iosBundleIdentifier => null;
@override
String get appName => '';
}
class FakeCache extends Fake implements Cache {
FakeCache({this.olderThanToolsStamp = false});
bool olderThanToolsStamp;
Map<String, bool> filesOlderThanToolsStamp = <String, bool>{};
@override
bool isOlderThanToolsStamp(FileSystemEntity entity) {
if (filesOlderThanToolsStamp.containsKey(entity.basename)) {
return filesOlderThanToolsStamp[entity.basename]!;
}
return olderThanToolsStamp;
}
}
class FakeFileSystemUtils extends Fake implements FileSystemUtils {
FakeFileSystemUtils({this.olderThanReference = false});
bool olderThanReference;
@override
bool isOlderThanReference({required FileSystemEntity entity, required File referenceFile}) {
return olderThanReference;
}
}

View File

@ -0,0 +1,307 @@
// 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_tools/src/base/error_handling_io.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import '../src/common.dart';
import 'test_utils.dart';
void main() {
const String customLLDBInitFileSchemeSetting =
r'customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"';
test(
'Ensure lldb is added to Xcode project',
() async {
final Directory workingDirectory = fileSystem.systemTempDirectory.createTempSync(
'lldb_test.',
);
try {
final String workingDirectoryPath = workingDirectory.path;
const String appName = 'lldb_test';
final ProcessResult createResult = await processManager.run(<String>[
flutterBin,
...getLocalEngineArguments(),
'create',
'--org',
'io.flutter.devicelab',
'-i',
'swift',
appName,
'--platforms=ios',
], workingDirectory: workingDirectory.path);
expect(
createResult.exitCode,
0,
reason:
'Failed to create app: \n'
'stdout: \n${createResult.stdout}\n'
'stderr: \n${createResult.stderr}\n',
);
final String appDirectoryPath = fileSystem.path.join(workingDirectoryPath, appName);
final File schemeFile = fileSystem
.directory(appDirectoryPath)
.childDirectory('ios')
.childDirectory('Runner.xcodeproj')
.childDirectory('xcshareddata')
.childDirectory('xcschemes')
.childFile('Runner.xcscheme');
expect(schemeFile.existsSync(), isTrue);
// Remove customLLDBInitFile from the scheme so we can validate it
// gets re-added if missing.
expect(schemeFile.readAsStringSync(), contains(customLLDBInitFileSchemeSetting));
schemeFile.writeAsStringSync(
schemeFile.readAsStringSync().replaceAll(customLLDBInitFileSchemeSetting, ''),
);
final File lldbInitFile = fileSystem
.directory(appDirectoryPath)
.childDirectory('ios')
.childDirectory('Flutter')
.childDirectory('ephemeral')
.childFile('flutter_lldbinit');
expect(lldbInitFile.existsSync(), isTrue);
final File lldbPythonFile = fileSystem
.directory(appDirectoryPath)
.childDirectory('ios')
.childDirectory('Flutter')
.childDirectory('ephemeral')
.childFile('flutter_lldb_helper.py');
expect(lldbPythonFile.existsSync(), isTrue);
// Delete LLDB files so we can verify they get re-added if missing.
lldbInitFile.deleteSync();
lldbPythonFile.deleteSync();
final ProcessResult buildResult = await processManager.run(<String>[
flutterBin,
...getLocalEngineArguments(),
'build',
'ios',
], workingDirectory: appDirectoryPath);
expect(
buildResult.exitCode,
0,
reason:
'Failed to build the app: \n'
'stdout: \n${buildResult.stdout}\n'
'stderr: \n${buildResult.stderr}\n',
);
expect(schemeFile.readAsStringSync(), contains(customLLDBInitFileSchemeSetting));
expect(lldbInitFile.existsSync(), isTrue);
expect(lldbPythonFile.existsSync(), isTrue);
} finally {
ErrorHandlingFileSystem.deleteIfExists(workingDirectory, recursive: true);
}
},
skip: !platform.isMacOS, // [intended] Can only build on macOS.
);
test(
'Ensure lldb is added to Xcode project when using flavor',
() async {
final Directory workingDirectory = fileSystem.systemTempDirectory.createTempSync(
'lldb_test.',
);
try {
final String workingDirectoryPath = workingDirectory.path;
const String appName = 'lldb_test';
const String flavor = 'vanilla';
final ProcessResult createResult = await processManager.run(<String>[
flutterBin,
...getLocalEngineArguments(),
'create',
'--org',
'io.flutter.devicelab',
'-i',
'swift',
appName,
'--platforms=ios',
], workingDirectory: workingDirectory.path);
expect(
createResult.exitCode,
0,
reason:
'Failed to create app: \n'
'stdout: \n${createResult.stdout}\n'
'stderr: \n${createResult.stderr}\n',
);
final String appDirectoryPath = fileSystem.path.join(workingDirectoryPath, appName);
final File schemeFile = fileSystem
.directory(appDirectoryPath)
.childDirectory('ios')
.childDirectory('Runner.xcodeproj')
.childDirectory('xcshareddata')
.childDirectory('xcschemes')
.childFile('Runner.xcscheme');
expect(schemeFile.existsSync(), isTrue);
final File pbxprojFile = fileSystem
.directory(appDirectoryPath)
.childDirectory('ios')
.childDirectory('Runner.xcodeproj')
.childFile('project.pbxproj');
expect(pbxprojFile.existsSync(), isTrue);
// Create flavor
final File flavorSchemeFile = fileSystem
.directory(appDirectoryPath)
.childDirectory('ios')
.childDirectory('Runner.xcodeproj')
.childDirectory('xcshareddata')
.childDirectory('xcschemes')
.childFile('$flavor.xcscheme');
flavorSchemeFile.createSync(recursive: true);
flavorSchemeFile.writeAsStringSync(schemeFile.readAsStringSync());
String pbxprojContents = pbxprojFile.readAsStringSync();
pbxprojContents = pbxprojContents.replaceAll('97C147071CF9000F007C117D /* Release */,', '''
97C147071CF9000F007C117D /* Release */,
78624EC12D71262400FF7985 /* Release-vanilla */,
''');
pbxprojContents = pbxprojContents.replaceAll('97C147041CF9000F007C117D /* Release */,', '''
97C147041CF9000F007C117D /* Release */,
78624EC02D71262400FF7985 /* Release-vanilla */,
''');
pbxprojContents = pbxprojContents.replaceAll(
'/* Begin XCBuildConfiguration section */',
r'''
/* Begin XCBuildConfiguration section */
78624EC12D71262400FF7985 /* Release-vanilla */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.lldb_test;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = "Release-vanilla";
};
78624EC02D71262400FF7985 /* Release-vanilla */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = "Release-vanilla";
};
''',
);
pbxprojFile.writeAsStringSync(pbxprojContents);
// Remove customLLDBInitFile from the flavor's scheme so we can validate
// it gets re-added later.
expect(flavorSchemeFile.readAsStringSync(), contains(customLLDBInitFileSchemeSetting));
flavorSchemeFile.writeAsStringSync(
flavorSchemeFile.readAsStringSync().replaceAll(customLLDBInitFileSchemeSetting, ''),
);
final ProcessResult buildResult = await processManager.run(<String>[
flutterBin,
...getLocalEngineArguments(),
'build',
'ios',
'--config-only',
'--flavor',
flavor,
], workingDirectory: appDirectoryPath);
expect(
buildResult.exitCode,
0,
reason:
'Failed to build config for the app: \n'
'stdout: \n${buildResult.stdout}\n'
'stderr: \n${buildResult.stderr}\n',
);
expect(flavorSchemeFile.readAsStringSync(), contains(customLLDBInitFileSchemeSetting));
final File lldbInitFile = fileSystem
.directory(appDirectoryPath)
.childDirectory('ios')
.childDirectory('Flutter')
.childDirectory('ephemeral')
.childFile('flutter_lldbinit');
expect(lldbInitFile.existsSync(), isTrue);
final File lldbPythonFile = fileSystem
.directory(appDirectoryPath)
.childDirectory('ios')
.childDirectory('Flutter')
.childDirectory('ephemeral')
.childFile('flutter_lldb_helper.py');
expect(lldbPythonFile.existsSync(), isTrue);
} finally {
ErrorHandlingFileSystem.deleteIfExists(workingDirectory, recursive: true);
}
},
skip: !platform.isMacOS, // [intended] Can only build on macOS.
);
}