diff --git a/.ci.yaml b/.ci.yaml index 27d8037631..abfdd8b777 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -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 diff --git a/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart b/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart index 8fe120d747..c4e3e8161b 100644 --- a/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart +++ b/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart @@ -456,6 +456,14 @@ Future _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'); diff --git a/packages/flutter_tools/bin/xcode_backend.dart b/packages/flutter_tools/bin/xcode_backend.dart index 6e5982b54c..add6bcf5ae 100644 --- a/packages/flutter_tools/bin/xcode_backend.dart +++ b/packages/flutter_tools/bin/xcode_backend.dart @@ -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') { diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index ac6ce0e8ff..4d14c14d4d 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -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'; diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart index 37331f279a..a8a2753cf8 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -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 get inputs => [ + const Source.pattern( + '{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart', + ), + ]; + + @override + List get outputs { + final FlutterProject flutterProject = FlutterProject.current(); + final String lldbInitFilePath = flutterProject.ios.lldbInitFile.path.replaceFirst( + flutterProject.directory.path, + '{PROJECT_DIR}/', + ); + return [Source.pattern(lldbInitFilePath)]; + } + + @override + List get dependencies => []; + + @visibleForOverriding + BuildMode get buildMode; + + @override + Future 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 get dependencies => [const DebugUniversalFramework(), ...super.dependencies]; + List get dependencies => [ + const DebugUniversalFramework(), + const DebugIosLLDBInit(), + ...super.dependencies, + ]; } /// IosAssetBundle with debug symbols, used for Profile and Release builds. diff --git a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart index 6a11eb56fe..b6287e1d23 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart @@ -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(); } diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 33eeed3bf7..7644fd8e91 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -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 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); diff --git a/packages/flutter_tools/lib/src/migrations/lldb_init_migration.dart b/packages/flutter_tools/lib/src/migrations/lldb_init_migration.dart new file mode 100644 index 0000000000..e9e985c1eb --- /dev/null +++ b/packages/flutter_tools/lib/src/migrations/lldb_init_migration.dart @@ -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 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 _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 _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? buildSettings; + if ((lldbInitFileLaunchPath != null && lldbInitFileLaunchPath.contains(r'$')) || + (lldbInitFileTestPath != null && lldbInitFileTestPath.contains(r'$'))) { + buildSettings = + await _xcodeProject.buildSettingsForBuildInfo( + _buildInfo, + environmentType: _environmentType, + deviceId: _deviceID, + ) ?? + {}; + } + + 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 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? 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; +} diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart index f38073e91d..858af002c4 100644 --- a/packages/flutter_tools/lib/src/xcode_project.dart +++ b/packages/flutter_tools/lib/src/xcode_project.dart @@ -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 ensureReadyForPlatformSpecificTooling() async { - await _regenerateFromTemplateIfNeeded(); + await _regenerateModuleFromTemplateIfNeeded(); + await _updateLLDBIfNeeded(); if (!_flutterLibRoot.existsSync()) { return; } @@ -713,7 +760,45 @@ class IosProject extends XcodeBasedProject { } } - Future _regenerateFromTemplateIfNeeded() async { + Future _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 _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 _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 _overwriteFromTemplate(String path, Directory target) async { final Template template = await Template.fromName( path, diff --git a/packages/flutter_tools/templates/app/ios-objc.tmpl/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme.tmpl b/packages/flutter_tools/templates/app/ios-objc.tmpl/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme.tmpl index 1d77702a1c..8b5000f8cf 100644 --- a/packages/flutter_tools/templates/app/ios-objc.tmpl/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme.tmpl +++ b/packages/flutter_tools/templates/app/ios-objc.tmpl/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme.tmpl @@ -46,6 +46,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> { + 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: { + 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: { + 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: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + Platform: () => macPlatform, + }, + ); + }); } class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter { diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart index 1840317fc3..cf1373ca73 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart @@ -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: ['--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 = ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +'''; diff --git a/packages/flutter_tools/test/general.shard/migrations/lldb_init_migration_test.dart b/packages/flutter_tools/test/general.shard/migrations/lldb_init_migration_test.dart new file mode 100644 index 0000000000..d3188eaa3e --- /dev/null +++ b/packages/flutter_tools/test/general.shard/migrations/lldb_init_migration_test.dart @@ -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([], [], [], 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: {'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()} ', + ); + + 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 ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +'''; +} + +const String _missingTestAction = ''' + + + + + + + + + + + + + + + + + +'''; + +const String _missingLaunchAction = ''' + + + + + + + + + + + + + + + + + + + + + + + +'''; + +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( + ['Runner'], + ['Debug', 'Release', 'Profile'], + ['Runner'], + logger, + ); + + Map? 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 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?> buildSettingsForBuildInfo( + BuildInfo? buildInfo, { + String? scheme, + String? configuration, + String? target, + EnvironmentType environmentType = EnvironmentType.physical, + String? deviceId, + bool isWatch = false, + }) async { + return buildSettings; + } +} diff --git a/packages/flutter_tools/test/general.shard/xcode_backend_test.dart b/packages/flutter_tools/test/general.shard/xcode_backend_test.dart index 66aec4180c..c5b7ab016a 100644 --- a/packages/flutter_tools/test/general.shard/xcode_backend_test.dart +++ b/packages/flutter_tools/test/general.shard/xcode_backend_test.dart @@ -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( ['build'], { @@ -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( @@ -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( ['prepare'], { @@ -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( @@ -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', ], diff --git a/packages/flutter_tools/test/general.shard/xcode_project_test.dart b/packages/flutter_tools/test/general.shard/xcode_project_test.dart index aabfe82b7a..90c5388796 100644 --- a/packages/flutter_tools/test/general.shard/xcode_project_test.dart +++ b/packages/flutter_tools/test/general.shard/xcode_project_test.dart @@ -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: {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: {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: { + 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 filesOlderThanToolsStamp = {}; + + @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; + } } diff --git a/packages/flutter_tools/test/integration.shard/lldb_init_test.dart b/packages/flutter_tools/test/integration.shard/lldb_init_test.dart new file mode 100644 index 0000000000..ace37263e9 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/lldb_init_test.dart @@ -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([ + 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([ + 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([ + 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([ + 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. + ); +}