// 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 '../base/common.dart'; import '../base/config.dart'; import '../base/error_handling_io.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; import '../base/project_migrator.dart'; import '../build_info.dart'; import '../convert.dart'; import '../darwin/darwin.dart'; import '../ios/plist_parser.dart'; import '../ios/xcodeproj.dart'; import '../macos/swift_package_manager.dart'; import '../plugins.dart'; import '../project.dart'; /// Swift Package Manager integration requires changes to the Xcode project's /// project.pbxproj and xcscheme. This class handles making those changes. class SwiftPackageManagerIntegrationMigration extends ProjectMigrator { SwiftPackageManagerIntegrationMigration( XcodeBasedProject project, FlutterDarwinPlatform platform, BuildInfo buildInfo, { required XcodeProjectInterpreter xcodeProjectInterpreter, required Logger logger, required FileSystem fileSystem, required PlistParser plistParser, required Config config, }) : _xcodeProject = project, _platform = platform, _buildInfo = buildInfo, _xcodeProjectInfoFile = project.xcodeProjectInfoFile, _xcodeProjectInterpreter = xcodeProjectInterpreter, _fileSystem = fileSystem, _plistParser = plistParser, _config = config, super(logger); final XcodeBasedProject _xcodeProject; final FlutterDarwinPlatform _platform; final BuildInfo _buildInfo; final XcodeProjectInterpreter _xcodeProjectInterpreter; final FileSystem _fileSystem; final File _xcodeProjectInfoFile; final PlistParser _plistParser; final Config _config; /// New identifier for FlutterGeneratedPluginSwiftPackage PBXBuildFile. static const _flutterPluginsSwiftPackageBuildFileIdentifier = '78A318202AECB46A00862997'; /// New identifier for FlutterGeneratedPluginSwiftPackage XCLocalSwiftPackageReference. static const _localFlutterPluginsSwiftPackageReferenceIdentifier = '781AD8BC2B33823900A9FFBB'; /// New identifier for FlutterGeneratedPluginSwiftPackage XCSwiftPackageProductDependency. static const _flutterPluginsSwiftPackageProductDependencyIdentifier = '78A3181F2AECB46A00862997'; /// New identifier for FlutterFramework PBXFileReference. static const _flutterFrameworkLocalOverrideFileIdentifier = '784666492D4C4C64000A1A5F'; /// New identifier for a plugin in an example app PBXFileReference. static const _flutterPluginLocalOverrideFileIdenitifier = '78DABEA22ED26510000E7860'; /// New identifier for FlutterGeneratedPluginSwiftPackage PBXFileReference. static const _flutterPluginsSwiftPackageFileIdentifer = '78E0A7A72DC9AD7400C4905E'; /// Existing iOS identifer for Flutter PBXGroup. static const _iosFlutterGroupIdentifier = '9740EEB11CF90186004384FC'; /// Existing macOS identifer for Flutter PBXGroup. static const _macosFlutterGroupIdentifier = '33CEB47122A05771004F2AC0'; /// Existing iOS identifier for Runner PBXFrameworksBuildPhase. static const _iosRunnerFrameworksBuildPhaseIdentifier = '97C146EB1CF9000F007C117D'; /// Existing macOS identifier for Runner PBXFrameworksBuildPhase. static const _macosRunnerFrameworksBuildPhaseIdentifier = '33CC10EA2044A3C60003C045'; /// Existing iOS identifier for Runner PBXNativeTarget. static const _iosRunnerNativeTargetIdentifier = '97C146ED1CF9000F007C117D'; /// Existing macOS identifier for Runner PBXNativeTarget. static const _macosRunnerNativeTargetIdentifier = '33CC10EC2044A3C60003C045'; /// Existing iOS identifier for Runner PBXProject. static const _iosProjectIdentifier = '97C146E61CF9000F007C117D'; /// Existing macOS identifier for Runner PBXProject. static const _macosProjectIdentifier = '33CC10E52044A3C60003C045'; File get backupProjectSettings => _fileSystem.directory(_xcodeProjectInfoFile.parent).childFile('project.pbxproj.backup'); String get _runnerFrameworksBuildPhaseIdentifier { return _platform == FlutterDarwinPlatform.ios ? _iosRunnerFrameworksBuildPhaseIdentifier : _macosRunnerFrameworksBuildPhaseIdentifier; } String get _runnerNativeTargetIdentifier { return _platform == FlutterDarwinPlatform.ios ? _iosRunnerNativeTargetIdentifier : _macosRunnerNativeTargetIdentifier; } String get _projectIdentifier { return _platform == FlutterDarwinPlatform.ios ? _iosProjectIdentifier : _macosProjectIdentifier; } String get _flutterGroupIdentifier { return _platform == FlutterDarwinPlatform.ios ? _iosFlutterGroupIdentifier : _macosFlutterGroupIdentifier; } /// The leading path for the `PBXFileReference` relative to the Flutter `PBXGroup`. /// /// The actual location for both iOS and macOS is `Flutter/ephemeral`. However, /// including the `Flutter/` prefix for macOS will cause it to resolve to /// `Flutter/Flutter/ephemeral`. This is likely due to the macOS Flutter `PBXGroup` /// using `path` whereas the iOS Flutter `PBXGroup` uses `name`. String get _relativeEphemeralPath { return _platform == FlutterDarwinPlatform.ios ? 'Flutter/ephemeral' : 'ephemeral'; } /// If the app being migrated is an example app for a plugin, this will return /// information about the plugin to be used to create a local package override within /// the example app. /// /// If the app is not an example app or the plugin cannot be found, this will return null. late final ({String name, String path})? _examplePlugin; void restoreFromBackup(SchemeInfo? schemeInfo) { if (backupProjectSettings.existsSync()) { logger.printTrace('Restoring project settings from backup file...'); backupProjectSettings.copySync(_xcodeProject.xcodeProjectInfoFile.path); } schemeInfo?.backupSchemeFile?.copySync(schemeInfo.schemeFile.path); } /// Add Swift Package Manager integration to Xcode project's project.pbxproj /// and Runner.xcscheme. /// /// If migration fails or project.pbxproj or Runner.xcscheme becomes invalid, /// will revert any changes made and throw an error. @override Future migrate() async { if (!_xcodeProject.usesSwiftPackageManager) { logger.printTrace( 'The Swift Package Manager feature is off. ' 'Skipping the migration that adds Swift Package Manager integration...', ); return; } if (!_xcodeProject.flutterPluginSwiftPackageManifest.existsSync()) { logger.printTrace( 'The tool did not generate a Swift package. ' "This can happen if the project doesn't have any plugins. " 'Skipping the migration that adds Swift Package Manager integration...', ); return; } Status? migrationStatus; SchemeInfo? schemeInfo; var optionalOnly = false; try { if (!_xcodeProjectInfoFile.existsSync()) { throw Exception('Xcode project not found.'); } _examplePlugin = await _loadPluginFromExampleProject( xcodeProject: _xcodeProject, fileSystem: _fileSystem, logger: logger, platform: _platform, ); schemeInfo = await _getSchemeFile(); // Check for specific strings in the xcscheme and pbxproj to see if the // project has been already migrated, whether automatically or manually. final bool isSchemeMigrated = _isSchemeMigrated(schemeInfo); final bool isPbxprojMigrated = _xcodeProject.flutterPluginSwiftPackageInProjectSettings; final bool isOptionalFilesMigrated = _areOptionalFilesMigrated(_xcodeProjectInfoFile); optionalOnly = isPbxprojMigrated && isSchemeMigrated && !isOptionalFilesMigrated; if (isSchemeMigrated && isPbxprojMigrated && isOptionalFilesMigrated) { return; } migrationStatus = logger.startProgress('Adding Swift Package Manager integration...'); if (isSchemeMigrated) { logger.printTrace('${schemeInfo.schemeFile.basename} already migrated. Skipping...'); } else { _migrateScheme(schemeInfo); } if (!isPbxprojMigrated) { _migratePbxproj(); } else if (!isOptionalFilesMigrated) { // _migratePbxproj also migrates the optional files, so only migrate the optional // specifically if _migratePbxproj is not called. _migrateOptionalPbxproj(); } else { logger.printTrace('${_xcodeProjectInfoFile.basename} already migrated. Skipping...'); } logger.printTrace('Validating project settings...'); // Re-parse the project settings to check for syntax errors. final ParsedProjectInfo updatedInfo = _parsePbxproj(); // If pbxproj was not already migrated, verify settings were set correctly. if (!isPbxprojMigrated) { if (!_isPbxprojMigratedCorrectly(updatedInfo, logErrorIfNotMigrated: true)) { throw Exception('Settings were not updated correctly.'); } } else if (!isOptionalFilesMigrated) { if (!_areOptionalFilesMigratedCorrectly(updatedInfo, logErrorIfNotMigrated: true)) { throw Exception('Settings were not updated correctly.'); } } // Get the project info to make sure it compiles with xcodebuild await _xcodeProjectInterpreter.getInfo( _xcodeProject, buildDirectory: _fileSystem.directory( _platform.buildDirectory(config: _config, fileSystem: _fileSystem), ), ); } on Exception catch (e) { restoreFromBackup(schemeInfo); if (optionalOnly) { // This part of the migration is optional. We'll log this for debugging sake but don't // really expect the user to see it. logger.printTrace( "An error occurred when adding the plugin's Swift package to the Xcode project: \n" ' $e\n\n' 'See instructions to add manually: https://docs.flutter.dev/packages-and-plugins/swift-package-manager/for-plugin-authors', ); } else { throwToolExit( 'An error occurred when adding Swift Package Manager integration:\n' ' $e\n\n' 'Swift Package Manager is currently an experimental feature, please file a bug at\n' ' https://github.com/flutter/flutter/issues/new?template=01_activation.yml \n' 'Consider including a copy of the following files in your bug report:\n' ' ${_platform.name}/Runner.xcodeproj/project.pbxproj\n' ' ${_platform.name}/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ' '(or the scheme for the flavor used)\n\n' 'To add Swift Package Manager integration manually, please use the following instructions:\n' 'https://docs.flutter.dev/to/add-swift-package-manager-manually\n\n' '$kDisableSwiftPMInstructions', ); } } finally { ErrorHandlingFileSystem.deleteIfExists(backupProjectSettings); if (schemeInfo?.backupSchemeFile != null) { ErrorHandlingFileSystem.deleteIfExists(schemeInfo!.backupSchemeFile!); } migrationStatus?.stop(); } } Future _getSchemeFile() 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); } bool _isSchemeMigrated(SchemeInfo schemeInfo) { if (schemeInfo.schemeContent.contains('xcode_backend.sh') || schemeInfo.schemeContent.contains('macos_assemble.sh')) { return true; } return false; } void _migrateScheme(SchemeInfo schemeInfo) { final File schemeFile = schemeInfo.schemeFile; final String schemeContent = schemeInfo.schemeContent; // The scheme should have a BuildableReference already in it with a // BlueprintIdentifier matching the Runner Native Target. Copy from it // since BuildableName, BlueprintName, ReferencedContainer may have been // changed from "Runner". Ensures the expected attributes are found. // Example: // // final XmlDocument document; try { document = XmlDocument.parse(schemeContent); } on XmlException catch (exception) { throw Exception('Failed to parse ${schemeFile.basename}: Invalid xml: $exception'); } final Iterable buildableReferences = document.findAllElements('BuildableReference'); XmlElement? targetReference; for (final ref in buildableReferences) { if (ref.getAttribute('BlueprintIdentifier') == _runnerNativeTargetIdentifier) { targetReference = ref; break; } } if (targetReference == null) { throw Exception( 'Failed to parse ${schemeFile.basename}: Could not find BuildableReference ' 'for ${_xcodeProject.hostAppProjectName}.', ); } final String? buildableNameAttr = targetReference.getAttribute('BuildableName'); if (buildableNameAttr == null) { throw Exception('Failed to parse ${schemeFile.basename}: Could not find BuildableName.'); } final buildableName = 'BuildableName = "$buildableNameAttr"'; final String? blueprintNameAttr = targetReference.getAttribute('BlueprintName'); if (blueprintNameAttr == null) { throw Exception('Failed to parse ${schemeFile.basename}: Could not find BlueprintName.'); } final blueprintName = 'BlueprintName = "$blueprintNameAttr"'; final String? referencedContainerAttr = targetReference.getAttribute('ReferencedContainer'); if (referencedContainerAttr == null) { throw Exception( 'Failed to parse ${schemeFile.basename}: Could not find ReferencedContainer.', ); } final referencedContainer = 'ReferencedContainer = "$referencedContainerAttr"'; schemeInfo.backupSchemeFile = schemeFile.parent.childFile('${schemeFile.basename}.backup'); schemeFile.copySync(schemeInfo.backupSchemeFile!.path); final List schemeLines = LineSplitter.split(schemeContent).toList(); final String scriptText; if (_platform == FlutterDarwinPlatform.ios) { scriptText = r'scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">'; } else { scriptText = r'scriptText = ""$FLUTTER_ROOT"/packages/flutter_tools/bin/macos_assemble.sh prepare ">'; } var newContent = ''' '''; var newScheme = schemeContent; if (schemeContent.contains('PreActions')) { newScheme = schemeContent.replaceFirst('', '\n$newContent'); } else { newContent = ''' $newContent '''; String? buildAction = schemeLines .where((String line) => line.contains('')) .firstOrNull; if (buildAction == null) { // If there are no BuildActionEntries, append before end of BuildAction. buildAction = schemeLines .where((String line) => line.contains('')) .firstOrNull; if (buildAction == null) { throw Exception('Failed to parse ${schemeFile.basename}: Could not find BuildAction.'); } } newScheme = schemeContent.replaceFirst(buildAction, '$newContent$buildAction'); } schemeFile.writeAsStringSync(newScheme); try { XmlDocument.parse(newScheme); } on XmlException catch (exception) { throw Exception( 'Failed to parse ${schemeFile.basename}: Invalid xml: $newScheme\n$exception', ); } } /// Parses the project.pbxproj into [ParsedProjectInfo]. Will throw an /// exception if it fails to parse. ParsedProjectInfo _parsePbxproj() { final String? results = _plistParser.plistJsonContent(_xcodeProjectInfoFile.path); if (results == null) { throw Exception('Failed to parse project settings.'); } try { final decodeResult = json.decode(results) as Object; if (decodeResult is! Map) { throw Exception('project.pbxproj returned unexpected JSON response: $results'); } return ParsedProjectInfo.fromJson(decodeResult); } on FormatException { throw Exception('project.pbxproj returned non-JSON response: $results'); } } /// Returns true if optional PBXFileReferences (FlutterGeneratedPluginSwiftPackage for all apps, /// plugin Swift package and FlutterFramework for plugin example app only) are already migrated. bool _areOptionalFilesMigrated(File xcodeProjectInfoFile) { final String fileContents = xcodeProjectInfoFile.readAsStringSync(); final bool flutterGeneratedPackageAsFileReference = fileContents.contains( '$_flutterPluginsSwiftPackageFileIdentifer /* $kFlutterGeneratedPluginSwiftPackageName */ = {isa = PBXFileReference', ); var pluginExampleAppMigrated = true; if (_examplePlugin != null) { final bool pluginPackageAsFileReference = fileContents.contains( '$_flutterPluginLocalOverrideFileIdenitifier /* ${_examplePlugin.name} */ = {isa = PBXFileReference', ); final bool flutterFrameworkMigrated = fileContents.contains( '$_flutterFrameworkLocalOverrideFileIdentifier /* $kFlutterGeneratedFrameworkSwiftPackageTargetName */ = {isa = PBXFileReference', ); pluginExampleAppMigrated = pluginPackageAsFileReference && flutterFrameworkMigrated; } return flutterGeneratedPackageAsFileReference && pluginExampleAppMigrated; } bool _areOptionalFilesMigratedCorrectly( ParsedProjectInfo projectInfo, { bool logErrorIfNotMigrated = false, }) { // Validate FlutterGeneratedPluginSwiftPackage is added final bool packageFileReferenceMigrated = _isFileReferenceMigrated( projectInfo, logErrorIfNotMigrated: logErrorIfNotMigrated, identifer: _flutterPluginsSwiftPackageFileIdentifer, name: kFlutterGeneratedPluginSwiftPackageName, ); final bool packageGroupMigrated = _isGroupMigrated( projectInfo, logErrorIfNotMigrated: logErrorIfNotMigrated, fileReferenceIdentifier: _flutterPluginsSwiftPackageFileIdentifer, ); // Validate plugin is added (for example app only) var pluginExampleAppMigrated = true; if (_examplePlugin != null) { final bool localPluginFileReferenceMigrated = _isFileReferenceMigrated( projectInfo, logErrorIfNotMigrated: logErrorIfNotMigrated, identifer: _flutterPluginLocalOverrideFileIdenitifier, name: _examplePlugin.name, ); final bool localPluginGroupMigrated = _isGroupMigrated( projectInfo, logErrorIfNotMigrated: logErrorIfNotMigrated, fileReferenceIdentifier: _flutterPluginLocalOverrideFileIdenitifier, ); // Validate FlutterFramework is added to example app final bool frameworkFileReferenceMigrated = _isFileReferenceMigrated( projectInfo, logErrorIfNotMigrated: logErrorIfNotMigrated, identifer: _flutterFrameworkLocalOverrideFileIdentifier, name: kFlutterGeneratedFrameworkSwiftPackageTargetName, ); final bool frameworkPackageGroupMigrated = _isGroupMigrated( projectInfo, logErrorIfNotMigrated: logErrorIfNotMigrated, fileReferenceIdentifier: _flutterFrameworkLocalOverrideFileIdentifier, ); pluginExampleAppMigrated = localPluginFileReferenceMigrated && localPluginGroupMigrated && frameworkFileReferenceMigrated && frameworkPackageGroupMigrated; } return packageFileReferenceMigrated && packageGroupMigrated && pluginExampleAppMigrated; } /// Checks if all sections have been migrated. If [logErrorIfNotMigrated] is /// true, will log an error for each section that is not migrated. bool _isPbxprojMigratedCorrectly( ParsedProjectInfo projectInfo, { bool logErrorIfNotMigrated = false, }) { // Validate FlutterGeneratedPluginSwiftPackage is added final bool buildFilesMigrated = _isBuildFilesMigrated( projectInfo, logErrorIfNotMigrated: logErrorIfNotMigrated, ); final bool frameworksBuildPhaseMigrated = _isFrameworksBuildPhaseMigrated( projectInfo, logErrorIfNotMigrated: logErrorIfNotMigrated, ); final bool nativeTargetsMigrated = _isNativeTargetMigrated( projectInfo, logErrorIfNotMigrated: logErrorIfNotMigrated, ); final bool projectObjectMigrated = _isProjectObjectMigrated( projectInfo, logErrorIfNotMigrated: logErrorIfNotMigrated, ); final bool localSwiftPackageMigrated = _isLocalSwiftPackageProductDependencyMigrated( projectInfo, logErrorIfNotMigrated: logErrorIfNotMigrated, ); final bool swiftPackageMigrated = _isSwiftPackageProductDependencyMigrated( projectInfo, logErrorIfNotMigrated: logErrorIfNotMigrated, ); final bool optionalMigrated = _areOptionalFilesMigratedCorrectly( projectInfo, logErrorIfNotMigrated: logErrorIfNotMigrated, ); return buildFilesMigrated && frameworksBuildPhaseMigrated && nativeTargetsMigrated && projectObjectMigrated && localSwiftPackageMigrated && swiftPackageMigrated && optionalMigrated; } void _migrateOptionalPbxproj() { final String originalProjectContents = _xcodeProjectInfoFile.readAsStringSync(); _ensureOptionalIdentifiersNotUsed(originalProjectContents); // Parse project.pbxproj into JSON final ParsedProjectInfo parsedInfo = _parsePbxproj(); List lines = LineSplitter.split(originalProjectContents).toList(); lines = _migrationOptionalFilesAndGroups(lines, parsedInfo); _updateProjectSettings(lines, originalProjectContents); } void _migratePbxproj() { final String originalProjectContents = _xcodeProjectInfoFile.readAsStringSync(); _ensureNewIdentifiersNotUsed(originalProjectContents); _ensureOptionalIdentifiersNotUsed(originalProjectContents); // Parse project.pbxproj into JSON final ParsedProjectInfo parsedInfo = _parsePbxproj(); List lines = LineSplitter.split(originalProjectContents).toList(); // Add FlutterGeneratedPluginSwiftPackage as a dependency and local package override lines = _migrateBuildFile(lines, parsedInfo); lines = _migrateFrameworksBuildPhase(lines, parsedInfo); lines = _migrateNativeTarget(lines, parsedInfo); lines = _migrateProjectObject(lines, parsedInfo); lines = _migrateLocalPackageProductDependencies(lines, parsedInfo); lines = _migratePackageProductDependencies(lines, parsedInfo); lines = _migrationOptionalFilesAndGroups(lines, parsedInfo); _updateProjectSettings(lines, originalProjectContents); } void _updateProjectSettings(List lines, String originalProjectContents) { final newProjectContents = '${lines.join('\n')}\n'; if (originalProjectContents != newProjectContents) { logger.printTrace('Updating project settings...'); _xcodeProjectInfoFile.copySync(backupProjectSettings.path); _xcodeProjectInfoFile.writeAsStringSync(newProjectContents); } } void _ensureNewIdentifiersNotUsed(String originalProjectContents) { if (!originalProjectContents.contains( '$_flutterPluginsSwiftPackageBuildFileIdentifier /* $kFlutterGeneratedPluginSwiftPackageName in Frameworks */', ) && originalProjectContents.contains(_flutterPluginsSwiftPackageBuildFileIdentifier)) { throw Exception('Duplicate id found for PBXBuildFile.'); } if (!originalProjectContents.contains( '$_flutterPluginsSwiftPackageProductDependencyIdentifier /* $kFlutterGeneratedPluginSwiftPackageName */', ) && originalProjectContents.contains(_flutterPluginsSwiftPackageProductDependencyIdentifier)) { throw Exception('Duplicate id found for XCSwiftPackageProductDependency.'); } if (!originalProjectContents.contains( '$_localFlutterPluginsSwiftPackageReferenceIdentifier /* XCLocalSwiftPackageReference', ) && originalProjectContents.contains(_localFlutterPluginsSwiftPackageReferenceIdentifier)) { throw Exception('Duplicate id found for XCLocalSwiftPackageReference.'); } } void _ensureOptionalIdentifiersNotUsed(String originalProjectContents) { if (!originalProjectContents.contains( '$_flutterPluginsSwiftPackageFileIdentifer /* $kFlutterGeneratedPluginSwiftPackageName */', ) && originalProjectContents.contains(_flutterPluginsSwiftPackageFileIdentifer)) { throw Exception( 'Duplicate id found for $kFlutterGeneratedPluginSwiftPackageName PBXFileReference.', ); } if (_examplePlugin != null) { if (!originalProjectContents.contains( '$_flutterPluginLocalOverrideFileIdenitifier /* ${_examplePlugin.name} */', ) && originalProjectContents.contains(_flutterPluginLocalOverrideFileIdenitifier)) { throw Exception('Duplicate id found for ${_examplePlugin.name} PBXFileReference.'); } if (!originalProjectContents.contains( '$_flutterFrameworkLocalOverrideFileIdentifier /* $kFlutterGeneratedFrameworkSwiftPackageTargetName */', ) && originalProjectContents.contains(_flutterFrameworkLocalOverrideFileIdentifier)) { throw Exception( 'Duplicate id found for $kFlutterGeneratedFrameworkSwiftPackageTargetName PBXFileReference.', ); } } } bool _isBuildFilesMigrated(ParsedProjectInfo projectInfo, {bool logErrorIfNotMigrated = false}) { final bool migrated = projectInfo.buildFileIdentifiers.contains( _flutterPluginsSwiftPackageBuildFileIdentifier, ); if (logErrorIfNotMigrated && !migrated) { logger.printError('PBXBuildFile was not migrated or was migrated incorrectly.'); } return migrated; } List _migrateBuildFile(List lines, ParsedProjectInfo projectInfo) { if (_isBuildFilesMigrated(projectInfo)) { logger.printTrace('PBXBuildFile already migrated. Skipping...'); return lines; } const newContent = ' $_flutterPluginsSwiftPackageBuildFileIdentifier /* $kFlutterGeneratedPluginSwiftPackageName in Frameworks */ = {isa = PBXBuildFile; productRef = $_flutterPluginsSwiftPackageProductDependencyIdentifier /* $kFlutterGeneratedPluginSwiftPackageName */; };'; final (int _, int endSectionIndex) = _sectionRange('PBXBuildFile', lines); lines.insert(endSectionIndex, newContent); return lines; } bool _isFileReferenceMigrated( ParsedProjectInfo projectInfo, { bool logErrorIfNotMigrated = false, required String identifer, required String name, }) { final bool migrated = projectInfo.fileReferenceIdentifiers.contains(identifer); if (logErrorIfNotMigrated && !migrated) { logger.printError('PBXFileReference for $name was not migrated or was migrated incorrectly.'); } return migrated; } List _migrateFileReference( List lines, ParsedProjectInfo projectInfo, { required String identifier, required String name, required String path, }) { if (_isFileReferenceMigrated(projectInfo, identifer: identifier, name: name)) { logger.printTrace('PBXFileReference for $identifier already migrated. Skipping...'); return lines; } final newContent = ' $identifier /* $name */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = $name; path = $path; sourceTree = ""; };'; final (int _, int endSectionIndex) = _sectionRange('PBXFileReference', lines); lines.insert(endSectionIndex, newContent); return lines; } bool _isFrameworksBuildPhaseMigrated( ParsedProjectInfo projectInfo, { bool logErrorIfNotMigrated = false, }) { final bool migrated = projectInfo.frameworksBuildPhases .where( (ParsedProjectFrameworksBuildPhase phase) => phase.identifier == _runnerFrameworksBuildPhaseIdentifier && phase.files != null && phase.files!.contains(_flutterPluginsSwiftPackageBuildFileIdentifier), ) .toList() .isNotEmpty; if (logErrorIfNotMigrated && !migrated) { logger.printError('PBXFrameworksBuildPhase was not migrated or was migrated incorrectly.'); } return migrated; } List _migrateFrameworksBuildPhase(List lines, ParsedProjectInfo projectInfo) { if (_isFrameworksBuildPhaseMigrated(projectInfo)) { logger.printTrace('PBXFrameworksBuildPhase already migrated. Skipping...'); return lines; } final (int startSectionIndex, int endSectionIndex) = _sectionRange( 'PBXFrameworksBuildPhase', lines, ); // Find index where Frameworks Build Phase for the Runner target begins. final int runnerFrameworksPhaseStartIndex = lines.indexWhere( (String line) => line.trim().startsWith('$_runnerFrameworksBuildPhaseIdentifier /* Frameworks */ = {'), startSectionIndex, ); if (runnerFrameworksPhaseStartIndex == -1 || runnerFrameworksPhaseStartIndex > endSectionIndex) { throw Exception( 'Unable to find PBXFrameworksBuildPhase for ${_xcodeProject.hostAppProjectName} target.', ); } // Get the Frameworks Build Phase for the Runner target from the parsed // project info. final ParsedProjectFrameworksBuildPhase? runnerFrameworksPhase = projectInfo .frameworksBuildPhases .where( (ParsedProjectFrameworksBuildPhase phase) => phase.identifier == _runnerFrameworksBuildPhaseIdentifier, ) .toList() .firstOrNull; if (runnerFrameworksPhase == null) { throw Exception( 'Unable to find parsed PBXFrameworksBuildPhase for ${_xcodeProject.hostAppProjectName} target.', ); } if (runnerFrameworksPhase.files == null) { // If files is null, the files field is missing and must be added. const newContent = ''' files = ( $_flutterPluginsSwiftPackageBuildFileIdentifier /* $kFlutterGeneratedPluginSwiftPackageName in Frameworks */, );'''; lines.insert(runnerFrameworksPhaseStartIndex + 1, newContent); } else { // Find the files field within the Frameworks PBXFrameworksBuildPhase for the Runner target. final int startFilesIndex = lines.indexWhere( (String line) => line.trim().contains('files = ('), runnerFrameworksPhaseStartIndex, ); if (startFilesIndex == -1 || startFilesIndex > endSectionIndex) { throw Exception( 'Unable to files for PBXFrameworksBuildPhase ${_xcodeProject.hostAppProjectName} target.', ); } const newContent = ' $_flutterPluginsSwiftPackageBuildFileIdentifier /* $kFlutterGeneratedPluginSwiftPackageName in Frameworks */,'; lines.insert(startFilesIndex + 1, newContent); } return lines; } bool _isNativeTargetMigrated( ParsedProjectInfo projectInfo, { bool logErrorIfNotMigrated = false, }) { final bool migrated = projectInfo.nativeTargets .where( (ParsedNativeTarget target) => target.identifier == _runnerNativeTargetIdentifier && target.packageProductDependencies != null && target.packageProductDependencies!.contains( _flutterPluginsSwiftPackageProductDependencyIdentifier, ), ) .toList() .isNotEmpty; if (logErrorIfNotMigrated && !migrated) { logger.printError('PBXNativeTarget was not migrated or was migrated incorrectly.'); } return migrated; } List _migrateNativeTarget(List lines, ParsedProjectInfo projectInfo) { if (_isNativeTargetMigrated(projectInfo)) { logger.printTrace('PBXNativeTarget already migrated. Skipping...'); return lines; } final (int startSectionIndex, int endSectionIndex) = _sectionRange('PBXNativeTarget', lines); // Find index where Native Target for the Runner target begins. final ParsedNativeTarget? runnerNativeTarget = projectInfo.nativeTargets .where((ParsedNativeTarget target) => target.identifier == _runnerNativeTargetIdentifier) .firstOrNull; if (runnerNativeTarget == null) { throw Exception( 'Unable to find parsed PBXNativeTarget for ${_xcodeProject.hostAppProjectName} target.', ); } final String subsectionLineStart = runnerNativeTarget.name != null ? '$_runnerNativeTargetIdentifier /* ${runnerNativeTarget.name} */ = {' : _runnerNativeTargetIdentifier; final int runnerNativeTargetStartIndex = lines.indexWhere( (String line) => line.trim().startsWith(subsectionLineStart), startSectionIndex, ); if (runnerNativeTargetStartIndex == -1 || runnerNativeTargetStartIndex > endSectionIndex) { throw Exception( 'Unable to find PBXNativeTarget for ${_xcodeProject.hostAppProjectName} target.', ); } if (runnerNativeTarget.packageProductDependencies == null) { // If packageProductDependencies is null, the packageProductDependencies field is missing and must be added. const newContent = [ ' packageProductDependencies = (', ' $_flutterPluginsSwiftPackageProductDependencyIdentifier /* $kFlutterGeneratedPluginSwiftPackageName */,', ' );', ]; lines.insertAll(runnerNativeTargetStartIndex + 1, newContent); } else { // Find the packageProductDependencies field within the Native Target for the Runner target. final int packageProductDependenciesIndex = lines.indexWhere( (String line) => line.trim().contains('packageProductDependencies'), runnerNativeTargetStartIndex, ); if (packageProductDependenciesIndex == -1 || packageProductDependenciesIndex > endSectionIndex) { throw Exception( 'Unable to find packageProductDependencies for ${_xcodeProject.hostAppProjectName} PBXNativeTarget.', ); } const newContent = ' $_flutterPluginsSwiftPackageProductDependencyIdentifier /* $kFlutterGeneratedPluginSwiftPackageName */,'; lines.insert(packageProductDependenciesIndex + 1, newContent); } return lines; } bool _isGroupMigrated( ParsedProjectInfo projectInfo, { bool logErrorIfNotMigrated = false, required String fileReferenceIdentifier, }) { final bool migrated = projectInfo.parsedGroups .where( (ParsedProjectGroup group) => group.identifier == _flutterGroupIdentifier && group.children != null && group.children!.contains(fileReferenceIdentifier), ) .toList() .isNotEmpty; if (logErrorIfNotMigrated && !migrated) { logger.printError('PBXGroup was not migrated or was migrated incorrectly.'); } return migrated; } List _migrateGroup( List lines, ParsedProjectInfo projectInfo, String fileReferenceIdentifier, String fileReferenceName, ) { if (_isGroupMigrated(projectInfo, fileReferenceIdentifier: fileReferenceIdentifier)) { logger.printTrace('PBXGroup for $fileReferenceIdentifier already migrated. Skipping...'); return lines; } final (int startSectionIndex, int endSectionIndex) = _sectionRange('PBXGroup', lines); // Find index where Flutter group begins. final int flutterGroupStartIndex = lines.indexWhere( (String line) => line.trim().startsWith('$_flutterGroupIdentifier /* Flutter */ = {'), startSectionIndex, ); if (flutterGroupStartIndex == -1 || flutterGroupStartIndex > endSectionIndex) { throw Exception('Unable to find Flutter PBXGroup.'); } // Get the Flutter Group from the parsed project info. final ParsedProjectGroup? parsedGroup = projectInfo.parsedGroups .where((ParsedProjectGroup group) => group.identifier == _flutterGroupIdentifier) .toList() .firstOrNull; if (parsedGroup == null) { throw Exception('Unable to find parsed Flutter PBXGroup.'); } // Find the children field within the Flutter PBXGroup. final int startChildrenIndex = lines.indexWhere( (String line) => line.trim().contains('children = ('), flutterGroupStartIndex, ); if (startChildrenIndex == -1 || startChildrenIndex > endSectionIndex) { final newContent = [ ' children = (', ' $fileReferenceIdentifier /* $fileReferenceName */,', ' );', ]; lines.insertAll(flutterGroupStartIndex + 1, newContent); } else { final newContent = ' $fileReferenceIdentifier /* $fileReferenceName */,'; lines.insert(startChildrenIndex + 1, newContent); } return lines; } bool _isProjectObjectMigrated( ParsedProjectInfo projectInfo, { bool logErrorIfNotMigrated = false, }) { final bool migrated = projectInfo.projects .where( (ParsedProject target) => target.identifier == _projectIdentifier && target.packageReferences != null && target.packageReferences!.contains( _localFlutterPluginsSwiftPackageReferenceIdentifier, ), ) .toList() .isNotEmpty; if (logErrorIfNotMigrated && !migrated) { logger.printError('PBXProject was not migrated or was migrated incorrectly.'); } return migrated; } List _migrateProjectObject(List lines, ParsedProjectInfo projectInfo) { if (_isProjectObjectMigrated(projectInfo)) { logger.printTrace('PBXProject already migrated. Skipping...'); return lines; } final (int startSectionIndex, int endSectionIndex) = _sectionRange('PBXProject', lines); // Find index where Runner Project begins. final int projectStartIndex = lines.indexWhere( (String line) => line.trim().startsWith('$_projectIdentifier /* Project object */ = {'), startSectionIndex, ); if (projectStartIndex == -1 || projectStartIndex > endSectionIndex) { throw Exception('Unable to find PBXProject for ${_xcodeProject.hostAppProjectName}.'); } // Get the Runner project from the parsed project info. final ParsedProject? projectObject = projectInfo.projects .where((ParsedProject project) => project.identifier == _projectIdentifier) .toList() .firstOrNull; if (projectObject == null) { throw Exception('Unable to find parsed PBXProject for ${_xcodeProject.hostAppProjectName}.'); } if (projectObject.packageReferences == null) { // If packageReferences is null, the packageReferences field is missing and must be added. final newContent = [ ' packageReferences = (', ' $_localFlutterPluginsSwiftPackageReferenceIdentifier /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/$kFlutterGeneratedPluginSwiftPackageName" */,', ' );', ]; lines.insertAll(projectStartIndex + 1, newContent); } else { // Find the packageReferences field within the Runner project. final int packageReferencesIndex = lines.indexWhere( (String line) => line.trim().contains('packageReferences'), projectStartIndex, ); if (packageReferencesIndex == -1 || packageReferencesIndex > endSectionIndex) { throw Exception( 'Unable to find packageReferences for ${_xcodeProject.hostAppProjectName} PBXProject.', ); } const newContent = ' $_localFlutterPluginsSwiftPackageReferenceIdentifier /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/$kFlutterGeneratedPluginSwiftPackageName" */,'; lines.insert(packageReferencesIndex + 1, newContent); } return lines; } bool _isLocalSwiftPackageProductDependencyMigrated( ParsedProjectInfo projectInfo, { bool logErrorIfNotMigrated = false, }) { final bool migrated = projectInfo.localSwiftPackageProductDependencies.contains( _localFlutterPluginsSwiftPackageReferenceIdentifier, ); if (logErrorIfNotMigrated && !migrated) { logger.printError( 'XCLocalSwiftPackageReference was not migrated or was migrated incorrectly.', ); } return migrated; } List _migrateLocalPackageProductDependencies( List lines, ParsedProjectInfo projectInfo, ) { if (_isLocalSwiftPackageProductDependencyMigrated(projectInfo)) { logger.printTrace('XCLocalSwiftPackageReference already migrated. Skipping...'); return lines; } final (int startSectionIndex, int endSectionIndex) = _sectionRange( 'XCLocalSwiftPackageReference', lines, throwIfMissing: false, ); if (startSectionIndex == -1) { // There isn't a XCLocalSwiftPackageReference section yet, so add it final newContent = [ '/* Begin XCLocalSwiftPackageReference section */', ' $_localFlutterPluginsSwiftPackageReferenceIdentifier /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/$kFlutterGeneratedPluginSwiftPackageName" */ = {', ' isa = XCLocalSwiftPackageReference;', ' relativePath = Flutter/ephemeral/Packages/$kFlutterGeneratedPluginSwiftPackageName;', ' };', '/* End XCLocalSwiftPackageReference section */', ]; final int index = lines.lastIndexWhere((String line) => line.trim().startsWith('/* End')); if (index == -1) { throw Exception('Unable to find any sections.'); } lines.insertAll(index + 1, newContent); return lines; } final newContent = [ ' $_localFlutterPluginsSwiftPackageReferenceIdentifier /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/$kFlutterGeneratedPluginSwiftPackageName" */ = {', ' isa = XCLocalSwiftPackageReference;', ' relativePath = Flutter/ephemeral/Packages/$kFlutterGeneratedPluginSwiftPackageName;', ' };', ]; lines.insertAll(endSectionIndex, newContent); return lines; } bool _isSwiftPackageProductDependencyMigrated( ParsedProjectInfo projectInfo, { bool logErrorIfNotMigrated = false, }) { final bool migrated = projectInfo.swiftPackageProductDependencies.contains( _flutterPluginsSwiftPackageProductDependencyIdentifier, ); if (logErrorIfNotMigrated && !migrated) { logger.printError( 'XCSwiftPackageProductDependency was not migrated or was migrated incorrectly.', ); } return migrated; } List _migratePackageProductDependencies( List lines, ParsedProjectInfo projectInfo, ) { if (_isSwiftPackageProductDependencyMigrated(projectInfo)) { logger.printTrace('XCSwiftPackageProductDependency already migrated. Skipping...'); return lines; } final (int startSectionIndex, int endSectionIndex) = _sectionRange( 'XCSwiftPackageProductDependency', lines, throwIfMissing: false, ); if (startSectionIndex == -1) { // There isn't a XCSwiftPackageProductDependency section yet, so add it final newContent = [ '/* Begin XCSwiftPackageProductDependency section */', ' $_flutterPluginsSwiftPackageProductDependencyIdentifier /* $kFlutterGeneratedPluginSwiftPackageName */ = {', ' isa = XCSwiftPackageProductDependency;', ' productName = $kFlutterGeneratedPluginSwiftPackageName;', ' };', '/* End XCSwiftPackageProductDependency section */', ]; final int index = lines.lastIndexWhere((String line) => line.trim().startsWith('/* End')); if (index == -1) { throw Exception('Unable to find any sections.'); } lines.insertAll(index + 1, newContent); return lines; } final newContent = [ ' $_flutterPluginsSwiftPackageProductDependencyIdentifier /* $kFlutterGeneratedPluginSwiftPackageName */ = {', ' isa = XCSwiftPackageProductDependency;', ' productName = $kFlutterGeneratedPluginSwiftPackageName;', ' };', ]; lines.insertAll(endSectionIndex, newContent); return lines; } List _migrationOptionalFilesAndGroups(List lines, ParsedProjectInfo projectInfo) { // Add FlutterGeneratedPluginSwiftPackage as a file reference lines = _migrateFileReference( lines, projectInfo, identifier: _flutterPluginsSwiftPackageFileIdentifer, name: kFlutterGeneratedPluginSwiftPackageName, path: _xcodeProject.flutterPluginSwiftPackageDirectory.path.replaceAll( _xcodeProject.ephemeralDirectory.path, _relativeEphemeralPath, ), ); lines = _migrateGroup( lines, projectInfo, _flutterPluginsSwiftPackageFileIdentifer, kFlutterGeneratedPluginSwiftPackageName, ); // Add the plugin Swift package and FlutterFramework as a local package overrides (for example // app only). The plugin Swift package is added as a PBXFileReference so that Xcode live issues // work properly (see https://github.com/flutter/flutter/issues/179032). The FlutterFramework // is also needed since it's referred to by relative path and adding the plugin as a local // package override changes the relative path. if (_examplePlugin != null) { lines = _migrateFileReference( lines, projectInfo, identifier: _flutterFrameworkLocalOverrideFileIdentifier, name: kFlutterGeneratedFrameworkSwiftPackageTargetName, path: _xcodeProject.flutterFrameworkSwiftPackageDirectory.path.replaceAll( _xcodeProject.ephemeralDirectory.path, _relativeEphemeralPath, ), ); lines = _migrateGroup( lines, projectInfo, _flutterFrameworkLocalOverrideFileIdentifier, kFlutterGeneratedFrameworkSwiftPackageTargetName, ); lines = _migrateFileReference( lines, projectInfo, identifier: _flutterPluginLocalOverrideFileIdenitifier, name: _examplePlugin.name, path: _examplePlugin.path, ); lines = _migrateGroup( lines, projectInfo, _flutterPluginLocalOverrideFileIdenitifier, _examplePlugin.name, ); } return lines; } (int, int) _sectionRange(String sectionName, List lines, {bool throwIfMissing = true}) { final int startSectionIndex = lines.indexOf('/* Begin $sectionName section */'); if (throwIfMissing && startSectionIndex == -1) { throw Exception('Unable to find beginning of $sectionName section.'); } final int endSectionIndex = lines.indexOf('/* End $sectionName section */'); if (throwIfMissing && endSectionIndex == -1) { throw Exception('Unable to find end of $sectionName section.'); } if (throwIfMissing && startSectionIndex > endSectionIndex) { throw Exception('Found the end of $sectionName section before the beginning.'); } return (startSectionIndex, endSectionIndex); } /// If the [xcodeProject] is within a plugin's example app, return the plugin's name and its /// path relative to ios/macos directory the [xcodeProject] is in. /// /// If the [xcodeProject] is not within an example app or the plugin can't be found, return null. Future<({String name, String path})?> _loadPluginFromExampleProject({ required XcodeBasedProject xcodeProject, required FileSystem fileSystem, required Logger logger, required FlutterDarwinPlatform platform, }) async { try { final FlutterProject flutterProject = xcodeProject.parent; if (flutterProject.directory.path.endsWith('example') && flutterProject.directory.parent.childFile('pubspec.yaml').existsSync()) { final FlutterProject parentProject = FlutterProject.fromDirectory( flutterProject.directory.parent, ); if (parentProject.isPlugin && parentProject.hasExampleApp) { final String pluginName = parentProject.manifest.appName; final List plugins = await xcodeProject.getPlugins(); final String? absolutePath = plugins .where((plugin) => plugin.name == pluginName) .firstOrNull ?.pluginSwiftPackagePath(fileSystem, platform.name); if (absolutePath == null) { return null; } final String relativePath; switch (platform) { case FlutterDarwinPlatform.ios: relativePath = fileSystem.path.relative( absolutePath, from: xcodeProject.hostAppRoot.path, ); case FlutterDarwinPlatform.macos: // The path is relative to the "Flutter" [managedDirectory] on macOS because the // Flutter `PBXGroup` in macOS pbxproj files uses `path` instead of `name`. relativePath = fileSystem.path.relative( absolutePath, from: xcodeProject.managedDirectory.path, ); } return (name: pluginName, path: relativePath); } } } on Exception catch (e) { logger.printTrace('Failed to load project: $e'); return null; } return null; } } class SchemeInfo { SchemeInfo({required this.schemeName, required this.schemeFile, required this.schemeContent}); final String schemeName; final File schemeFile; final String schemeContent; File? backupSchemeFile; } /// Representation of data parsed from Xcode project's project.pbxproj. class ParsedProjectInfo { ParsedProjectInfo._({ required this.buildFileIdentifiers, required this.fileReferenceIdentifiers, required this.parsedGroups, required this.frameworksBuildPhases, required this.nativeTargets, required this.projects, required this.swiftPackageProductDependencies, required this.localSwiftPackageProductDependencies, }); factory ParsedProjectInfo.fromJson(Map data) { final buildFiles = []; final references = []; final groups = []; final buildPhases = []; final native = []; final project = []; final parsedSwiftPackageProductDependencies = []; final parsedLocalSwiftPackageProductDependencies = []; if (data case {'objects': final Map values}) { for (final String key in values.keys) { if (values[key] case final Map details) { switch (details['isa']) { case 'PBXBuildFile': buildFiles.add(key); case 'PBXFileReference': references.add(key); case 'PBXGroup': groups.add(ParsedProjectGroup.fromJson(key, details)); case 'PBXFrameworksBuildPhase': buildPhases.add(ParsedProjectFrameworksBuildPhase.fromJson(key, details)); case 'PBXNativeTarget': native.add(ParsedNativeTarget.fromJson(key, details)); case 'PBXProject': project.add(ParsedProject.fromJson(key, details)); case 'XCSwiftPackageProductDependency': parsedSwiftPackageProductDependencies.add(key); case 'XCLocalSwiftPackageReference': parsedLocalSwiftPackageProductDependencies.add(key); } } } } return ParsedProjectInfo._( buildFileIdentifiers: buildFiles, fileReferenceIdentifiers: references, parsedGroups: groups, frameworksBuildPhases: buildPhases, nativeTargets: native, projects: project, swiftPackageProductDependencies: parsedSwiftPackageProductDependencies, localSwiftPackageProductDependencies: parsedLocalSwiftPackageProductDependencies, ); } /// List of identifiers under PBXBuildFile section. List buildFileIdentifiers; /// List of identifiers under PBXFileReference section. List fileReferenceIdentifiers; /// List of [ParsedProjectGroup] items under PBXGroup section. List parsedGroups; /// List of [ParsedProjectFrameworksBuildPhase] items under PBXFrameworksBuildPhase section. List frameworksBuildPhases; /// List of [ParsedNativeTarget] items under PBXNativeTarget section. List nativeTargets; /// List of [ParsedProject] items under PBXProject section. List projects; /// List of identifiers under XCSwiftPackageProductDependency section. List swiftPackageProductDependencies; /// List of identifiers under XCLocalSwiftPackageReference section. /// Introduced in Xcode 15. List localSwiftPackageProductDependencies; } /// Representation of data parsed from PBXGroup section in Xcode project's project.pbxproj. class ParsedProjectGroup { ParsedProjectGroup.fromJson(this.identifier, Map data) : children = switch (data['children']) { final List children => children.whereType().toList(), _ => null, }, name = switch (data) { {'name': final String name} => name, {'path': final String path} => path, _ => null, }; final String identifier; final List? children; final String? name; } /// Representation of data parsed from PBXFrameworksBuildPhase section in Xcode /// project's project.pbxproj. class ParsedProjectFrameworksBuildPhase { ParsedProjectFrameworksBuildPhase.fromJson(this.identifier, Map data) : files = switch (data['files']) { final List files => files.whereType().toList(), _ => null, }; final String identifier; final List? files; } /// Representation of data parsed from PBXNativeTarget section in Xcode project's /// project.pbxproj. class ParsedNativeTarget { ParsedNativeTarget.fromJson(this.identifier, this.data) : name = switch (data) { {'name': final String name} => name, _ => null, }, packageProductDependencies = switch (data['packageProductDependencies']) { final List dependencies => dependencies.whereType().toList(), _ => null, }; final Map data; final String identifier; final String? name; final List? packageProductDependencies; } /// Representation of data parsed from PBXProject section in Xcode project's /// project.pbxproj. class ParsedProject { ParsedProject.fromJson(this.identifier, this.data) : packageReferences = switch (data['packageReferences']) { final List references => references.whereType().toList(), _ => null, }; final Map data; final String identifier; final List? packageReferences; }