From eb07c51230c70a0ecaf010a02e7862f69be3b32b Mon Sep 17 00:00:00 2001 From: Victoria Ashworth <15619084+vashworth@users.noreply.github.com> Date: Thu, 6 Mar 2025 15:21:52 -0600 Subject: [PATCH] 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]. [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 --- .ci.yaml | 16 + .../build_ios_framework_module_test.dart | 8 + packages/flutter_tools/bin/xcode_backend.dart | 2 + .../flutter_tools/lib/src/build_info.dart | 9 + .../lib/src/build_system/targets/ios.dart | 123 +++- .../lib/src/commands/build_ios_framework.dart | 21 + packages/flutter_tools/lib/src/ios/mac.dart | 9 + .../src/migrations/lldb_init_migration.dart | 285 ++++++++ .../flutter_tools/lib/src/xcode_project.dart | 97 ++- .../xcschemes/Runner.xcscheme.tmpl | 2 + .../xcschemes/Runner.xcscheme.tmpl | 2 + .../xcshareddata/xcschemes/Runner.xcscheme | 2 + .../xcschemes/Runner.xcscheme.tmpl | 2 + .../build_system/targets/ios_test.dart | 94 +++ .../ios_device_start_nonprebuilt_test.dart | 83 ++- .../migrations/lldb_init_migration_test.dart | 692 ++++++++++++++++++ .../general.shard/xcode_backend_test.dart | 26 + .../general.shard/xcode_project_test.dart | 163 ++++- .../integration.shard/lldb_init_test.dart | 307 ++++++++ 19 files changed, 1935 insertions(+), 8 deletions(-) create mode 100644 packages/flutter_tools/lib/src/migrations/lldb_init_migration.dart create mode 100644 packages/flutter_tools/test/general.shard/migrations/lldb_init_migration_test.dart create mode 100644 packages/flutter_tools/test/integration.shard/lldb_init_test.dart 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. + ); +}