diff --git a/dev/bots/analyze.dart b/dev/bots/analyze.dart index 30597115967..dc8ecd4a3ce 100644 --- a/dev/bots/analyze.dart +++ b/dev/bots/analyze.dart @@ -793,6 +793,9 @@ Future _verifyNoMissingLicenseForExtension( if (contents.isEmpty) { continue; // let's not go down the /bin/true rabbit hole } + if (path.basename(file.path) == 'Package.swift') { + continue; + } if (!contents.startsWith(RegExp(header + licensePattern))) { errors.add(file.path); } diff --git a/packages/flutter_tools/bin/macos_assemble.sh b/packages/flutter_tools/bin/macos_assemble.sh index 71f69ef22e1..78467193681 100755 --- a/packages/flutter_tools/bin/macos_assemble.sh +++ b/packages/flutter_tools/bin/macos_assemble.sh @@ -144,11 +144,23 @@ BuildApp() { if [[ -n "$CODE_SIZE_DIRECTORY" ]]; then flutter_args+=("-dCodeSizeDirectory=${CODE_SIZE_DIRECTORY}") fi - flutter_args+=("${build_mode}_macos_bundle_flutter_assets") + + # Run flutter assemble with the build mode specific target that was passed in. + # If no target was passed it, default to build mode specific + # macos_bundle_flutter_assets target. + if [[ -n "$1" ]]; then + flutter_args+=("${build_mode}$1") + else + flutter_args+=("${build_mode}_macos_bundle_flutter_assets") + fi RunCommand "${flutter_args[@]}" } +PrepareFramework() { + BuildApp "_unpack_macos" +} + # Adds the App.framework as an embedded binary, the flutter_assets as # resources, and the native assets. EmbedFrameworks() { @@ -192,5 +204,7 @@ else BuildApp ;; "embed") EmbedFrameworks ;; + "prepare") + PrepareFramework ;; esac fi diff --git a/packages/flutter_tools/bin/podhelper.rb b/packages/flutter_tools/bin/podhelper.rb index 3d64737ae69..a26f324e4a4 100644 --- a/packages/flutter_tools/bin/podhelper.rb +++ b/packages/flutter_tools/bin/podhelper.rb @@ -302,7 +302,10 @@ def flutter_install_plugin_pods(application_path = nil, relative_symlink_dir, pl system('mkdir', '-p', symlink_plugins_dir) plugins_file = File.join(application_path, '..', '.flutter-plugins-dependencies') - plugin_pods = flutter_parse_plugins_file(plugins_file, platform) + dependencies_hash = flutter_parse_plugins_file(plugins_file) + plugin_pods = flutter_get_plugins_list(dependencies_hash, platform) + swift_package_manager_enabled = flutter_get_swift_package_manager_enabled(dependencies_hash) + plugin_pods.each do |plugin_hash| plugin_name = plugin_hash['name'] plugin_path = plugin_hash['path'] @@ -319,25 +322,43 @@ def flutter_install_plugin_pods(application_path = nil, relative_symlink_dir, pl # Keep pod path relative so it can be checked into Podfile.lock. relative = flutter_relative_path_from_podfile(symlink) + # If Swift Package Manager is enabled and the plugin has a Package.swift, + # skip from installing as a pod. + swift_package_exists = File.exists?(File.join(relative, platform_directory, plugin_name, "Package.swift")) + next if swift_package_manager_enabled && swift_package_exists + + # If a plugin is Swift Package Manager compatible but not CocoaPods compatible, skip it. + # The tool will print an error about it. + next if swift_package_exists && !File.exists?(File.join(relative, platform_directory, plugin_name + ".podspec")) + pod plugin_name, path: File.join(relative, platform_directory) end end -# .flutter-plugins-dependencies format documented at -# https://flutter.dev/go/plugins-list-migration -def flutter_parse_plugins_file(file, platform) +def flutter_parse_plugins_file(file) file_path = File.expand_path(file) return [] unless File.exist? file_path dependencies_file = File.read(file) - dependencies_hash = JSON.parse(dependencies_file) + JSON.parse(dependencies_file) +end +# .flutter-plugins-dependencies format documented at +# https://flutter.dev/go/plugins-list-migration +def flutter_get_plugins_list(dependencies_hash, platform) # dependencies_hash.dig('plugins', 'ios') not available until Ruby 2.3 + return [] unless dependencies_hash.any? return [] unless dependencies_hash.has_key?('plugins') return [] unless dependencies_hash['plugins'].has_key?(platform) dependencies_hash['plugins'][platform] || [] end +def flutter_get_swift_package_manager_enabled(dependencies_hash) + return false unless dependencies_hash.any? + return false unless dependencies_hash.has_key?('swift_package_manager_enabled') + dependencies_hash['swift_package_manager_enabled'] == true +end + def flutter_relative_path_from_podfile(path) # defined_in_file is set by CocoaPods and is a Pathname to the Podfile. project_directory_pathname = defined_in_file.dirname diff --git a/packages/flutter_tools/bin/xcode_backend.dart b/packages/flutter_tools/bin/xcode_backend.dart index dca13dad5f7..f657defd98c 100644 --- a/packages/flutter_tools/bin/xcode_backend.dart +++ b/packages/flutter_tools/bin/xcode_backend.dart @@ -49,6 +49,8 @@ class Context { switch (subCommand) { case 'build': buildApp(); + case 'prepare': + prepare(); case 'thin': // No-op, thinning is handled during the bundle asset assemble build target. break; @@ -351,21 +353,67 @@ class Context { } } - void buildApp() { - final bool verbose = environment['VERBOSE_SCRIPT_LOGGING'] != null && environment['VERBOSE_SCRIPT_LOGGING'] != ''; + void prepare() { + final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty; final String sourceRoot = environment['SOURCE_ROOT'] ?? ''; - String projectPath = '$sourceRoot/..'; - if (environment['FLUTTER_APPLICATION_PATH'] != null) { - projectPath = environment['FLUTTER_APPLICATION_PATH']!; + final String projectPath = environment['FLUTTER_APPLICATION_PATH'] ?? '$sourceRoot/..'; + + final String buildMode = parseFlutterBuildMode(); + + final List flutterArgs = _generateFlutterArgsForAssemble(buildMode, verbose); + + flutterArgs.add('${buildMode}_unpack_ios'); + + final ProcessResult result = runSync( + '${environmentEnsure('FLUTTER_ROOT')}/bin/flutter', + flutterArgs, + verbose: verbose, + allowFail: true, + workingDirectory: projectPath, // equivalent of RunCommand pushd "${project_path}" + ); + + if (result.exitCode != 0) { + echoError('Failed to copy Flutter framework.'); + exitApp(-1); + } + } + + void buildApp() { + final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty; + final String sourceRoot = environment['SOURCE_ROOT'] ?? ''; + final String projectPath = environment['FLUTTER_APPLICATION_PATH'] ?? '$sourceRoot/..'; + + final String buildMode = parseFlutterBuildMode(); + + final List flutterArgs = _generateFlutterArgsForAssemble(buildMode, verbose); + + flutterArgs.add('${buildMode}_ios_bundle_flutter_assets'); + + final ProcessResult result = runSync( + '${environmentEnsure('FLUTTER_ROOT')}/bin/flutter', + flutterArgs, + verbose: verbose, + allowFail: true, + workingDirectory: projectPath, // equivalent of RunCommand pushd "${project_path}" + ); + + if (result.exitCode != 0) { + echoError('Failed to package $projectPath.'); + exitApp(-1); } + streamOutput('done'); + streamOutput(' └─Compiling, linking and signing...'); + + echo('Project $projectPath built and packaged successfully.'); + } + + List _generateFlutterArgsForAssemble(String buildMode, bool verbose) { String targetPath = 'lib/main.dart'; if (environment['FLUTTER_TARGET'] != null) { targetPath = environment['FLUTTER_TARGET']!; } - final String buildMode = parseFlutterBuildMode(); - // Warn the user if not archiving (ACTION=install) in release mode. final String? action = environment['ACTION']; if (action == 'install' && buildMode != 'release') { @@ -432,24 +480,6 @@ class Context { flutterArgs.add('-dCodeSizeDirectory=${environment['CODE_SIZE_DIRECTORY']}'); } - flutterArgs.add('${buildMode}_ios_bundle_flutter_assets'); - - final ProcessResult result = runSync( - '${environmentEnsure('FLUTTER_ROOT')}/bin/flutter', - flutterArgs, - verbose: verbose, - allowFail: true, - workingDirectory: projectPath, // equivalent of RunCommand pushd "${project_path}" - ); - - if (result.exitCode != 0) { - echoError('Failed to package $projectPath.'); - exitApp(-1); - } - - streamOutput('done'); - streamOutput(' └─Compiling, linking and signing...'); - - echo('Project $projectPath built and packaged successfully.'); + return flutterArgs; } } diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart index 5ac969e9d86..56278362ab8 100644 --- a/packages/flutter_tools/lib/src/commands/assemble.dart +++ b/packages/flutter_tools/lib/src/commands/assemble.dart @@ -41,6 +41,9 @@ List _kDefaultTargets = [ const DebugMacOSBundleFlutterAssets(), const ProfileMacOSBundleFlutterAssets(), const ReleaseMacOSBundleFlutterAssets(), + const DebugUnpackMacOS(), + const ProfileUnpackMacOS(), + const ReleaseUnpackMacOS(), // Linux targets const DebugBundleLinuxAssets(TargetPlatform.linux_x64), const DebugBundleLinuxAssets(TargetPlatform.linux_arm64), @@ -72,6 +75,9 @@ List _kDefaultTargets = [ const DebugIosApplicationBundle(), const ProfileIosApplicationBundle(), const ReleaseIosApplicationBundle(), + const DebugUnpackIOS(), + const ProfileUnpackIOS(), + const ReleaseUnpackIOS(), // Windows targets const UnpackWindows(TargetPlatform.windows_x64), const UnpackWindows(TargetPlatform.windows_arm64), diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart index acf34b7c092..3f87c993000 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios.dart @@ -23,6 +23,7 @@ import '../globals.dart' as globals; import '../ios/application_package.dart'; import '../ios/mac.dart'; import '../ios/plist_parser.dart'; +import '../project.dart'; import '../reporting/reporting.dart'; import '../runner/flutter_command.dart'; import 'build.dart'; @@ -686,7 +687,15 @@ abstract class _BuildIOSSubCommand extends BuildSubCommand { xcodeBuildResult = result; if (!result.success) { - await diagnoseXcodeBuildFailure(result, globals.flutterUsage, globals.logger, globals.analytics); + await diagnoseXcodeBuildFailure( + result, + analytics: globals.analytics, + fileSystem: globals.fs, + flutterUsage: globals.flutterUsage, + logger: globals.logger, + platform: SupportedPlatform.ios, + project: app.project.parent, + ); final String presentParticiple = xcodeBuildAction == XcodeBuildAction.build ? 'building' : 'archiving'; throwToolExit('Encountered error while $presentParticiple for $logTarget.'); } 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 e8e224e5739..1228758eb16 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart @@ -272,7 +272,12 @@ class BuildIOSFrameworkCommand extends BuildFrameworkCommand { buildInfo, modeDirectory, iPhoneBuildOutput, simulatorBuildOutput); // Build and copy plugins. - await processPodsIfNeeded(project.ios, getIosBuildDirectory(), buildInfo.mode); + await processPodsIfNeeded( + project.ios, + getIosBuildDirectory(), + buildInfo.mode, + forceCocoaPodsOnly: true, + ); if (boolArg('plugins') && hasPlugins(project)) { await _producePlugins(buildInfo.mode, xcodeBuildConfiguration, iPhoneBuildOutput, simulatorBuildOutput, modeDirectory); } diff --git a/packages/flutter_tools/lib/src/commands/build_macos_framework.dart b/packages/flutter_tools/lib/src/commands/build_macos_framework.dart index 30d1a705afe..715e2446d15 100644 --- a/packages/flutter_tools/lib/src/commands/build_macos_framework.dart +++ b/packages/flutter_tools/lib/src/commands/build_macos_framework.dart @@ -94,7 +94,12 @@ class BuildMacOSFrameworkCommand extends BuildFrameworkCommand { await _produceAppFramework(buildInfo, modeDirectory, buildOutput); // Build and copy plugins. - await processPodsIfNeeded(project.macos, getMacOSBuildDirectory(), buildInfo.mode); + await processPodsIfNeeded( + project.macos, + getMacOSBuildDirectory(), + buildInfo.mode, + forceCocoaPodsOnly: true, + ); if (boolArg('plugins') && hasPlugins(project)) { await _producePlugins(xcodeBuildConfiguration, buildOutput, modeDirectory); } diff --git a/packages/flutter_tools/lib/src/features.dart b/packages/flutter_tools/lib/src/features.dart index fcc49731824..7a8a209f5c7 100644 --- a/packages/flutter_tools/lib/src/features.dart +++ b/packages/flutter_tools/lib/src/features.dart @@ -51,6 +51,9 @@ abstract class FeatureFlags { /// Whether native assets compilation and bundling is enabled. bool get isPreviewDeviceEnabled => true; + /// Whether Swift Package Manager dependency management is enabled. + bool get isSwiftPackageManagerEnabled => false; + /// Whether a particular feature is enabled for the current channel. /// /// Prefer using one of the specific getters above instead of this API. @@ -70,6 +73,7 @@ const List allFeatures = [ cliAnimation, nativeAssets, previewDevice, + swiftPackageManager, ]; /// All current Flutter feature flags that can be configured. @@ -175,6 +179,16 @@ const Feature previewDevice = Feature( ), ); +/// Enable Swift Package Mangaer as a darwin dependency manager. +const Feature swiftPackageManager = Feature( + name: 'support for Swift Package Manager for iOS and macOS', + configSetting: 'enable-swift-package-manager', + environmentOverride: 'SWIFT_PACKAGE_MANAGER', + master: FeatureChannelSetting( + available: true, + ), +); + /// A [Feature] is a process for conditionally enabling tool features. /// /// All settings are optional, and if not provided will generally default to diff --git a/packages/flutter_tools/lib/src/flutter_features.dart b/packages/flutter_tools/lib/src/flutter_features.dart index f9e961c219a..df5b5c3be8f 100644 --- a/packages/flutter_tools/lib/src/flutter_features.dart +++ b/packages/flutter_tools/lib/src/flutter_features.dart @@ -58,6 +58,9 @@ class FlutterFeatureFlags implements FeatureFlags { @override bool get isPreviewDeviceEnabled => isEnabled(previewDevice); + @override + bool get isSwiftPackageManagerEnabled => isEnabled(swiftPackageManager); + @override bool isEnabled(Feature feature) { final String currentChannel = _flutterVersion.channel; diff --git a/packages/flutter_tools/lib/src/flutter_manifest.dart b/packages/flutter_tools/lib/src/flutter_manifest.dart index 736e00ba02e..04455506570 100644 --- a/packages/flutter_tools/lib/src/flutter_manifest.dart +++ b/packages/flutter_tools/lib/src/flutter_manifest.dart @@ -136,6 +136,12 @@ class FlutterManifest { return _flutterDescriptor['uses-material-design'] as bool? ?? false; } + /// If true, does not use Swift Package Manager as a dependency manager. + /// CocoaPods will be used instead. + bool get disabledSwiftPackageManager { + return _flutterDescriptor['disable-swift-package-manager'] as bool? ?? false; + } + /// True if this Flutter module should use AndroidX dependencies. /// /// If false the deprecated Android Support library will be used. @@ -547,6 +553,10 @@ void _validateFlutter(YamlMap? yaml, List errors) { break; case 'deferred-components': _validateDeferredComponents(kvp, errors); + case 'disable-swift-package-manager': + if (yamlValue is! bool) { + errors.add('Expected "$yamlKey" to be a bool, but got $yamlValue (${yamlValue.runtimeType}).'); + } default: errors.add('Unexpected child "$yamlKey" found under "flutter".'); break; diff --git a/packages/flutter_tools/lib/src/flutter_plugins.dart b/packages/flutter_tools/lib/src/flutter_plugins.dart index dc693fea296..9e1090295b1 100644 --- a/packages/flutter_tools/lib/src/flutter_plugins.dart +++ b/packages/flutter_tools/lib/src/flutter_plugins.dart @@ -22,6 +22,8 @@ import 'dart/language_version.dart'; import 'dart/package_map.dart'; import 'features.dart'; import 'globals.dart' as globals; +import 'macos/darwin_dependency_management.dart'; +import 'macos/swift_package_manager.dart'; import 'platform_plugins.dart'; import 'plugins.dart'; import 'project.dart'; @@ -160,7 +162,11 @@ const String _kFlutterPluginsSharedDarwinSource = 'shared_darwin_source'; /// /// /// Finally, returns [true] if the plugins list has changed, otherwise returns [false]. -bool _writeFlutterPluginsList(FlutterProject project, List plugins) { +bool _writeFlutterPluginsList( + FlutterProject project, + List plugins, { + bool forceCocoaPodsOnly = false, +}) { final File pluginsFile = project.flutterPluginsDependenciesFile; if (plugins.isEmpty) { return ErrorHandlingFileSystem.deleteIfExists(pluginsFile); @@ -190,6 +196,7 @@ bool _writeFlutterPluginsList(FlutterProject project, List plugins) { result['dependencyGraph'] = _createPluginLegacyDependencyGraph(plugins); result['date_created'] = globals.systemClock.now().toString(); result['version'] = globals.flutterVersion.frameworkVersion; + result['swift_package_manager_enabled'] = !forceCocoaPodsOnly && project.usesSwiftPackageManager; // Only notify if the plugins list has changed. [date_created] will always be different, // [version] is not relevant for this check. @@ -1000,6 +1007,7 @@ Future refreshPluginsList( FlutterProject project, { bool iosPlatform = false, bool macOSPlatform = false, + bool forceCocoaPodsOnly = false, }) async { final List plugins = await findPlugins(project); // Sort the plugins by name to keep ordering stable in generated files. @@ -1008,8 +1016,12 @@ Future refreshPluginsList( // Write the legacy plugin files to avoid breaking existing apps. final bool legacyChanged = _writeFlutterPluginsListLegacy(project, plugins); - final bool changed = _writeFlutterPluginsList(project, plugins); - if (changed || legacyChanged) { + final bool changed = _writeFlutterPluginsList( + project, + plugins, + forceCocoaPodsOnly: forceCocoaPodsOnly, + ); + if (changed || legacyChanged || forceCocoaPodsOnly) { createPluginSymlinks(project, force: true); if (iosPlatform) { globals.cocoaPods?.invalidatePodInstallOutput(project.ios); @@ -1069,6 +1081,7 @@ Future injectPlugins( bool macOSPlatform = false, bool windowsPlatform = false, Iterable? allowedPlugins, + DarwinDependencyManagement? darwinDependencyManagement, }) async { final List plugins = await findPlugins(project); // Sort the plugins by name to keep ordering stable in generated files. @@ -1088,20 +1101,27 @@ Future injectPlugins( if (windowsPlatform) { await writeWindowsPluginFiles(project, plugins, globals.templateRenderer, allowedPlugins: allowedPlugins); } - if (!project.isModule) { - final List darwinProjects = [ - if (iosPlatform) project.ios, - if (macOSPlatform) project.macos, - ]; - for (final XcodeBasedProject subproject in darwinProjects) { - if (plugins.isNotEmpty) { - await globals.cocoaPods?.setupPodfile(subproject); - } - /// The user may have a custom maintained Podfile that they're running `pod install` - /// on themselves. - else if (subproject.podfile.existsSync() && subproject.podfileLock.existsSync()) { - globals.cocoaPods?.addPodsDependencyToFlutterXcconfig(subproject); - } + if (iosPlatform || macOSPlatform) { + final DarwinDependencyManagement darwinDependencyManagerSetup = darwinDependencyManagement ?? DarwinDependencyManagement( + project: project, + plugins: plugins, + cocoapods: globals.cocoaPods!, + swiftPackageManager: SwiftPackageManager( + fileSystem: globals.fs, + templateRenderer: globals.templateRenderer, + ), + fileSystem: globals.fs, + logger: globals.logger, + ); + if (iosPlatform) { + await darwinDependencyManagerSetup.setUp( + platform: SupportedPlatform.ios, + ); + } + if (macOSPlatform) { + await darwinDependencyManagerSetup.setUp( + platform: SupportedPlatform.macos, + ); } } } diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index ad462a76ef0..650f495aff4 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -496,7 +496,15 @@ class IOSDevice extends Device { ); if (!buildResult.success) { _logger.printError('Could not build the precompiled application for the device.'); - await diagnoseXcodeBuildFailure(buildResult, globals.flutterUsage, _logger, globals.analytics); + await diagnoseXcodeBuildFailure( + buildResult, + analytics: globals.analytics, + fileSystem: globals.fs, + flutterUsage: globals.flutterUsage, + logger: globals.logger, + platform: SupportedPlatform.ios, + project: package.project.parent, + ); _logger.printError(''); return LaunchResult.failed(); } diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 9a65ba0995d..acbbf4e9f62 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -19,12 +19,16 @@ import '../build_info.dart'; import '../cache.dart'; import '../device.dart'; import '../flutter_manifest.dart'; +import '../flutter_plugins.dart'; import '../globals.dart' as globals; import '../macos/cocoapod_utils.dart'; +import '../macos/swift_package_manager.dart'; import '../macos/xcode.dart'; +import '../migrations/swift_package_manager_integration_migration.dart'; import '../migrations/xcode_project_object_version_migration.dart'; import '../migrations/xcode_script_build_phase_migration.dart'; import '../migrations/xcode_thin_binary_build_phase_input_paths_migration.dart'; +import '../plugins.dart'; import '../project.dart'; import '../reporting/reporting.dart'; import 'application_package.dart'; @@ -148,6 +152,8 @@ Future buildXcodeProject({ return XcodeBuildResult(success: false); } + final FlutterProject project = FlutterProject.current(); + final List migrators = [ RemoveFrameworkLinkAndEmbeddingMigration(app.project, globals.logger, globals.flutterUsage, globals.analytics), XcodeBuildSystemMigration(app.project, globals.logger), @@ -160,6 +166,16 @@ Future buildXcodeProject({ RemoveBitcodeMigration(app.project, globals.logger), XcodeThinBinaryBuildPhaseInputPathsMigration(app.project, globals.logger), UIApplicationMainDeprecationMigration(app.project, globals.logger), + if (project.usesSwiftPackageManager && app.project.flutterPluginSwiftPackageManifest.existsSync()) + SwiftPackageManagerIntegrationMigration( + app.project, + SupportedPlatform.ios, + buildInfo, + xcodeProjectInterpreter: globals.xcodeProjectInterpreter!, + logger: globals.logger, + fileSystem: globals.fs, + plistParser: globals.plistParser, + ), ]; final ProjectMigration migration = ProjectMigration(migrators); @@ -245,12 +261,21 @@ Future buildXcodeProject({ ); } - final FlutterProject project = FlutterProject.current(); await updateGeneratedXcodeProperties( project: project, targetOverride: targetOverride, buildInfo: buildInfo, ); + if (project.usesSwiftPackageManager) { + final String? iosDeploymentTarget = buildSettings['IPHONEOS_DEPLOYMENT_TARGET']; + if (iosDeploymentTarget != null) { + SwiftPackageManager.updateMinimumDeployment( + platform: SupportedPlatform.ios, + project: project.ios, + deploymentTarget: iosDeploymentTarget, + ); + } + } await processPodsIfNeeded(project.ios, getIosBuildDirectory(), buildInfo.mode); if (configOnly) { return XcodeBuildResult(success: true); @@ -596,11 +621,14 @@ return result.exitCode != 0 && } Future diagnoseXcodeBuildFailure( - XcodeBuildResult result, - Usage flutterUsage, - Logger logger, - Analytics analytics, -) async { + XcodeBuildResult result, { + required Analytics analytics, + required Logger logger, + required FileSystem fileSystem, + required Usage flutterUsage, + required SupportedPlatform platform, + required FlutterProject project, +}) async { final XcodeBuildExecution? xcodeBuildExecution = result.xcodeBuildExecution; if (xcodeBuildExecution != null && xcodeBuildExecution.environmentType == EnvironmentType.physical @@ -627,7 +655,14 @@ Future diagnoseXcodeBuildFailure( } // Handle errors. - final bool issueDetected = _handleIssues(result.xcResult, logger, xcodeBuildExecution); + final bool issueDetected = await _handleIssues( + result, + xcodeBuildExecution, + project: project, + platform: platform, + logger: logger, + fileSystem: fileSystem, + ); if (!issueDetected && xcodeBuildExecution != null) { // Fallback to use stdout to detect and print issues. @@ -728,7 +763,11 @@ bool upgradePbxProjWithFlutterAssets(IosProject project, Logger logger) { return true; } -_XCResultIssueHandlingResult _handleXCResultIssue({required XCResultIssue issue, required Logger logger}) { +_XCResultIssueHandlingResult _handleXCResultIssue({ + required XCResultIssue issue, + required XcodeBuildResult result, + required Logger logger, +}) { // Issue summary from xcresult. final StringBuffer issueSummaryBuffer = StringBuffer(); issueSummaryBuffer.write(issue.subType ?? 'Unknown'); @@ -761,20 +800,61 @@ _XCResultIssueHandlingResult _handleXCResultIssue({required XCResultIssue issue, if (missingPlatform != null) { return _XCResultIssueHandlingResult(requiresProvisioningProfile: false, hasProvisioningProfileIssue: false, missingPlatform: missingPlatform); } + } else if (message.toLowerCase().contains('redefinition of module')) { + final String? duplicateModule = _parseModuleRedefinition(message); + return _XCResultIssueHandlingResult( + requiresProvisioningProfile: false, + hasProvisioningProfileIssue: false, + duplicateModule: duplicateModule, + ); + } else if (message.toLowerCase().contains('duplicate symbols')) { + // The message does not contain the plugin name, must parse the stdout. + String? duplicateModule; + if (result.stdout != null) { + duplicateModule = _parseDuplicateSymbols(result.stdout!); + } + return _XCResultIssueHandlingResult( + requiresProvisioningProfile: false, + hasProvisioningProfileIssue: false, + duplicateModule: duplicateModule, + ); + } else if (message.toLowerCase().contains('not found')) { + final String? missingModule = _parseMissingModule(message); + if (missingModule != null) { + return _XCResultIssueHandlingResult( + requiresProvisioningProfile: false, + hasProvisioningProfileIssue: false, + missingModule: missingModule, + ); + } } return _XCResultIssueHandlingResult(requiresProvisioningProfile: false, hasProvisioningProfileIssue: false); } // Returns `true` if at least one issue is detected. -bool _handleIssues(XCResult? xcResult, Logger logger, XcodeBuildExecution? xcodeBuildExecution) { +Future _handleIssues( + XcodeBuildResult result, + XcodeBuildExecution? xcodeBuildExecution, { + required FlutterProject project, + required SupportedPlatform platform, + required Logger logger, + required FileSystem fileSystem, +}) async { bool requiresProvisioningProfile = false; bool hasProvisioningProfileIssue = false; bool issueDetected = false; String? missingPlatform; + final List duplicateModules = []; + final List missingModules = []; + final XCResult? xcResult = result.xcResult; if (xcResult != null && xcResult.parseSuccess) { for (final XCResultIssue issue in xcResult.issues) { - final _XCResultIssueHandlingResult handlingResult = _handleXCResultIssue(issue: issue, logger: logger); + final _XCResultIssueHandlingResult handlingResult = _handleXCResultIssue( + issue: issue, + result: result, + logger: logger, + ); if (handlingResult.hasProvisioningProfileIssue) { hasProvisioningProfileIssue = true; } @@ -782,12 +862,20 @@ bool _handleIssues(XCResult? xcResult, Logger logger, XcodeBuildExecution? xcode requiresProvisioningProfile = true; } missingPlatform = handlingResult.missingPlatform; + if (handlingResult.duplicateModule != null) { + duplicateModules.add(handlingResult.duplicateModule!); + } + if (handlingResult.missingModule != null) { + missingModules.add(handlingResult.missingModule!); + } issueDetected = true; } } else if (xcResult != null) { globals.printTrace('XCResult parsing error: ${xcResult.parsingErrorMessage}'); } + final XcodeBasedProject xcodeProject = platform == SupportedPlatform.ios ? project.ios : project.macos; + if (requiresProvisioningProfile) { logger.printError(noProvisioningProfileInstruction, emphasis: true); } else if ((!issueDetected || hasProvisioningProfileIssue) && _missingDevelopmentTeam(xcodeBuildExecution)) { @@ -803,11 +891,84 @@ bool _handleIssues(XCResult? xcResult, Logger logger, XcodeBuildExecution? xcode logger.printError("Also try selecting 'Product > Build' to fix the problem."); } else if (missingPlatform != null) { logger.printError(missingPlatformInstructions(missingPlatform), emphasis: true); + } else if (duplicateModules.isNotEmpty) { + final bool usesCocoapods = xcodeProject.podfile.existsSync(); + final bool usesSwiftPackageManager = project.usesSwiftPackageManager; + if (usesCocoapods && usesSwiftPackageManager) { + logger.printError( + 'Your project uses both CocoaPods and Swift Package Manager, which can ' + 'cause the above error. It may be caused by there being both a CocoaPod ' + 'and Swift Package Manager dependency for the following module(s): ' + '${duplicateModules.join(', ')}.\n\n' + 'You can try to identify which Pod the conflicting module is from by ' + 'looking at your "ios/Podfile.lock" dependency tree and requesting the ' + 'author add Swift Package Manager compatibility. See https://stackoverflow.com/a/27955017 ' + 'to learn more about understanding Podlock dependency tree. \n\n' + 'You can also disable Swift Package Manager for the project by adding the ' + 'following in the project\'s pubspec.yaml under the "flutter" section:\n' + ' "disable-swift-package-manager: true"\n', + ); + } + } else if (missingModules.isNotEmpty) { + final bool usesCocoapods = xcodeProject.podfile.existsSync(); + final bool usesSwiftPackageManager = project.usesSwiftPackageManager; + if (usesCocoapods && !usesSwiftPackageManager) { + final List swiftPackageOnlyPlugins = []; + for (final String module in missingModules) { + if (await _isPluginSwiftPackageOnly( + platform: platform, + project: project, + pluginName: module, + fileSystem: fileSystem, + )) { + swiftPackageOnlyPlugins.add(module); + } + } + if (swiftPackageOnlyPlugins.isNotEmpty) { + logger.printError( + 'Your project uses CocoaPods as a dependency manager, but the following ' + 'plugin(s) only support Swift Package Manager: ${swiftPackageOnlyPlugins.join(', ')}.\n' + 'Try enabling Swift Package Manager with "flutter config --enable-swift-package-manager".', + ); + } + } } - return issueDetected; } +/// Returns true if a Package.swift is found for the plugin and a podspec is not. +Future _isPluginSwiftPackageOnly({ + required SupportedPlatform platform, + required FlutterProject project, + required String pluginName, + required FileSystem fileSystem, +}) async { + final List plugins = await findPlugins(project); + final Plugin? matched = plugins + .where((Plugin plugin) => + plugin.name.toLowerCase() == pluginName.toLowerCase() && + plugin.platforms[platform.name] != null) + .firstOrNull; + if (matched == null) { + return false; + } + final String? swiftPackagePath = matched.pluginSwiftPackageManifestPath( + fileSystem, + platform.name, + ); + final bool swiftPackageExists = swiftPackagePath != null && + fileSystem.file(swiftPackagePath).existsSync(); + + final String? podspecPath = matched.pluginPodspecPath( + fileSystem, + platform.name, + ); + final bool podspecExists = podspecPath != null && + fileSystem.file(podspecPath).existsSync(); + + return swiftPackageExists && !podspecExists; +} + // Return 'true' a missing development team issue is detected. bool _missingDevelopmentTeam(XcodeBuildExecution? xcodeBuildExecution) { // Make sure the user has specified one of: @@ -852,22 +1013,68 @@ String? _parseMissingPlatform(String message) { return pattern.firstMatch(message)?.group(1); } +String? _parseModuleRedefinition(String message) { + // Example: "Redefinition of module 'plugin_1_name'" + final RegExp pattern = RegExp(r"Redefinition of module '(.*?)'"); + final RegExpMatch? match = pattern.firstMatch(message); + if (match != null && match.groupCount > 0) { + final String? version = match.group(1); + return version; + } + return null; +} + +String? _parseDuplicateSymbols(String message) { + // Example: "duplicate symbol '_$s29plugin_1_name23PluginNamePluginC9setDouble3key5valueySS_SdtF' in: + // /Users/username/path/to/app/build/ios/Debug-iphonesimulator/plugin_1_name/plugin_1_name.framework/plugin_1_name[arm64][5](PluginNamePlugin.o) + final RegExp pattern = RegExp(r'duplicate symbol [\s|\S]*?\/(.*)\.o'); + final RegExpMatch? match = pattern.firstMatch(message); + if (match != null && match.groupCount > 0) { + final String? version = match.group(1); + if (version != null) { + return version.split('/').last.split('[').first.split('(').first; + } + return version; + } + return null; +} + +String? _parseMissingModule(String message) { + // Example: "Module 'plugin_1_name' not found" + final RegExp pattern = RegExp(r"Module '(.*?)' not found"); + final RegExpMatch? match = pattern.firstMatch(message); + if (match != null && match.groupCount > 0) { + final String? version = match.group(1); + return version; + } + return null; +} + // The result of [_handleXCResultIssue]. class _XCResultIssueHandlingResult { - _XCResultIssueHandlingResult({ required this.requiresProvisioningProfile, required this.hasProvisioningProfileIssue, this.missingPlatform, + this.duplicateModule, + this.missingModule, }); - // An issue indicates that user didn't provide the provisioning profile. + /// An issue indicates that user didn't provide the provisioning profile. final bool requiresProvisioningProfile; - // An issue indicates that there is a provisioning profile issue. + /// An issue indicates that there is a provisioning profile issue. final bool hasProvisioningProfileIssue; final String? missingPlatform; + + /// An issue indicates a module is declared twice, potentially due to being + /// used in both Swift Package Manager and CocoaPods. + final String? duplicateModule; + + /// An issue indicates a module was imported but not found, potentially due + /// to it being Swift Package Manager compatible only. + final String? missingModule; } const String _kResultBundlePath = 'temporary_xcresult_bundle'; diff --git a/packages/flutter_tools/lib/src/ios/plist_parser.dart b/packages/flutter_tools/lib/src/ios/plist_parser.dart index 7f7f4cc90c7..8117a3c73a6 100644 --- a/packages/flutter_tools/lib/src/ios/plist_parser.dart +++ b/packages/flutter_tools/lib/src/ios/plist_parser.dart @@ -64,6 +64,35 @@ class PlistParser { } } + /// Returns the content, converted to JSON, of the plist file located at + /// [filePath]. + /// + /// If [filePath] points to a non-existent file or a file that's not a + /// valid property list file, this will return null. + String? plistJsonContent(String filePath) { + if (!_fileSystem.isFileSync(_plutilExecutable)) { + throw const FileNotFoundException(_plutilExecutable); + } + final List args = [ + _plutilExecutable, + '-convert', + 'json', + '-o', + '-', + filePath, + ]; + try { + final String jsonContent = _processUtils.runSync( + args, + throwOnError: true, + ).stdout.trim(); + return jsonContent; + } on ProcessException catch (error) { + _logger.printError('$error'); + return null; + } + } + /// Replaces the string key in the given plist file with the given value. /// /// If the value is null, then the key will be removed. diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index 9a1d0cf4e34..fe6b1398f57 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -571,7 +571,15 @@ class IOSSimulator extends Device { deviceID: id, ); if (!buildResult.success) { - await diagnoseXcodeBuildFailure(buildResult, globals.flutterUsage, globals.logger, globals.analytics); + await diagnoseXcodeBuildFailure( + buildResult, + analytics: globals.analytics, + fileSystem: globals.fs, + flutterUsage: globals.flutterUsage, + logger: globals.logger, + platform: SupportedPlatform.ios, + project: app.project.parent, + ); throwToolExit('Could not build the application for the simulator.'); } diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart index e4307170b1c..575b60d5c1f 100644 --- a/packages/flutter_tools/lib/src/macos/build_macos.dart +++ b/packages/flutter_tools/lib/src/macos/build_macos.dart @@ -16,6 +16,7 @@ import '../convert.dart'; import '../globals.dart' as globals; import '../ios/xcode_build_settings.dart'; import '../ios/xcodeproj.dart'; +import '../migrations/swift_package_manager_integration_migration.dart'; import '../migrations/xcode_project_object_version_migration.dart'; import '../migrations/xcode_script_build_phase_migration.dart'; import '../migrations/xcode_thin_binary_build_phase_input_paths_migration.dart'; @@ -85,6 +86,16 @@ Future buildMacOS({ XcodeThinBinaryBuildPhaseInputPathsMigration(flutterProject.macos, globals.logger), FlutterApplicationMigration(flutterProject.macos, globals.logger), NSApplicationMainDeprecationMigration(flutterProject.macos, globals.logger), + if (flutterProject.usesSwiftPackageManager && flutterProject.macos.flutterPluginSwiftPackageManifest.existsSync()) + SwiftPackageManagerIntegrationMigration( + flutterProject.macos, + SupportedPlatform.macos, + buildInfo, + xcodeProjectInterpreter: globals.xcodeProjectInterpreter!, + logger: globals.logger, + fileSystem: globals.fs, + plistParser: globals.plistParser, + ), ]; final ProjectMigration migration = ProjectMigration(migrators); @@ -101,6 +112,11 @@ Future buildMacOS({ targetOverride: targetOverride, useMacOSConfig: true, ); + + // TODO(vashworth): Call `SwiftPackageManager.updateMinimumDeployment` + // using MACOSX_DEPLOYMENT_TARGET once https://github.com/flutter/flutter/issues/146204 + // is fixed. + await processPodsIfNeeded(flutterProject.macos, getMacOSBuildDirectory(), buildInfo.mode); // If the xcfilelists do not exist, create empty version. if (!flutterProject.macos.inputFileList.existsSync()) { diff --git a/packages/flutter_tools/lib/src/macos/cocoapod_utils.dart b/packages/flutter_tools/lib/src/macos/cocoapod_utils.dart index 7b6213eb2e1..eab5413263c 100644 --- a/packages/flutter_tools/lib/src/macos/cocoapod_utils.dart +++ b/packages/flutter_tools/lib/src/macos/cocoapod_utils.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import '../base/error_handling_io.dart'; import '../base/fingerprint.dart'; import '../build_info.dart'; import '../cache.dart'; @@ -14,20 +15,57 @@ import '../project.dart'; Future processPodsIfNeeded( XcodeBasedProject xcodeProject, String buildDirectory, - BuildMode buildMode) async { + BuildMode buildMode, { + bool forceCocoaPodsOnly = false, +}) async { final FlutterProject project = xcodeProject.parent; - // Ensure that the plugin list is up to date, since hasPlugins relies on it. - await refreshPluginsList(project, macOSPlatform: project.macos.existsSync()); - if (!(hasPlugins(project) || (project.isModule && xcodeProject.podfile.existsSync()))) { + + // When using Swift Package Manager, the Podfile may not exist so if there + // isn't a Podfile, skip processing pods. + if (project.usesSwiftPackageManager && !xcodeProject.podfile.existsSync() && !forceCocoaPodsOnly) { return; } - // If the Xcode project, Podfile, or generated xcconfig have changed since - // last run, pods should be updated. + // Ensure that the plugin list is up to date, since hasPlugins relies on it. + await refreshPluginsList( + project, + iosPlatform: project.ios.existsSync(), + macOSPlatform: project.macos.existsSync(), + forceCocoaPodsOnly: forceCocoaPodsOnly, + ); + + // If there are no plugins and if the project is a not module with an existing + // podfile, skip processing pods + if (!hasPlugins(project) && !(project.isModule && xcodeProject.podfile.existsSync())) { + return; + } + + // If forcing the use of only CocoaPods, but the project is using Swift + // Package Manager, print a warning that CocoaPods will be used. + if (forceCocoaPodsOnly && project.usesSwiftPackageManager) { + globals.logger.printWarning( + 'Swift Package Manager does not yet support this command. ' + 'CocoaPods will be used instead.'); + + // If CocoaPods has been deintegrated, add it back. + if (!xcodeProject.podfile.existsSync()) { + await globals.cocoaPods?.setupPodfile(xcodeProject); + } + + // Delete Swift Package Manager manifest to invalidate fingerprinter + ErrorHandlingFileSystem.deleteIfExists( + xcodeProject.flutterPluginSwiftPackageManifest, + ); + } + + // If the Xcode project, Podfile, generated plugin Swift Package, or podhelper + // have changed since last run, pods should be updated. final Fingerprinter fingerprinter = Fingerprinter( fingerprintPath: globals.fs.path.join(buildDirectory, 'pod_inputs.fingerprint'), paths: [ xcodeProject.xcodeProjectInfoFile.path, xcodeProject.podfile.path, + if (xcodeProject.flutterPluginSwiftPackageManifest.existsSync()) + xcodeProject.flutterPluginSwiftPackageManifest.path, globals.fs.path.join( Cache.flutterRoot!, 'packages', diff --git a/packages/flutter_tools/lib/src/macos/cocoapods.dart b/packages/flutter_tools/lib/src/macos/cocoapods.dart index 4098fda34e0..e75bfe44837 100644 --- a/packages/flutter_tools/lib/src/macos/cocoapods.dart +++ b/packages/flutter_tools/lib/src/macos/cocoapods.dart @@ -21,8 +21,8 @@ import '../cache.dart'; import '../ios/xcodeproj.dart'; import '../migrations/cocoapods_script_symlink.dart'; import '../migrations/cocoapods_toolchain_directory_migration.dart'; +import '../project.dart'; import '../reporting/reporting.dart'; -import '../xcode_project.dart'; const String noCocoaPodsConsequence = ''' CocoaPods is a package manager for iOS or macOS platform code. @@ -166,6 +166,10 @@ class CocoaPods { bool dependenciesChanged = true, }) async { if (!xcodeProject.podfile.existsSync()) { + // Swift Package Manager doesn't need Podfile, so don't error. + if (xcodeProject.parent.usesSwiftPackageManager) { + return false; + } throwToolExit('Podfile missing'); } _warnIfPodfileOutOfDate(xcodeProject); @@ -258,6 +262,18 @@ class CocoaPods { addPodsDependencyToFlutterXcconfig(xcodeProject); return; } + final File podfileTemplate = await getPodfileTemplate( + xcodeProject, + runnerProject, + ); + podfileTemplate.copySync(podfile.path); + addPodsDependencyToFlutterXcconfig(xcodeProject); + } + + Future getPodfileTemplate( + XcodeBasedProject xcodeProject, + Directory runnerProject, + ) async { String podfileTemplateName; if (xcodeProject is MacOSProject) { podfileTemplateName = 'Podfile-macos'; @@ -268,7 +284,7 @@ class CocoaPods { )).containsKey('SWIFT_VERSION'); podfileTemplateName = isSwift ? 'Podfile-ios-swift' : 'Podfile-ios-objc'; } - final File podfileTemplate = _fileSystem.file(_fileSystem.path.join( + return _fileSystem.file(_fileSystem.path.join( Cache.flutterRoot!, 'packages', 'flutter_tools', @@ -276,8 +292,6 @@ class CocoaPods { 'cocoapods', podfileTemplateName, )); - podfileTemplate.copySync(podfile.path); - addPodsDependencyToFlutterXcconfig(xcodeProject); } /// Ensures all `Flutter/Xxx.xcconfig` files for the given Xcode-based @@ -287,12 +301,24 @@ class CocoaPods { _addPodsDependencyToFlutterXcconfig(xcodeProject, 'Release'); } + String includePodsXcconfig(String mode) { + return 'Pods/Target Support Files/Pods-Runner/Pods-Runner.${mode + .toLowerCase()}.xcconfig'; + } + + bool xcconfigIncludesPods(File xcodeConfig) { + if (xcodeConfig.existsSync()) { + final String content = xcodeConfig.readAsStringSync(); + return content.contains('Pods/Target Support Files/Pods-'); + } + return false; + } + void _addPodsDependencyToFlutterXcconfig(XcodeBasedProject xcodeProject, String mode) { final File file = xcodeProject.xcodeConfigFor(mode); if (file.existsSync()) { final String content = file.readAsStringSync(); - final String includeFile = 'Pods/Target Support Files/Pods-Runner/Pods-Runner.${mode - .toLowerCase()}.xcconfig'; + final String includeFile = includePodsXcconfig(mode); final String include = '#include? "$includeFile"'; if (!content.contains('Pods/Target Support Files/Pods-')) { file.writeAsStringSync('$include\n$content', flush: true); diff --git a/packages/flutter_tools/lib/src/macos/darwin_dependency_management.dart b/packages/flutter_tools/lib/src/macos/darwin_dependency_management.dart new file mode 100644 index 00000000000..af1cebe54ed --- /dev/null +++ b/packages/flutter_tools/lib/src/macos/darwin_dependency_management.dart @@ -0,0 +1,257 @@ +// 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 '../base/common.dart'; +import '../base/file_system.dart'; +import '../base/logger.dart'; +import '../plugins.dart'; +import '../project.dart'; +import 'cocoapods.dart'; +import 'swift_package_manager.dart'; + +/// Flutter has two dependency management solutions for iOS and macOS +/// applications: CocoaPods and Swift Package Manager. They may be used +/// individually or together. This class handles setting up required files and +/// project settings for the dependency manager(s) being used. +class DarwinDependencyManagement { + DarwinDependencyManagement({ + required FlutterProject project, + required List plugins, + required CocoaPods cocoapods, + required SwiftPackageManager swiftPackageManager, + required FileSystem fileSystem, + required Logger logger, + }) : _project = project, + _plugins = plugins, + _cocoapods = cocoapods, + _swiftPackageManager = swiftPackageManager, + _fileSystem = fileSystem, + _logger = logger; + + final FlutterProject _project; + final List _plugins; + final CocoaPods _cocoapods; + final SwiftPackageManager _swiftPackageManager; + final FileSystem _fileSystem; + final Logger _logger; + + /// Generates/updates required files and project settings for Darwin + /// Dependency Managers (CocoaPods and Swift Package Manager). Projects may + /// use only CocoaPods (if no SPM compatible dependencies or SPM has been + /// disabled), only Swift Package Manager (if no CocoaPod dependencies), or + /// both. This only generates files for the manager(s) being used. + /// + /// CocoaPods requires a Podfile and certain values in the Flutter xcconfig + /// files. + /// + /// Swift Package Manager requires a generated Package.swift and certain + /// settings in the Xcode project's project.pbxproj and xcscheme (done later + /// before build). + Future setUp({ + required SupportedPlatform platform, + }) async { + if (platform != SupportedPlatform.ios && + platform != SupportedPlatform.macos) { + throwToolExit( + 'The platform ${platform.name} is incompatible with Darwin Dependency Managers. Only iOS and macOS are allowed.', + ); + } + final XcodeBasedProject xcodeProject = platform == SupportedPlatform.ios + ? _project.ios + : _project.macos; + if (_project.usesSwiftPackageManager) { + await _swiftPackageManager.generatePluginsSwiftPackage( + _plugins, + platform, + xcodeProject, + ); + } else if (xcodeProject.flutterPluginSwiftPackageInProjectSettings) { + // If Swift Package Manager is not enabled but the project is already + // integrated for Swift Package Manager, pass no plugins to the generator. + // This will still generate the required Package.swift, but it will have + // no dependencies. + await _swiftPackageManager.generatePluginsSwiftPackage( + [], + platform, + xcodeProject, + ); + } + + // Skip updating Podfile if project is a module, since it will use a + // different module-specific Podfile. + if (_project.isModule) { + return; + } + final (:int totalCount, :int swiftPackageCount, :int podCount) = await _evaluatePluginsAndPrintWarnings( + platform: platform, + xcodeProject: xcodeProject, + ); + + final bool useCocoapods; + if (_project.usesSwiftPackageManager) { + useCocoapods = _usingCocoaPodsPlugin( + pluginCount: totalCount, + swiftPackageCount: swiftPackageCount, + cocoapodCount: podCount, + ); + } else { + // When Swift Package Manager is not enabled, set up Podfile if plugins + // is not empty, regardless of if plugins are CocoaPod compatible. This + // is done because `processPodsIfNeeded` uses `hasPlugins` to determine + // whether to run. + useCocoapods = _plugins.isNotEmpty; + } + if (useCocoapods) { + await _cocoapods.setupPodfile(xcodeProject); + } + /// The user may have a custom maintained Podfile that they're running `pod install` + /// on themselves. + else if (xcodeProject.podfile.existsSync() && xcodeProject.podfileLock.existsSync()) { + _cocoapods.addPodsDependencyToFlutterXcconfig(xcodeProject); + } + } + + bool _usingCocoaPodsPlugin({ + required int pluginCount, + required int swiftPackageCount, + required int cocoapodCount, + }) { + if (_project.usesSwiftPackageManager) { + if (pluginCount == swiftPackageCount) { + return false; + } + } + return cocoapodCount > 0; + } + + /// Returns count of total number of plugins, number of Swift Package Manager + /// compatible plugins, and number of CocoaPods compatible plugins. A plugin + /// can be both Swift Package Manager and CocoaPods compatible. + /// + /// Prints warnings when using a plugin incompatible with the available Darwin + /// Dependency Manager (Swift Package Manager or CocoaPods). + /// + /// Prints message prompting the user to deintegrate CocoaPods if using all + /// Swift Package plugins. + Future<({int totalCount, int swiftPackageCount, int podCount})> _evaluatePluginsAndPrintWarnings({ + required SupportedPlatform platform, + required XcodeBasedProject xcodeProject, + }) async { + int pluginCount = 0; + int swiftPackageCount = 0; + int cocoapodCount = 0; + for (final Plugin plugin in _plugins) { + if (plugin.platforms[platform.name] == null) { + continue; + } + final String? swiftPackagePath = plugin.pluginSwiftPackageManifestPath( + _fileSystem, + platform.name, + ); + final bool swiftPackageManagerCompatible = swiftPackagePath != null && + _fileSystem.file(swiftPackagePath).existsSync(); + + final String? podspecPath = plugin.pluginPodspecPath( + _fileSystem, + platform.name, + ); + final bool cocoaPodsCompatible = + podspecPath != null && _fileSystem.file(podspecPath).existsSync(); + + // If a plugin is missing both a Package.swift and Podspec, it won't be + // included by either Swift Package Manager or Cocoapods. This can happen + // when a plugin doesn't have native platform code. + // For example, image_picker_macos only uses dart code. + if (!swiftPackageManagerCompatible && !cocoaPodsCompatible) { + continue; + } + + pluginCount += 1; + if (swiftPackageManagerCompatible) { + swiftPackageCount += 1; + } + if (cocoaPodsCompatible) { + cocoapodCount += 1; + } + + // If not using Swift Package Manager and plugin does not have podspec + // but does have a Package.swift, throw an error. Otherwise, it'll error + // when it builds. + if (!_project.usesSwiftPackageManager && + !cocoaPodsCompatible && + swiftPackageManagerCompatible) { + throwToolExit( + 'Plugin ${plugin.name} is only Swift Package Manager compatible. Try ' + 'enabling Swift Package Manager by running ' + '"flutter config --enable-swift-package-manager" or remove the ' + 'plugin as a dependency.'); + } + } + + // Only show warnings to remove CocoaPods if the project is using Swift + // Package Manager, has already been migrated to have SPM integration, and + // all plugins are Swift Packages. + if (_project.usesSwiftPackageManager && + xcodeProject.flutterPluginSwiftPackageInProjectSettings && + pluginCount == swiftPackageCount && + swiftPackageCount != 0) { + final bool podfileExists = xcodeProject.podfile.existsSync(); + if (podfileExists) { + // If all plugins are Swift Packages and the Podfile matches the + // default template, recommend pod deintegration. + final File podfileTemplate = await _cocoapods.getPodfileTemplate( + xcodeProject, + xcodeProject.xcodeProject, + ); + + final String configWarning = '${_podIncludeInConfigWarning(xcodeProject, 'Debug')}' + '${_podIncludeInConfigWarning(xcodeProject, 'Release')}'; + + if (xcodeProject.podfile.readAsStringSync() == + podfileTemplate.readAsStringSync()) { + _logger.printWarning( + 'All plugins found for ${platform.name} are Swift Packages, but your ' + 'project still has CocoaPods integration. To remove CocoaPods ' + 'integration, complete the following steps:\n' + ' * In the ${platform.name}/ directory run "pod deintegrate"\n' + ' * Also in the ${platform.name}/ directory, delete the Podfile\n' + '$configWarning\n' + "Removing CocoaPods integration will improve the project's build time."); + } else { + // If all plugins are Swift Packages, but the Podfile has custom logic, + // recommend migrating manually. + _logger.printWarning( + 'All plugins found for ${platform.name} are Swift Packages, but your ' + 'project still has CocoaPods integration. Your project uses a ' + 'non-standard Podfile and will need to be migrated to Swift Package ' + 'Manager manually. Some steps you may need to complete include:\n' + ' * In the ${platform.name}/ directory run "pod deintegrate"\n' + ' * Transition any Pod dependencies to Swift Package equivalents. ' + 'See https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app\n' + ' * Transition any custom logic\n' + '$configWarning\n' + "Removing CocoaPods integration will improve the project's build time."); + } + } + } + + return ( + totalCount: pluginCount, + swiftPackageCount: swiftPackageCount, + podCount: cocoapodCount, + ); + } + + String _podIncludeInConfigWarning(XcodeBasedProject xcodeProject, String mode) { + final File xcconfigFile = xcodeProject.xcodeConfigFor(mode); + final bool configIncludesPods = _cocoapods.xcconfigIncludesPods(xcconfigFile); + if (configIncludesPods) { + return ' * Remove the include to ' + '"${_cocoapods.includePodsXcconfig(mode)}" in your ' + '${xcconfigFile.parent.parent.basename}/${xcconfigFile.parent.basename}/${xcconfigFile.basename}\n'; + } + + return ''; + } +} diff --git a/packages/flutter_tools/lib/src/macos/swift_package_manager.dart b/packages/flutter_tools/lib/src/macos/swift_package_manager.dart new file mode 100644 index 00000000000..b5575c11bbb --- /dev/null +++ b/packages/flutter_tools/lib/src/macos/swift_package_manager.dart @@ -0,0 +1,196 @@ +// 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 '../base/common.dart'; +import '../base/file_system.dart'; +import '../base/template.dart'; +import '../base/version.dart'; +import '../plugins.dart'; +import '../project.dart'; +import 'swift_packages.dart'; + +/// Swift Package Manager is a dependency management solution for iOS and macOS +/// applications. +/// +/// See also: +/// * https://www.swift.org/documentation/package-manager/ - documentation on +/// Swift Package Manager. +/// * https://developer.apple.com/documentation/packagedescription/package - +/// documentation on Swift Package Manager manifest file, Package.swift. +class SwiftPackageManager { + const SwiftPackageManager({ + required FileSystem fileSystem, + required TemplateRenderer templateRenderer, + }) : _fileSystem = fileSystem, + _templateRenderer = templateRenderer; + + final FileSystem _fileSystem; + final TemplateRenderer _templateRenderer; + + static const String _defaultFlutterPluginsSwiftPackageName = 'FlutterGeneratedPluginSwiftPackage'; + + static final SwiftPackageSupportedPlatform _iosSwiftPackageSupportedPlatform = SwiftPackageSupportedPlatform( + platform: SwiftPackagePlatform.ios, + version: Version(12, 0, null), + ); + + static final SwiftPackageSupportedPlatform _macosSwiftPackageSupportedPlatform = SwiftPackageSupportedPlatform( + platform: SwiftPackagePlatform.macos, + version: Version(10, 14, null), + ); + + /// Creates a Swift Package called 'FlutterGeneratedPluginSwiftPackage' that + /// has dependencies on Flutter plugins that are compatible with Swift + /// Package Manager. + Future generatePluginsSwiftPackage( + List plugins, + SupportedPlatform platform, + XcodeBasedProject project, + ) async { + _validatePlatform(platform); + + final ( + List packageDependencies, + List targetDependencies + ) = _dependenciesForPlugins(plugins, platform); + + // If there aren't any Swift Package plugins and the project hasn't been + // migrated yet, don't generate a Swift package or migrate the app since + // it's not needed. If the project has already been migrated, regenerate + // the Package.swift even if there are no dependencies in case there + // were dependencies previously. + if (packageDependencies.isEmpty && !project.flutterPluginSwiftPackageInProjectSettings) { + return; + } + + final SwiftPackageSupportedPlatform swiftSupportedPlatform; + if (platform == SupportedPlatform.ios) { + swiftSupportedPlatform = _iosSwiftPackageSupportedPlatform; + } else { + swiftSupportedPlatform = _macosSwiftPackageSupportedPlatform; + } + + // FlutterGeneratedPluginSwiftPackage must be statically linked to ensure + // any dynamic dependencies are linked to Runner and prevent undefined symbols. + final SwiftPackageProduct generatedProduct = SwiftPackageProduct( + name: _defaultFlutterPluginsSwiftPackageName, + targets: [_defaultFlutterPluginsSwiftPackageName], + libraryType: SwiftPackageLibraryType.static, + ); + + final SwiftPackageTarget generatedTarget = SwiftPackageTarget.defaultTarget( + name: _defaultFlutterPluginsSwiftPackageName, + dependencies: targetDependencies, + ); + + final SwiftPackage pluginsPackage = SwiftPackage( + manifest: project.flutterPluginSwiftPackageManifest, + name: _defaultFlutterPluginsSwiftPackageName, + platforms: [swiftSupportedPlatform], + products: [generatedProduct], + dependencies: packageDependencies, + targets: [generatedTarget], + templateRenderer: _templateRenderer, + ); + pluginsPackage.createSwiftPackage(); + } + + (List, List) _dependenciesForPlugins( + List plugins, + SupportedPlatform platform, + ) { + final List packageDependencies = + []; + final List targetDependencies = + []; + + for (final Plugin plugin in plugins) { + final String? pluginSwiftPackageManifestPath = plugin.pluginSwiftPackageManifestPath( + _fileSystem, + platform.name, + ); + if (plugin.platforms[platform.name] == null || + pluginSwiftPackageManifestPath == null || + !_fileSystem.file(pluginSwiftPackageManifestPath).existsSync()) { + continue; + } + + packageDependencies.add(SwiftPackagePackageDependency( + name: plugin.name, + path: _fileSystem.file(pluginSwiftPackageManifestPath).parent.path, + )); + + // The target dependency product name is hyphen separated because it's + // the dependency's library name, which Swift Package Manager will + // automatically use as the CFBundleIdentifier if linked dynamically. The + // CFBundleIdentifier cannot contain underscores. + targetDependencies.add(SwiftPackageTargetDependency.product( + name: plugin.name.replaceAll('_', '-'), + packageName: plugin.name, + )); + } + return (packageDependencies, targetDependencies); + } + + /// Validates the platform is either iOS or macOS, otherwise throw an error. + static void _validatePlatform(SupportedPlatform platform) { + if (platform != SupportedPlatform.ios && + platform != SupportedPlatform.macos) { + throwToolExit( + 'The platform ${platform.name} is not compatible with Swift Package Manager. ' + 'Only iOS and macOS are allowed.', + ); + } + } + + /// If the project's IPHONEOS_DEPLOYMENT_TARGET/MACOSX_DEPLOYMENT_TARGET is + /// higher than the FlutterGeneratedPluginSwiftPackage's default + /// SupportedPlatform, increase the SupportedPlatform to match the project's + /// deployment target. + /// + /// This is done for the use case of a plugin requiring a higher iOS/macOS + /// version than FlutterGeneratedPluginSwiftPackage. + /// + /// Swift Package Manager emits an error if a dependency isn’t compatible + /// with the top-level package’s deployment version. The deployment target of + /// a package’s dependencies must be lower than or equal to the top-level + /// package’s deployment target version for a particular platform. + /// + /// To still be able to use the plugin, the user can increase the Xcode + /// project's iOS/macOS deployment target and this will then increase the + /// deployment target for FlutterGeneratedPluginSwiftPackage. + static void updateMinimumDeployment({ + required XcodeBasedProject project, + required SupportedPlatform platform, + required String deploymentTarget, + }) { + final Version? projectDeploymentTargetVersion = Version.parse(deploymentTarget); + final SwiftPackageSupportedPlatform defaultPlatform; + final SwiftPackagePlatform packagePlatform; + if (platform == SupportedPlatform.ios) { + defaultPlatform = _iosSwiftPackageSupportedPlatform; + packagePlatform = SwiftPackagePlatform.ios; + } else { + defaultPlatform = _macosSwiftPackageSupportedPlatform; + packagePlatform = SwiftPackagePlatform.macos; + } + + if (projectDeploymentTargetVersion == null || + projectDeploymentTargetVersion <= defaultPlatform.version || + !project.flutterPluginSwiftPackageManifest.existsSync()) { + return; + } + + final String manifestContents = project.flutterPluginSwiftPackageManifest.readAsStringSync(); + final String oldSupportedPlatform = defaultPlatform.format(); + final String newSupportedPlatform = SwiftPackageSupportedPlatform( + platform: packagePlatform, + version: projectDeploymentTargetVersion, + ).format(); + + project.flutterPluginSwiftPackageManifest.writeAsStringSync( + manifestContents.replaceFirst(oldSupportedPlatform, newSupportedPlatform), + ); + } +} diff --git a/packages/flutter_tools/lib/src/macos/swift_packages.dart b/packages/flutter_tools/lib/src/macos/swift_packages.dart new file mode 100644 index 00000000000..501fc9ebf93 --- /dev/null +++ b/packages/flutter_tools/lib/src/macos/swift_packages.dart @@ -0,0 +1,403 @@ +// 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 '../base/file_system.dart'; +import '../base/template.dart'; +import '../base/version.dart'; + +/// Swift toolchain version included with Xcode 15.0. +const String minimumSwiftToolchainVersion = '5.9'; + +const String _swiftPackageTemplate = ''' +// swift-tools-version: {{swiftToolsVersion}} +// The swift-tools-version declares the minimum version of Swift required to build this package. +// +// Generated file. Do not edit. +// + +import PackageDescription + +let package = Package( + name: "{{packageName}}", + {{#platforms}} + platforms: [ + {{platforms}} + ], + {{/platforms}} + products: [ + {{products}} + ], + dependencies: [ + {{dependencies}} + ], + targets: [ + {{targets}} + ] +) +'''; + +const String _swiftPackageSourceTemplate = ''' +// +// Generated file. Do not edit. +// +'''; + +const String _singleIndent = ' '; +const String _doubleIndent = '$_singleIndent$_singleIndent'; + +/// A Swift Package is reusable code that can be shared across projects and +/// with other developers in iOS and macOS applications. A Swift Package +/// requires a Package.swift. This class handles the formatting and creation of +/// a Package.swift. +/// +/// See https://developer.apple.com/documentation/packagedescription/package +/// for more information about Swift Packages and Package.swift. +class SwiftPackage { + SwiftPackage({ + required File manifest, + required String name, + required List platforms, + required List products, + required List dependencies, + required List targets, + required TemplateRenderer templateRenderer, + }) : _manifest = manifest, + _name = name, + _platforms = platforms, + _products = products, + _dependencies = dependencies, + _targets = targets, + _templateRenderer = templateRenderer; + + /// [File] for Package.swift. + final File _manifest; + + /// The name of the Swift package. + final String _name; + + /// The list of minimum versions for platforms supported by the package. + final List _platforms; + + /// The list of products that this package vends and that clients can use. + final List _products; + + /// The list of package dependencies. + final List _dependencies; + + /// The list of targets that are part of this package. + final List _targets; + + final TemplateRenderer _templateRenderer; + + /// Context for the [_swiftPackageTemplate] template. + Map get _templateContext { + return { + 'swiftToolsVersion': minimumSwiftToolchainVersion, + 'packageName': _name, + // Supported platforms can't be empty, so only include if not null. + 'platforms': _formatPlatforms() ?? false, + 'products': _formatProducts(), + 'dependencies': _formatDependencies(), + 'targets': _formatTargets(), + }; + } + + /// Create a Package.swift using settings from [_templateContext]. + void createSwiftPackage() { + // Swift Packages require at least one source file per non-binary target, + // whether it be in Swift or Objective C. If the target does not have any + // files yet, create an empty Swift file. + for (final SwiftPackageTarget target in _targets) { + if (target.targetType == SwiftPackageTargetType.binaryTarget) { + continue; + } + final Directory targetDirectory = _manifest.parent + .childDirectory('Sources') + .childDirectory(target.name); + if (!targetDirectory.existsSync() || targetDirectory.listSync().isEmpty) { + final File requiredSwiftFile = targetDirectory.childFile( + '${target.name}.swift', + ); + requiredSwiftFile.createSync(recursive: true); + requiredSwiftFile.writeAsStringSync(_swiftPackageSourceTemplate); + } + } + + final String renderedTemplate = _templateRenderer.renderString( + _swiftPackageTemplate, + _templateContext, + ); + _manifest.createSync(recursive: true); + _manifest.writeAsStringSync(renderedTemplate); + } + + String? _formatPlatforms() { + if (_platforms.isEmpty) { + return null; + } + final List platformStrings = _platforms + .map((SwiftPackageSupportedPlatform platform) => platform.format()) + .toList(); + return platformStrings.join(',\n$_doubleIndent'); + } + + String _formatProducts() { + if (_products.isEmpty) { + return ''; + } + final List libraries = _products + .map((SwiftPackageProduct product) => product.format()) + .toList(); + return libraries.join(',\n$_doubleIndent'); + } + + String _formatDependencies() { + if (_dependencies.isEmpty) { + return ''; + } + final List packages = _dependencies + .map((SwiftPackagePackageDependency dependency) => dependency.format()) + .toList(); + return packages.join(',\n$_doubleIndent'); + } + + String _formatTargets() { + if (_targets.isEmpty) { + return ''; + } + final List targetList = + _targets.map((SwiftPackageTarget target) => target.format()).toList(); + return targetList.join(',\n$_doubleIndent'); + } +} + +enum SwiftPackagePlatform { + ios(name: '.iOS'), + macos(name: '.macOS'), + tvos(name: '.tvOS'), + watchos(name: '.watchOS'); + + const SwiftPackagePlatform({required this.name}); + + final String name; +} + +/// A platform that the Swift package supports. +/// +/// Representation of SupportedPlatform from +/// https://developer.apple.com/documentation/packagedescription/supportedplatform. +class SwiftPackageSupportedPlatform { + SwiftPackageSupportedPlatform({ + required this.platform, + required this.version, + }); + + final SwiftPackagePlatform platform; + final Version version; + + String format() { + // platforms: [ + // .macOS("10.14"), + // .iOS("12.0"), + // ], + return '${platform.name}("$version")'; + } +} + +/// Types of library linking. +/// +/// Representation of Product.Library.LibraryType from +/// https://developer.apple.com/documentation/packagedescription/product/library/librarytype. +enum SwiftPackageLibraryType { + dynamic(name: '.dynamic'), + static(name: '.static'); + + const SwiftPackageLibraryType({required this.name}); + + final String name; +} + +/// An externally visible build artifact that's available to clients of the +/// package. +/// +/// Representation of Product from +/// https://developer.apple.com/documentation/packagedescription/product. +class SwiftPackageProduct { + SwiftPackageProduct({ + required this.name, + required this.targets, + this.libraryType, + }); + + final String name; + final SwiftPackageLibraryType? libraryType; + final List targets; + + String format() { + // products: [ + // .library(name: "FlutterGeneratedPluginSwiftPackage", targets: ["FlutterGeneratedPluginSwiftPackage"]), + // .library(name: "FlutterDependenciesPackage", type: .dynamic, targets: ["FlutterDependenciesPackage"]), + // ], + String targetsString = ''; + if (targets.isNotEmpty) { + final List quotedTargets = + targets.map((String target) => '"$target"').toList(); + targetsString = ', targets: [${quotedTargets.join(', ')}]'; + } + String libraryTypeString = ''; + if (libraryType != null) { + libraryTypeString = ', type: ${libraryType!.name}'; + } + return '.library(name: "$name"$libraryTypeString$targetsString)'; + } +} + +/// A package dependency of a Swift package. +/// +/// Representation of Package.Dependency from +/// https://developer.apple.com/documentation/packagedescription/package/dependency. +class SwiftPackagePackageDependency { + SwiftPackagePackageDependency({ + required this.name, + required this.path, + }); + + final String name; + final String path; + + String format() { + // dependencies: [ + // .package(name: "image_picker_ios", path: "/path/to/packages/image_picker/image_picker_ios/ios/image_picker_ios"), + // ], + return '.package(name: "$name", path: "$path")'; + } +} + +/// Type of Target constructor. +/// +/// See https://developer.apple.com/documentation/packagedescription/target for +/// more information. +enum SwiftPackageTargetType { + target(name: '.target'), + binaryTarget(name: '.binaryTarget'); + + const SwiftPackageTargetType({required this.name}); + + final String name; +} + +/// A building block of a Swift Package that contains a set of source files +/// that Swift Package Manager compiles into a module. +/// +/// Representation of Target from +/// https://developer.apple.com/documentation/packagedescription/target. +class SwiftPackageTarget { + SwiftPackageTarget.defaultTarget({ + required this.name, + this.dependencies, + }) : path = null, + targetType = SwiftPackageTargetType.target; + + SwiftPackageTarget.binaryTarget({ + required this.name, + required String relativePath, + }) : path = relativePath, + dependencies = null, + targetType = SwiftPackageTargetType.binaryTarget; + + final String name; + final String? path; + final List? dependencies; + final SwiftPackageTargetType targetType; + + String format() { + // targets: [ + // .binaryTarget( + // name: "Flutter", + // path: "Flutter.xcframework" + // ), + // .target( + // name: "FlutterGeneratedPluginSwiftPackage", + // dependencies: [ + // .target(name: "Flutter"), + // .product(name: "image_picker_ios", package: "image_picker_ios") + // ] + // ), + // ] + const String targetIndent = _doubleIndent; + const String targetDetailsIndent = '$_doubleIndent$_singleIndent'; + + final List targetDetails = []; + + final String nameString = 'name: "$name"'; + targetDetails.add(nameString); + + if (path != null) { + final String pathString = 'path: "$path"'; + targetDetails.add(pathString); + } + + if (dependencies != null && dependencies!.isNotEmpty) { + final List targetDependencies = dependencies! + .map((SwiftPackageTargetDependency dependency) => dependency.format()) + .toList(); + final String dependenciesString = ''' +dependencies: [ +${targetDependencies.join(",\n")} +$targetDetailsIndent]'''; + targetDetails.add(dependenciesString); + } + + return ''' +${targetType.name}( +$targetDetailsIndent${targetDetails.join(",\n$targetDetailsIndent")} +$targetIndent)'''; + } +} + +/// Type of Target.Dependency constructor. +/// +/// See https://developer.apple.com/documentation/packagedescription/target/dependency +/// for more information. +enum SwiftPackageTargetDependencyType { + product(name: '.product'), + target(name: '.target'); + + const SwiftPackageTargetDependencyType({required this.name}); + + final String name; +} + +/// A dependency for the Target on a product from a package dependency or from +/// another Target in the same package. +/// +/// Representation of Target.Dependency from +/// https://developer.apple.com/documentation/packagedescription/target/dependency. +class SwiftPackageTargetDependency { + SwiftPackageTargetDependency.product({ + required this.name, + required String packageName, + }) : package = packageName, + dependencyType = SwiftPackageTargetDependencyType.product; + + SwiftPackageTargetDependency.target({ + required this.name, + }) : package = null, + dependencyType = SwiftPackageTargetDependencyType.target; + + final String name; + final String? package; + final SwiftPackageTargetDependencyType dependencyType; + + String format() { + // dependencies: [ + // .target(name: "Flutter"), + // .product(name: "image_picker_ios", package: "image_picker_ios") + // ] + if (dependencyType == SwiftPackageTargetDependencyType.product) { + return '$_doubleIndent$_doubleIndent${dependencyType.name}(name: "$name", package: "$package")'; + } + return '$_doubleIndent$_doubleIndent${dependencyType.name}(name: "$name")'; + } +} diff --git a/packages/flutter_tools/lib/src/migrations/swift_package_manager_integration_migration.dart b/packages/flutter_tools/lib/src/migrations/swift_package_manager_integration_migration.dart new file mode 100644 index 00000000000..2886f2128cc --- /dev/null +++ b/packages/flutter_tools/lib/src/migrations/swift_package_manager_integration_migration.dart @@ -0,0 +1,1048 @@ +// 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/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 '../ios/plist_parser.dart'; +import '../ios/xcodeproj.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, + SupportedPlatform platform, + BuildInfo buildInfo, { + required XcodeProjectInterpreter xcodeProjectInterpreter, + required Logger logger, + required FileSystem fileSystem, + required PlistParser plistParser, + }) : _xcodeProject = project, + _platform = platform, + _buildInfo = buildInfo, + _xcodeProjectInfoFile = project.xcodeProjectInfoFile, + _xcodeProjectInterpreter = xcodeProjectInterpreter, + _fileSystem = fileSystem, + _plistParser = plistParser, + super(logger); + + final XcodeBasedProject _xcodeProject; + final SupportedPlatform _platform; + final BuildInfo _buildInfo; + final XcodeProjectInterpreter _xcodeProjectInterpreter; + final FileSystem _fileSystem; + final File _xcodeProjectInfoFile; + final PlistParser _plistParser; + + /// New identifer for FlutterGeneratedPluginSwiftPackage PBXBuildFile. + static const String _flutterPluginsSwiftPackageBuildFileIdentifier = '78A318202AECB46A00862997'; + + /// New identifer for FlutterGeneratedPluginSwiftPackage XCLocalSwiftPackageReference. + static const String _localFlutterPluginsSwiftPackageReferenceIdentifer = '781AD8BC2B33823900A9FFBB'; + + /// New identifer for FlutterGeneratedPluginSwiftPackage XCSwiftPackageProductDependency. + static const String _flutterPluginsSwiftPackageProductDependencyIdentifer = '78A3181F2AECB46A00862997'; + + /// Existing iOS identifer for Runner PBXFrameworksBuildPhase. + static const String _iosRunnerFrameworksBuildPhaseIdentifer = '97C146EB1CF9000F007C117D'; + + /// Existing macOS identifer for Runner PBXFrameworksBuildPhase. + static const String _macosRunnerFrameworksBuildPhaseIdentifer = '33CC10EA2044A3C60003C045'; + + /// Existing iOS identifer for Runner PBXNativeTarget. + static const String _iosRunnerNativeTargetIdentifer = '97C146ED1CF9000F007C117D'; + + /// Existing macOS identifer for Runner PBXNativeTarget. + static const String _macosRunnerNativeTargetIdentifer = '33CC10EC2044A3C60003C045'; + + /// Existing iOS identifer for Runner PBXProject. + static const String _iosProjectIdentifier = '97C146E61CF9000F007C117D'; + + /// Existing macOS identifer for Runner PBXProject. + static const String _macosProjectIdentifier = '33CC10E52044A3C60003C045'; + + File get backupProjectSettings => _fileSystem + .directory(_xcodeProjectInfoFile.parent) + .childFile('project.pbxproj.backup'); + + String get _runnerFrameworksBuildPhaseIdentifer { + return _platform == SupportedPlatform.ios + ? _iosRunnerFrameworksBuildPhaseIdentifer + : _macosRunnerFrameworksBuildPhaseIdentifer; + } + + String get _runnerNativeTargetIdentifer { + return _platform == SupportedPlatform.ios + ? _iosRunnerNativeTargetIdentifer + : _macosRunnerNativeTargetIdentifer; + } + + String get _projectIdentifier { + return _platform == SupportedPlatform.ios + ? _iosProjectIdentifier + : _macosProjectIdentifier; + } + + 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 { + Status? migrationStatus; + SchemeInfo? schemeInfo; + try { + if (!_xcodeProjectInfoFile.existsSync()) { + throw Exception('Xcode project not found.'); + } + + 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; + if (isSchemeMigrated && isPbxprojMigrated) { + return; + } + + migrationStatus = logger.startProgress( + 'Adding Swift Package Manager integration...', + ); + + if (isSchemeMigrated) { + logger.printTrace('${schemeInfo.schemeFile.basename} already migrated. Skipping...'); + } else { + _migrateScheme(schemeInfo); + } + if (isPbxprojMigrated) { + logger.printTrace('${_xcodeProjectInfoFile.basename} already migrated. Skipping...'); + } else { + _migratePbxproj(); + } + + 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.'); + } + } + + // Get the project info to make sure it compiles with xcodebuild + await _xcodeProjectInterpreter.getInfo( + _xcodeProject.hostAppRoot.path, + ); + } on Exception catch (e) { + restoreFromBackup(schemeInfo); + // TODO(vashworth): Add link to instructions on how to manually integrate + // once available on website. + throwToolExit( + 'An error occured 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=1_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 avoid this failure, disable Flutter Swift Package Manager integration for the project\n' + 'by adding the following in the project\'s pubspec.yaml under the "flutter" section:\n' + ' "disable-swift-package-manager: true"\n' + 'Alternatively, disable Flutter Swift Package Manager integration globally with the\n' + 'following command:\n' + ' "flutter config --no-enable-swift-package-manager"\n'); + } 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('Run Prepare Flutter Framework Script')) { + 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 List schemeLines = LineSplitter.split(schemeContent).toList(); + final int index = schemeLines.indexWhere((String line) => + line.contains('BlueprintIdentifier = "$_runnerNativeTargetIdentifer"'), + ); + if (index == -1 || index + 3 >= schemeLines.length) { + throw Exception( + 'Failed to parse ${schemeFile.basename}: Could not find BuildableReference ' + 'for ${_xcodeProject.hostAppProjectName}.'); + } + + final String buildableName = schemeLines[index + 1].trim(); + if (!buildableName.contains('BuildableName')) { + throw Exception('Failed to parse ${schemeFile.basename}: Could not find BuildableName.'); + } + + final String blueprintName = schemeLines[index + 2].trim(); + if (!blueprintName.contains('BlueprintName')) { + throw Exception('Failed to parse ${schemeFile.basename}: Could not find BlueprintName.'); + } + + final String referencedContainer = schemeLines[index + 3].trim(); + if (!referencedContainer.contains('ReferencedContainer')) { + throw Exception('Failed to parse ${schemeFile.basename}: Could not find ReferencedContainer.'); + } + + schemeInfo.backupSchemeFile = schemeFile.parent.childFile('${schemeFile.basename}.backup'); + schemeFile.copySync(schemeInfo.backupSchemeFile!.path); + + final String scriptText; + if (_platform == SupportedPlatform.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 ">'; + } + + String newContent = ''' + + + + + + '''; + String newScheme = schemeContent; + if (schemeContent.contains('PreActions')) { + newScheme = schemeContent.replaceFirst('', '\n$newContent'); + } else { + newContent = ''' + +$newContent + +'''; + final String? buildActionEntries = schemeLines.where((String line) => line.contains('')).firstOrNull; + if (buildActionEntries == null) { + throw Exception('Failed to parse ${schemeFile.basename}: Could not find BuildActionEntries.'); + } else { + newScheme = schemeContent.replaceFirst(buildActionEntries, '$newContent$buildActionEntries'); + } + } + + 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 Object 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'); + } + } + + /// 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, + }) { + 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, + ); + return buildFilesMigrated && + frameworksBuildPhaseMigrated && + nativeTargetsMigrated && + projectObjectMigrated && + localSwiftPackageMigrated && + swiftPackageMigrated; + } + + void _migratePbxproj() { + final String originalProjectContents = + _xcodeProjectInfoFile.readAsStringSync(); + + _ensureNewIdentifiersNotUsed(originalProjectContents); + + // Parse project.pbxproj into JSON + final ParsedProjectInfo parsedInfo = _parsePbxproj(); + + List lines = LineSplitter.split(originalProjectContents).toList(); + lines = _migrateBuildFile(lines, parsedInfo); + lines = _migrateFrameworksBuildPhase(lines, parsedInfo); + lines = _migrateNativeTarget(lines, parsedInfo); + lines = _migrateProjectObject(lines, parsedInfo); + lines = _migrateLocalPackageProductDependencies(lines, parsedInfo); + lines = _migratePackageProductDependencies(lines, parsedInfo); + + final String 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)) { + throw Exception('Duplicate id found for PBXBuildFile.'); + } + if (originalProjectContents.contains(_flutterPluginsSwiftPackageProductDependencyIdentifer)) { + throw Exception('Duplicate id found for XCSwiftPackageProductDependency.'); + } + if (originalProjectContents.contains(_localFlutterPluginsSwiftPackageReferenceIdentifer)) { + throw Exception('Duplicate id found for XCLocalSwiftPackageReference.'); + } + } + + 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 String newContent = + ' $_flutterPluginsSwiftPackageBuildFileIdentifier /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = $_flutterPluginsSwiftPackageProductDependencyIdentifer /* FlutterGeneratedPluginSwiftPackage */; };'; + + final (int _, int endSectionIndex) = _sectionRange('PBXBuildFile', lines); + + lines.insert(endSectionIndex, newContent); + return lines; + } + + bool _isFrameworksBuildPhaseMigrated( + ParsedProjectInfo projectInfo, { + bool logErrorIfNotMigrated = false, + }) { + final bool migrated = projectInfo.frameworksBuildPhases + .where((ParsedProjectFrameworksBuildPhase phase) => + phase.identifier == _runnerFrameworksBuildPhaseIdentifer && + 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( + '$_runnerFrameworksBuildPhaseIdentifer /* 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 == _runnerFrameworksBuildPhaseIdentifer) + .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 String newContent = ''' + files = ( + $_flutterPluginsSwiftPackageBuildFileIdentifier /* FlutterGeneratedPluginSwiftPackage 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 String newContent = + ' $_flutterPluginsSwiftPackageBuildFileIdentifier /* FlutterGeneratedPluginSwiftPackage 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 == _runnerNativeTargetIdentifer && + target.packageProductDependencies != null && + target.packageProductDependencies! + .contains(_flutterPluginsSwiftPackageProductDependencyIdentifer)) + .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 == _runnerNativeTargetIdentifer) + .firstOrNull; + if (runnerNativeTarget == null) { + throw Exception( + 'Unable to find parsed PBXNativeTarget for ${_xcodeProject.hostAppProjectName} target.', + ); + } + final String subsectionLineStart = runnerNativeTarget.name != null + ? '$_runnerNativeTargetIdentifer /* ${runnerNativeTarget.name} */ = {' + : _runnerNativeTargetIdentifer; + 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 List newContent = [ + ' packageProductDependencies = (', + ' $_flutterPluginsSwiftPackageProductDependencyIdentifer /* FlutterGeneratedPluginSwiftPackage */,', + ' );', + ]; + 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 String newContent = + ' $_flutterPluginsSwiftPackageProductDependencyIdentifer /* FlutterGeneratedPluginSwiftPackage */,'; + lines.insert(packageProductDependenciesIndex + 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(_localFlutterPluginsSwiftPackageReferenceIdentifer)) + .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. + const List newContent = [ + ' packageReferences = (', + ' $_localFlutterPluginsSwiftPackageReferenceIdentifer /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,', + ' );', + ]; + 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 String newContent = + ' $_localFlutterPluginsSwiftPackageReferenceIdentifer /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,'; + lines.insert(packageReferencesIndex + 1, newContent); + } + return lines; + } + + bool _isLocalSwiftPackageProductDependencyMigrated( + ParsedProjectInfo projectInfo, { + bool logErrorIfNotMigrated = false, + }) { + final bool migrated = projectInfo.localSwiftPackageProductDependencies + .contains(_localFlutterPluginsSwiftPackageReferenceIdentifer); + 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 List newContent = [ + '/* Begin XCLocalSwiftPackageReference section */', + ' $_localFlutterPluginsSwiftPackageReferenceIdentifer /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {', + ' isa = XCLocalSwiftPackageReference;', + ' relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;', + ' };', + '/* 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 List newContent = [ + ' $_localFlutterPluginsSwiftPackageReferenceIdentifer /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {', + ' isa = XCLocalSwiftPackageReference;', + ' relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;', + ' };', + ]; + + lines.insertAll(endSectionIndex, newContent); + + return lines; + } + + bool _isSwiftPackageProductDependencyMigrated( + ParsedProjectInfo projectInfo, { + bool logErrorIfNotMigrated = false, + }) { + final bool migrated = projectInfo.swiftPackageProductDependencies + .contains(_flutterPluginsSwiftPackageProductDependencyIdentifer); + 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 List newContent = [ + '/* Begin XCSwiftPackageProductDependency section */', + ' $_flutterPluginsSwiftPackageProductDependencyIdentifer /* FlutterGeneratedPluginSwiftPackage */ = {', + ' isa = XCSwiftPackageProductDependency;', + ' productName = FlutterGeneratedPluginSwiftPackage;', + ' };', + '/* 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 List newContent = [ + ' $_flutterPluginsSwiftPackageProductDependencyIdentifer /* FlutterGeneratedPluginSwiftPackage */ = {', + ' isa = XCSwiftPackageProductDependency;', + ' productName = FlutterGeneratedPluginSwiftPackage;', + ' };', + ]; + + lines.insertAll(endSectionIndex, newContent); + + 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); + } +} + +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.fileReferenceIndentifiers, + required this.parsedGroups, + required this.frameworksBuildPhases, + required this.nativeTargets, + required this.projects, + required this.swiftPackageProductDependencies, + required this.localSwiftPackageProductDependencies, + }); + + factory ParsedProjectInfo.fromJson(Map data) { + final List buildFiles = []; + final List references = []; + final List groups = []; + final List buildPhases = + []; + final List native = []; + final List project = []; + final List parsedSwiftPackageProductDependencies = []; + final List parsedLocalSwiftPackageProductDependencies = []; + + if (data['objects'] is Map) { + final Map values = + data['objects']! as Map; + for (final String key in values.keys) { + if (values[key] is Map) { + final Map details = + values[key]! as Map; + if (details['isa'] is String) { + final String objectType = details['isa']! as String; + if (objectType == 'PBXBuildFile') { + buildFiles.add(key); + } else if (objectType == 'PBXFileReference') { + references.add(key); + } else if (objectType == 'PBXGroup') { + groups.add(ParsedProjectGroup.fromJson(key, details)); + } else if (objectType == 'PBXFrameworksBuildPhase') { + buildPhases.add( + ParsedProjectFrameworksBuildPhase.fromJson(key, details)); + } else if (objectType == 'PBXNativeTarget') { + native.add(ParsedNativeTarget.fromJson(key, details)); + } else if (objectType == 'PBXProject') { + project.add(ParsedProject.fromJson(key, details)); + } else if (objectType == 'XCSwiftPackageProductDependency') { + parsedSwiftPackageProductDependencies.add(key); + } else if (objectType == 'XCLocalSwiftPackageReference') { + parsedLocalSwiftPackageProductDependencies.add(key); + } + } + } + } + } + + return ParsedProjectInfo._( + buildFileIdentifiers: buildFiles, + fileReferenceIndentifiers: 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 fileReferenceIndentifiers; + + /// 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._(this.identifier, this.children, this.name); + + factory ParsedProjectGroup.fromJson(String key, Map data) { + String? name; + if (data['name'] is String) { + name = data['name']! as String; + } else if (data['path'] is String) { + name = data['path']! as String; + } + + final List parsedChildren = []; + if (data['children'] is List) { + for (final Object? item in data['children']! as List) { + if (item is String) { + parsedChildren.add(item); + } + } + return ParsedProjectGroup._(key, parsedChildren, name); + } + return ParsedProjectGroup._(key, null, name); + } + + 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._(this.identifier, this.files); + + factory ParsedProjectFrameworksBuildPhase.fromJson( + String key, Map data) { + final List parsedFiles = []; + if (data['files'] is List) { + for (final Object? item in data['files']! as List) { + if (item is String) { + parsedFiles.add(item); + } + } + return ParsedProjectFrameworksBuildPhase._(key, parsedFiles); + } + return ParsedProjectFrameworksBuildPhase._(key, null); + } + + final String identifier; + final List? files; +} + +/// Representation of data parsed from PBXNativeTarget section in Xcode project's +/// project.pbxproj. +class ParsedNativeTarget { + ParsedNativeTarget._( + this.data, + this.identifier, + this.name, + this.packageProductDependencies, + ); + + factory ParsedNativeTarget.fromJson(String key, Map data) { + String? name; + if (data['name'] is String) { + name = data['name']! as String; + } + + final List parsedChildren = []; + if (data['packageProductDependencies'] is List) { + for (final Object? item + in data['packageProductDependencies']! as List) { + if (item is String) { + parsedChildren.add(item); + } + } + return ParsedNativeTarget._(data, key, name, parsedChildren); + } + return ParsedNativeTarget._(data, key, name, 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._( + this.data, + this.identifier, + this.packageReferences, + ); + + factory ParsedProject.fromJson(String key, Map data) { + final List parsedChildren = []; + if (data['packageReferences'] is List) { + for (final Object? item in data['packageReferences']! as List) { + if (item is String) { + parsedChildren.add(item); + } + } + return ParsedProject._(data, key, parsedChildren); + } + return ParsedProject._(data, key, null); + } + + final Map data; + final String identifier; + final List? packageReferences; +} diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart index f43309a4e7c..d8e1c80f5c6 100644 --- a/packages/flutter_tools/lib/src/plugins.dart +++ b/packages/flutter_tools/lib/src/plugins.dart @@ -397,6 +397,51 @@ class Plugin { /// Whether this plugin is a direct dependency of the app. /// If [false], the plugin is a dependency of another plugin. final bool isDirectDependency; + + /// Expected path to the plugin's Package.swift. Returns null if the plugin + /// does not support the [platform] or the [platform] is not iOS or macOS. + String? pluginSwiftPackageManifestPath( + FileSystem fileSystem, + String platform, + ) { + final String? platformDirectoryName = _darwinPluginDirectoryName(platform); + if (platformDirectoryName == null) { + return null; + } + return fileSystem.path.join( + path, + platformDirectoryName, + name, + 'Package.swift', + ); + } + + /// Expected path to the plugin's podspec. Returns null if the plugin does + /// not support the [platform] or the [platform] is not iOS or macOS. + String? pluginPodspecPath(FileSystem fileSystem, String platform) { + final String? platformDirectoryName = _darwinPluginDirectoryName(platform); + if (platformDirectoryName == null) { + return null; + } + return fileSystem.path.join(path, platformDirectoryName, '$name.podspec'); + } + + String? _darwinPluginDirectoryName(String platform) { + final PluginPlatform? platformPlugin = platforms[platform]; + if (platformPlugin == null || + (platform != IOSPlugin.kConfigKey && + platform != MacOSPlugin.kConfigKey)) { + return null; + } + + // iOS and macOS code can be shared in "darwin" directory, otherwise + // respectively in "ios" or "macos" directories. + if (platformPlugin is DarwinPlugin && + (platformPlugin as DarwinPlugin).sharedDarwinSource) { + return 'darwin'; + } + return platform; + } } /// Metadata associated with the resolution of a platform interface of a plugin. diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index b4c2e693990..0d8f849583c 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -21,6 +21,7 @@ import 'features.dart'; import 'flutter_manifest.dart'; import 'flutter_plugins.dart'; import 'globals.dart' as globals; +import 'macos/xcode.dart'; import 'platform_plugins.dart'; import 'project_validator_result.dart'; import 'template.dart'; @@ -31,14 +32,20 @@ export 'xcode_project.dart'; /// Enum for each officially supported platform. enum SupportedPlatform { - android, - ios, - linux, - macos, - web, - windows, - fuchsia, - root, // Special platform to represent the root project directory + android(name: 'android'), + ios(name: 'ios'), + linux(name: 'linux'), + macos(name: 'macos'), + web(name: 'web'), + windows(name: 'windows'), + fuchsia(name: 'fuchsia'), + root(name: 'root'); // Special platform to represent the root project directory + + const SupportedPlatform({ + required this.name, + }); + + final String name; } class FlutterProjectFactory { @@ -261,6 +268,24 @@ class FlutterProject { /// True if this project has an example application. bool get hasExampleApp => _exampleDirectory(directory).existsSync(); + /// True if this project doesn't have Swift Package Manager disabled in the + /// pubspec, has either an iOS or macOS platform implementation, is not a + /// module project, Xcode is 15 or greater, and the Swift Package Manager + /// feature is enabled. + bool get usesSwiftPackageManager { + if (!manifest.disabledSwiftPackageManager && + (ios.existsSync() || macos.existsSync()) && + !isModule) { + final Xcode? xcode = globals.xcode; + final Version? xcodeVersion = xcode?.currentVersion; + if (xcodeVersion == null || xcodeVersion.major < 15) { + return false; + } + return featureFlags.isSwiftPackageManagerEnabled; + } + return false; + } + /// Returns a list of platform names that are supported by the project. List getSupportedPlatforms({bool includeRoot = false}) { final List platforms = includeRoot ? [SupportedPlatform.root] : []; diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart index 2b519410771..e4ca075d832 100644 --- a/packages/flutter_tools/lib/src/xcode_project.dart +++ b/packages/flutter_tools/lib/src/xcode_project.dart @@ -115,6 +115,46 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform { .childDirectory('Pods') .childDirectory('Target Support Files') .childDirectory('Pods-Runner'); + + /// The directory in the project that is managed by Flutter. As much as + /// possible, files that are edited by Flutter tooling after initial project + /// creation should live here. + Directory get managedDirectory => hostAppRoot.childDirectory('Flutter'); + + /// The subdirectory of [managedDirectory] that contains files that are + /// generated on the fly. All generated files that are not intended to be + /// checked in should live here. + Directory get ephemeralDirectory => managedDirectory + .childDirectory('ephemeral'); + + /// The Flutter generated directory for the Swift Package handling plugin + /// dependencies. + Directory get flutterPluginSwiftPackageDirectory => ephemeralDirectory + .childDirectory('Packages') + .childDirectory('FlutterGeneratedPluginSwiftPackage'); + + /// The Flutter generated Swift Package manifest (Package.swift) for plugin + /// dependencies. + File get flutterPluginSwiftPackageManifest => + flutterPluginSwiftPackageDirectory.childFile('Package.swift'); + + /// Checks if FlutterGeneratedPluginSwiftPackage has been added to the + /// project's build settings by checking the contents of the pbxproj. + bool get flutterPluginSwiftPackageInProjectSettings { + return xcodeProjectInfoFile.existsSync() && + xcodeProjectInfoFile + .readAsStringSync() + .contains('FlutterGeneratedPluginSwiftPackage'); + } + + Future projectInfo() async { + final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter; + if (!xcodeProject.existsSync() || xcodeProjectInterpreter == null || !xcodeProjectInterpreter.isInstalled) { + return null; + } + return _projectInfo ??= await xcodeProjectInterpreter.getInfo(hostAppRoot.path); + } + XcodeProjectInfo? _projectInfo; } /// Represents the iOS sub-project of a Flutter project. @@ -167,16 +207,16 @@ class IosProject extends XcodeBasedProject { /// Whether the Flutter application has an iOS project. bool get exists => hostAppRoot.existsSync(); - /// Put generated files here. - Directory get ephemeralDirectory => _flutterLibRoot.childDirectory('Flutter').childDirectory('ephemeral'); + @override + Directory get managedDirectory => _flutterLibRoot.childDirectory('Flutter'); @override - File xcodeConfigFor(String mode) => _flutterLibRoot.childDirectory('Flutter').childFile('$mode.xcconfig'); + File xcodeConfigFor(String mode) => managedDirectory.childFile('$mode.xcconfig'); @override - File get generatedEnvironmentVariableExportScript => _flutterLibRoot.childDirectory('Flutter').childFile('flutter_export_environment.sh'); + File get generatedEnvironmentVariableExportScript => managedDirectory.childFile('flutter_export_environment.sh'); - File get appFrameworkInfoPlist => _flutterLibRoot.childDirectory('Flutter').childFile('AppFrameworkInfo.plist'); + File get appFrameworkInfoPlist => managedDirectory.childFile('AppFrameworkInfo.plist'); /// The 'AppDelegate.swift' file of the host app. This file might not exist if the app project uses Objective-C. File get appDelegateSwift => _editableDirectory.childDirectory('Runner').childFile('AppDelegate.swift'); @@ -444,15 +484,6 @@ class IosProject extends XcodeBasedProject { final Map> _buildSettingsByBuildContext = >{}; - Future projectInfo() async { - final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter; - if (!xcodeProject.existsSync() || xcodeProjectInterpreter == null || !xcodeProjectInterpreter.isInstalled) { - return null; - } - return _projectInfo ??= await xcodeProjectInterpreter.getInfo(hostAppRoot.path); - } - XcodeProjectInfo? _projectInfo; - Future?> _xcodeProjectBuildSettings(XcodeProjectBuildContext buildContext) async { final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter; if (xcodeProjectInterpreter == null || !xcodeProjectInterpreter.isInstalled) { @@ -692,16 +723,6 @@ class MacOSProject extends XcodeBasedProject { @override Directory get hostAppRoot => parent.directory.childDirectory('macos'); - /// The directory in the project that is managed by Flutter. As much as - /// possible, files that are edited by Flutter tooling after initial project - /// creation should live here. - Directory get managedDirectory => hostAppRoot.childDirectory('Flutter'); - - /// The subdirectory of [managedDirectory] that contains files that are - /// generated on the fly. All generated files that are not intended to be - /// checked in should live here. - Directory get ephemeralDirectory => managedDirectory.childDirectory('ephemeral'); - /// The xcfilelist used to track the inputs for the Flutter script phase in /// the Xcode build. File get inputFileList => ephemeralDirectory.childFile('FlutterInputs.xcfilelist'); diff --git a/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart index 7ff6ac46a73..bfe67bab6b4 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart @@ -66,9 +66,11 @@ void main() { expect(projectUnderTest.ios.deprecatedCompiledDartFramework, isNot(exists)); expect(projectUnderTest.ios.deprecatedProjectFlutterFramework, isNot(exists)); expect(projectUnderTest.ios.flutterPodspec, isNot(exists)); + expect(projectUnderTest.ios.flutterPluginSwiftPackageDirectory, isNot(exists)); expect(projectUnderTest.linux.ephemeralDirectory, isNot(exists)); expect(projectUnderTest.macos.ephemeralDirectory, isNot(exists)); + expect(projectUnderTest.macos.flutterPluginSwiftPackageDirectory, isNot(exists)); expect(projectUnderTest.windows.ephemeralDirectory, isNot(exists)); expect(projectUnderTest.flutterPluginsFile, isNot(exists)); @@ -239,9 +241,11 @@ FlutterProject setupProjectUnderTest(Directory currentDirectory, bool setupXcode projectUnderTest.ios.deprecatedCompiledDartFramework.createSync(recursive: true); projectUnderTest.ios.deprecatedProjectFlutterFramework.createSync(recursive: true); projectUnderTest.ios.flutterPodspec.createSync(recursive: true); + projectUnderTest.ios.flutterPluginSwiftPackageDirectory.createSync(recursive: true); projectUnderTest.linux.ephemeralDirectory.createSync(recursive: true); projectUnderTest.macos.ephemeralDirectory.createSync(recursive: true); + projectUnderTest.macos.flutterPluginSwiftPackageDirectory.createSync(recursive: true); projectUnderTest.windows.ephemeralDirectory.createSync(recursive: true); projectUnderTest.flutterPluginsFile.createSync(recursive: true); projectUnderTest.flutterPluginsDependenciesFile.createSync(recursive: true); diff --git a/packages/flutter_tools/test/general.shard/features_test.dart b/packages/flutter_tools/test/general.shard/features_test.dart index c997384c691..994ae392d6c 100644 --- a/packages/flutter_tools/test/general.shard/features_test.dart +++ b/packages/flutter_tools/test/general.shard/features_test.dart @@ -402,5 +402,14 @@ void main() { expect(nativeAssets.stable.enabledByDefault, false); expect(nativeAssets.stable.available, false); }); + + test('${swiftPackageManager.name} availability and default enabled', () { + expect(swiftPackageManager.master.enabledByDefault, false); + expect(swiftPackageManager.master.available, true); + expect(swiftPackageManager.beta.enabledByDefault, false); + expect(swiftPackageManager.beta.available, false); + expect(swiftPackageManager.stable.enabledByDefault, false); + expect(swiftPackageManager.stable.available, false); + }); }); } diff --git a/packages/flutter_tools/test/general.shard/flutter_manifest_test.dart b/packages/flutter_tools/test/general.shard/flutter_manifest_test.dart index a7b8ad7a98f..c46d9450341 100644 --- a/packages/flutter_tools/test/general.shard/flutter_manifest_test.dart +++ b/packages/flutter_tools/test/general.shard/flutter_manifest_test.dart @@ -1415,6 +1415,39 @@ name: test expect(flutterManifest, isNotNull); expect(flutterManifest!.dependencies, isEmpty); }); + + testWithoutContext('FlutterManifest knows if Swift Package Manager is disabled', () async { + const String manifest = ''' +name: test +dependencies: + flutter: + sdk: flutter +flutter: + disable-swift-package-manager: true +'''; + final FlutterManifest flutterManifest = FlutterManifest.createFromString( + manifest, + logger: logger, + )!; + + expect(flutterManifest.disabledSwiftPackageManager, true); + }); + + testWithoutContext('FlutterManifest does not disable Swift Package Manager if missing', () async { + const String manifest = ''' +name: test +dependencies: + flutter: + sdk: flutter +flutter: +'''; + final FlutterManifest flutterManifest = FlutterManifest.createFromString( + manifest, + logger: logger, + )!; + + expect(flutterManifest.disabledSwiftPackageManager, false); + }); } Matcher matchesManifest({ diff --git a/packages/flutter_tools/test/general.shard/ios/mac_test.dart b/packages/flutter_tools/test/general.shard/ios/mac_test.dart index c95405ac673..dc2c017a0c7 100644 --- a/packages/flutter_tools/test/general.shard/ios/mac_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/mac_test.dart @@ -11,6 +11,7 @@ import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/flutter_manifest.dart'; import 'package:flutter_tools/src/ios/code_signing.dart'; import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/xcresult.dart'; @@ -20,6 +21,7 @@ import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; +import '../../src/context.dart'; import '../../src/fake_process_manager.dart'; import '../../src/fakes.dart'; @@ -217,8 +219,16 @@ void main() { buildSettings: buildSettings, ), ); - - await diagnoseXcodeBuildFailure(buildResult, testUsage, logger, fakeAnalytics); + final MemoryFileSystem fs = MemoryFileSystem.test(); + await diagnoseXcodeBuildFailure( + buildResult, + flutterUsage: testUsage, + logger: logger, + analytics: fakeAnalytics, + fileSystem: fs, + platform: SupportedPlatform.ios, + project: FakeFlutterProject(fileSystem: fs), + ); expect(testUsage.events, contains( TestUsageEvent( 'build', @@ -310,8 +320,16 @@ Error launching application on iPhone.''', buildSettings: buildSettingsWithDevTeam, ), ); - - await diagnoseXcodeBuildFailure(buildResult, testUsage, logger, fakeAnalytics); + final MemoryFileSystem fs = MemoryFileSystem.test(); + await diagnoseXcodeBuildFailure( + buildResult, + flutterUsage: testUsage, + logger: logger, + analytics: fakeAnalytics, + fileSystem: fs, + platform: SupportedPlatform.ios, + project: FakeFlutterProject(fileSystem: fs), + ); expect( logger.errorText, contains(noProvisioningProfileInstruction), @@ -348,8 +366,16 @@ Error launching application on iPhone.''', buildSettings: buildSettingsWithDevTeam, ), ); - - await diagnoseXcodeBuildFailure(buildResult, testUsage, logger, fakeAnalytics); + final MemoryFileSystem fs = MemoryFileSystem.test(); + await diagnoseXcodeBuildFailure( + buildResult, + flutterUsage: testUsage, + logger: logger, + analytics: fakeAnalytics, + fileSystem: fs, + platform: SupportedPlatform.ios, + project: FakeFlutterProject(fileSystem: fs), + ); expect( logger.errorText, contains(missingPlatformInstructions('iOS 17.0')), @@ -388,8 +414,16 @@ Could not build the precompiled application for the device.''', buildSettings: buildSettings, ), ); - - await diagnoseXcodeBuildFailure(buildResult, testUsage, logger, fakeAnalytics); + final MemoryFileSystem fs = MemoryFileSystem.test(); + await diagnoseXcodeBuildFailure( + buildResult, + flutterUsage: testUsage, + logger: logger, + analytics: fakeAnalytics, + fileSystem: fs, + platform: SupportedPlatform.ios, + project: FakeFlutterProject(fileSystem: fs), + ); expect( logger.errorText, contains('Building a deployable iOS app requires a selected Development Team with a \nProvisioning Profile.'), @@ -432,10 +466,227 @@ Could not build the precompiled application for the device.''', ]) ); - await diagnoseXcodeBuildFailure(buildResult, testUsage, logger, fakeAnalytics); + final MemoryFileSystem fs = MemoryFileSystem.test(); + await diagnoseXcodeBuildFailure( + buildResult, + flutterUsage: testUsage, + logger: logger, + analytics: fakeAnalytics, + fileSystem: fs, + platform: SupportedPlatform.ios, + project: FakeFlutterProject(fileSystem: fs), + ); expect(logger.errorText, contains('Error (Xcode): Target aot_assembly_release failed')); expect(logger.errorText, isNot(contains('Building a deployable iOS app requires a selected Development Team'))); }); + + testWithoutContext('parses redefinition of module error', () async{ + const List buildCommands = ['xcrun', 'cc', 'blah']; + final XcodeBuildResult buildResult = XcodeBuildResult( + success: false, + stdout: '', + xcodeBuildExecution: XcodeBuildExecution( + buildCommands: buildCommands, + appDirectory: '/blah/blah', + environmentType: EnvironmentType.physical, + buildSettings: buildSettings, + ), + xcResult: XCResult.test(issues: [ + XCResultIssue.test(message: "Redefinition of module 'plugin_1_name'", subType: 'Error'), + XCResultIssue.test(message: "Redefinition of module 'plugin_2_name'", subType: 'Error'), + ]), + ); + final MemoryFileSystem fs = MemoryFileSystem.test(); + final FakeFlutterProject project = FakeFlutterProject( + fileSystem: fs, + usesSwiftPackageManager: true, + ); + project.ios.podfile.createSync(recursive: true); + await diagnoseXcodeBuildFailure( + buildResult, + flutterUsage: testUsage, + logger: logger, + analytics: fakeAnalytics, + fileSystem: fs, + platform: SupportedPlatform.ios, + project: project, + ); + expect(logger.errorText, contains( + 'Your project uses both CocoaPods and Swift Package Manager, which can ' + 'cause the above error. It may be caused by there being both a CocoaPod ' + 'and Swift Package Manager dependency for the following module(s): ' + 'plugin_1_name, plugin_2_name.' + )); + }); + + testWithoutContext('parses duplicate symbols error with arch and number', () async{ + const List buildCommands = ['xcrun', 'cc', 'blah']; + final XcodeBuildResult buildResult = XcodeBuildResult( + success: false, + stdout: r''' +duplicate symbol '_$s29plugin_1_name23PluginNamePluginC9setDouble3key5valueySS_SdtF' in: + /Users/username/path/to/app/build/ios/Debug-iphonesimulator/plugin_1_name/plugin_1_name.framework/plugin_1_name[arm64][5](PluginNamePlugin.o) + /Users/username/path/to/app/build/ios/Debug-iphonesimulator/plugin_1_name.o + duplicate symbol '_$s29plugin_1_name23PluginNamePluginCAA15UserDefaultsApiAAWP' in: + /Users/username/path/to/app/build/ios/Debug-iphonesimulator/plugin_1_name/plugin_1_name.framework/plugin_1_name[arm64][5](PluginNamePlugin.o) + /Users/username/path/to/app/build/ios/Debug-iphonesimulator/plugin_1_name.o +''', + xcodeBuildExecution: XcodeBuildExecution( + buildCommands: buildCommands, + appDirectory: '/blah/blah', + environmentType: EnvironmentType.physical, + buildSettings: buildSettings, + ), + xcResult: XCResult.test(issues: [ + XCResultIssue.test(message: '37 duplicate symbols', subType: 'Error'), + ]), + ); + final MemoryFileSystem fs = MemoryFileSystem.test(); + final FakeFlutterProject project = FakeFlutterProject( + fileSystem: fs, + usesSwiftPackageManager: true, + ); + project.ios.podfile.createSync(recursive: true); + await diagnoseXcodeBuildFailure( + buildResult, + flutterUsage: testUsage, + logger: logger, + analytics: fakeAnalytics, + fileSystem: fs, + platform: SupportedPlatform.ios, + project: project, + ); + expect(logger.errorText, contains( + 'Your project uses both CocoaPods and Swift Package Manager, which can ' + 'cause the above error. It may be caused by there being both a CocoaPod ' + 'and Swift Package Manager dependency for the following module(s): ' + 'plugin_1_name.' + )); + }); + + testWithoutContext('parses duplicate symbols error with number', () async{ + const List buildCommands = ['xcrun', 'cc', 'blah']; + final XcodeBuildResult buildResult = XcodeBuildResult( + success: false, + stdout: r''' +duplicate symbol '_$s29plugin_1_name23PluginNamePluginC9setDouble3key5valueySS_SdtF' in: + /Users/username/path/to/app/build/ios/Debug-iphonesimulator/plugin_1_name/plugin_1_name.framework/plugin_1_name[5](PluginNamePlugin.o) + /Users/username/path/to/app/build/ios/Debug-iphonesimulator/plugin_1_name.o +''', + xcodeBuildExecution: XcodeBuildExecution( + buildCommands: buildCommands, + appDirectory: '/blah/blah', + environmentType: EnvironmentType.physical, + buildSettings: buildSettings, + ), + xcResult: XCResult.test(issues: [ + XCResultIssue.test(message: '37 duplicate symbols', subType: 'Error'), + ]), + ); + final MemoryFileSystem fs = MemoryFileSystem.test(); + final FakeFlutterProject project = FakeFlutterProject( + fileSystem: fs, + usesSwiftPackageManager: true, + ); + project.ios.podfile.createSync(recursive: true); + await diagnoseXcodeBuildFailure( + buildResult, + flutterUsage: testUsage, + logger: logger, + analytics: fakeAnalytics, + fileSystem: fs, + platform: SupportedPlatform.ios, + project: project, + ); + expect(logger.errorText, contains( + 'Your project uses both CocoaPods and Swift Package Manager, which can ' + 'cause the above error. It may be caused by there being both a CocoaPod ' + 'and Swift Package Manager dependency for the following module(s): ' + 'plugin_1_name.' + )); + }); + + testWithoutContext('parses duplicate symbols error without arch and number', () async{ + const List buildCommands = ['xcrun', 'cc', 'blah']; + final XcodeBuildResult buildResult = XcodeBuildResult( + success: false, + stdout: r''' +duplicate symbol '_$s29plugin_1_name23PluginNamePluginC9setDouble3key5valueySS_SdtF' in: + /Users/username/path/to/app/build/ios/Debug-iphonesimulator/plugin_1_name/plugin_1_name.framework/plugin_1_name(PluginNamePlugin.o) +''', + xcodeBuildExecution: XcodeBuildExecution( + buildCommands: buildCommands, + appDirectory: '/blah/blah', + environmentType: EnvironmentType.physical, + buildSettings: buildSettings, + ), + xcResult: XCResult.test(issues: [ + XCResultIssue.test(message: '37 duplicate symbols', subType: 'Error'), + ]), + ); + final MemoryFileSystem fs = MemoryFileSystem.test(); + final FakeFlutterProject project = FakeFlutterProject( + fileSystem: fs, + usesSwiftPackageManager: true, + ); + project.ios.podfile.createSync(recursive: true); + await diagnoseXcodeBuildFailure( + buildResult, + flutterUsage: testUsage, + logger: logger, + analytics: fakeAnalytics, + fileSystem: fs, + platform: SupportedPlatform.ios, + project: project, + ); + expect(logger.errorText, contains( + 'Your project uses both CocoaPods and Swift Package Manager, which can ' + 'cause the above error. It may be caused by there being both a CocoaPod ' + 'and Swift Package Manager dependency for the following module(s): ' + 'plugin_1_name.' + )); + }); + + testUsingContext('parses missing module error', () async{ + const List buildCommands = ['xcrun', 'cc', 'blah']; + final XcodeBuildResult buildResult = XcodeBuildResult( + success: false, + stdout: '', + xcodeBuildExecution: XcodeBuildExecution( + buildCommands: buildCommands, + appDirectory: '/blah/blah', + environmentType: EnvironmentType.physical, + buildSettings: buildSettings, + ), + xcResult: XCResult.test(issues: [ + XCResultIssue.test(message: "Module 'plugin_1_name' not found", subType: 'Error'), + XCResultIssue.test(message: "Module 'plugin_2_name' not found", subType: 'Error'), + ]), + ); + final MemoryFileSystem fs = MemoryFileSystem.test(); + final FakeFlutterProject project = FakeFlutterProject(fileSystem: fs); + project.ios.podfile.createSync(recursive: true); + project.directory.childFile('.packages').createSync(recursive: true); + project.manifest = FakeFlutterManifest(); + createFakePlugins(project, fs, ['plugin_1_name', 'plugin_2_name']); + fs.systemTempDirectory.childFile('cache/plugin_1_name/ios/plugin_1_name/Package.swift') + .createSync(recursive: true); + fs.systemTempDirectory.childFile('cache/plugin_2_name/ios/plugin_2_name/Package.swift') + .createSync(recursive: true); + await diagnoseXcodeBuildFailure( + buildResult, + flutterUsage: testUsage, + logger: logger, + analytics: fakeAnalytics, + fileSystem: fs, + platform: SupportedPlatform.ios, + project: project, + ); + expect(logger.errorText, contains( + 'Your project uses CocoaPods as a dependency manager, but the following plugin(s) ' + 'only support Swift Package Manager: plugin_1_name, plugin_2_name.' + )); + }); }); group('Upgrades project.pbxproj for old asset usage', () { @@ -454,9 +705,10 @@ Could not build the precompiled application for the device.''', 'another line'; testWithoutContext('upgradePbxProjWithFlutterAssets', () async { - final File pbxprojFile = MemoryFileSystem.test().file('project.pbxproj') + final FakeIosProject project = FakeIosProject(fileSystem: MemoryFileSystem.test()); + final File pbxprojFile = project.xcodeProjectInfoFile + ..createSync(recursive: true) ..writeAsStringSync(flutterAssetPbxProjLines); - final FakeIosProject project = FakeIosProject(pbxprojFile); bool result = upgradePbxProjWithFlutterAssets(project, logger); expect(result, true); @@ -528,14 +780,88 @@ Could not build the precompiled application for the device.''', }); } +void createFakePlugins( + FlutterProject flutterProject, + FileSystem fileSystem, + List pluginNames, +) { + const String pluginYamlTemplate = ''' + flutter: + plugin: + platforms: + ios: + pluginClass: PLUGIN_CLASS + macos: + pluginClass: PLUGIN_CLASS + '''; + + final Directory fakePubCache = fileSystem.systemTempDirectory.childDirectory('cache'); + final File packagesFile = flutterProject.directory.childFile('.packages') + ..createSync(recursive: true); + for (final String name in pluginNames) { + final Directory pluginDirectory = fakePubCache.childDirectory(name); + packagesFile.writeAsStringSync( + '$name:${pluginDirectory.childFile('lib').uri}\n', + mode: FileMode.writeOnlyAppend); + pluginDirectory.childFile('pubspec.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(pluginYamlTemplate.replaceAll('PLUGIN_CLASS', name)); + } +} + class FakeIosProject extends Fake implements IosProject { - FakeIosProject(this.xcodeProjectInfoFile); + FakeIosProject({ + required MemoryFileSystem fileSystem, + }) : hostAppRoot = fileSystem.directory('app_name').childDirectory('ios'); + @override - final File xcodeProjectInfoFile; + Directory hostAppRoot; + + @override + File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj'); @override Future hostAppBundleName(BuildInfo? buildInfo) async => 'UnitTestRunner.app'; @override - Directory get xcodeProject => xcodeProjectInfoFile.fileSystem.directory('Runner.xcodeproj'); + Directory get xcodeProject => hostAppRoot.childDirectory('Runner.xcodeproj'); + + @override + File get podfile => hostAppRoot.childFile('Podfile'); +} + +class FakeFlutterProject extends Fake implements FlutterProject { + FakeFlutterProject({ + required this.fileSystem, + this.usesSwiftPackageManager = false, + this.isModule = false, + }); + + MemoryFileSystem fileSystem; + + @override + late final Directory directory = fileSystem.directory('app_name'); + + @override + late FlutterManifest manifest; + + @override + File get flutterPluginsFile => directory.childFile('.flutter-plugins'); + + @override + File get flutterPluginsDependenciesFile => directory.childFile('.flutter-plugins-dependencies'); + + @override + late final IosProject ios = FakeIosProject(fileSystem: fileSystem); + + @override + final bool usesSwiftPackageManager; + + @override + final bool isModule; +} + +class FakeFlutterManifest extends Fake implements FlutterManifest { + @override + Set get dependencies => {}; } diff --git a/packages/flutter_tools/test/general.shard/macos/cocoapod_utils_test.dart b/packages/flutter_tools/test/general.shard/macos/cocoapod_utils_test.dart new file mode 100644 index 00000000000..0a12510e8e6 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/macos/cocoapod_utils_test.dart @@ -0,0 +1,555 @@ +// 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/flutter_manifest.dart'; +import 'package:flutter_tools/src/macos/cocoapod_utils.dart'; +import 'package:flutter_tools/src/macos/cocoapods.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:test/fake.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; + +void main() { + group('processPodsIfNeeded', () { + late MemoryFileSystem fs; + late FakeCocoaPods cocoaPods; + late BufferLogger logger; + + // Adds basic properties to the flutterProject and its subprojects. + void setUpProject(FakeFlutterProject flutterProject, MemoryFileSystem fileSystem) { + flutterProject + ..manifest = FakeFlutterManifest() + ..directory = fileSystem.systemTempDirectory.childDirectory('app') + ..flutterPluginsFile = flutterProject.directory.childFile('.flutter-plugins') + ..flutterPluginsDependenciesFile = flutterProject.directory.childFile('.flutter-plugins-dependencies') + ..ios = FakeIosProject(fileSystem: fileSystem, parent: flutterProject) + ..macos = FakeMacOSProject(fileSystem: fileSystem, parent: flutterProject) + ..android = FakeAndroidProject() + ..web = FakeWebProject() + ..windows = FakeWindowsProject() + ..linux = FakeLinuxProject(); + flutterProject.directory.childFile('.packages').createSync(recursive: true); + } + + setUp(() async { + fs = MemoryFileSystem.test(); + cocoaPods = FakeCocoaPods(); + logger = BufferLogger.test(); + }); + + void createFakePlugins( + FlutterProject flutterProject, + FileSystem fileSystem, + List pluginNames, + ) { + const String pluginYamlTemplate = ''' + flutter: + plugin: + platforms: + ios: + pluginClass: PLUGIN_CLASS + macos: + pluginClass: PLUGIN_CLASS + '''; + + final Directory fakePubCache = fileSystem.systemTempDirectory.childDirectory('cache'); + final File packagesFile = flutterProject.directory.childFile('.packages') + ..createSync(recursive: true); + for (final String name in pluginNames) { + final Directory pluginDirectory = fakePubCache.childDirectory(name); + packagesFile.writeAsStringSync( + '$name:${pluginDirectory.childFile('lib').uri}\n', + mode: FileMode.writeOnlyAppend); + pluginDirectory.childFile('pubspec.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(pluginYamlTemplate.replaceAll('PLUGIN_CLASS', name)); + } + } + + group('for iOS', () { + group('using CocoaPods only', () { + testUsingContext('processes when there are plugins', () async { + final FakeFlutterProject flutterProject = FakeFlutterProject(); + setUpProject(flutterProject, fs); + createFakePlugins(flutterProject, fs, [ + 'plugin_one', + 'plugin_two' + ]); + + await processPodsIfNeeded( + flutterProject.ios, + fs.currentDirectory.childDirectory('build').path, + BuildMode.debug, + ); + expect(cocoaPods.processedPods, isTrue); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + CocoaPods: () => cocoaPods, + }); + + testUsingContext('processes when no plugins but the project is a module and podfile exists', () async { + final FakeFlutterProject flutterProject = FakeFlutterProject(); + setUpProject(flutterProject, fs); + flutterProject.isModule = true; + flutterProject.ios.podfile.createSync(recursive: true); + + await processPodsIfNeeded( + flutterProject.ios, + fs.currentDirectory.childDirectory('build').path, + BuildMode.debug, + ); + expect(cocoaPods.processedPods, isTrue); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + CocoaPods: () => cocoaPods, + }); + + testUsingContext("skips when no plugins and the project is a module but podfile doesn't exist", () async { + final FakeFlutterProject flutterProject = FakeFlutterProject(); + setUpProject(flutterProject, fs); + flutterProject.isModule = true; + + await processPodsIfNeeded( + flutterProject.ios, + fs.currentDirectory.childDirectory('build').path, + BuildMode.debug, + ); + expect(cocoaPods.processedPods, isFalse); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + CocoaPods: () => cocoaPods, + }); + + testUsingContext('skips when no plugins and project is not a module', () async { + final FakeFlutterProject flutterProject = FakeFlutterProject(); + setUpProject(flutterProject, fs); + + await processPodsIfNeeded( + flutterProject.ios, + fs.currentDirectory.childDirectory('build').path, + BuildMode.debug, + ); + expect(cocoaPods.processedPods, isFalse); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + CocoaPods: () => cocoaPods, + }); + }); + + group('using Swift Package Manager', () { + testUsingContext('processes if podfile exists', () async { + final FakeFlutterProject flutterProject = FakeFlutterProject(); + setUpProject(flutterProject, fs); + createFakePlugins(flutterProject, fs, [ + 'plugin_one', + 'plugin_two' + ]); + flutterProject.usesSwiftPackageManager = true; + flutterProject.ios.podfile.createSync(recursive: true); + + await processPodsIfNeeded( + flutterProject.ios, + fs.currentDirectory.childDirectory('build').path, + BuildMode.debug, + ); + expect(cocoaPods.processedPods, isTrue); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + CocoaPods: () => cocoaPods, + }); + + testUsingContext('skip if podfile does not exists', () async { + final FakeFlutterProject flutterProject = FakeFlutterProject(); + setUpProject(flutterProject, fs); + createFakePlugins(flutterProject, fs, [ + 'plugin_one', + 'plugin_two' + ]); + flutterProject.usesSwiftPackageManager = true; + + await processPodsIfNeeded( + flutterProject.ios, + fs.currentDirectory.childDirectory('build').path, + BuildMode.debug, + ); + expect(cocoaPods.processedPods, isFalse); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + CocoaPods: () => cocoaPods, + }); + + testUsingContext('process if podfile does not exists but forceCocoaPodsOnly is true', () async { + final FakeFlutterProject flutterProject = FakeFlutterProject(); + setUpProject(flutterProject, fs); + createFakePlugins(flutterProject, fs, [ + 'plugin_one', + 'plugin_two' + ]); + flutterProject.usesSwiftPackageManager = true; + flutterProject.ios.flutterPluginSwiftPackageManifest.createSync(recursive: true); + + await processPodsIfNeeded( + flutterProject.ios, + fs.currentDirectory.childDirectory('build').path, + BuildMode.debug, + forceCocoaPodsOnly: true, + ); + expect(cocoaPods.processedPods, isTrue); + expect(cocoaPods.podfileSetup, isTrue); + expect( + logger.warningText, + 'Swift Package Manager does not yet support this command. ' + 'CocoaPods will be used instead.\n'); + expect( + flutterProject.ios.flutterPluginSwiftPackageManifest.existsSync(), + isFalse, + ); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + CocoaPods: () => cocoaPods, + Logger: () => logger, + }); + }); + }); + + group('for macOS', () { + group('using CocoaPods only', () { + testUsingContext('processes when there are plugins', () async { + final FakeFlutterProject flutterProject = FakeFlutterProject(); + setUpProject(flutterProject, fs); + createFakePlugins(flutterProject, fs, [ + 'plugin_one', + 'plugin_two' + ]); + + await processPodsIfNeeded( + flutterProject.macos, + fs.currentDirectory.childDirectory('build').path, + BuildMode.debug, + ); + expect(cocoaPods.processedPods, isTrue); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + CocoaPods: () => cocoaPods, + }); + + testUsingContext('processes when no plugins but the project is a module and podfile exists', () async { + final FakeFlutterProject flutterProject = FakeFlutterProject(); + setUpProject(flutterProject, fs); + flutterProject.isModule = true; + flutterProject.macos.podfile.createSync(recursive: true); + + await processPodsIfNeeded( + flutterProject.macos, + fs.currentDirectory.childDirectory('build').path, + BuildMode.debug, + ); + expect(cocoaPods.processedPods, isTrue); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + CocoaPods: () => cocoaPods, + }); + + testUsingContext("skips when no plugins and the project is a module but podfile doesn't exist", () async { + final FakeFlutterProject flutterProject = FakeFlutterProject(); + setUpProject(flutterProject, fs); + flutterProject.isModule = true; + + await processPodsIfNeeded( + flutterProject.macos, + fs.currentDirectory.childDirectory('build').path, + BuildMode.debug, + ); + expect(cocoaPods.processedPods, isFalse); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + CocoaPods: () => cocoaPods, + }); + + testUsingContext('skips when no plugins and project is not a module', () async { + final FakeFlutterProject flutterProject = FakeFlutterProject(); + setUpProject(flutterProject, fs); + + await processPodsIfNeeded( + flutterProject.macos, + fs.currentDirectory.childDirectory('build').path, + BuildMode.debug, + ); + expect(cocoaPods.processedPods, isFalse); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + CocoaPods: () => cocoaPods, + }); + }); + + group('using Swift Package Manager', () { + testUsingContext('processes if podfile exists', () async { + final FakeFlutterProject flutterProject = FakeFlutterProject(); + setUpProject(flutterProject, fs); + createFakePlugins(flutterProject, fs, [ + 'plugin_one', + 'plugin_two' + ]); + flutterProject.usesSwiftPackageManager = true; + flutterProject.macos.podfile.createSync(recursive: true); + + await processPodsIfNeeded( + flutterProject.macos, + fs.currentDirectory.childDirectory('build').path, + BuildMode.debug, + ); + expect(cocoaPods.processedPods, isTrue); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + CocoaPods: () => cocoaPods, + }); + + testUsingContext('skip if podfile does not exists', () async { + final FakeFlutterProject flutterProject = FakeFlutterProject(); + setUpProject(flutterProject, fs); + createFakePlugins(flutterProject, fs, [ + 'plugin_one', + 'plugin_two' + ]); + flutterProject.usesSwiftPackageManager = true; + + await processPodsIfNeeded( + flutterProject.macos, + fs.currentDirectory.childDirectory('build').path, + BuildMode.debug, + ); + expect(cocoaPods.processedPods, isFalse); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + CocoaPods: () => cocoaPods, + }); + + testUsingContext('process if podfile does not exists but forceCocoaPodsOnly is true', () async { + final FakeFlutterProject flutterProject = FakeFlutterProject(); + setUpProject(flutterProject, fs); + createFakePlugins(flutterProject, fs, [ + 'plugin_one', + 'plugin_two' + ]); + flutterProject.usesSwiftPackageManager = true; + flutterProject.macos.flutterPluginSwiftPackageManifest.createSync(recursive: true); + + await processPodsIfNeeded( + flutterProject.macos, + fs.currentDirectory.childDirectory('build').path, + BuildMode.debug, + forceCocoaPodsOnly: true, + ); + expect(cocoaPods.processedPods, isTrue); + expect(cocoaPods.podfileSetup, isTrue); + expect( + logger.warningText, + 'Swift Package Manager does not yet support this command. ' + 'CocoaPods will be used instead.\n'); + expect( + flutterProject.macos.flutterPluginSwiftPackageManifest.existsSync(), + isFalse, + ); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + CocoaPods: () => cocoaPods, + Logger: () => logger, + }); + }); + }); + }); +} + +class FakeFlutterManifest extends Fake implements FlutterManifest { + @override + Set get dependencies => {}; +} + +class FakeFlutterProject extends Fake implements FlutterProject { + @override + bool isModule = false; + + @override + bool usesSwiftPackageManager = false; + + @override + late FlutterManifest manifest; + + @override + late Directory directory; + + @override + late File flutterPluginsFile; + + @override + late File flutterPluginsDependenciesFile; + + @override + late IosProject ios; + + @override + late MacOSProject macos; + + @override + late AndroidProject android; + + @override + late WebProject web; + + @override + late LinuxProject linux; + + @override + late WindowsProject windows; +} + +class FakeMacOSProject extends Fake implements MacOSProject { + FakeMacOSProject({ + required MemoryFileSystem fileSystem, + required this.parent, + }) : hostAppRoot = fileSystem.directory('app_name').childDirectory('ios'); + + @override + String pluginConfigKey = 'macos'; + + @override + final FlutterProject parent; + + @override + Directory hostAppRoot; + + bool exists = true; + + @override + bool existsSync() => exists; + + @override + File get podfile => hostAppRoot.childFile('Podfile'); + + @override + File get xcodeProjectInfoFile => hostAppRoot + .childDirectory('Runner.xcodeproj') + .childFile('project.pbxproj'); + + @override + File get flutterPluginSwiftPackageManifest => hostAppRoot + .childDirectory('Flutter') + .childDirectory('ephemeral') + .childDirectory('Packages') + .childDirectory('FlutterGeneratedPluginSwiftPackage') + .childFile('Package.swift'); +} + +class FakeIosProject extends Fake implements IosProject { + FakeIosProject({ + required MemoryFileSystem fileSystem, + required this.parent, + }) : hostAppRoot = fileSystem.directory('app_name').childDirectory('ios'); + + @override + String pluginConfigKey = 'ios'; + + @override + final FlutterProject parent; + + @override + Directory hostAppRoot; + + @override + bool exists = true; + + @override + bool existsSync() => exists; + + @override + File get podfile => hostAppRoot.childFile('Podfile'); + + @override + File get xcodeProjectInfoFile => hostAppRoot + .childDirectory('Runner.xcodeproj') + .childFile('project.pbxproj'); + + @override + File get flutterPluginSwiftPackageManifest => hostAppRoot + .childDirectory('Flutter') + .childDirectory('ephemeral') + .childDirectory('Packages') + .childDirectory('FlutterGeneratedPluginSwiftPackage') + .childFile('Package.swift'); +} + +class FakeAndroidProject extends Fake implements AndroidProject { + @override + String pluginConfigKey = 'android'; + + @override + bool existsSync() => false; +} + +class FakeWebProject extends Fake implements WebProject { + @override + String pluginConfigKey = 'web'; + + @override + bool existsSync() => false; +} + +class FakeWindowsProject extends Fake implements WindowsProject { + @override + String pluginConfigKey = 'windows'; + + @override + bool existsSync() => false; +} + +class FakeLinuxProject extends Fake implements LinuxProject { + @override + String pluginConfigKey = 'linux'; + + @override + bool existsSync() => false; +} + +class FakeCocoaPods extends Fake implements CocoaPods { + bool podfileSetup = false; + bool processedPods = false; + + @override + Future processPods({ + required XcodeBasedProject xcodeProject, + required BuildMode buildMode, + bool dependenciesChanged = true, + }) async { + processedPods = true; + return true; + } + + @override + Future setupPodfile(XcodeBasedProject xcodeProject) async { + podfileSetup = true; + } + + @override + void invalidatePodInstallOutput(XcodeBasedProject xcodeProject) {} +} diff --git a/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart b/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart index b1091b3e71c..d25ad4cfec0 100644 --- a/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart @@ -9,6 +9,7 @@ import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/flutter_plugins.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/macos/cocoapods.dart'; @@ -457,6 +458,19 @@ void main() { expect(fakeProcessManager, hasNoRemainingExpectations); }); + testUsingContext("doesn't throw, if using Swift Package Manager and Podfile is missing.", () async { + final FlutterProject projectUnderTest = setupProjectUnderTest(); + final bool didInstall = await cocoaPodsUnderTest.processPods( + xcodeProject: projectUnderTest.ios, + buildMode: BuildMode.debug, + ); + expect(didInstall, isFalse); + expect(fakeProcessManager, hasNoRemainingExpectations); + }, overrides: { + FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true), + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)), + }); + testUsingContext('throws, if specs repo is outdated.', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); @@ -1380,7 +1394,11 @@ Specs satisfying the `$fakePluginName (from `Flutter/ephemeral/.symlinks/plugins } class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter { - FakeXcodeProjectInterpreter({this.isInstalled = true, this.buildSettings = const {}}); + FakeXcodeProjectInterpreter({ + this.isInstalled = true, + this.buildSettings = const {}, + this.version, + }); @override final bool isInstalled; @@ -1393,4 +1411,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete }) async => buildSettings; final Map buildSettings; + + @override + Version? version; } diff --git a/packages/flutter_tools/test/general.shard/macos/darwin_dependency_management_test.dart b/packages/flutter_tools/test/general.shard/macos/darwin_dependency_management_test.dart new file mode 100644 index 00000000000..ba539c3e1e8 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/macos/darwin_dependency_management_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/macos/cocoapods.dart'; +import 'package:flutter_tools/src/macos/darwin_dependency_management.dart'; +import 'package:flutter_tools/src/macos/swift_package_manager.dart'; +import 'package:flutter_tools/src/platform_plugins.dart'; +import 'package:flutter_tools/src/plugins.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:test/fake.dart'; + +import '../../src/common.dart'; + +void main() { + const List supportedPlatforms = [ + SupportedPlatform.ios, + SupportedPlatform.macos, + ]; + + group('DarwinDependencyManagement', () { + for (final SupportedPlatform platform in supportedPlatforms) { + group('for ${platform.name}', () { + group('generatePluginsSwiftPackage', () { + testWithoutContext('throw if invalid platform', () async { + final MemoryFileSystem fs = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + + final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement( + project: FakeFlutterProject(fileSystem: fs), + plugins: [], + cocoapods: FakeCocoaPods(), + swiftPackageManager: FakeSwiftPackageManager(), + fileSystem: fs, + logger: testLogger, + ); + + await expectLater(() => dependencyManagement.setUp( + platform: SupportedPlatform.android, + ), + throwsToolExit( + message: 'The platform android is incompatible with Darwin Dependency Managers. Only iOS and macOS are allowed.', + ), + ); + }); + group('when using Swift Package Manager', () { + testWithoutContext('with only CocoaPod plugins', () async { + final MemoryFileSystem fs = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final File cocoapodPluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1.podspec') + ..createSync(recursive: true); + final List plugins = [ + FakePlugin( + name: 'cocoapod_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + pluginPodspecPath: cocoapodPluginPodspec.path, + ), + ]; + final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager( + expectedPlugins: plugins, + ); + final FakeCocoaPods cocoaPods = FakeCocoaPods(); + + final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement( + project: FakeFlutterProject( + usesSwiftPackageManager: true, + fileSystem: fs, + ), + plugins: plugins, + cocoapods: cocoaPods, + swiftPackageManager: swiftPackageManager, + fileSystem: fs, + logger: testLogger, + ); + await dependencyManagement.setUp( + platform: platform, + ); + expect(swiftPackageManager.generated, isTrue); + expect(testLogger.warningText, isEmpty); + expect(testLogger.statusText, isEmpty); + expect(cocoaPods.podfileSetup, isTrue); + }); + + testWithoutContext('with only Swift Package Manager plugins and no pod integration', () async { + final MemoryFileSystem fs = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final File swiftPackagePluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1/Package.swift') + ..createSync(recursive: true); + final List plugins = [ + FakePlugin( + name: 'swift_package_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + pluginSwiftPackageManifestPath: swiftPackagePluginPodspec.path, + ), + ]; + final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager( + expectedPlugins: plugins, + ); + final FakeCocoaPods cocoaPods = FakeCocoaPods(); + + final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement( + project: FakeFlutterProject( + usesSwiftPackageManager: true, + fileSystem: fs, + ), + plugins: plugins, + cocoapods: cocoaPods, + swiftPackageManager: swiftPackageManager, + fileSystem: fs, + logger: testLogger, + ); + await dependencyManagement.setUp( + platform: platform, + ); + expect(swiftPackageManager.generated, isTrue); + expect(testLogger.warningText, isEmpty); + expect(testLogger.statusText, isEmpty); + expect(cocoaPods.podfileSetup, isFalse); + }); + + testWithoutContext('with only Swift Package Manager plugins but project not migrated', () async { + final MemoryFileSystem fs = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final File swiftPackagePluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1/Package.swift') + ..createSync(recursive: true); + final List plugins = [ + FakePlugin( + name: 'swift_package_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + pluginSwiftPackageManifestPath: swiftPackagePluginPodspec.path, + ), + ]; + final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager( + expectedPlugins: plugins, + ); + final File projectPodfile = fs.file('/path/to/Podfile')..createSync(recursive: true); + projectPodfile.writeAsStringSync('Standard Podfile template'); + final FakeCocoaPods cocoaPods = FakeCocoaPods( + podFile: projectPodfile, + ); + final FakeFlutterProject project = FakeFlutterProject( + usesSwiftPackageManager: true, + fileSystem: fs, + ); + final XcodeBasedProject xcodeProject = platform == SupportedPlatform.ios ? project.ios : project.macos; + xcodeProject.podfile.createSync(recursive: true); + xcodeProject.podfile.writeAsStringSync('Standard Podfile template'); + + final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement( + project: project, + plugins: plugins, + cocoapods: cocoaPods, + swiftPackageManager: swiftPackageManager, + fileSystem: fs, + logger: testLogger, + ); + await dependencyManagement.setUp( + platform: platform, + ); + expect(swiftPackageManager.generated, isTrue); + expect(testLogger.warningText, isEmpty); + expect(testLogger.statusText, isEmpty); + expect(cocoaPods.podfileSetup, isFalse); + }); + + testWithoutContext('with only Swift Package Manager plugins with preexisting standard CocoaPods Podfile', () async { + final MemoryFileSystem fs = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final File swiftPackagePluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1/Package.swift') + ..createSync(recursive: true); + final List plugins = [ + FakePlugin( + name: 'swift_package_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + pluginSwiftPackageManifestPath: swiftPackagePluginPodspec.path, + ), + ]; + final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager( + expectedPlugins: plugins, + ); + final File projectPodfile = fs.file('/path/to/Podfile')..createSync(recursive: true); + projectPodfile.writeAsStringSync('Standard Podfile template'); + final FakeCocoaPods cocoaPods = FakeCocoaPods( + podFile: projectPodfile, + ); + final FakeFlutterProject project = FakeFlutterProject( + usesSwiftPackageManager: true, + fileSystem: fs, + ); + final XcodeBasedProject xcodeProject = platform == SupportedPlatform.ios ? project.ios : project.macos; + xcodeProject.podfile.createSync(recursive: true); + xcodeProject.podfile.writeAsStringSync('Standard Podfile template'); + xcodeProject.xcodeProjectInfoFile.createSync(recursive: true); + xcodeProject.xcodeProjectInfoFile.writeAsStringSync('FlutterGeneratedPluginSwiftPackage'); + + final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement( + project: project, + plugins: plugins, + cocoapods: cocoaPods, + swiftPackageManager: swiftPackageManager, + fileSystem: fs, + logger: testLogger, + ); + await dependencyManagement.setUp( + platform: platform, + ); + expect(swiftPackageManager.generated, isTrue); + final String xcconfigPrefix = platform == SupportedPlatform.macos ? 'Flutter-' : ''; + expect(testLogger.warningText, contains( + 'All plugins found for ${platform.name} are Swift Packages, ' + 'but your project still has CocoaPods integration. To remove ' + 'CocoaPods integration, complete the following steps:\n' + ' * In the ${platform.name}/ directory run "pod deintegrate"\n' + ' * Also in the ${platform.name}/ directory, delete the Podfile\n' + ' * Remove the include to "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" ' + 'in your ${platform.name}/Flutter/${xcconfigPrefix}Debug.xcconfig\n' + ' * Remove the include to "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" ' + 'in your ${platform.name}/Flutter/${xcconfigPrefix}Release.xcconfig\n\n' + "Removing CocoaPods integration will improve the project's build time.\n" + )); + expect(testLogger.statusText, isEmpty); + expect(cocoaPods.podfileSetup, isFalse); + }); + + testWithoutContext('with only Swift Package Manager plugins with preexisting custom CocoaPods Podfile', () async { + final MemoryFileSystem fs = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final File swiftPackagePluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1/Package.swift') + ..createSync(recursive: true); + final List plugins = [ + FakePlugin( + name: 'swift_package_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + pluginSwiftPackageManifestPath: swiftPackagePluginPodspec.path, + ), + ]; + final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager( + expectedPlugins: plugins, + ); + final File projectPodfile = fs.file('/path/to/Podfile')..createSync(recursive: true); + projectPodfile.writeAsStringSync('Standard Podfile template'); + final FakeCocoaPods cocoaPods = FakeCocoaPods( + podFile: projectPodfile, + ); + final FakeFlutterProject project = FakeFlutterProject( + usesSwiftPackageManager: true, + fileSystem: fs, + ); + final XcodeBasedProject xcodeProject = platform == SupportedPlatform.ios ? project.ios : project.macos; + xcodeProject.podfile.createSync(recursive: true); + xcodeProject.podfile.writeAsStringSync('Non-Standard Podfile template'); + xcodeProject.xcodeProjectInfoFile.createSync(recursive: true); + xcodeProject.xcodeProjectInfoFile.writeAsStringSync('FlutterGeneratedPluginSwiftPackage'); + + final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement( + project: project, + plugins: plugins, + cocoapods: cocoaPods, + swiftPackageManager: swiftPackageManager, + fileSystem: fs, + logger: testLogger, + ); + await dependencyManagement.setUp( + platform: platform, + ); + expect(swiftPackageManager.generated, isTrue); + final String xcconfigPrefix = platform == SupportedPlatform.macos ? 'Flutter-' : ''; + expect(testLogger.warningText, contains( + 'All plugins found for ${platform.name} are Swift Packages, ' + 'but your project still has CocoaPods integration. Your ' + 'project uses a non-standard Podfile and will need to be ' + 'migrated to Swift Package Manager manually. Some steps you ' + 'may need to complete include:\n' + ' * In the ${platform.name}/ directory run "pod deintegrate"\n' + ' * Transition any Pod dependencies to Swift Package equivalents. ' + 'See https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app\n' + ' * Transition any custom logic\n' + ' * Remove the include to "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" ' + 'in your ${platform.name}/Flutter/${xcconfigPrefix}Debug.xcconfig\n' + ' * Remove the include to "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" ' + 'in your ${platform.name}/Flutter/${xcconfigPrefix}Release.xcconfig\n\n' + "Removing CocoaPods integration will improve the project's build time.\n" + )); + expect(testLogger.statusText, isEmpty); + expect(cocoaPods.podfileSetup, isFalse); + }); + + testWithoutContext('with mixed plugins', () async { + final MemoryFileSystem fs = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final File cocoapodPluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1.podspec') + ..createSync(recursive: true); + final File swiftPackagePluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1/Package.swift') + ..createSync(recursive: true); + final List plugins = [ + FakePlugin( + name: 'cocoapod_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + pluginPodspecPath: cocoapodPluginPodspec.path, + ), + FakePlugin( + name: 'swift_package_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + pluginSwiftPackageManifestPath: swiftPackagePluginPodspec.path, + ), + FakePlugin( + name: 'neither_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + ), + ]; + final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager( + expectedPlugins: plugins, + ); + final FakeCocoaPods cocoaPods = FakeCocoaPods(); + + final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement( + project: FakeFlutterProject( + usesSwiftPackageManager: true, + fileSystem: fs, + ), + plugins: plugins, + cocoapods: cocoaPods, + swiftPackageManager: swiftPackageManager, + fileSystem: fs, + logger: testLogger, + ); + await dependencyManagement.setUp( + platform: platform, + ); + expect(swiftPackageManager.generated, isTrue); + expect(testLogger.warningText, isEmpty); + expect(testLogger.statusText, isEmpty); + expect(cocoaPods.podfileSetup, isTrue); + }); + }); + + group('when not using Swift Package Manager', () { + testWithoutContext('but project already migrated', () async { + final MemoryFileSystem fs = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final File cocoapodPluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1.podspec') + ..createSync(recursive: true); + final List plugins = [ + FakePlugin( + name: 'cocoapod_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + pluginPodspecPath: cocoapodPluginPodspec.path, + ), + ]; + final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager( + expectedPlugins: plugins, + ); + final FakeCocoaPods cocoaPods = FakeCocoaPods(); + final FakeFlutterProject project = FakeFlutterProject( + usesSwiftPackageManager: true, + fileSystem: fs, + ); + final XcodeBasedProject xcodeProject = platform == SupportedPlatform.ios ? project.ios : project.macos; + xcodeProject.xcodeProjectInfoFile.createSync(recursive: true); + xcodeProject.xcodeProjectInfoFile.writeAsStringSync( + 'FlutterGeneratedPluginSwiftPackage', + ); + + final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement( + project: project, + plugins: plugins, + cocoapods: cocoaPods, + swiftPackageManager: swiftPackageManager, + fileSystem: fs, + logger: testLogger, + ); + await dependencyManagement.setUp( + platform: platform, + ); + expect(swiftPackageManager.generated, isTrue); + expect(testLogger.warningText, isEmpty); + expect(testLogger.statusText, isEmpty); + expect(cocoaPods.podfileSetup, isTrue); + }); + + testWithoutContext('with only CocoaPod plugins', () async { + final MemoryFileSystem fs = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final File cocoapodPluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1.podspec') + ..createSync(recursive: true); + final List plugins = [ + FakePlugin( + name: 'cocoapod_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + pluginPodspecPath: cocoapodPluginPodspec.path, + ), + ]; + final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager( + expectedPlugins: plugins, + ); + final FakeCocoaPods cocoaPods = FakeCocoaPods(); + + final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement( + project: FakeFlutterProject( + fileSystem: fs, + ), + plugins: plugins, + cocoapods: cocoaPods, + swiftPackageManager: swiftPackageManager, + fileSystem: fs, + logger: testLogger, + ); + await dependencyManagement.setUp( + platform: platform, + ); + expect(swiftPackageManager.generated, isFalse); + expect(testLogger.warningText, isEmpty); + expect(testLogger.statusText, isEmpty); + expect(cocoaPods.podfileSetup, isTrue); + }); + + testWithoutContext('with only Swift Package Manager plugins', () async { + final MemoryFileSystem fs = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final File swiftPackagePluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1/Package.swift') + ..createSync(recursive: true); + final List plugins = [ + FakePlugin( + name: 'swift_package_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + pluginSwiftPackageManifestPath: swiftPackagePluginPodspec.path, + ), + ]; + final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager( + expectedPlugins: plugins, + ); + final FakeCocoaPods cocoaPods = FakeCocoaPods(); + + final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement( + project: FakeFlutterProject( + fileSystem: fs, + ), + plugins: plugins, + cocoapods: cocoaPods, + swiftPackageManager: swiftPackageManager, + fileSystem: fs, + logger: testLogger, + ); + await expectLater(() => dependencyManagement.setUp( + platform: platform, + ), + throwsToolExit( + message: 'Plugin swift_package_plugin_1 is only Swift Package Manager compatible. Try ' + 'enabling Swift Package Manager by running ' + '"flutter config --enable-swift-package-manager" or remove the ' + 'plugin as a dependency.', + ), + ); + expect(swiftPackageManager.generated, isFalse); + expect(cocoaPods.podfileSetup, isFalse); + }); + + testWithoutContext('when project is a module', () async { + final MemoryFileSystem fs = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final File cocoapodPluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1.podspec') + ..createSync(recursive: true); + final List plugins = [ + FakePlugin( + name: 'cocoapod_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + pluginPodspecPath: cocoapodPluginPodspec.path, + ), + ]; + final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager( + expectedPlugins: plugins, + ); + final FakeCocoaPods cocoaPods = FakeCocoaPods(); + + final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement( + project: FakeFlutterProject( + fileSystem: fs, + isModule: true, + ), + plugins: plugins, + cocoapods: cocoaPods, + swiftPackageManager: swiftPackageManager, + fileSystem: fs, + logger: testLogger, + ); + await dependencyManagement.setUp( + platform: platform, + ); + expect(swiftPackageManager.generated, isFalse); + expect(testLogger.warningText, isEmpty); + expect(testLogger.statusText, isEmpty); + expect(cocoaPods.podfileSetup, isFalse); + }); + + }); + }); + }); + } + }); +} + +class FakeIosProject extends Fake implements IosProject { + FakeIosProject({ + required MemoryFileSystem fileSystem, + }) : hostAppRoot = fileSystem.directory('app_name').childDirectory('ios'); + + @override + Directory hostAppRoot; + + @override + File get podfile => hostAppRoot.childFile('Podfile'); + + @override + File get podfileLock => hostAppRoot.childFile('Podfile.lock'); + + @override + Directory get xcodeProject => hostAppRoot.childDirectory('Runner.xcodeproj'); + + @override + File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj'); + + @override + bool get flutterPluginSwiftPackageInProjectSettings { + return xcodeProjectInfoFile.existsSync() && + xcodeProjectInfoFile + .readAsStringSync() + .contains('FlutterGeneratedPluginSwiftPackage'); + } + + @override + Directory get managedDirectory => hostAppRoot.childDirectory('Flutter'); + + @override + File xcodeConfigFor(String mode) => managedDirectory.childFile('$mode.xcconfig'); +} + +class FakeMacOSProject extends Fake implements MacOSProject { + FakeMacOSProject({ + required MemoryFileSystem fileSystem, + }) : hostAppRoot = fileSystem.directory('app_name').childDirectory('macos'); + + @override + Directory hostAppRoot; + + @override + File get podfile => hostAppRoot.childFile('Podfile'); + + @override + File get podfileLock => hostAppRoot.childFile('Podfile.lock'); + + @override + Directory get xcodeProject => hostAppRoot.childDirectory('Runner.xcodeproj'); + + @override + File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj'); + + @override + bool get flutterPluginSwiftPackageInProjectSettings { + return xcodeProjectInfoFile.existsSync() && + xcodeProjectInfoFile + .readAsStringSync() + .contains('FlutterGeneratedPluginSwiftPackage'); + } + + @override + Directory get managedDirectory => hostAppRoot.childDirectory('Flutter'); + + @override + File xcodeConfigFor(String mode) => managedDirectory.childFile('Flutter-$mode.xcconfig'); +} + +class FakeFlutterProject extends Fake implements FlutterProject { + FakeFlutterProject({ + required this.fileSystem, + this.usesSwiftPackageManager = false, + this.isModule = false, + }); + + MemoryFileSystem fileSystem; + + + @override + late final IosProject ios = FakeIosProject(fileSystem: fileSystem); + + @override + late final MacOSProject macos = FakeMacOSProject(fileSystem: fileSystem); + + @override + final bool usesSwiftPackageManager; + + @override + final bool isModule; +} + +class FakeSwiftPackageManager extends Fake implements SwiftPackageManager { + FakeSwiftPackageManager({ + this.expectedPlugins, + }); + + bool generated = false; + final List? expectedPlugins; + + @override + Future generatePluginsSwiftPackage( + List plugins, + SupportedPlatform platform, + XcodeBasedProject project, + ) async { + generated = true; + expect(plugins, expectedPlugins); + } +} + +class FakeCocoaPods extends Fake implements CocoaPods { + FakeCocoaPods({ + this.podFile, + this.configIncludesPods = true, + }); + + File? podFile; + + bool podfileSetup = false; + bool addedPodDependencyToFlutterXcconfig = false; + bool configIncludesPods; + + @override + Future setupPodfile(XcodeBasedProject xcodeProject) async { + podfileSetup = true; + } + + @override + void addPodsDependencyToFlutterXcconfig(XcodeBasedProject xcodeProject) { + addedPodDependencyToFlutterXcconfig = true; + } + + @override + Future getPodfileTemplate(XcodeBasedProject xcodeProject, Directory runnerProject) async { + return podFile!; + } + + @override + bool xcconfigIncludesPods(File xcodeConfig) { + return configIncludesPods; + } + + @override + String includePodsXcconfig(String mode) { + return 'Pods/Target Support Files/Pods-Runner/Pods-Runner.${mode + .toLowerCase()}.xcconfig'; + } +} + +class FakePlugin extends Fake implements Plugin { + FakePlugin({ + required this.name, + required this.platforms, + String? pluginSwiftPackageManifestPath, + String? pluginPodspecPath, + }) : _pluginSwiftPackageManifestPath = pluginSwiftPackageManifestPath, + _pluginPodspecPath = pluginPodspecPath; + + final String? _pluginSwiftPackageManifestPath; + + final String? _pluginPodspecPath; + + @override + final String name; + + @override + final Map platforms; + + @override + String? pluginSwiftPackageManifestPath( + FileSystem fileSystem, + String platform, + ) { + return _pluginSwiftPackageManifestPath; + } + + @override + String? pluginPodspecPath( + FileSystem fileSystem, + String platform, + ) { + return _pluginPodspecPath; + } +} + +class FakePluginPlatform extends Fake implements PluginPlatform {} diff --git a/packages/flutter_tools/test/general.shard/macos/swift_package_manager_test.dart b/packages/flutter_tools/test/general.shard/macos/swift_package_manager_test.dart new file mode 100644 index 00000000000..8eb11f017ed --- /dev/null +++ b/packages/flutter_tools/test/general.shard/macos/swift_package_manager_test.dart @@ -0,0 +1,424 @@ +// 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/isolated/mustache_template.dart'; +import 'package:flutter_tools/src/macos/swift_package_manager.dart'; +import 'package:flutter_tools/src/platform_plugins.dart'; +import 'package:flutter_tools/src/plugins.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:test/fake.dart'; + +import '../../src/common.dart'; + +const String _doubleIndent = ' '; + +void main() { + const List supportedPlatforms = [ + SupportedPlatform.ios, + SupportedPlatform.macos, + ]; + + group('SwiftPackageManager', () { + for (final SupportedPlatform platform in supportedPlatforms) { + group('for ${platform.name}', () { + + group('generatePluginsSwiftPackage', () { + testWithoutContext('throw if invalid platform', () async { + final MemoryFileSystem fs = MemoryFileSystem(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: fs, + ); + + final SwiftPackageManager spm = SwiftPackageManager( + fileSystem: fs, + templateRenderer: const MustacheTemplateRenderer(), + ); + + await expectLater(() => spm.generatePluginsSwiftPackage( + [], + SupportedPlatform.android, + project, + ), + throwsToolExit(message: 'The platform android is not compatible with Swift Package Manager. Only iOS and macOS are allowed.'), + ); + }); + + testWithoutContext('skip if no dependencies and not already migrated', () async { + final MemoryFileSystem fs = MemoryFileSystem(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: fs, + ); + + final SwiftPackageManager spm = SwiftPackageManager( + fileSystem: fs, + templateRenderer: const MustacheTemplateRenderer(), + ); + await spm.generatePluginsSwiftPackage( + [], + platform, + project, + ); + + expect(project.flutterPluginSwiftPackageManifest.existsSync(), isFalse); + }); + + testWithoutContext('generate if no dependencies and already migrated', () async { + final MemoryFileSystem fs = MemoryFileSystem(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: fs, + ); + project.xcodeProjectInfoFile.createSync(recursive: true); + project.xcodeProjectInfoFile.writeAsStringSync(''' +' 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };'; +'''); + + final SwiftPackageManager spm = SwiftPackageManager( + fileSystem: fs, + templateRenderer: const MustacheTemplateRenderer(), + ); + await spm.generatePluginsSwiftPackage( + [], + platform, + project, + ); + + final String supportedPlatform = platform == SupportedPlatform.ios ? '.iOS("12.0")' : '.macOS("10.14")'; + expect(project.flutterPluginSwiftPackageManifest.existsSync(), isTrue); + expect(project.flutterPluginSwiftPackageManifest.readAsStringSync(), ''' +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// +// Generated file. Do not edit. +// + +import PackageDescription + +let package = Package( + name: "FlutterGeneratedPluginSwiftPackage", + platforms: [ + $supportedPlatform + ], + products: [ + .library(name: "FlutterGeneratedPluginSwiftPackage", type: .static, targets: ["FlutterGeneratedPluginSwiftPackage"]) + ], + dependencies: [ +$_doubleIndent + ], + targets: [ + .target( + name: "FlutterGeneratedPluginSwiftPackage" + ) + ] +) +'''); + }); + + testWithoutContext('generate with single dependency', () async { + final MemoryFileSystem fs = MemoryFileSystem(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: fs, + ); + + final File validPlugin1Manifest = fs.file('/local/path/to/plugins/valid_plugin_1/Package.swift')..createSync(recursive: true); + final FakePlugin validPlugin1 = FakePlugin( + name: 'valid_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + pluginSwiftPackageManifestPath: validPlugin1Manifest.path, + ); + final SwiftPackageManager spm = SwiftPackageManager( + fileSystem: fs, + templateRenderer: const MustacheTemplateRenderer(), + ); + await spm.generatePluginsSwiftPackage( + [validPlugin1], + platform, + project, + ); + + final String supportedPlatform = platform == SupportedPlatform.ios ? '.iOS("12.0")' : '.macOS("10.14")'; + expect(project.flutterPluginSwiftPackageManifest.existsSync(), isTrue); + expect(project.flutterPluginSwiftPackageManifest.readAsStringSync(), ''' +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// +// Generated file. Do not edit. +// + +import PackageDescription + +let package = Package( + name: "FlutterGeneratedPluginSwiftPackage", + platforms: [ + $supportedPlatform + ], + products: [ + .library(name: "FlutterGeneratedPluginSwiftPackage", type: .static, targets: ["FlutterGeneratedPluginSwiftPackage"]) + ], + dependencies: [ + .package(name: "valid_plugin_1", path: "/local/path/to/plugins/valid_plugin_1") + ], + targets: [ + .target( + name: "FlutterGeneratedPluginSwiftPackage", + dependencies: [ + .product(name: "valid-plugin-1", package: "valid_plugin_1") + ] + ) + ] +) +'''); + }); + + testWithoutContext('generate with multiple dependencies', () async { + final MemoryFileSystem fs = MemoryFileSystem(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: fs, + ); + final FakePlugin nonPlatformCompatiblePlugin = FakePlugin( + name: 'invalid_plugin_due_to_incompatible_platform', + platforms: {}, + pluginSwiftPackageManifestPath: '/some/path', + ); + final FakePlugin pluginSwiftPackageManifestIsNull = FakePlugin( + name: 'invalid_plugin_due_to_null_plugin_swift_package_path', + platforms: {platform.name: FakePluginPlatform()}, + pluginSwiftPackageManifestPath: null, + ); + final FakePlugin pluginSwiftPackageManifestNotExists = FakePlugin( + name: 'invalid_plugin_due_to_plugin_swift_package_path_does_not_exist', + platforms: {platform.name: FakePluginPlatform()}, + pluginSwiftPackageManifestPath: '/some/path', + ); + + final File validPlugin1Manifest = fs.file('/local/path/to/plugins/valid_plugin_1/Package.swift')..createSync(recursive: true); + final FakePlugin validPlugin1 = FakePlugin( + name: 'valid_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + pluginSwiftPackageManifestPath: validPlugin1Manifest.path, + ); + final File validPlugin2Manifest = fs.file('/.pub-cache/plugins/valid_plugin_2/Package.swift')..createSync(recursive: true); + final FakePlugin validPlugin2 = FakePlugin( + name: 'valid_plugin_2', + platforms: {platform.name: FakePluginPlatform()}, + pluginSwiftPackageManifestPath: validPlugin2Manifest.path, + ); + + final SwiftPackageManager spm = SwiftPackageManager( + fileSystem: fs, + templateRenderer: const MustacheTemplateRenderer(), + ); + await spm.generatePluginsSwiftPackage( + [ + nonPlatformCompatiblePlugin, + pluginSwiftPackageManifestIsNull, + pluginSwiftPackageManifestNotExists, + validPlugin1, + validPlugin2, + ], + platform, + project, + ); + + final String supportedPlatform = platform == SupportedPlatform.ios + ? '.iOS("12.0")' + : '.macOS("10.14")'; + expect(project.flutterPluginSwiftPackageManifest.existsSync(), isTrue); + expect(project.flutterPluginSwiftPackageManifest.readAsStringSync(), ''' +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// +// Generated file. Do not edit. +// + +import PackageDescription + +let package = Package( + name: "FlutterGeneratedPluginSwiftPackage", + platforms: [ + $supportedPlatform + ], + products: [ + .library(name: "FlutterGeneratedPluginSwiftPackage", type: .static, targets: ["FlutterGeneratedPluginSwiftPackage"]) + ], + dependencies: [ + .package(name: "valid_plugin_1", path: "/local/path/to/plugins/valid_plugin_1"), + .package(name: "valid_plugin_2", path: "/.pub-cache/plugins/valid_plugin_2") + ], + targets: [ + .target( + name: "FlutterGeneratedPluginSwiftPackage", + dependencies: [ + .product(name: "valid-plugin-1", package: "valid_plugin_1"), + .product(name: "valid-plugin-2", package: "valid_plugin_2") + ] + ) + ] +) +'''); + }); + }); + + group('updateMinimumDeployment', () { + testWithoutContext('return if invalid deploymentTarget', () { + final MemoryFileSystem fs = MemoryFileSystem(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: fs, + ); + final String supportedPlatform = platform == SupportedPlatform.ios ? '.iOS("12.0")' : '.macOS("10.14")'; + project.flutterPluginSwiftPackageManifest.createSync(recursive: true); + project.flutterPluginSwiftPackageManifest.writeAsStringSync(supportedPlatform); + SwiftPackageManager.updateMinimumDeployment( + project: project, + platform: platform, + deploymentTarget: '', + ); + expect( + project.flutterPluginSwiftPackageManifest.readAsLinesSync(), + contains(supportedPlatform), + ); + }); + + testWithoutContext('return if deploymentTarget is lower than default', () { + final MemoryFileSystem fs = MemoryFileSystem(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: fs, + ); + final String supportedPlatform = platform == SupportedPlatform.ios ? '.iOS("12.0")' : '.macOS("10.14")'; + project.flutterPluginSwiftPackageManifest.createSync(recursive: true); + project.flutterPluginSwiftPackageManifest.writeAsStringSync(supportedPlatform); + SwiftPackageManager.updateMinimumDeployment( + project: project, + platform: platform, + deploymentTarget: '9.0', + ); + expect( + project.flutterPluginSwiftPackageManifest.readAsLinesSync(), + contains(supportedPlatform), + ); + }); + + testWithoutContext('return if deploymentTarget is same than default', () { + final MemoryFileSystem fs = MemoryFileSystem(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: fs, + ); + final String supportedPlatform = platform == SupportedPlatform.ios ? '.iOS("12.0")' : '.macOS("10.14")'; + project.flutterPluginSwiftPackageManifest.createSync(recursive: true); + project.flutterPluginSwiftPackageManifest.writeAsStringSync(supportedPlatform); + SwiftPackageManager.updateMinimumDeployment( + project: project, + platform: platform, + deploymentTarget: platform == SupportedPlatform.ios ? '12.0' : '10.14', + ); + expect( + project.flutterPluginSwiftPackageManifest.readAsLinesSync(), + contains(supportedPlatform), + ); + }); + + testWithoutContext('update if deploymentTarget is higher than default', () { + final MemoryFileSystem fs = MemoryFileSystem(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: fs, + ); + final String supportedPlatform = platform == SupportedPlatform.ios ? '.iOS("12.0")' : '.macOS("10.14")'; + project.flutterPluginSwiftPackageManifest.createSync(recursive: true); + project.flutterPluginSwiftPackageManifest.writeAsStringSync(supportedPlatform); + SwiftPackageManager.updateMinimumDeployment( + project: project, + platform: platform, + deploymentTarget: '14.0', + ); + expect( + project.flutterPluginSwiftPackageManifest + .readAsLinesSync() + .contains(supportedPlatform), + isFalse, + ); + expect( + project.flutterPluginSwiftPackageManifest.readAsLinesSync(), + contains(platform == SupportedPlatform.ios ? '.iOS("14.0")' : '.macOS("14.0")'), + ); + }); + }); + }); + } + }); +} + +class FakeXcodeProject extends Fake implements IosProject { + FakeXcodeProject({ + required MemoryFileSystem fileSystem, + required String platform, + }) : hostAppRoot = fileSystem.directory('app_name').childDirectory(platform); + + @override + Directory hostAppRoot; + + @override + Directory get xcodeProject => hostAppRoot.childDirectory('$hostAppProjectName.xcodeproj'); + + @override + File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj'); + + @override + String hostAppProjectName = 'Runner'; + + @override + Directory get flutterPluginSwiftPackageDirectory => hostAppRoot + .childDirectory('Flutter') + .childDirectory('ephemeral') + .childDirectory('Packages') + .childDirectory('FlutterGeneratedPluginSwiftPackage'); + + @override + File get flutterPluginSwiftPackageManifest => + flutterPluginSwiftPackageDirectory.childFile('Package.swift'); + + @override + bool get flutterPluginSwiftPackageInProjectSettings { + return xcodeProjectInfoFile.existsSync() && + xcodeProjectInfoFile + .readAsStringSync() + .contains('FlutterGeneratedPluginSwiftPackage'); + } +} + +class FakePlugin extends Fake implements Plugin { + FakePlugin({ + required this.name, + required this.platforms, + required String? pluginSwiftPackageManifestPath, + }) : _pluginSwiftPackageManifestPath = pluginSwiftPackageManifestPath; + + final String? _pluginSwiftPackageManifestPath; + + @override + final String name; + + @override + final Map platforms; + + @override + String? pluginSwiftPackageManifestPath( + FileSystem fileSystem, + String platform, + ) { + return _pluginSwiftPackageManifestPath; + } +} + +class FakePluginPlatform extends Fake implements PluginPlatform {} diff --git a/packages/flutter_tools/test/general.shard/macos/swift_packages_test.dart b/packages/flutter_tools/test/general.shard/macos/swift_packages_test.dart new file mode 100644 index 00000000000..89e6674496a --- /dev/null +++ b/packages/flutter_tools/test/general.shard/macos/swift_packages_test.dart @@ -0,0 +1,375 @@ +// 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/version.dart'; +import 'package:flutter_tools/src/isolated/mustache_template.dart'; +import 'package:flutter_tools/src/macos/swift_packages.dart'; + +import '../../src/common.dart'; + +const String _doubleIndent = ' '; + +void main() { + group('SwiftPackage', () { + testWithoutContext('createSwiftPackage also creates source file for each default target', () { + final MemoryFileSystem fs = MemoryFileSystem(); + final File swiftPackageFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Package.swift'); + const String target1Name = 'Target1'; + const String target2Name = 'Target2'; + final File target1SourceFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Sources/$target1Name/$target1Name.swift'); + final File target2SourceFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Sources/$target2Name/$target2Name.swift'); + final SwiftPackage swiftPackage = SwiftPackage( + manifest: swiftPackageFile, + name: 'FlutterGeneratedPluginSwiftPackage', + platforms: [], + products: [], + dependencies: [], + targets: [ + SwiftPackageTarget.defaultTarget(name: target1Name), + SwiftPackageTarget.defaultTarget(name: 'Target2'), + ], + templateRenderer: const MustacheTemplateRenderer(), + ); + swiftPackage.createSwiftPackage(); + expect(swiftPackageFile.existsSync(), isTrue); + expect(target1SourceFile.existsSync(), isTrue); + expect(target2SourceFile.existsSync(), isTrue); + }); + + testWithoutContext('createSwiftPackage also creates source file for binary target', () { + final MemoryFileSystem fs = MemoryFileSystem(); + final File swiftPackageFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Package.swift'); + final SwiftPackage swiftPackage = SwiftPackage( + manifest: swiftPackageFile, + name: 'FlutterGeneratedPluginSwiftPackage', + platforms: [], + products: [], + dependencies: [], + targets: [ + SwiftPackageTarget.binaryTarget(name: 'BinaryTarget', relativePath: ''), + ], + templateRenderer: const MustacheTemplateRenderer(), + ); + swiftPackage.createSwiftPackage(); + expect(swiftPackageFile.existsSync(), isTrue); + expect(fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Sources/BinaryTarget/BinaryTarget.swift').existsSync(), isFalse); + }); + + testWithoutContext('createSwiftPackage does not creates source file if already exists', () { + final MemoryFileSystem fs = MemoryFileSystem(); + final File swiftPackageFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Package.swift'); + const String target1Name = 'Target1'; + const String target2Name = 'Target2'; + final File target1SourceFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Sources/$target1Name/$target1Name.swift'); + final File target2SourceFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Sources/$target2Name/$target2Name.swift'); + + fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Sources/$target1Name/SomeSourceFile.swift').createSync(recursive: true); + fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Sources/$target2Name/SomeSourceFile.swift').createSync(recursive: true); + + final SwiftPackage swiftPackage = SwiftPackage( + manifest: swiftPackageFile, + name: 'FlutterGeneratedPluginSwiftPackage', + platforms: [], + products: [], + dependencies: [], + targets: [ + SwiftPackageTarget.defaultTarget(name: target1Name), + SwiftPackageTarget.defaultTarget(name: 'Target2'), + ], + templateRenderer: const MustacheTemplateRenderer(), + ); + swiftPackage.createSwiftPackage(); + expect(swiftPackageFile.existsSync(), isTrue); + expect(target1SourceFile.existsSync(), isFalse); + expect(target2SourceFile.existsSync(), isFalse); + }); + + group('create Package.swift from template', () { + testWithoutContext('with none in each field', () { + final MemoryFileSystem fs = MemoryFileSystem(); + final File swiftPackageFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Package.swift'); + final SwiftPackage swiftPackage = SwiftPackage( + manifest: swiftPackageFile, + name: 'FlutterGeneratedPluginSwiftPackage', + platforms: [], + products: [], + dependencies: [], + targets: [], + templateRenderer: const MustacheTemplateRenderer(), + ); + swiftPackage.createSwiftPackage(); + expect(swiftPackageFile.readAsStringSync(), ''' +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// +// Generated file. Do not edit. +// + +import PackageDescription + +let package = Package( + name: "FlutterGeneratedPluginSwiftPackage", + products: [ +$_doubleIndent + ], + dependencies: [ +$_doubleIndent + ], + targets: [ +$_doubleIndent + ] +) +'''); + }); + + testWithoutContext('with single in each field', () { + final MemoryFileSystem fs = MemoryFileSystem(); + final File swiftPackageFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Package.swift'); + final SwiftPackage swiftPackage = SwiftPackage( + manifest: swiftPackageFile, + name: 'FlutterGeneratedPluginSwiftPackage', + platforms: [ + SwiftPackageSupportedPlatform(platform: SwiftPackagePlatform.ios, version: Version(12, 0, null)), + ], + products: [ + SwiftPackageProduct(name: 'Product1', targets: ['Target1']), + ], + dependencies: [ + SwiftPackagePackageDependency(name: 'Dependency1', path: '/path/to/dependency1'), + ], + targets: [ + SwiftPackageTarget.defaultTarget( + name: 'Target1', + dependencies: [ + SwiftPackageTargetDependency.product(name: 'TargetDependency1', packageName: 'TargetDependency1Package'), + ], + ) + ], + templateRenderer: const MustacheTemplateRenderer(), + ); + swiftPackage.createSwiftPackage(); + expect(swiftPackageFile.readAsStringSync(), ''' +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// +// Generated file. Do not edit. +// + +import PackageDescription + +let package = Package( + name: "FlutterGeneratedPluginSwiftPackage", + platforms: [ + .iOS("12.0") + ], + products: [ + .library(name: "Product1", targets: ["Target1"]) + ], + dependencies: [ + .package(name: "Dependency1", path: "/path/to/dependency1") + ], + targets: [ + .target( + name: "Target1", + dependencies: [ + .product(name: "TargetDependency1", package: "TargetDependency1Package") + ] + ) + ] +) +'''); + }); + + testWithoutContext('with multiple in each field', () { + final MemoryFileSystem fs = MemoryFileSystem(); + final File swiftPackageFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Package.swift'); + final SwiftPackage swiftPackage = SwiftPackage( + manifest: swiftPackageFile, + name: 'FlutterGeneratedPluginSwiftPackage', + platforms: [ + SwiftPackageSupportedPlatform(platform: SwiftPackagePlatform.ios, version: Version(12, 0, null)), + SwiftPackageSupportedPlatform(platform: SwiftPackagePlatform.macos, version: Version(10, 14, null)), + ], + products: [ + SwiftPackageProduct(name: 'Product1', targets: ['Target1']), + SwiftPackageProduct(name: 'Product2', targets: ['Target2']) + ], + dependencies: [ + SwiftPackagePackageDependency(name: 'Dependency1', path: '/path/to/dependency1'), + SwiftPackagePackageDependency(name: 'Dependency2', path: '/path/to/dependency2'), + ], + targets: [ + SwiftPackageTarget.binaryTarget(name: 'Target1', relativePath: '/path/to/target1'), + SwiftPackageTarget.defaultTarget( + name: 'Target2', + dependencies: [ + SwiftPackageTargetDependency.target(name: 'TargetDependency1'), + SwiftPackageTargetDependency.product(name: 'TargetDependency2', packageName: 'TargetDependency2Package'), + ], + ) + ], + templateRenderer: const MustacheTemplateRenderer(), + ); + swiftPackage.createSwiftPackage(); + expect(swiftPackageFile.readAsStringSync(), ''' +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// +// Generated file. Do not edit. +// + +import PackageDescription + +let package = Package( + name: "FlutterGeneratedPluginSwiftPackage", + platforms: [ + .iOS("12.0"), + .macOS("10.14") + ], + products: [ + .library(name: "Product1", targets: ["Target1"]), + .library(name: "Product2", targets: ["Target2"]) + ], + dependencies: [ + .package(name: "Dependency1", path: "/path/to/dependency1"), + .package(name: "Dependency2", path: "/path/to/dependency2") + ], + targets: [ + .binaryTarget( + name: "Target1", + path: "/path/to/target1" + ), + .target( + name: "Target2", + dependencies: [ + .target(name: "TargetDependency1"), + .product(name: "TargetDependency2", package: "TargetDependency2Package") + ] + ) + ] +) +'''); + }); + }); + }); + + testWithoutContext('Format SwiftPackageSupportedPlatform', () { + final SwiftPackageSupportedPlatform supportedPlatform = SwiftPackageSupportedPlatform( + platform: SwiftPackagePlatform.ios, + version: Version(17, 0, null), + ); + expect(supportedPlatform.format(), '.iOS("17.0")'); + }); + + group('Format SwiftPackageProduct', () { + testWithoutContext('without targets and libraryType', () { + final SwiftPackageProduct product = SwiftPackageProduct( + name: 'ProductName', + targets: [], + ); + expect(product.format(), '.library(name: "ProductName")'); + }); + + testWithoutContext('with targets', () { + final SwiftPackageProduct singleProduct = SwiftPackageProduct( + name: 'ProductName', + targets: ['Target1'], + ); + expect(singleProduct.format(), '.library(name: "ProductName", targets: ["Target1"])'); + + final SwiftPackageProduct multipleProducts = SwiftPackageProduct( + name: 'ProductName', + targets: ['Target1', 'Target2'], + ); + expect(multipleProducts.format(), '.library(name: "ProductName", targets: ["Target1", "Target2"])'); + }); + + testWithoutContext('with libraryType', () { + final SwiftPackageProduct product = SwiftPackageProduct( + name: 'ProductName', + targets: [], + libraryType: SwiftPackageLibraryType.dynamic, + ); + expect(product.format(), '.library(name: "ProductName", type: .dynamic)'); + }); + + testWithoutContext('with targets and libraryType', () { + final SwiftPackageProduct product = SwiftPackageProduct( + name: 'ProductName', + targets: ['Target1', 'Target2'], + libraryType: SwiftPackageLibraryType.dynamic, + ); + expect(product.format(), '.library(name: "ProductName", type: .dynamic, targets: ["Target1", "Target2"])'); + }); + }); + + testWithoutContext('Format SwiftPackagePackageDependency', () { + final SwiftPackagePackageDependency supportedPlatform = SwiftPackagePackageDependency( + name: 'DependencyName', + path: '/path/to/dependency', + ); + expect(supportedPlatform.format(), '.package(name: "DependencyName", path: "/path/to/dependency")'); + }); + + group('Format SwiftPackageTarget', () { + testWithoutContext('as default target with multiple SwiftPackageTargetDependency', () { + final SwiftPackageTarget product = SwiftPackageTarget.defaultTarget( + name: 'ProductName', + dependencies: [ + SwiftPackageTargetDependency.target(name: 'Dependency1'), + SwiftPackageTargetDependency.product(name: 'Dependency2', packageName: 'Dependency2Package'), + ], + ); + expect(product.format(), ''' +.target( + name: "ProductName", + dependencies: [ + .target(name: "Dependency1"), + .product(name: "Dependency2", package: "Dependency2Package") + ] + )'''); + }); + + testWithoutContext('as default target with no SwiftPackageTargetDependency', () { + final SwiftPackageTarget product = SwiftPackageTarget.defaultTarget( + name: 'ProductName', + ); + expect(product.format(), ''' +.target( + name: "ProductName" + )'''); + }); + + testWithoutContext('as binaryTarget', () { + final SwiftPackageTarget product = SwiftPackageTarget.binaryTarget( + name: 'ProductName', + relativePath: '/path/to/target', + ); + expect(product.format(), ''' +.binaryTarget( + name: "ProductName", + path: "/path/to/target" + )'''); + }); + }); + + group('Format SwiftPackageTargetDependency', () { + testWithoutContext('with only name', () { + final SwiftPackageTargetDependency targetDependency = SwiftPackageTargetDependency.target( + name: 'DependencyName', + ); + expect(targetDependency.format(), ' .target(name: "DependencyName")'); + }); + + testWithoutContext('with name and package', () { + final SwiftPackageTargetDependency targetDependency = SwiftPackageTargetDependency.product( + name: 'DependencyName', + packageName: 'PackageName', + ); + expect(targetDependency.format(), ' .product(name: "DependencyName", package: "PackageName")'); + }); + }); +} diff --git a/packages/flutter_tools/test/general.shard/migrations/swift_package_manager_integration_migration_test.dart b/packages/flutter_tools/test/general.shard/migrations/swift_package_manager_integration_migration_test.dart new file mode 100644 index 00000000000..74f60ec85d8 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/migrations/swift_package_manager_integration_migration_test.dart @@ -0,0 +1,3270 @@ +// 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/common.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/ios/plist_parser.dart'; +import 'package:flutter_tools/src/ios/xcodeproj.dart'; +import 'package:flutter_tools/src/migrations/swift_package_manager_integration_migration.dart'; + +import 'package:flutter_tools/src/project.dart'; +import 'package:test/fake.dart'; + +import '../../src/common.dart'; + +const List supportedPlatforms = [ + SupportedPlatform.ios, + SupportedPlatform.macos +]; + +void main() { + group('Flutter Package Migration', () { + testWithoutContext('fails if Xcode project not found', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + FakeXcodeProject( + platform: SupportedPlatform.ios.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ), + SupportedPlatform.ios, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Xcode project not found.'), + ); + expect(testLogger.traceText, isEmpty); + expect(testLogger.statusText, isEmpty); + }); + + 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, SupportedPlatform.ios); + project._projectInfo = null; + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + SupportedPlatform.ios, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to get Xcode project info.'), + ); + expect(testLogger.traceText, isEmpty); + expect(testLogger.statusText, isEmpty); + }); + + 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, SupportedPlatform.ios); + project.xcodeWorkspace = null; + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + SupportedPlatform.ios, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Xcode workspace not found.'), + ); + expect(testLogger.traceText, isEmpty); + expect(testLogger.statusText, isEmpty); + }); + + 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, SupportedPlatform.ios); + project._projectInfo = XcodeProjectInfo( + [], + [], + [], + testLogger, + ); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + SupportedPlatform.ios, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'You must specify a --flavor option to select one of the available schemes.'), + ); + expect(testLogger.traceText, isEmpty); + expect(testLogger.statusText, isEmpty); + }); + + 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, + SupportedPlatform.ios, + createSchemeFile: false, + ); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + SupportedPlatform.ios, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to get scheme file for Runner.'), + ); + expect(testLogger.traceText, isEmpty); + expect(testLogger.statusText, isEmpty); + }); + }); + + testWithoutContext('does not migrate if 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, SupportedPlatform.ios); + project.xcodeProjectSchemeFile().writeAsStringSync( + _validBuildActions(SupportedPlatform.ios, hasFrameworkScript: true), + ); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(_allSectionsMigrated(SupportedPlatform.ios)), + ); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + SupportedPlatform.ios, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(), + ); + await projectMigration.migrate(); + expect(testLogger.traceText, isEmpty); + expect(testLogger.statusText, isEmpty); + expect(testLogger.warningText, isEmpty); + expect(testLogger.errorText, isEmpty); + }); + + group('migrate scheme', () { + testWithoutContext('skipped if already updated', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: SupportedPlatform.ios.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, SupportedPlatform.ios); + project.xcodeProjectSchemeFile().writeAsStringSync( + _validBuildActions(SupportedPlatform.ios, hasFrameworkScript: true), + ); + + project.xcodeProjectInfoFile.writeAsStringSync(''); + + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsMigratedAsJson(SupportedPlatform.ios), + ]; + settingsAsJsonBeforeMigration.removeAt(_buildFileSectionIndex); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + SupportedPlatform.ios, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput(settingsAsJsonBeforeMigration), + ), + ); + await expectLater(() => projectMigration.migrate(), throwsToolExit()); + expect( + testLogger.traceText, + contains('Runner.xcscheme already migrated. Skipping...'), + ); + }); + + for (final SupportedPlatform platform in supportedPlatforms) { + group('for ${platform.name}', () { + testWithoutContext('fails if scheme is missing BlueprintIdentifier for Runner native target', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + project.xcodeProjectSchemeFile().writeAsStringSync(''); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Failed to parse Runner.xcscheme: Could not find BuildableReference'), + ); + }); + + testWithoutContext('fails if BuildableName does not follow BlueprintIdentifier in scheme', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + project.xcodeProjectSchemeFile().writeAsStringSync(''' + + +''' + ); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(), + ); + + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Failed to parse Runner.xcscheme: Could not find BuildableName'), + ); + }); + + testWithoutContext('fails if BlueprintName does not follow BuildableName in scheme', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + project.xcodeProjectSchemeFile().writeAsStringSync(''' + + +''' + ); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(), + ); + + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Failed to parse Runner.xcscheme: Could not find BlueprintName'), + ); + }); + + testWithoutContext('fails if ReferencedContainer does not follow BlueprintName in scheme', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + project.xcodeProjectSchemeFile().writeAsStringSync(''' + + +''' + ); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(), + ); + + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Failed to parse Runner.xcscheme: Could not find ReferencedContainer'), + ); + }); + + testWithoutContext('fails if cannot find BuildActionEntries in scheme', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + project.xcodeProjectSchemeFile().writeAsStringSync( + _validBuildableReference(platform), + ); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(), + ); + + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Failed to parse Runner.xcscheme: Could not find BuildActionEntries'), + ); + }); + + testWithoutContext('fails if updated scheme is not valid xml', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + project.xcodeProjectSchemeFile().writeAsStringSync( + '${_validBuildActions(platform)} ', + ); + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(), + ); + + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Failed to parse Runner.xcscheme: Invalid xml:'), + ); + }); + + testWithoutContext('successfully updates scheme with preexisting PreActions', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + project.xcodeProjectSchemeFile().writeAsStringSync( + _validBuildActions(platform, hasPreActions: true), + ); + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(_allSectionsMigratedAsJson(platform)), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + + await projectMigration.migrate(); + expect( + project.xcodeProjectSchemeFile().readAsStringSync(), + _validBuildActions(platform, hasFrameworkScript: true), + ); + }); + + testWithoutContext('successfully updates scheme with no preexisting PreActions', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + project.xcodeProjectSchemeFile().writeAsStringSync( + _validBuildActions(platform), + ); + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(_allSectionsMigratedAsJson(platform)), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + + await projectMigration.migrate(); + expect( + project.xcodeProjectSchemeFile().readAsStringSync(), + _validBuildActions(platform, hasFrameworkScript: true), + ); + }); + }); + } + }); + + group('migrate pbxproj', () { + testWithoutContext('skipped if already updated', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: SupportedPlatform.ios.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, SupportedPlatform.ios); + + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(_allSectionsMigrated(SupportedPlatform.ios)), + ); + + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsMigratedAsJson(SupportedPlatform.ios), + ]; + settingsAsJsonBeforeMigration.removeAt(_buildFileSectionIndex); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + SupportedPlatform.ios, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput(settingsAsJsonBeforeMigration), + ), + ); + await projectMigration.migrate(); + expect( + testLogger.traceText, + contains('project.pbxproj already migrated. Skipping...'), + ); + }); + + group('fails if parsing project.pbxproj', () { + testWithoutContext('fails plutil command', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: SupportedPlatform.ios.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, SupportedPlatform.ios); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + SupportedPlatform.ios, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Failed to parse project settings.'), + ); + }); + + testWithoutContext('returns unexpected JSON', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: SupportedPlatform.ios.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, SupportedPlatform.ios); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + SupportedPlatform.ios, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(json: '[]'), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'project.pbxproj returned unexpected JSON response'), + ); + }); + + testWithoutContext('returns non-JSON', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: SupportedPlatform.ios.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, SupportedPlatform.ios); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + SupportedPlatform.ios, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(json: 'this is not json'), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'project.pbxproj returned non-JSON response'), + ); + }); + }); + + group('fails if duplicate id', () { + testWithoutContext('for PBXBuildFile', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: SupportedPlatform.ios.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, SupportedPlatform.ios); + project.xcodeProjectInfoFile.writeAsStringSync('78A318202AECB46A00862997'); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + SupportedPlatform.ios, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(), + ); + expect( + () => projectMigration.migrate(), + throwsToolExit(message: 'Duplicate id found for PBXBuildFile'), + ); + }); + + testWithoutContext('for XCSwiftPackageProductDependency', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: SupportedPlatform.ios.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, SupportedPlatform.ios); + project.xcodeProjectInfoFile.writeAsStringSync('78A3181F2AECB46A00862997'); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + SupportedPlatform.ios, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(), + ); + expect( + () => projectMigration.migrate(), + throwsToolExit(message: 'Duplicate id found for XCSwiftPackageProductDependency'), + ); + }); + + testWithoutContext('for XCLocalSwiftPackageReference', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: SupportedPlatform.ios.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, SupportedPlatform.ios); + project.xcodeProjectInfoFile.writeAsStringSync('781AD8BC2B33823900A9FFBB'); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + SupportedPlatform.ios, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser(), + ); + expect( + () => projectMigration.migrate(), + throwsToolExit(message: 'Duplicate id found for XCLocalSwiftPackageReference'), + ); + }); + }); + + for (final SupportedPlatform platform in supportedPlatforms) { + group('for ${platform.name}', () { + group('migrate PBXBuildFile', () { + testWithoutContext('fails if missing Begin PBXBuildFile section', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput([]), + ), + ); + expect( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to find beginning of PBXBuildFile section'), + ); + }); + + testWithoutContext('fails if missing End PBXBuildFile section', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_buildFileSectionIndex] = ''' +/* Begin PBXBuildFile section */ +'''; + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsMigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration[_buildFileSectionIndex] = unmigratedBuildFileSectionAsJson; + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput([]), + ), + ); + expect( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to find end of PBXBuildFile section'), + ); + }); + + testWithoutContext('fails if End before Begin for PBXBuildFile section', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_buildFileSectionIndex] = ''' +/* End PBXBuildFile section */ +/* Begin PBXBuildFile section */ +'''; + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsMigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration[_buildFileSectionIndex] = unmigratedBuildFileSectionAsJson; + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput([]), + ), + ); + expect( + () => projectMigration.migrate(), + throwsToolExit(message: 'Found the end of PBXBuildFile section before the beginning.'), + ); + }); + + testWithoutContext('successfully added', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_buildFileSectionIndex] = unmigratedBuildFileSection; + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsMigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration[_buildFileSectionIndex] = unmigratedBuildFileSectionAsJson; + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(settingsAsJsonBeforeMigration), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await projectMigration.migrate(); + expect(testLogger.errorText, isEmpty); + expect( + testLogger.traceText.contains('PBXBuildFile already migrated. Skipping...'), + isFalse, + ); + settingsBeforeMigration[_buildFileSectionIndex] = migratedBuildFileSection; + expect( + project.xcodeProjectInfoFile.readAsStringSync(), + _projectSettings(settingsBeforeMigration), + ); + expect(plistParser.hasRemainingExpectations, isFalse); + }); + }); + + group('migrate PBXFrameworksBuildPhase', () { + testWithoutContext('fails if missing PBXFrameworksBuildPhase section', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration.removeAt(_frameworksBuildPhaseSectionIndex); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsMigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration.removeAt(_frameworksBuildPhaseSectionIndex); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput(settingsAsJsonBeforeMigration), + ), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to find beginning of PBXFrameworksBuildPhase section'), + ); + }); + + testWithoutContext('fails if missing Runner target subsection following PBXFrameworksBuildPhase begin header', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_frameworksBuildPhaseSectionIndex] = ''' +/* Begin PBXFrameworksBuildPhase section */ +/* End PBXFrameworksBuildPhase section */ +'''; + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsMigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration.removeAt(_frameworksBuildPhaseSectionIndex); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput(settingsAsJsonBeforeMigration), + ), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to find PBXFrameworksBuildPhase for Runner target'), + ); + }); + + testWithoutContext('fails if missing Runner target subsection before PBXFrameworksBuildPhase end header', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_frameworksBuildPhaseSectionIndex] = ''' +/* Begin PBXFrameworksBuildPhase section */ +/* End PBXFrameworksBuildPhase section */ +/* Begin NonExistant section */ + ${_runnerFrameworksBuildPhaseIdentifer(platform)} /* Frameworks */ = { + }; +/* End NonExistant section */ +'''; + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsMigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration.removeAt(_frameworksBuildPhaseSectionIndex); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput(settingsAsJsonBeforeMigration), + ), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to find PBXFrameworksBuildPhase for Runner target'), + ); + }); + + testWithoutContext('fails if missing Runner target in parsed settings', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_frameworksBuildPhaseSectionIndex] = unmigratedFrameworksBuildPhaseSection(platform); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsMigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration.removeAt(_frameworksBuildPhaseSectionIndex); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput(settingsAsJsonBeforeMigration), + ), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to find parsed PBXFrameworksBuildPhase for Runner target'), + ); + }); + + testWithoutContext('successfully added when files field is missing', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_frameworksBuildPhaseSectionIndex] = unmigratedFrameworksBuildPhaseSection( + platform, + missingFiles: true, + ); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration[_frameworksBuildPhaseSectionIndex] = unmigratedFrameworksBuildPhaseSectionAsJson( + platform, + missingFiles: true, + ); + final List expectedSettings = [ + ..._allSectionsMigrated(platform), + ]; + expectedSettings[_frameworksBuildPhaseSectionIndex] = migratedFrameworksBuildPhaseSection( + platform, + missingFiles: true, + ); + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(settingsAsJsonBeforeMigration), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await projectMigration.migrate(); + expect(testLogger.errorText, isEmpty); + expect( + testLogger.traceText.contains('PBXFrameworksBuildPhase already migrated. Skipping...'), + isFalse, + ); + expect( + project.xcodeProjectInfoFile.readAsStringSync(), + _projectSettings(expectedSettings), + ); + expect(plistParser.hasRemainingExpectations, isFalse); + }); + + testWithoutContext('successfully added when files field is empty', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(settingsAsJsonBeforeMigration), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await projectMigration.migrate(); + expect(testLogger.errorText, isEmpty); + expect( + testLogger.traceText.contains('PBXFrameworksBuildPhase already migrated. Skipping...'), + isFalse, + ); + expect( + project.xcodeProjectInfoFile.readAsStringSync(), + _projectSettings(_allSectionsMigrated(platform)), + ); + expect(plistParser.hasRemainingExpectations, isFalse); + }); + + testWithoutContext('successfully added when files field is not empty', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_frameworksBuildPhaseSectionIndex] = unmigratedFrameworksBuildPhaseSection( + platform, + withCocoapods: true, + ); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration[_frameworksBuildPhaseSectionIndex] = unmigratedFrameworksBuildPhaseSectionAsJson( + platform, + withCocoapods: true, + ); + final List expectedSettings = [ + ..._allSectionsMigrated(platform), + ]; + expectedSettings[_frameworksBuildPhaseSectionIndex] = migratedFrameworksBuildPhaseSection( + platform, + withCocoapods: true, + ); + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(settingsAsJsonBeforeMigration), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await projectMigration.migrate(); + expect(testLogger.errorText, isEmpty); + expect( + testLogger.traceText.contains('PBXFrameworksBuildPhase already migrated. Skipping...'), + isFalse, + ); + expect( + project.xcodeProjectInfoFile.readAsStringSync(), + _projectSettings(expectedSettings), + ); + expect(plistParser.hasRemainingExpectations, isFalse); + }); + }); + + group('migrate PBXNativeTarget', () { + testWithoutContext('fails if missing PBXNativeTarget section', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration.removeAt(_nativeTargetSectionIndex); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration.removeAt(_nativeTargetSectionIndex); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput(settingsAsJsonBeforeMigration), + ), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to find beginning of PBXNativeTarget section'), + ); + }); + + testWithoutContext('fails if missing Runner target in parsed settings', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_nativeTargetSectionIndex] = unmigratedNativeTargetSection(platform); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration.removeAt(_nativeTargetSectionIndex); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput(settingsAsJsonBeforeMigration), + ), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to find parsed PBXNativeTarget for Runner target'), + ); + }); + + testWithoutContext('fails if missing Runner target subsection following PBXNativeTarget begin header', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_nativeTargetSectionIndex] = ''' +/* Begin PBXNativeTarget section */ +/* End PBXNativeTarget section */ +'''; + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput(settingsAsJsonBeforeMigration), + ), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to find PBXNativeTarget for Runner target'), + ); + }); + + testWithoutContext('fails if missing Runner target subsection before PBXNativeTarget end header', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_nativeTargetSectionIndex] = ''' +/* Begin PBXNativeTarget section */ +/* End PBXNativeTarget section */ +/* Begin NonExistant section */ + ${_runnerNativeTargetIdentifer(platform)} /* Runner */ = { + }; +/* End NonExistant section */ +'''; + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput(settingsAsJsonBeforeMigration), + ), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to find PBXNativeTarget for Runner target'), + ); + }); + + testWithoutContext('successfully added when packageProductDependencies field is missing', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_nativeTargetSectionIndex] = unmigratedNativeTargetSection( + platform, + missingPackageProductDependencies: true, + ); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration[_nativeTargetSectionIndex] = unmigratedNativeTargetSectionAsJson( + platform, + missingPackageProductDependencies: true, + ); + final List expectedSettings = [ + ..._allSectionsMigrated(platform), + ]; + expectedSettings[_nativeTargetSectionIndex] = migratedNativeTargetSection( + platform, + missingPackageProductDependencies: true, + ); + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(settingsAsJsonBeforeMigration), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await projectMigration.migrate(); + expect(testLogger.errorText, isEmpty); + expect( + testLogger.traceText.contains('PBXNativeTarget already migrated. Skipping...'), + isFalse, + ); + expect( + project.xcodeProjectInfoFile.readAsStringSync(), + _projectSettings(expectedSettings), + ); + expect(plistParser.hasRemainingExpectations, isFalse); + }); + + testWithoutContext('successfully added when packageProductDependencies field is empty', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_nativeTargetSectionIndex] = unmigratedNativeTargetSection(platform); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(settingsAsJsonBeforeMigration), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await projectMigration.migrate(); + expect(testLogger.errorText, isEmpty); + expect( + testLogger.traceText.contains('PBXNativeTarget already migrated. Skipping...'), + isFalse, + ); + expect( + project.xcodeProjectInfoFile.readAsStringSync(), + _projectSettings(_allSectionsMigrated(platform)), + ); + expect(plistParser.hasRemainingExpectations, isFalse); + }); + + testWithoutContext('successfully added when packageProductDependencies field is not empty', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_nativeTargetSectionIndex] = unmigratedNativeTargetSection( + platform, + withOtherDependency: true, + ); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + final List expectedSettings = [ + ..._allSectionsMigrated(platform), + ]; + expectedSettings[_nativeTargetSectionIndex] = migratedNativeTargetSection( + platform, + withOtherDependency: true, + ); + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(settingsAsJsonBeforeMigration), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await projectMigration.migrate(); + expect(testLogger.errorText, isEmpty); + expect( + testLogger.traceText.contains('PBXNativeTarget already migrated. Skipping...'), + isFalse, + ); + expect( + project.xcodeProjectInfoFile.readAsStringSync(), + _projectSettings(expectedSettings), + ); + expect(plistParser.hasRemainingExpectations, isFalse); + }); + }); + + group('migrate PBXProject', () { + testWithoutContext('fails if missing PBXProject section', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration.removeAt(_projectSectionIndex); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration.removeAt(_projectSectionIndex); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput(settingsAsJsonBeforeMigration), + ), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to find beginning of PBXProject section'), + ); + }); + + testWithoutContext('fails if missing Runner project subsection following PBXProject begin header', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_projectSectionIndex] = ''' +/* Begin PBXProject section */ +/* End PBXProject section */ +'''; + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration.removeAt(_projectSectionIndex); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput(settingsAsJsonBeforeMigration), + ), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to find PBXProject for Runner'), + ); + }); + + testWithoutContext('fails if missing Runner project subsection before PBXProject end header', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_projectSectionIndex] = ''' +/* Begin PBXProject section */ +/* End PBXProject section */ +/* Begin NonExistant section */ + ${_projectIdentifier(platform)} /* Project object */ = { + }; +/* End NonExistant section */ +'''; + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration.removeAt(_projectSectionIndex); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput(settingsAsJsonBeforeMigration), + ), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to find PBXProject for Runner'), + ); + }); + + testWithoutContext('fails if missing Runner project in parsed settings', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_projectSectionIndex] = unmigratedProjectSection(platform); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration.removeAt(_projectSectionIndex); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput(settingsAsJsonBeforeMigration), + ), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to find parsed PBXProject for Runner'), + ); + }); + + testWithoutContext('successfully added when packageReferences field is missing', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_projectSectionIndex] = unmigratedProjectSection( + platform, + missingPackageReferences: true, + ); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration[_projectSectionIndex] = unmigratedProjectSectionAsJson( + platform, + missingPackageReferences: true, + ); + + final List expectedSettings = [ + ..._allSectionsMigrated(platform), + ]; + expectedSettings[_projectSectionIndex] = migratedProjectSection( + platform, + missingPackageReferences: true, + ); + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(settingsAsJsonBeforeMigration), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await projectMigration.migrate(); + expect(testLogger.errorText, isEmpty); + expect( + testLogger.traceText.contains('PBXProject already migrated. Skipping...'), + isFalse, + ); + expect( + project.xcodeProjectInfoFile.readAsStringSync(), + _projectSettings(expectedSettings), + ); + expect(plistParser.hasRemainingExpectations, isFalse); + }); + + testWithoutContext('successfully added when packageReferences field is empty', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_projectSectionIndex] = unmigratedProjectSection(platform); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(settingsAsJsonBeforeMigration), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await projectMigration.migrate(); + expect(testLogger.errorText, isEmpty); + expect( + testLogger.traceText.contains('PBXProject already migrated. Skipping...'), + isFalse, + ); + expect( + project.xcodeProjectInfoFile.readAsStringSync(), + _projectSettings(_allSectionsMigrated(platform)), + ); + expect(plistParser.hasRemainingExpectations, isFalse); + }); + + testWithoutContext('successfully added when packageReferences field is not empty', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_projectSectionIndex] = unmigratedProjectSection( + platform, + withOtherReference: true, + ); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + final List expectedSettings = [ + ..._allSectionsMigrated(platform), + ]; + expectedSettings[_projectSectionIndex] = migratedProjectSection( + platform, + withOtherReference: true, + ); + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(settingsAsJsonBeforeMigration), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await projectMigration.migrate(); + expect(testLogger.errorText, isEmpty); + expect( + testLogger.traceText.contains('PBXProject already migrated. Skipping...'), + isFalse, + ); + expect( + project.xcodeProjectInfoFile.readAsStringSync(), + _projectSettings(expectedSettings), + ); + expect(plistParser.hasRemainingExpectations, isFalse); + }); + }); + + group('migrate XCLocalSwiftPackageReference', () { + testWithoutContext('fails if unable to find section to append it after', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsMigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration.removeAt(_localSwiftPackageReferenceSectionIndex); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput(settingsAsJsonBeforeMigration), + ), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to find any sections'), + ); + }); + + testWithoutContext('successfully added when section is missing', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration.removeAt(_localSwiftPackageReferenceSectionIndex); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + final List expectedSettings = [ + ..._allSectionsMigrated(platform), + ]; + expectedSettings.removeAt(_localSwiftPackageReferenceSectionIndex); + expectedSettings.add(migratedLocalSwiftPackageReferenceSection()); + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(settingsAsJsonBeforeMigration), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await projectMigration.migrate(); + expect(testLogger.errorText, isEmpty); + expect( + testLogger.traceText.contains('XCLocalSwiftPackageReference already migrated. Skipping...'), + isFalse, + ); + expect( + project.xcodeProjectInfoFile.readAsStringSync(), + _projectSettings(expectedSettings), + ); + expect(plistParser.hasRemainingExpectations, isFalse); + }); + + testWithoutContext('successfully added when section is empty', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_localSwiftPackageReferenceSectionIndex] = unmigratedLocalSwiftPackageReferenceSection(); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(settingsAsJsonBeforeMigration), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await projectMigration.migrate(); + expect(testLogger.errorText, isEmpty); + expect( + testLogger.traceText.contains('XCLocalSwiftPackageReference already migrated. Skipping...'), + isFalse, + ); + expect( + project.xcodeProjectInfoFile.readAsStringSync(), + _projectSettings(_allSectionsMigrated(platform)), + ); + expect(plistParser.hasRemainingExpectations, isFalse); + }); + + testWithoutContext('successfully added when section is not empty', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_localSwiftPackageReferenceSectionIndex] = unmigratedLocalSwiftPackageReferenceSection( + withOtherReference: true, + ); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + final List expectedSettings = [ + ..._allSectionsMigrated(platform), + ]; + expectedSettings[_localSwiftPackageReferenceSectionIndex] = migratedLocalSwiftPackageReferenceSection( + withOtherReference: true, + ); + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(settingsAsJsonBeforeMigration), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await projectMigration.migrate(); + expect(testLogger.errorText, isEmpty); + expect( + testLogger.traceText.contains('XCLocalSwiftPackageReference already migrated. Skipping...'), + isFalse, + ); + expect( + project.xcodeProjectInfoFile.readAsStringSync(), + _projectSettings(expectedSettings), + ); + expect(plistParser.hasRemainingExpectations, isFalse); + }); + }); + + group('migrate XCSwiftPackageProductDependency', () { + testWithoutContext('fails if unable to find section to append it after', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsMigratedAsJson(platform), + ]; + settingsAsJsonBeforeMigration.removeAt(_swiftPackageProductDependencySectionIndex); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: FakePlistParser( + json: _plutilOutput(settingsAsJsonBeforeMigration), + ), + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to find any sections'), + ); + }); + + testWithoutContext('successfully added when section is missing', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration.removeAt(_swiftPackageProductDependencySectionIndex); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(settingsAsJsonBeforeMigration), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await projectMigration.migrate(); + expect(testLogger.errorText, isEmpty); + expect( + testLogger.traceText.contains('XCSwiftPackageProductDependency already migrated. Skipping...'), + isFalse, + ); + expect( + project.xcodeProjectInfoFile.readAsStringSync(), + _projectSettings(_allSectionsMigrated(platform)), + ); + expect(plistParser.hasRemainingExpectations, isFalse); + }); + + testWithoutContext('successfully added when section is empty', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_swiftPackageProductDependencySectionIndex] = unmigratedSwiftPackageProductDependencySection(); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(settingsAsJsonBeforeMigration), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await projectMigration.migrate(); + expect(testLogger.errorText, isEmpty); + expect( + testLogger.traceText.contains('XCSwiftPackageProductDependency already migrated. Skipping...'), + isFalse, + ); + expect( + project.xcodeProjectInfoFile.readAsStringSync(), + _projectSettings(_allSectionsMigrated(platform)), + ); + expect(plistParser.hasRemainingExpectations, isFalse); + }); + + testWithoutContext('successfully added when section is not empty', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final List settingsBeforeMigration = [ + ..._allSectionsUnmigrated(platform), + ]; + settingsBeforeMigration[_swiftPackageProductDependencySectionIndex] = unmigratedSwiftPackageProductDependencySection( + withOtherDependency: true, + ); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(settingsBeforeMigration), + ); + final List settingsAsJsonBeforeMigration = [ + ..._allSectionsUnmigratedAsJson(platform), + ]; + final List expectedSettings = [ + ..._allSectionsMigrated(platform), + ]; + expectedSettings[_swiftPackageProductDependencySectionIndex] = migratedSwiftPackageProductDependencySection( + withOtherDependency: true, + ); + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(settingsAsJsonBeforeMigration), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await projectMigration.migrate(); + expect(testLogger.errorText, isEmpty); + expect( + testLogger.traceText.contains('XCSwiftPackageProductDependency already migrated. Skipping...'), + isFalse, + ); + expect( + project.xcodeProjectInfoFile.readAsStringSync(), + _projectSettings(expectedSettings), + ); + expect(plistParser.hasRemainingExpectations, isFalse); + }); + }); + + testWithoutContext('throw if settings not updated correctly', () async{ + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(_allSectionsUnmigrated(platform)), + ); + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(_allSectionsUnmigratedAsJson(platform)), + _plutilOutput(_allSectionsUnmigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Settings were not updated correctly.'), + ); + expect( + testLogger.errorText, + contains('PBXBuildFile was not migrated or was migrated incorrectly.'), + ); + expect( + testLogger.errorText, + contains('PBXFrameworksBuildPhase was not migrated or was migrated incorrectly.'), + ); + expect( + testLogger.errorText, + contains('PBXNativeTarget was not migrated or was migrated incorrectly.'), + ); + expect( + testLogger.errorText, + contains('PBXProject was not migrated or was migrated incorrectly.'), + ); + expect( + testLogger.errorText, + contains('XCLocalSwiftPackageReference was not migrated or was migrated incorrectly.'), + ); + expect( + testLogger.errorText, + contains('XCSwiftPackageProductDependency was not migrated or was migrated incorrectly.'), + ); + }); + }); + } + }); + + group('validate project settings', () { + testWithoutContext('throw if settings fail to compile', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + const SupportedPlatform platform = SupportedPlatform.ios; + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(_allSectionsUnmigrated(platform)), + ); + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(_allSectionsUnmigratedAsJson(platform)), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final SwiftPackageManagerIntegrationMigration projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter( + throwErrorOnGetInfo: true, + ), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + ); + await expectLater( + () => projectMigration.migrate(), + throwsToolExit(message: 'Unable to get Xcode project information'), + ); + }); + + testWithoutContext('restore project settings from backup on failure', () async { + final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); + final BufferLogger testLogger = BufferLogger.test(); + const SupportedPlatform platform = SupportedPlatform.ios; + final FakeXcodeProject project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + + final String originalProjectInfo = _projectSettings( + _allSectionsUnmigrated(platform), + ); + project.xcodeProjectInfoFile.writeAsStringSync(originalProjectInfo); + final String originalSchemeContents = _validBuildActions(platform); + + final FakePlistParser plistParser = FakePlistParser.multiple([ + _plutilOutput(_allSectionsUnmigratedAsJson(platform)), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final FakeSwiftPackageManagerIntegrationMigration projectMigration = FakeSwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter( + throwErrorOnGetInfo: true, + ), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + validateBackup: true, + ); + await expectLater( + () async => projectMigration.migrate(), + throwsToolExit(), + ); + expect( + testLogger.traceText, + contains('Restoring project settings from backup file...'), + ); + expect( + project.xcodeProjectInfoFile.readAsStringSync(), + originalProjectInfo, + ); + expect( + project.xcodeProjectSchemeFile().readAsStringSync(), + originalSchemeContents, + ); + }); + }); + }); +} + +void _createProjectFiles( + FakeXcodeProject project, + SupportedPlatform platform, { + 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( + _validBuildActions(platform), + ); + } +} + +String _validBuildActions( + SupportedPlatform platform, { + bool hasPreActions = false, + bool hasFrameworkScript = false, +}) { + final String scriptText; + if (platform == SupportedPlatform.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 ">'; + } + String preActions = ''; + if (hasFrameworkScript) { + preActions = ''' +\n + + + + + + + + '''; + } else if (hasPreActions) { + preActions = ''' +\n + '''; + } + return ''' + $preActions + + +${_validBuildableReference(platform)} + + + +'''; +} + +String _validBuildableReference(SupportedPlatform platform) { + return ''' + + '''; +} + +const int _buildFileSectionIndex = 0; +const int _frameworksBuildPhaseSectionIndex = 1; +const int _nativeTargetSectionIndex = 2; +const int _projectSectionIndex = 3; +const int _localSwiftPackageReferenceSectionIndex = 4; +const int _swiftPackageProductDependencySectionIndex = 5; + +List _allSectionsMigrated(SupportedPlatform platform) { + return [ + migratedBuildFileSection, + migratedFrameworksBuildPhaseSection(platform), + migratedNativeTargetSection(platform), + migratedProjectSection(platform), + migratedLocalSwiftPackageReferenceSection(), + migratedSwiftPackageProductDependencySection(), + ]; +} + +List _allSectionsMigratedAsJson(SupportedPlatform platform) { + return [ + migratedBuildFileSectionAsJson, + migratedFrameworksBuildPhaseSectionAsJson(platform), + migratedNativeTargetSectionAsJson(platform), + migratedProjectSectionAsJson(platform), + migratedLocalSwiftPackageReferenceSectionAsJson, + migratedSwiftPackageProductDependencySectionAsJson, + ]; +} + +List _allSectionsUnmigrated(SupportedPlatform platform) { + return [ + unmigratedBuildFileSection, + unmigratedFrameworksBuildPhaseSection(platform), + unmigratedNativeTargetSection(platform), + unmigratedProjectSection(platform), + unmigratedLocalSwiftPackageReferenceSection(), + unmigratedSwiftPackageProductDependencySection(), + ]; +} + +List _allSectionsUnmigratedAsJson(SupportedPlatform platform) { + return [ + unmigratedBuildFileSectionAsJson, + unmigratedFrameworksBuildPhaseSectionAsJson(platform), + unmigratedNativeTargetSectionAsJson(platform), + unmigratedProjectSectionAsJson(platform), + ]; +} + +String _plutilOutput(List objects) { + return ''' +{ + "archiveVersion" : "1", + "classes" : { + + }, + "objects" : { +${objects.join(',\n')} + } +} +'''; +} + +String _projectSettings(List objects) { + return ''' +${objects.join('\n')} +'''; +} + +String _runnerFrameworksBuildPhaseIdentifer(SupportedPlatform platform) { + return platform == SupportedPlatform.ios + ? '97C146EB1CF9000F007C117D' + : '33CC10EA2044A3C60003C045'; +} + +String _runnerNativeTargetIdentifer(SupportedPlatform platform) { + return platform == SupportedPlatform.ios + ? '97C146ED1CF9000F007C117D' + : '33CC10EC2044A3C60003C045'; +} + +String _projectIdentifier(SupportedPlatform platform) { + return platform == SupportedPlatform.ios + ? '97C146E61CF9000F007C117D' + : '33CC10E52044A3C60003C045'; +} + +// PBXBuildFile +const String unmigratedBuildFileSection = ''' +/* Begin PBXBuildFile section */ + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; +/* End PBXBuildFile section */ +'''; +const String migratedBuildFileSection = ''' +/* Begin PBXBuildFile section */ + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; +/* End PBXBuildFile section */ +'''; +const String unmigratedBuildFileSectionAsJson = ''' + "97C146FC1CF9000F007C117D" : { + "fileRef" : "97C146FA1CF9000F007C117D", + "isa" : "PBXBuildFile" + }, + "74858FAF1ED2DC5600515810" : { + "fileRef" : "74858FAE1ED2DC5600515810", + "isa" : "PBXBuildFile" + }'''; +const String migratedBuildFileSectionAsJson = ''' + "78A318202AECB46A00862997" : { + "isa" : "PBXBuildFile", + "productRef" : "78A3181F2AECB46A00862997" + }, + "97C146FC1CF9000F007C117D" : { + "fileRef" : "97C146FA1CF9000F007C117D", + "isa" : "PBXBuildFile" + }, + "74858FAF1ED2DC5600515810" : { + "fileRef" : "74858FAE1ED2DC5600515810", + "isa" : "PBXBuildFile" + }'''; + +// PBXFrameworksBuildPhase +String unmigratedFrameworksBuildPhaseSection( + SupportedPlatform platform, { + bool withCocoapods = false, + bool missingFiles = false, +}) { + return [ + '/* Begin PBXFrameworksBuildPhase section */', + ' ${_runnerFrameworksBuildPhaseIdentifer(platform)} /* Frameworks */ = {', + ' isa = PBXFrameworksBuildPhase;', + ' buildActionMask = 2147483647;', + if (!missingFiles) ...[ + ' files = (', + if (withCocoapods) + ' FD5BB45FB410D26C457F3823 /* Pods_Runner.framework in Frameworks */,', + ' );', + ], + ' runOnlyForDeploymentPostprocessing = 0;', + ' };', + '/* End PBXFrameworksBuildPhase section */', + ].join('\n'); +} + +String migratedFrameworksBuildPhaseSection( + SupportedPlatform platform, { + bool withCocoapods = false, + bool missingFiles = false, +}) { + final List filesField = [ + ' files = (', + ' 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,', + if (withCocoapods) + ' FD5BB45FB410D26C457F3823 /* Pods_Runner.framework in Frameworks */,', + ' );', + ]; + return [ + '/* Begin PBXFrameworksBuildPhase section */', + ' ${_runnerFrameworksBuildPhaseIdentifer(platform)} /* Frameworks */ = {', + if (missingFiles) ...filesField, + ' isa = PBXFrameworksBuildPhase;', + ' buildActionMask = 2147483647;', + if (!missingFiles) ...filesField, + ' runOnlyForDeploymentPostprocessing = 0;', + ' };', + '/* End PBXFrameworksBuildPhase section */', + ].join('\n'); +} + +String unmigratedFrameworksBuildPhaseSectionAsJson( + SupportedPlatform platform, { + bool withCocoapods = false, + bool missingFiles = false, +}) { + return [ + ' "${_runnerFrameworksBuildPhaseIdentifer(platform)}" : {', + ' "buildActionMask" : "2147483647",', + if (!missingFiles) ...[ + ' "files" : [', + if (withCocoapods) ' "FD5BB45FB410D26C457F3823"', + ' ],', + ], + ' "isa" : "PBXFrameworksBuildPhase",', + ' "runOnlyForDeploymentPostprocessing" : "0"', + ' }', + ].join('\n'); +} + +String migratedFrameworksBuildPhaseSectionAsJson(SupportedPlatform platform) { + return ''' + "${_runnerFrameworksBuildPhaseIdentifer(platform)}" : { + "buildActionMask" : "2147483647", + "files" : [ + "78A318202AECB46A00862997" + ], + "isa" : "PBXFrameworksBuildPhase", + "runOnlyForDeploymentPostprocessing" : "0" + }'''; +} + +// PBXNativeTarget +String unmigratedNativeTargetSection( + SupportedPlatform platform, { + bool missingPackageProductDependencies = false, + bool withOtherDependency = false, +}) { + return [ + '/* Begin PBXNativeTarget section */', + ' ${_runnerNativeTargetIdentifer(platform)} /* Runner */ = {', + ' isa = PBXNativeTarget;', + ' buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;', + ' buildPhases = (', + ' 9740EEB61CF901F6004384FC /* Run Script */,', + ' 97C146EA1CF9000F007C117D /* Sources */,', + ' ${_runnerFrameworksBuildPhaseIdentifer(platform)} /* Frameworks */,', + ' 97C146EC1CF9000F007C117D /* Resources */,', + ' 9705A1C41CF9048500538489 /* Embed Frameworks */,', + ' 3B06AD1E1E4923F5004D2608 /* Thin Binary */,', + ' );', + ' buildRules = (', + ' );', + ' dependencies = (', + ' );', + ' name = Runner;', + if (!missingPackageProductDependencies) ...[ + ' packageProductDependencies = (', + if (withOtherDependency) + ' 010101010101010101010101 /* SomeOtherPackage */,', + ' );', + ], + ' productName = Runner;', + ' productReference = 97C146EE1CF9000F007C117D /* Runner.app */;', + ' productType = "com.apple.product-type.application";', + ' };', + '/* End PBXNativeTarget section */', + ].join('\n'); +} + +String migratedNativeTargetSection( + SupportedPlatform platform, { + bool missingPackageProductDependencies = false, + bool withOtherDependency = false, +}) { + final List packageDependencies = [ + ' packageProductDependencies = (', + ' 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,', + if (withOtherDependency) + ' 010101010101010101010101 /* SomeOtherPackage */,', + ' );', + ]; + return [ + '/* Begin PBXNativeTarget section */', + ' ${_runnerNativeTargetIdentifer(platform)} /* Runner */ = {', + if (missingPackageProductDependencies) ...packageDependencies, + ' isa = PBXNativeTarget;', + ' buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;', + ' buildPhases = (', + ' 9740EEB61CF901F6004384FC /* Run Script */,', + ' 97C146EA1CF9000F007C117D /* Sources */,', + ' ${_runnerFrameworksBuildPhaseIdentifer(platform)} /* Frameworks */,', + ' 97C146EC1CF9000F007C117D /* Resources */,', + ' 9705A1C41CF9048500538489 /* Embed Frameworks */,', + ' 3B06AD1E1E4923F5004D2608 /* Thin Binary */,', + ' );', + ' buildRules = (', + ' );', + ' dependencies = (', + ' );', + ' name = Runner;', + if (!missingPackageProductDependencies) ...packageDependencies, + ' productName = Runner;', + ' productReference = 97C146EE1CF9000F007C117D /* Runner.app */;', + ' productType = "com.apple.product-type.application";', + ' };', + '/* End PBXNativeTarget section */', + ].join('\n'); +} + +String unmigratedNativeTargetSectionAsJson( + SupportedPlatform platform, { + bool missingPackageProductDependencies = false, +}) { + return [ + ' "${_runnerNativeTargetIdentifer(platform)}" : {', + ' "buildConfigurationList" : "97C147051CF9000F007C117D",', + ' "buildPhases" : [', + ' "9740EEB61CF901F6004384FC",', + ' "97C146EA1CF9000F007C117D",', + ' "${_runnerFrameworksBuildPhaseIdentifer(platform)}",', + ' "97C146EC1CF9000F007C117D",', + ' "9705A1C41CF9048500538489",', + ' "3B06AD1E1E4923F5004D2608"', + ' ],', + ' "buildRules" : [', + ' ],', + ' "dependencies" : [', + ' ],', + ' "isa" : "PBXNativeTarget",', + ' "name" : "Runner",', + if (!missingPackageProductDependencies) ...[ + ' "packageProductDependencies" : [', + ' ],', + ], + ' "productName" : "Runner",', + ' "productReference" : "97C146EE1CF9000F007C117D",', + ' "productType" : "com.apple.product-type.application"', + ' }', + ].join('\n'); +} + +String migratedNativeTargetSectionAsJson(SupportedPlatform platform) { + return ''' + "${_runnerNativeTargetIdentifer(platform)}" : { + "buildConfigurationList" : "97C147051CF9000F007C117D", + "buildPhases" : [ + "9740EEB61CF901F6004384FC", + "97C146EA1CF9000F007C117D", + "${_runnerFrameworksBuildPhaseIdentifer(platform)}", + "97C146EC1CF9000F007C117D", + "9705A1C41CF9048500538489", + "3B06AD1E1E4923F5004D2608" + ], + "buildRules" : [ + + ], + "dependencies" : [ + + ], + "isa" : "PBXNativeTarget", + "name" : "Runner", + "packageProductDependencies" : [ + "78A3181F2AECB46A00862997" + ], + "productName" : "Runner", + "productReference" : "97C146EE1CF9000F007C117D", + "productType" : "com.apple.product-type.application" + }'''; +} + +// PBXProject +String unmigratedProjectSection( + SupportedPlatform platform, { + bool missingPackageReferences = false, + bool withOtherReference = false, +}) { + return [ + '/* Begin PBXProject section */', + ' ${_projectIdentifier(platform)} /* Project object */ = {', + ' isa = PBXProject;', + ' attributes = {', + ' BuildIndependentTargetsInParallel = YES;', + ' LastUpgradeCheck = 1510;', + ' ORGANIZATIONNAME = "";', + ' TargetAttributes = {', + ' 331C8080294A63A400263BE5 = {', + ' CreatedOnToolsVersion = 14.0;', + ' TestTargetID = ${_runnerNativeTargetIdentifer(platform)};', + ' };', + ' ${_runnerNativeTargetIdentifer(platform)} = {', + ' CreatedOnToolsVersion = 7.3.1;', + ' LastSwiftMigration = 1100;', + ' };', + ' };', + ' };', + ' buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;', + ' compatibilityVersion = "Xcode 9.3";', + ' developmentRegion = en;', + ' hasScannedForEncodings = 0;', + ' knownRegions = (', + ' en,', + ' Base,', + ' );', + ' mainGroup = 97C146E51CF9000F007C117D;', + if (!missingPackageReferences) ...[ + ' packageReferences = (', + if (withOtherReference) + ' 010101010101010101010101 /* XCLocalSwiftPackageReference "SomeOtherPackage" */,', + ' );', + ], + ' productRefGroup = 97C146EF1CF9000F007C117D /* Products */;', + ' projectDirPath = "";', + ' projectRoot = "";', + ' targets = (', + ' ${_runnerNativeTargetIdentifer(platform)} /* Runner */,', + ' 331C8080294A63A400263BE5 /* RunnerTests */,', + ' );', + ' };', + '/* End PBXProject section */', + ].join('\n'); +} + +String migratedProjectSection( + SupportedPlatform platform, { + bool missingPackageReferences = false, + bool withOtherReference = false, +}) { + final List packageDependencies = [ + ' packageReferences = (', + ' 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,', + if (withOtherReference) + ' 010101010101010101010101 /* XCLocalSwiftPackageReference "SomeOtherPackage" */,', + ' );', + ]; + return [ + '/* Begin PBXProject section */', + ' ${_projectIdentifier(platform)} /* Project object */ = {', + if (missingPackageReferences) ...packageDependencies, + ' isa = PBXProject;', + ' attributes = {', + ' BuildIndependentTargetsInParallel = YES;', + ' LastUpgradeCheck = 1510;', + ' ORGANIZATIONNAME = "";', + ' TargetAttributes = {', + ' 331C8080294A63A400263BE5 = {', + ' CreatedOnToolsVersion = 14.0;', + ' TestTargetID = ${_runnerNativeTargetIdentifer(platform)};', + ' };', + ' ${_runnerNativeTargetIdentifer(platform)} = {', + ' CreatedOnToolsVersion = 7.3.1;', + ' LastSwiftMigration = 1100;', + ' };', + ' };', + ' };', + ' buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;', + ' compatibilityVersion = "Xcode 9.3";', + ' developmentRegion = en;', + ' hasScannedForEncodings = 0;', + ' knownRegions = (', + ' en,', + ' Base,', + ' );', + ' mainGroup = 97C146E51CF9000F007C117D;', + if (!missingPackageReferences) ...packageDependencies, + ' productRefGroup = 97C146EF1CF9000F007C117D /* Products */;', + ' projectDirPath = "";', + ' projectRoot = "";', + ' targets = (', + ' ${_runnerNativeTargetIdentifer(platform)} /* Runner */,', + ' 331C8080294A63A400263BE5 /* RunnerTests */,', + ' );', + ' };', + '/* End PBXProject section */', + ].join('\n'); +} + +String unmigratedProjectSectionAsJson( + SupportedPlatform platform, { + bool missingPackageReferences = false, +}) { + return [ + ' "${_projectIdentifier(platform)}" : {', + ' "attributes" : {', + ' "BuildIndependentTargetsInParallel" : "YES",', + ' "LastUpgradeCheck" : "1510",', + ' "ORGANIZATIONNAME" : "",', + ' "TargetAttributes" : {', + ' "${_runnerNativeTargetIdentifer(platform)}" : {', + ' "CreatedOnToolsVersion" : "7.3.1",', + ' "LastSwiftMigration" : "1100"', + ' },', + ' "331C8080294A63A400263BE5" : {', + ' "CreatedOnToolsVersion" : "14.0",', + ' "TestTargetID" : "${_runnerNativeTargetIdentifer(platform)}"', + ' }', + ' }', + ' },', + ' "buildConfigurationList" : "97C146E91CF9000F007C117D",', + ' "compatibilityVersion" : "Xcode 9.3",', + ' "developmentRegion" : "en",', + ' "hasScannedForEncodings" : "0",', + ' "isa" : "PBXProject",', + ' "knownRegions" : [', + ' "en",', + ' "Base"', + ' ],', + ' "mainGroup" : "97C146E51CF9000F007C117D",', + if (!missingPackageReferences) ...[ + ' "packageReferences" : [', + ' ],', + ], + ' "productRefGroup" : "97C146EF1CF9000F007C117D",', + ' "projectDirPath" : "",', + ' "projectRoot" : "",', + ' "targets" : [', + ' "${_runnerNativeTargetIdentifer(platform)}",', + ' "331C8080294A63A400263BE5"', + ' ]', + ' }', + ].join('\n'); +} + +String migratedProjectSectionAsJson(SupportedPlatform platform) { + return ''' + "${_projectIdentifier(platform)}" : { + "attributes" : { + "BuildIndependentTargetsInParallel" : "YES", + "LastUpgradeCheck" : "1510", + "ORGANIZATIONNAME" : "", + "TargetAttributes" : { + "${_runnerNativeTargetIdentifer(platform)}" : { + "CreatedOnToolsVersion" : "7.3.1", + "LastSwiftMigration" : "1100" + }, + "331C8080294A63A400263BE5" : { + "CreatedOnToolsVersion" : "14.0", + "TestTargetID" : "${_runnerNativeTargetIdentifer(platform)}" + } + } + }, + "buildConfigurationList" : "97C146E91CF9000F007C117D", + "compatibilityVersion" : "Xcode 9.3", + "developmentRegion" : "en", + "hasScannedForEncodings" : "0", + "isa" : "PBXProject", + "knownRegions" : [ + "en", + "Base" + ], + "mainGroup" : "97C146E51CF9000F007C117D", + "packageReferences" : [ + "781AD8BC2B33823900A9FFBB" + ], + "productRefGroup" : "97C146EF1CF9000F007C117D", + "projectDirPath" : "", + "projectRoot" : "", + "targets" : [ + "${_runnerNativeTargetIdentifer(platform)}", + "331C8080294A63A400263BE5" + ] + }'''; +} + +// XCLocalSwiftPackageReference +String unmigratedLocalSwiftPackageReferenceSection({ + bool withOtherReference = false, +}) { + return [ + '/* Begin XCLocalSwiftPackageReference section */', + if (withOtherReference) ...[ + ' 010101010101010101010101 /* XCLocalSwiftPackageReference "SomeOtherPackage" */ = {', + ' isa = XCLocalSwiftPackageReference;', + ' relativePath = SomeOtherPackage;', + ' };', + ], + '/* End XCLocalSwiftPackageReference section */', + ].join('\n'); +} + +String migratedLocalSwiftPackageReferenceSection({ + bool withOtherReference = false, +}) { + return [ + '/* Begin XCLocalSwiftPackageReference section */', + if (withOtherReference) ...[ + ' 010101010101010101010101 /* XCLocalSwiftPackageReference "SomeOtherPackage" */ = {', + ' isa = XCLocalSwiftPackageReference;', + ' relativePath = SomeOtherPackage;', + ' };', + ], + ' 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {', + ' isa = XCLocalSwiftPackageReference;', + ' relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;', + ' };', + '/* End XCLocalSwiftPackageReference section */', + ].join('\n'); +} + +const String migratedLocalSwiftPackageReferenceSectionAsJson = ''' + "781AD8BC2B33823900A9FFBB" : { + "isa" : "XCLocalSwiftPackageReference", + "relativePath" : "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" + }'''; + +// XCSwiftPackageProductDependency +String unmigratedSwiftPackageProductDependencySection({ + bool withOtherDependency = false, +}) { + return [ + '/* Begin XCSwiftPackageProductDependency section */', + if (withOtherDependency) ...[ + ' 010101010101010101010101 /* SomeOtherPackage */ = {', + ' isa = XCSwiftPackageProductDependency;', + ' productName = SomeOtherPackage;', + ' };', + ], + '/* End XCSwiftPackageProductDependency section */', + ].join('\n'); +} + +String migratedSwiftPackageProductDependencySection({ + bool withOtherDependency = false, +}) { + return [ + '/* Begin XCSwiftPackageProductDependency section */', + if (withOtherDependency) ...[ + ' 010101010101010101010101 /* SomeOtherPackage */ = {', + ' isa = XCSwiftPackageProductDependency;', + ' productName = SomeOtherPackage;', + ' };', + ], + ' 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {', + ' isa = XCSwiftPackageProductDependency;', + ' productName = FlutterGeneratedPluginSwiftPackage;', + ' };', + '/* End XCSwiftPackageProductDependency section */', + ].join('\n'); +} + +const String migratedSwiftPackageProductDependencySectionAsJson = ''' + "78A3181F2AECB46A00862997" : { + "isa" : "XCSwiftPackageProductDependency", + "productName" : "FlutterGeneratedPluginSwiftPackage" + }'''; + +class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter { + FakeXcodeProjectInterpreter({ + this.throwErrorOnGetInfo = false, + }); + + @override + bool isInstalled = false; + + @override + List xcrunCommand() => ['xcrun']; + + final bool throwErrorOnGetInfo; + + @override + Future getInfo(String projectPath, {String? projectFilename}) async { + if (throwErrorOnGetInfo) { + throwToolExit('Unable to get Xcode project information'); + } + return null; + } +} + +class FakePlistParser extends Fake implements PlistParser { + FakePlistParser({ + String? json, + }) : _outputPerCall = (json != null) ? [json] : null; + + FakePlistParser.multiple(this._outputPerCall); + + final List? _outputPerCall; + + @override + String? plistJsonContent(String filePath) { + if (_outputPerCall != null && _outputPerCall.isNotEmpty) { + return _outputPerCall.removeAt(0); + } + return null; + } + + bool get hasRemainingExpectations { + return _outputPerCall != null && _outputPerCall.isNotEmpty; + } +} + +class FakeXcodeProject extends Fake implements IosProject { + FakeXcodeProject({ + required MemoryFileSystem fileSystem, + required String platform, + required this.logger, + }) : hostAppRoot = fileSystem.directory('app_name').childDirectory(platform), + parent = FakeFlutterProject(fileSystem: fileSystem); + + final Logger logger; + late XcodeProjectInfo? _projectInfo = XcodeProjectInfo( + ['Runner'], + ['Debug', 'Release', 'Profile'], + ['Runner'], + logger, + ); + + @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 + Directory get flutterPluginSwiftPackageDirectory => hostAppRoot + .childDirectory('Flutter') + .childDirectory('ephemeral') + .childDirectory('Packages') + .childDirectory('FlutterGeneratedPluginSwiftPackage'); + + @override + File get flutterPluginSwiftPackageManifest => + flutterPluginSwiftPackageDirectory.childFile('Package.swift'); + + @override + bool get flutterPluginSwiftPackageInProjectSettings { + return xcodeProjectInfoFile.existsSync() && + xcodeProjectInfoFile + .readAsStringSync() + .contains('FlutterGeneratedPluginSwiftPackage'); + } + + @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'); + } +} + +class FakeFlutterProject extends Fake implements FlutterProject { + FakeFlutterProject({ + required MemoryFileSystem fileSystem, + }) : directory = fileSystem.directory('app_name'); + + @override + Directory directory; +} + +class FakeSwiftPackageManagerIntegrationMigration extends SwiftPackageManagerIntegrationMigration { + FakeSwiftPackageManagerIntegrationMigration( + super.project, + super.platform, + super.buildInfo, { + required super.xcodeProjectInterpreter, + required super.logger, + required super.fileSystem, + required super.plistParser, + this.validateBackup = false, + }) : _xcodeProject = project; + + final XcodeBasedProject _xcodeProject; + + final bool validateBackup; + @override + void restoreFromBackup(SchemeInfo? schemeInfo) { + if (validateBackup) { + expect(backupProjectSettings.existsSync(), isTrue); + final String originalSettings = backupProjectSettings.readAsStringSync(); + expect( + _xcodeProject.xcodeProjectInfoFile.readAsStringSync() == originalSettings, + isFalse, + ); + + expect(schemeInfo?.backupSchemeFile, isNotNull); + final File backupScheme = schemeInfo!.backupSchemeFile!; + expect(backupScheme.existsSync(), isTrue); + final String originalScheme = backupScheme.readAsStringSync(); + expect( + _xcodeProject.xcodeProjectSchemeFile().readAsStringSync() == originalScheme, + isFalse, + ); + + super.restoreFromBackup(schemeInfo); + expect( + _xcodeProject.xcodeProjectInfoFile.readAsStringSync(), + originalSettings, + ); + expect( + _xcodeProject.xcodeProjectSchemeFile().readAsStringSync(), + originalScheme, + ); + } else { + super.restoreFromBackup(schemeInfo); + } + } +} diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart index 2084c4227a8..802d382ef6d 100644 --- a/packages/flutter_tools/test/general.shard/plugins_test.dart +++ b/packages/flutter_tools/test/general.shard/plugins_test.dart @@ -16,6 +16,8 @@ import 'package:flutter_tools/src/flutter_manifest.dart'; import 'package:flutter_tools/src/flutter_plugins.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/ios/xcodeproj.dart'; +import 'package:flutter_tools/src/macos/darwin_dependency_management.dart'; +import 'package:flutter_tools/src/platform_plugins.dart'; import 'package:flutter_tools/src/plugins.dart'; import 'package:flutter_tools/src/preview_device.dart'; import 'package:flutter_tools/src/project.dart'; @@ -522,6 +524,7 @@ dependencies: expect(jsonContent['dependencyGraph'], expectedDependencyGraph); expect(jsonContent['date_created'], dateCreated.toString()); expect(jsonContent['version'], '1.0.0'); + expect(jsonContent['swift_package_manager_enabled'], false); // Make sure tests are updated if a new object is added/removed. final List expectedKeys = [ @@ -530,6 +533,7 @@ dependencies: 'dependencyGraph', 'date_created', 'version', + 'swift_package_manager_enabled', ]; expect(jsonContent.keys, expectedKeys); }, overrides: { @@ -609,6 +613,79 @@ dependencies: FlutterVersion: () => flutterVersion, }); + testUsingContext( + '.flutter-plugins-dependencies contains swift_package_manager_enabled true when project is using Swift Package Manager', () async { + createPlugin( + name: 'plugin-a', + platforms: const { + // Native-only; should include native build. + 'android': _PluginPlatformInfo(pluginClass: 'Foo', androidPackage: 'bar.foo'), + // Hybrid native and Dart; should include native build. + 'ios': _PluginPlatformInfo(pluginClass: 'Foo', dartPluginClass: 'Bar', sharedDarwinSource: true), + // Web; should not have the native build key at all since it doesn't apply. + 'web': _PluginPlatformInfo(pluginClass: 'Foo', fileName: 'lib/foo.dart'), + // Dart-only; should not include native build. + 'windows': _PluginPlatformInfo(dartPluginClass: 'Foo'), + }); + iosProject.testExists = true; + + final DateTime dateCreated = DateTime(1970); + systemClock.currentTime = dateCreated; + + flutterProject.usesSwiftPackageManager = true; + + await refreshPluginsList(flutterProject); + + expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true); + final String pluginsString = flutterProject.flutterPluginsDependenciesFile + .readAsStringSync(); + final Map jsonContent = json.decode(pluginsString) as Map; + + expect(jsonContent['swift_package_manager_enabled'], true); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + SystemClock: () => systemClock, + FlutterVersion: () => flutterVersion, + }); + + testUsingContext( + '.flutter-plugins-dependencies contains swift_package_manager_enabled false when project is using Swift Package Manager but forceCocoaPodsOnly is true', + () async { + createPlugin( + name: 'plugin-a', + platforms: const { + // Native-only; should include native build. + 'android': _PluginPlatformInfo(pluginClass: 'Foo', androidPackage: 'bar.foo'), + // Hybrid native and Dart; should include native build. + 'ios': _PluginPlatformInfo(pluginClass: 'Foo', dartPluginClass: 'Bar', sharedDarwinSource: true), + // Web; should not have the native build key at all since it doesn't apply. + 'web': _PluginPlatformInfo(pluginClass: 'Foo', fileName: 'lib/foo.dart'), + // Dart-only; should not include native build. + 'windows': _PluginPlatformInfo(dartPluginClass: 'Foo'), + }); + iosProject.testExists = true; + + final DateTime dateCreated = DateTime(1970); + systemClock.currentTime = dateCreated; + + flutterProject.usesSwiftPackageManager = true; + + await refreshPluginsList(flutterProject, forceCocoaPodsOnly: true); + + expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true); + final String pluginsString = flutterProject.flutterPluginsDependenciesFile + .readAsStringSync(); + final Map jsonContent = json.decode(pluginsString) as Map; + + expect(jsonContent['swift_package_manager_enabled'], false); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + SystemClock: () => systemClock, + FlutterVersion: () => flutterVersion, + }); + testUsingContext('Changes to the plugin list invalidates the Cocoapod lockfiles', () async { simulatePodInstallRun(iosProject); simulatePodInstallRun(macosProject); @@ -886,8 +963,12 @@ flutter: ios: dartPluginClass: SomePlugin '''); - - await injectPlugins(flutterProject, iosPlatform: true); + final FakeDarwinDependencyManagement dependencyManagement = FakeDarwinDependencyManagement(); + await injectPlugins( + flutterProject, + iosPlatform: true, + darwinDependencyManagement: dependencyManagement, + ); final File registrantFile = iosProject.pluginRegistrantImplementation; @@ -909,8 +990,12 @@ flutter: macos: dartPluginClass: SomePlugin '''); - - await injectPlugins(flutterProject, macOSPlatform: true); + final FakeDarwinDependencyManagement dependencyManagement = FakeDarwinDependencyManagement(); + await injectPlugins( + flutterProject, + macOSPlatform: true, + darwinDependencyManagement: dependencyManagement, + ); final File registrantFile = macosProject.managedDirectory.childFile('GeneratedPluginRegistrant.swift'); @@ -933,8 +1018,12 @@ flutter: pluginClass: none dartPluginClass: SomePlugin '''); - - await injectPlugins(flutterProject, macOSPlatform: true); + final FakeDarwinDependencyManagement dependencyManagement = FakeDarwinDependencyManagement(); + await injectPlugins( + flutterProject, + macOSPlatform: true, + darwinDependencyManagement: dependencyManagement, + ); final File registrantFile = macosProject.managedDirectory.childFile('GeneratedPluginRegistrant.swift'); @@ -953,8 +1042,12 @@ flutter: pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(r''' "aws ... \"Branch\": $BITBUCKET_BRANCH, \"Date\": $(date +"%m-%d-%y"), \"Time\": $(date +"%T")}\" '''); - - await injectPlugins(flutterProject, macOSPlatform: true); + final FakeDarwinDependencyManagement dependencyManagement = FakeDarwinDependencyManagement(); + await injectPlugins( + flutterProject, + macOSPlatform: true, + darwinDependencyManagement: dependencyManagement, + ); final File registrantFile = macosProject.managedDirectory.childFile('GeneratedPluginRegistrant.swift'); @@ -1194,6 +1287,35 @@ The Flutter Preview device does not support the following plugins from your pubs FileSystem: () => fsWindows, ProcessManager: () => FakeProcessManager.empty(), }); + + testUsingContext('iOS and macOS project setup up Darwin Dependency Management', () async { + final FakeDarwinDependencyManagement dependencyManagement = FakeDarwinDependencyManagement(); + await injectPlugins( + flutterProject, + iosPlatform: true, + macOSPlatform: true, + darwinDependencyManagement: dependencyManagement, + ); + expect( + dependencyManagement.setupPlatforms, + [SupportedPlatform.ios, SupportedPlatform.macos], + ); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('non-iOS or macOS project does not setup up Darwin Dependency Management', () async { + final FakeDarwinDependencyManagement dependencyManagement = FakeDarwinDependencyManagement(); + await injectPlugins( + flutterProject, + darwinDependencyManagement: dependencyManagement, + ); + expect(dependencyManagement.setupPlatforms, []); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + }); }); group('createPluginSymlinks', () { @@ -1390,6 +1512,154 @@ The Flutter Preview device does not support the following plugins from your pubs }); + group('Plugin files', () { + testWithoutContext('pluginSwiftPackageManifestPath for iOS and macOS plugins', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final Plugin plugin = Plugin( + name: 'test', + path: '/path/to/test/', + defaultPackagePlatforms: const {}, + pluginDartClassPlatforms: const {}, + platforms: const { + IOSPlugin.kConfigKey: IOSPlugin(name: 'test', classPrefix: ''), + MacOSPlugin.kConfigKey: MacOSPlugin(name: 'test'), + }, + dependencies: [], + isDirectDependency: true, + ); + + expect( + plugin.pluginSwiftPackageManifestPath(fs, IOSPlugin.kConfigKey), + '/path/to/test/ios/test/Package.swift', + ); + expect( + plugin.pluginSwiftPackageManifestPath(fs, MacOSPlugin.kConfigKey), + '/path/to/test/macos/test/Package.swift', + ); + }); + + testWithoutContext('pluginSwiftPackageManifestPath for darwin plugins', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final Plugin plugin = Plugin( + name: 'test', + path: '/path/to/test/', + defaultPackagePlatforms: const {}, + pluginDartClassPlatforms: const {}, + platforms: const { + IOSPlugin.kConfigKey: IOSPlugin(name: 'test', classPrefix: '', sharedDarwinSource: true), + MacOSPlugin.kConfigKey: MacOSPlugin(name: 'test', sharedDarwinSource: true), + }, + dependencies: [], + isDirectDependency: true, + ); + + expect( + plugin.pluginSwiftPackageManifestPath(fs, IOSPlugin.kConfigKey), + '/path/to/test/darwin/test/Package.swift', + ); + expect( + plugin.pluginSwiftPackageManifestPath(fs, MacOSPlugin.kConfigKey), + '/path/to/test/darwin/test/Package.swift', + ); + }); + + testWithoutContext('pluginSwiftPackageManifestPath for non darwin plugins', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final Plugin plugin = Plugin( + name: 'test', + path: '/path/to/test/', + defaultPackagePlatforms: const {}, + pluginDartClassPlatforms: const {}, + platforms: const { + WindowsPlugin.kConfigKey: WindowsPlugin(name: 'test', pluginClass: ''), + }, + dependencies: [], + isDirectDependency: true, + ); + + expect( + plugin.pluginSwiftPackageManifestPath(fs, IOSPlugin.kConfigKey), + isNull, + ); + expect( + plugin.pluginSwiftPackageManifestPath(fs, MacOSPlugin.kConfigKey), + isNull, + ); + expect( + plugin.pluginSwiftPackageManifestPath(fs, WindowsPlugin.kConfigKey), + isNull, + ); + }); + + testWithoutContext('pluginPodspecPath for iOS and macOS plugins', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final Plugin plugin = Plugin( + name: 'test', + path: '/path/to/test/', + defaultPackagePlatforms: const {}, + pluginDartClassPlatforms: const {}, + platforms: const { + IOSPlugin.kConfigKey: IOSPlugin(name: 'test', classPrefix: ''), + MacOSPlugin.kConfigKey: MacOSPlugin(name: 'test'), + }, + dependencies: [], + isDirectDependency: true, + ); + + expect( + plugin.pluginPodspecPath(fs, IOSPlugin.kConfigKey), + '/path/to/test/ios/test.podspec', + ); + expect( + plugin.pluginPodspecPath(fs, MacOSPlugin.kConfigKey), + '/path/to/test/macos/test.podspec', + ); + }); + + testWithoutContext('pluginPodspecPath for darwin plugins', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final Plugin plugin = Plugin( + name: 'test', + path: '/path/to/test/', + defaultPackagePlatforms: const {}, + pluginDartClassPlatforms: const {}, + platforms: const { + IOSPlugin.kConfigKey: IOSPlugin(name: 'test', classPrefix: '', sharedDarwinSource: true), + MacOSPlugin.kConfigKey: MacOSPlugin(name: 'test', sharedDarwinSource: true), + }, + dependencies: [], + isDirectDependency: true, + ); + + expect( + plugin.pluginPodspecPath(fs, IOSPlugin.kConfigKey), + '/path/to/test/darwin/test.podspec', + ); + expect( + plugin.pluginPodspecPath(fs, MacOSPlugin.kConfigKey), + '/path/to/test/darwin/test.podspec', + ); + }); + + testWithoutContext('pluginPodspecPath for non darwin plugins', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final Plugin plugin = Plugin( + name: 'test', + path: '/path/to/test/', + defaultPackagePlatforms: const {}, + pluginDartClassPlatforms: const {}, + platforms: const { + WindowsPlugin.kConfigKey: WindowsPlugin(name: 'test', pluginClass: ''), + }, + dependencies: [], + isDirectDependency: true, + ); + + expect(plugin.pluginPodspecPath(fs, IOSPlugin.kConfigKey), isNull); + expect(plugin.pluginPodspecPath(fs, MacOSPlugin.kConfigKey), isNull); + expect(plugin.pluginPodspecPath(fs, WindowsPlugin.kConfigKey), isNull); + }); + }); testWithoutContext('Symlink failures give developer mode instructions on recent versions of Windows', () async { final Platform platform = FakePlatform(operatingSystem: 'windows'); final FakeOperatingSystemUtils os = FakeOperatingSystemUtils('Microsoft Windows [Version 10.0.14972.1]'); @@ -1498,6 +1768,9 @@ class FakeFlutterProject extends Fake implements FlutterProject { @override bool isModule = false; + @override + bool usesSwiftPackageManager = false; + @override late FlutterManifest manifest; @@ -1684,3 +1957,14 @@ class FakeSystemClock extends Fake implements SystemClock { return currentTime; } } + +class FakeDarwinDependencyManagement extends Fake implements DarwinDependencyManagement { + List setupPlatforms = []; + + @override + Future setUp({ + required SupportedPlatform platform, + }) async { + setupPlatforms.add(platform); + } +} diff --git a/packages/flutter_tools/test/general.shard/project_test.dart b/packages/flutter_tools/test/general.shard/project_test.dart index 2934a2d5f8f..c6e4e52678e 100644 --- a/packages/flutter_tools/test/general.shard/project_test.dart +++ b/packages/flutter_tools/test/general.shard/project_test.dart @@ -375,6 +375,91 @@ void main() { }); }); + group('usesSwiftPackageManager', () { + testUsingContext('is true when iOS project exists', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final Directory projectDirectory = fs.directory('path'); + projectDirectory.childDirectory('ios').createSync(recursive: true); + final FlutterManifest manifest = FakeFlutterManifest(); + final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest); + expect(project.usesSwiftPackageManager, isTrue); + }, overrides: { + FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true), + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)), + }); + + testUsingContext('is true when macOS project exists', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final Directory projectDirectory = fs.directory('path'); + projectDirectory.childDirectory('macos').createSync(recursive: true); + final FlutterManifest manifest = FakeFlutterManifest(); + final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest); + expect(project.usesSwiftPackageManager, isTrue); + }, overrides: { + FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true), + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)), + }); + + testUsingContext('is false when disabled via manifest', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final Directory projectDirectory = fs.directory('path'); + projectDirectory.childDirectory('ios').createSync(recursive: true); + final FlutterManifest manifest = FakeFlutterManifest(disabledSwiftPackageManager: true); + final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest); + expect(project.usesSwiftPackageManager, isFalse); + }, overrides: { + FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true), + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)), + }); + + testUsingContext("is false when iOS and macOS project don't exist", () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final Directory projectDirectory = fs.directory('path'); + final FlutterManifest manifest = FakeFlutterManifest(); + final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest); + expect(project.usesSwiftPackageManager, isFalse); + }, overrides: { + FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true), + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)), + }); + + testUsingContext('is false when Xcode is less than 15', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final Directory projectDirectory = fs.directory('path'); + projectDirectory.childDirectory('ios').createSync(recursive: true); + final FlutterManifest manifest = FakeFlutterManifest(); + final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest); + expect(project.usesSwiftPackageManager, isFalse); + }, overrides: { + FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true), + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(14, 0, 0)), + }); + + testUsingContext('is false when Swift Package Manager feature is not enabled', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final Directory projectDirectory = fs.directory('path'); + projectDirectory.childDirectory('ios').createSync(recursive: true); + final FlutterManifest manifest = FakeFlutterManifest(); + final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest); + expect(project.usesSwiftPackageManager, isFalse); + }, overrides: { + FeatureFlags: () => TestFeatureFlags(), + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)), + }); + + testUsingContext('is false when project is a module', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final Directory projectDirectory = fs.directory('path'); + projectDirectory.childDirectory('ios').createSync(recursive: true); + final FlutterManifest manifest = FakeFlutterManifest(isModule: true); + final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest); + expect(project.usesSwiftPackageManager, isFalse); + }, overrides: { + FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true), + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)), + }); + }); + group('java gradle agp compatibility', () { Future configureGradleAgpForTest({ required String gradleV, @@ -1046,6 +1131,31 @@ plugins { expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject'); }); }); + + group('flutterSwiftPackageInProjectSettings', () { + testWithMocks('is false if pbxproj missing', () async { + final FlutterProject project = await someProject(); + expect(project.ios.xcodeProjectInfoFile.existsSync(), isFalse); + expect(project.ios.flutterPluginSwiftPackageInProjectSettings, isFalse); + }); + + testWithMocks('is false if pbxproj does not contain FlutterGeneratedPluginSwiftPackage in build process', () async { + final FlutterProject project = await someProject(); + project.ios.xcodeProjectInfoFile.createSync(recursive: true); + expect(project.ios.xcodeProjectInfoFile.existsSync(), isTrue); + expect(project.ios.flutterPluginSwiftPackageInProjectSettings, isFalse); + }); + + testWithMocks('is true if pbxproj does contain FlutterGeneratedPluginSwiftPackage in build process', () async { + final FlutterProject project = await someProject(); + project.ios.xcodeProjectInfoFile.createSync(recursive: true); + project.ios.xcodeProjectInfoFile.writeAsStringSync(''' +' 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };'; +'''); + expect(project.ios.xcodeProjectInfoFile.existsSync(), isTrue); + expect(project.ios.flutterPluginSwiftPackageInProjectSettings, isTrue); + }); + }); }); group('application bundle name', () { @@ -1724,6 +1834,10 @@ File androidPluginRegistrant(Directory parent) { } class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter { + FakeXcodeProjectInterpreter({ + this.version, + }); + final Map> buildSettingsByBuildContext = >{}; late XcodeProjectInfo xcodeProjectInfo; @@ -1745,6 +1859,9 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override bool get isInstalled => true; + + @override + Version? version; } class FakeAndroidSdkWithDir extends Fake implements AndroidSdk { @@ -1755,3 +1872,16 @@ class FakeAndroidSdkWithDir extends Fake implements AndroidSdk { @override Directory get directory => _directory; } + +class FakeFlutterManifest extends Fake implements FlutterManifest { + FakeFlutterManifest({ + this.disabledSwiftPackageManager = false, + this.isModule = false, + }); + + @override + bool disabledSwiftPackageManager; + + @override + bool isModule; +} 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 b91968bd9fd..61722bd8f80 100644 --- a/packages/flutter_tools/test/general.shard/xcode_backend_test.dart +++ b/packages/flutter_tools/test/general.shard/xcode_backend_test.dart @@ -223,6 +223,179 @@ void main() { ); }); }); + + group('prepare', () { + test('exits with useful error message when build mode not set', () { + final Directory buildDir = fileSystem.directory('/path/to/builds') + ..createSync(recursive: true); + final Directory flutterRoot = fileSystem.directory('/path/to/flutter') + ..createSync(recursive: true); + final File pipe = fileSystem.file('/tmp/pipe') + ..createSync(recursive: true); + const String buildMode = 'Debug'; + final TestContext context = TestContext( + ['prepare'], + { + 'ACTION': 'build', + 'BUILT_PRODUCTS_DIR': buildDir.path, + 'FLUTTER_ROOT': flutterRoot.path, + 'INFOPLIST_PATH': 'Info.plist', + }, + commands: [ + FakeCommand( + command: [ + '${flutterRoot.path}/bin/flutter', + 'assemble', + '--no-version-check', + '--output=${buildDir.path}/', + '-dTargetPlatform=ios', + '-dTargetFile=lib/main.dart', + '-dBuildMode=${buildMode.toLowerCase()}', + '-dIosArchs=', + '-dSdkRoot=', + '-dSplitDebugInfo=', + '-dTreeShakeIcons=', + '-dTrackWidgetCreation=', + '-dDartObfuscation=', + '-dAction=build', + '-dFrontendServerStarterPath=', + '--ExtraGenSnapshotOptions=', + '--DartDefines=', + '--ExtraFrontEndOptions=', + 'debug_unpack_ios', + ], + ), + ], + fileSystem: fileSystem, + scriptOutputStreamFile: pipe, + ); + expect( + () => context.run(), + throwsException, + ); + expect( + context.stderr, + contains('ERROR: Unknown FLUTTER_BUILD_MODE: null.\n'), + ); + }); + test('calls flutter assemble', () { + final Directory buildDir = fileSystem.directory('/path/to/builds') + ..createSync(recursive: true); + final Directory flutterRoot = fileSystem.directory('/path/to/flutter') + ..createSync(recursive: true); + final File pipe = fileSystem.file('/tmp/pipe') + ..createSync(recursive: true); + const String buildMode = 'Debug'; + final TestContext context = TestContext( + ['prepare'], + { + 'BUILT_PRODUCTS_DIR': buildDir.path, + 'CONFIGURATION': buildMode, + 'FLUTTER_ROOT': flutterRoot.path, + 'INFOPLIST_PATH': 'Info.plist', + }, + commands: [ + FakeCommand( + command: [ + '${flutterRoot.path}/bin/flutter', + 'assemble', + '--no-version-check', + '--output=${buildDir.path}/', + '-dTargetPlatform=ios', + '-dTargetFile=lib/main.dart', + '-dBuildMode=${buildMode.toLowerCase()}', + '-dIosArchs=', + '-dSdkRoot=', + '-dSplitDebugInfo=', + '-dTreeShakeIcons=', + '-dTrackWidgetCreation=', + '-dDartObfuscation=', + '-dAction=', + '-dFrontendServerStarterPath=', + '--ExtraGenSnapshotOptions=', + '--DartDefines=', + '--ExtraFrontEndOptions=', + 'debug_unpack_ios', + ], + ), + ], + fileSystem: fileSystem, + scriptOutputStreamFile: pipe, + )..run(); + expect(context.stderr, isEmpty); + }); + + test('forwards all env variables to flutter assemble', () { + final Directory buildDir = fileSystem.directory('/path/to/builds') + ..createSync(recursive: true); + final Directory flutterRoot = fileSystem.directory('/path/to/flutter') + ..createSync(recursive: true); + const String archs = 'arm64'; + const String buildMode = 'Release'; + const String dartObfuscation = 'false'; + const String dartDefines = 'flutter.inspector.structuredErrors%3Dtrue'; + const String expandedCodeSignIdentity = 'F1326572E0B71C3C8442805230CB4B33B708A2E2'; + const String extraFrontEndOptions = '--some-option'; + const String extraGenSnapshotOptions = '--obfuscate'; + const String frontendServerStarterPath = '/path/to/frontend_server_starter.dart'; + const String sdkRoot = '/path/to/sdk'; + const String splitDebugInfo = '/path/to/split/debug/info'; + const String trackWidgetCreation = 'true'; + const String treeShake = 'true'; + final TestContext context = TestContext( + ['prepare'], + { + 'ACTION': 'install', + 'ARCHS': archs, + 'BUILT_PRODUCTS_DIR': buildDir.path, + 'CODE_SIGNING_REQUIRED': 'YES', + 'CONFIGURATION': buildMode, + 'DART_DEFINES': dartDefines, + 'DART_OBFUSCATION': dartObfuscation, + 'EXPANDED_CODE_SIGN_IDENTITY': expandedCodeSignIdentity, + 'EXTRA_FRONT_END_OPTIONS': extraFrontEndOptions, + 'EXTRA_GEN_SNAPSHOT_OPTIONS': extraGenSnapshotOptions, + 'FLUTTER_ROOT': flutterRoot.path, + 'FRONTEND_SERVER_STARTER_PATH': frontendServerStarterPath, + 'INFOPLIST_PATH': 'Info.plist', + 'SDKROOT': sdkRoot, + 'FLAVOR': 'strawberry', + 'SPLIT_DEBUG_INFO': splitDebugInfo, + 'TRACK_WIDGET_CREATION': trackWidgetCreation, + 'TREE_SHAKE_ICONS': treeShake, + }, + commands: [ + FakeCommand( + command: [ + '${flutterRoot.path}/bin/flutter', + 'assemble', + '--no-version-check', + '--output=${buildDir.path}/', + '-dTargetPlatform=ios', + '-dTargetFile=lib/main.dart', + '-dBuildMode=${buildMode.toLowerCase()}', + '-dFlavor=strawberry', + '-dIosArchs=$archs', + '-dSdkRoot=$sdkRoot', + '-dSplitDebugInfo=$splitDebugInfo', + '-dTreeShakeIcons=$treeShake', + '-dTrackWidgetCreation=$trackWidgetCreation', + '-dDartObfuscation=$dartObfuscation', + '-dAction=install', + '-dFrontendServerStarterPath=$frontendServerStarterPath', + '--ExtraGenSnapshotOptions=$extraGenSnapshotOptions', + '--DartDefines=$dartDefines', + '--ExtraFrontEndOptions=$extraFrontEndOptions', + '-dCodesignIdentity=$expandedCodeSignIdentity', + 'release_unpack_ios', + ], + ), + ], + fileSystem: fileSystem, + )..run(); + expect(context.stderr, isEmpty); + }); + }); } class TestContext extends Context { diff --git a/packages/flutter_tools/test/general.shard/xcode_project_test.dart b/packages/flutter_tools/test/general.shard/xcode_project_test.dart new file mode 100644 index 00000000000..dfaf6096ccc --- /dev/null +++ b/packages/flutter_tools/test/general.shard/xcode_project_test.dart @@ -0,0 +1,213 @@ +// 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/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/ios/xcodeproj.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:test/fake.dart'; + +import '../src/common.dart'; +import '../src/context.dart'; + + +void main() { + group('IosProject', () { + testWithoutContext('managedDirectory', () { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final IosProject project = IosProject.fromFlutter( + FakeFlutterProject(fileSystem: fs), + ); + expect(project.managedDirectory.path, 'app_name/ios/Flutter'); + }); + + testWithoutContext('module managedDirectory', () { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final IosProject project = IosProject.fromFlutter( + FakeFlutterProject(fileSystem: fs, isModule: true), + ); + expect(project.managedDirectory.path, 'app_name/.ios/Flutter'); + }); + + testWithoutContext('ephemeralDirectory', () { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final IosProject project = IosProject.fromFlutter( + FakeFlutterProject(fileSystem: fs), + ); + expect(project.ephemeralDirectory.path, 'app_name/ios/Flutter/ephemeral'); + }); + + testWithoutContext('module ephemeralDirectory', () { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final IosProject project = IosProject.fromFlutter( + FakeFlutterProject(fileSystem: fs, isModule: true), + ); + expect(project.ephemeralDirectory.path, 'app_name/.ios/Flutter/ephemeral'); + }); + + testWithoutContext('flutterPluginSwiftPackageDirectory', () { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final IosProject project = IosProject.fromFlutter( + FakeFlutterProject(fileSystem: fs), + ); + expect( + project.flutterPluginSwiftPackageDirectory.path, + 'app_name/ios/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage', + ); + }); + + testWithoutContext('module flutterPluginSwiftPackageDirectory', () { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final IosProject project = IosProject.fromFlutter( + FakeFlutterProject(fileSystem: fs, isModule: true), + ); + expect( + project.flutterPluginSwiftPackageDirectory.path, + 'app_name/.ios/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage', + ); + }); + + testWithoutContext('xcodeConfigFor', () { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final IosProject project = IosProject.fromFlutter( + FakeFlutterProject(fileSystem: fs), + ); + expect( + project.xcodeConfigFor('Debug').path, + 'app_name/ios/Flutter/Debug.xcconfig', + ); + }); + + group('projectInfo', () { + testUsingContext('is null if XcodeProjectInterpreter is null', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final IosProject project = IosProject.fromFlutter( + FakeFlutterProject(fileSystem: fs), + ); + project.xcodeProject.createSync(recursive: true); + expect(await project.projectInfo(), isNull); + }, overrides: { + XcodeProjectInterpreter: () => null, + }); + + testUsingContext('is null if XcodeProjectInterpreter is not installed', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final IosProject project = IosProject.fromFlutter( + FakeFlutterProject(fileSystem: fs), + ); + project.xcodeProject.createSync(recursive: true); + expect(await project.projectInfo(), isNull); + }, overrides: { + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter( + isInstalled: false, + ), + }); + + testUsingContext('is null if xcodeproj does not exist', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final IosProject project = IosProject.fromFlutter( + FakeFlutterProject(fileSystem: fs), + ); + expect(await project.projectInfo(), isNull); + }, overrides: { + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(), + }); + + testUsingContext('returns XcodeProjectInfo', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final IosProject project = IosProject.fromFlutter( + FakeFlutterProject(fileSystem: fs), + ); + project.xcodeProject.createSync(recursive: true); + expect(await project.projectInfo(), isNotNull); + }, overrides: { + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(), + }); + }); + }); + + group('MacOSProject', () { + testWithoutContext('managedDirectory', () { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final MacOSProject project = MacOSProject.fromFlutter( + FakeFlutterProject(fileSystem: fs), + ); + expect(project.managedDirectory.path, 'app_name/macos/Flutter'); + }); + + testWithoutContext('module managedDirectory', () { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final MacOSProject project = MacOSProject.fromFlutter( + FakeFlutterProject(fileSystem: fs), + ); + expect(project.managedDirectory.path, 'app_name/macos/Flutter'); + }); + + testWithoutContext('ephemeralDirectory', () { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final MacOSProject project = MacOSProject.fromFlutter( + FakeFlutterProject(fileSystem: fs), + ); + expect(project.ephemeralDirectory.path, 'app_name/macos/Flutter/ephemeral'); + }); + + testWithoutContext('flutterPluginSwiftPackageDirectory', () { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final MacOSProject project = MacOSProject.fromFlutter( + FakeFlutterProject(fileSystem: fs), + ); + expect( + project.flutterPluginSwiftPackageDirectory.path, + 'app_name/macos/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage', + ); + }); + + testWithoutContext('xcodeConfigFor', () { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final MacOSProject project = MacOSProject.fromFlutter( + FakeFlutterProject(fileSystem: fs), + ); + expect( + project.xcodeConfigFor('Debug').path, + 'app_name/macos/Flutter/Flutter-Debug.xcconfig', + ); + }); + }); +} + +class FakeFlutterProject extends Fake implements FlutterProject { + FakeFlutterProject({ + required this.fileSystem, + this.isModule = false, + }); + + MemoryFileSystem fileSystem; + + @override + late final Directory directory = fileSystem.directory('app_name'); + + @override + bool isModule = false; +} + +class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter { + FakeXcodeProjectInterpreter({ + this.isInstalled = true, + }); + + @override + final bool isInstalled; + + @override + Future getInfo(String projectPath, {String? projectFilename}) async { + return XcodeProjectInfo( + [], + [], + ['Runner'], + BufferLogger.test(), + ); + } +} diff --git a/packages/flutter_tools/test/integration.shard/plist_parser_test.dart b/packages/flutter_tools/test/integration.shard/plist_parser_test.dart index da1dd99108b..3b6f5c61d09 100644 --- a/packages/flutter_tools/test/integration.shard/plist_parser_test.dart +++ b/packages/flutter_tools/test/integration.shard/plist_parser_test.dart @@ -213,4 +213,80 @@ void main() { expect(logger.statusText, isEmpty); expect(logger.errorText, isEmpty); }, skip: !platform.isMacOS); // [intended] requires macos tool chain. + + testWithoutContext('PlistParser.plistJsonContent can parse pbxproj file', () async { + final String xcodeProjectFile = fileSystem.path.join( + getFlutterRoot(), + 'dev', + 'integration_tests', + 'flutter_gallery', + 'ios', + 'Runner.xcodeproj', + 'project.pbxproj' + ); + + final BufferLogger logger = BufferLogger( + terminal: Terminal.test(), + outputPreferences: OutputPreferences(), + ); + + final PlistParser parser = PlistParser( + fileSystem: fileSystem, + processManager: processManager, + logger: logger, + ); + + final String? projectFileAsJson = parser.plistJsonContent(xcodeProjectFile); + expect(projectFileAsJson, isNotNull); + expect(projectFileAsJson, contains('"PRODUCT_NAME":"Flutter Gallery"')); + expect(logger.errorText, isEmpty); + }, skip: !platform.isMacOS); // [intended] requires macos tool chain. + + testWithoutContext('PlistParser.plistJsonContent can parse pbxproj file with unicode and emojis', () async { + String xcodeProjectFile = fileSystem.path.join( + getFlutterRoot(), + 'dev', + 'integration_tests', + 'flutter_gallery', + 'ios', + 'Runner.xcodeproj', + 'project.pbxproj' + ); + + final BufferLogger logger = BufferLogger( + terminal: Terminal.test(), + outputPreferences: OutputPreferences(), + ); + + final PlistParser parser = PlistParser( + fileSystem: fileSystem, + processManager: processManager, + logger: logger, + ); + + xcodeProjectFile = xcodeProjectFile.replaceAll('AppDelegate.m', 'AppDélegate.m'); + xcodeProjectFile = xcodeProjectFile.replaceAll('AppDelegate.h', 'App👍Delegate.h'); + + final String? projectFileAsJson = parser.plistJsonContent(xcodeProjectFile); + expect(projectFileAsJson, isNotNull); + expect(projectFileAsJson, contains('"PRODUCT_NAME":"Flutter Gallery"')); + expect(logger.errorText, isEmpty); + }, skip: !platform.isMacOS); // [intended] requires macos tool chain. + + testWithoutContext('PlistParser.plistJsonContent returns null when errors', () async { + final BufferLogger logger = BufferLogger( + terminal: Terminal.test(), + outputPreferences: OutputPreferences(), + ); + + final PlistParser parser = PlistParser( + fileSystem: fileSystem, + processManager: processManager, + logger: logger, + ); + + final String? projectFileAsJson = parser.plistJsonContent('bad/path'); + expect(projectFileAsJson, isNull); + expect(logger.errorText, isNotEmpty); + }, skip: !platform.isMacOS); // [intended] requires macos tool chain. } diff --git a/packages/flutter_tools/test/integration.shard/swift_package_manager_test.dart b/packages/flutter_tools/test/integration.shard/swift_package_manager_test.dart new file mode 100644 index 00000000000..85b157f310d --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/swift_package_manager_test.dart @@ -0,0 +1,770 @@ +// 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 'dart:convert'; + +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() { + final String flutterBin = fileSystem.path.join( + getFlutterRoot(), + 'bin', + 'flutter', + ); + + final List platforms = ['ios', 'macos']; + for (final String platformName in platforms) { + final List iosLanguages = [ + if (platformName == 'ios') 'objc', + 'swift', + ]; + final _Plugin integrationTestPlugin = _integrationTestPlugin(platformName); + + for (final String iosLanguage in iosLanguages) { + test('Swift Package Manager not used when feature is disabled for $platformName with $iosLanguage', () async { + final Directory workingDirectory = fileSystem.systemTempDirectory + .createTempSync('swift_package_manager_disabled.'); + final String workingDirectoryPath = workingDirectory.path; + try { + await _disableSwiftPackageManager(flutterBin, workingDirectoryPath); + + // Create and build an app using the CocoaPods version of + // integration_test. + final String appDirectoryPath = await _createApp( + flutterBin, + workingDirectoryPath, + iosLanguage: iosLanguage, + platform: platformName, + options: ['--platforms=$platformName'], + ); + _addDependency( + appDirectoryPath: appDirectoryPath, + plugin: integrationTestPlugin, + ); + await _buildApp( + flutterBin, + appDirectoryPath, + options: [platformName, '--debug', '-v'], + expectedLines: _expectedLines( + platform: platformName, + appDirectoryPath: appDirectoryPath, + cococapodsPlugin: integrationTestPlugin, + ), + unexpectedLines: _unexpectedLines( + platform: platformName, + appDirectoryPath: appDirectoryPath, + cococapodsPlugin: integrationTestPlugin, + ), + ); + expect( + fileSystem + .directory(appDirectoryPath) + .childDirectory(platformName) + .childFile('Podfile') + .existsSync(), + isTrue, + ); + expect( + fileSystem + .directory(appDirectoryPath) + .childDirectory(platformName) + .childDirectory('Flutter') + .childDirectory('ephemeral') + .childDirectory('Packages') + .childDirectory('FlutterGeneratedPluginSwiftPackage') + .existsSync(), + isFalse, + ); + } finally { + ErrorHandlingFileSystem.deleteIfExists( + workingDirectory, + recursive: true, + ); + } + }, skip: !platform.isMacOS); // [intended] Swift Package Manager only works on macos. + + test('Swift Package Manager integration for $platformName with $iosLanguage', () async { + final Directory workingDirectory = fileSystem.systemTempDirectory + .createTempSync('swift_package_manager_enabled.'); + final String workingDirectoryPath = workingDirectory.path; + try { + // Create and build an app using the Swift Package Manager version of + // integration_test. + await _enableSwiftPackageManager(flutterBin, workingDirectoryPath); + + final String appDirectoryPath = await _createApp( + flutterBin, + workingDirectoryPath, + iosLanguage: iosLanguage, + platform: platformName, + usesSwiftPackageManager: true, + options: ['--platforms=$platformName'], + ); + _addDependency(appDirectoryPath: appDirectoryPath, plugin: integrationTestPlugin); + await _buildApp( + flutterBin, + appDirectoryPath, + options: [platformName, '--debug', '-v'], + expectedLines: _expectedLines( + platform: platformName, + appDirectoryPath: appDirectoryPath, + swiftPackageMangerEnabled: true, + swiftPackagePlugin: integrationTestPlugin, + ), + unexpectedLines: _unexpectedLines( + platform: platformName, + appDirectoryPath: appDirectoryPath, + swiftPackageMangerEnabled: true, + swiftPackagePlugin: integrationTestPlugin, + ), + ); + + expect( + fileSystem + .directory(appDirectoryPath) + .childDirectory(platformName) + .childFile('Podfile') + .existsSync(), + isFalse, + ); + expect( + fileSystem + .directory(appDirectoryPath) + .childDirectory(platformName) + .childDirectory('Flutter') + .childDirectory('ephemeral') + .childDirectory('Packages') + .childDirectory('FlutterGeneratedPluginSwiftPackage') + .existsSync(), + isTrue, + ); + + // Build an app using both a CocoaPods and Swift Package Manager plugin. + await _cleanApp(flutterBin, appDirectoryPath); + final _Plugin createdCocoaPodsPlugin = await _createPlugin( + flutterBin, + workingDirectoryPath, + platform: platformName, + iosLanguage: iosLanguage, + ); + _addDependency( + appDirectoryPath: appDirectoryPath, + plugin: createdCocoaPodsPlugin, + ); + await _buildApp( + flutterBin, + appDirectoryPath, + options: [platformName, '--debug', '-v'], + expectedLines: _expectedLines( + platform: platformName, + appDirectoryPath: appDirectoryPath, + cococapodsPlugin: createdCocoaPodsPlugin, + swiftPackageMangerEnabled: true, + swiftPackagePlugin: integrationTestPlugin, + ), + unexpectedLines: _unexpectedLines( + platform: platformName, + appDirectoryPath: appDirectoryPath, + cococapodsPlugin: createdCocoaPodsPlugin, + swiftPackageMangerEnabled: true, + swiftPackagePlugin: integrationTestPlugin, + ), + ); + + expect( + fileSystem + .directory(appDirectoryPath) + .childDirectory(platformName) + .childFile('Podfile') + .existsSync(), + isTrue, + ); + expect( + fileSystem + .directory(appDirectoryPath) + .childDirectory(platformName) + .childDirectory('Flutter') + .childDirectory('ephemeral') + .childDirectory('Packages') + .childDirectory('FlutterGeneratedPluginSwiftPackage') + .existsSync(), + isTrue, + ); + + // Build app again but with Swift Package Manager disabled by config. + // App will now use CocoaPods version of integration_test plugin. + await _disableSwiftPackageManager(flutterBin, workingDirectoryPath); + await _cleanApp(flutterBin, appDirectoryPath); + await _buildApp( + flutterBin, + appDirectoryPath, + options: [platformName, '--debug', '-v'], + expectedLines: _expectedLines( + platform: platformName, + appDirectoryPath: appDirectoryPath, + cococapodsPlugin: integrationTestPlugin, + ), + unexpectedLines: _unexpectedLines( + platform: platformName, + appDirectoryPath: appDirectoryPath, + cococapodsPlugin: integrationTestPlugin, + ), + ); + + // Build app again but with Swift Package Manager disabled by pubspec. + // App will still use CocoaPods version of integration_test plugin. + await _enableSwiftPackageManager(flutterBin, workingDirectoryPath); + await _cleanApp(flutterBin, appDirectoryPath); + _disableSwiftPackageManagerByPubspec(appDirectoryPath: appDirectoryPath); + await _buildApp( + flutterBin, + appDirectoryPath, + options: [platformName, '--debug', '-v'], + expectedLines: _expectedLines( + platform: platformName, + appDirectoryPath: appDirectoryPath, + cococapodsPlugin: integrationTestPlugin, + ), + unexpectedLines: _unexpectedLines( + platform: platformName, + appDirectoryPath: appDirectoryPath, + cococapodsPlugin: integrationTestPlugin, + ), + ); + } finally { + await _disableSwiftPackageManager(flutterBin, workingDirectoryPath); + ErrorHandlingFileSystem.deleteIfExists( + workingDirectory, + recursive: true, + ); + } + }, skip: !platform.isMacOS); // [intended] Swift Package Manager only works on macos. + } + + test('Build $platformName-framework with non-module app uses CocoaPods', () async { + final Directory workingDirectory = fileSystem.systemTempDirectory + .createTempSync('swift_package_manager_build_framework.'); + final String workingDirectoryPath = workingDirectory.path; + try { + // Create and build an app using the Swift Package Manager version of + // integration_test. + await _enableSwiftPackageManager(flutterBin, workingDirectoryPath); + + final String appDirectoryPath = await _createApp( + flutterBin, + workingDirectoryPath, + iosLanguage: 'swift', + platform: platformName, + usesSwiftPackageManager: true, + options: ['--platforms=$platformName'], + ); + _addDependency(appDirectoryPath: appDirectoryPath, plugin: integrationTestPlugin); + + await _buildApp( + flutterBin, + appDirectoryPath, + options: [platformName, '--config-only', '-v'], + expectedLines: [ + 'Adding Swift Package Manager integration...' + ] + ); + + expect( + fileSystem + .directory(appDirectoryPath) + .childDirectory(platformName) + .childFile('Podfile') + .existsSync(), + isFalse, + ); + expect( + fileSystem + .directory(appDirectoryPath) + .childDirectory(platformName) + .childDirectory('Flutter') + .childDirectory('ephemeral') + .childDirectory('Packages') + .childDirectory('FlutterGeneratedPluginSwiftPackage') + .existsSync(), + isTrue, + ); + + // Create and build framework using the CocoaPods version of + // integration_test even though Swift Package Manager is enabled. + await _buildApp( + flutterBin, + appDirectoryPath, + options: [ + '$platformName-framework', + '--no-debug', + '--no-profile', + '-v', + ], + expectedLines: [ + 'Swift Package Manager does not yet support this command. CocoaPods will be used instead.' + ] + ); + + expect( + fileSystem + .directory(appDirectoryPath) + .childDirectory('build') + .childDirectory(platformName) + .childDirectory('framework') + .childDirectory('Release') + .childDirectory('${integrationTestPlugin.pluginName}.xcframework') + .existsSync(), + isTrue, + ); + } finally { + await _disableSwiftPackageManager(flutterBin, workingDirectoryPath); + ErrorHandlingFileSystem.deleteIfExists( + workingDirectory, + recursive: true, + ); + } + }, skip: !platform.isMacOS); // [intended] Swift Package Manager only works on macos. + } + + test('Build ios-framework with module app uses CocoaPods', () async { + final Directory workingDirectory = fileSystem.systemTempDirectory + .createTempSync('swift_package_manager_build_framework_module.'); + final String workingDirectoryPath = workingDirectory.path; + try { + // Create and build module and framework using the CocoaPods version of + // integration_test even though Swift Package Manager is enabled. + await _enableSwiftPackageManager(flutterBin, workingDirectoryPath); + + final String appDirectoryPath = await _createApp( + flutterBin, + workingDirectoryPath, + iosLanguage: 'swift', + platform: 'ios', + usesSwiftPackageManager: true, + options: ['--template=module'], + ); + final _Plugin integrationTestPlugin = _integrationTestPlugin('ios'); + _addDependency(appDirectoryPath: appDirectoryPath, plugin: integrationTestPlugin); + + await _buildApp( + flutterBin, + appDirectoryPath, + options: ['ios', '--config-only', '-v'], + unexpectedLines: [ + 'Adding Swift Package Manager integration...' + ] + ); + + expect( + fileSystem + .directory(appDirectoryPath) + .childDirectory('.ios') + .childFile('Podfile') + .existsSync(), + isTrue, + ); + expect( + fileSystem + .directory(appDirectoryPath) + .childDirectory('.ios') + .childDirectory('Flutter') + .childDirectory('ephemeral') + .childDirectory('Packages') + .childDirectory('FlutterGeneratedPluginSwiftPackage') + .existsSync(), + isFalse, + ); + + await _buildApp( + flutterBin, + appDirectoryPath, + options: [ + 'ios-framework', + '--no-debug', + '--no-profile', + '-v', + ], + unexpectedLines: [ + 'Adding Swift Package Manager integration...', + 'Swift Package Manager does not yet support this command. CocoaPods will be used instead.' + ] + ); + + expect( + fileSystem + .directory(appDirectoryPath) + .childDirectory('build') + .childDirectory('ios') + .childDirectory('framework') + .childDirectory('Release') + .childDirectory('${integrationTestPlugin.pluginName}.xcframework') + .existsSync(), + isTrue, + ); + } finally { + await _disableSwiftPackageManager(flutterBin, workingDirectoryPath); + ErrorHandlingFileSystem.deleteIfExists( + workingDirectory, + recursive: true, + ); + } + }, skip: !platform.isMacOS); // [intended] Swift Package Manager only works on macos. +} + +Future _enableSwiftPackageManager( + String flutterBin, + String workingDirectory, +) async { + final ProcessResult result = await processManager.run( + [ + flutterBin, + ...getLocalEngineArguments(), + 'config', + '--enable-swift-package-manager', + '-v', + ], + workingDirectory: workingDirectory, + ); + expect( + result.exitCode, + 0, + reason: 'Failed to enable Swift Package Manager: \n' + 'stdout: \n${result.stdout}\n' + 'stderr: \n${result.stderr}\n', + verbose: true, + ); +} + +Future _disableSwiftPackageManager( + String flutterBin, + String workingDirectory, +) async { + final ProcessResult result = await processManager.run( + [ + flutterBin, + ...getLocalEngineArguments(), + 'config', + '--no-enable-swift-package-manager', + '-v', + ], + workingDirectory: workingDirectory, + ); + expect( + result.exitCode, + 0, + reason: 'Failed to disable Swift Package Manager: \n' + 'stdout: \n${result.stdout}\n' + 'stderr: \n${result.stderr}\n', + verbose: true, + ); +} + +Future _createApp( + String flutterBin, + String workingDirectory, { + required String platform, + required String iosLanguage, + required List options, + bool usesSwiftPackageManager = false, +}) async { + final String appTemplateType = usesSwiftPackageManager ? 'spm' : 'default'; + + final String appName = '${platform}_${iosLanguage}_${appTemplateType}_app'; + final ProcessResult result = await processManager.run( + [ + flutterBin, + ...getLocalEngineArguments(), + 'create', + '--org', + 'io.flutter.devicelab', + '-i', + iosLanguage, + ...options, + appName, + ], + workingDirectory: workingDirectory, + ); + + expect( + result.exitCode, + 0, + reason: 'Failed to create app: \n' + 'stdout: \n${result.stdout}\n' + 'stderr: \n${result.stderr}\n', + ); + + return fileSystem.path.join( + workingDirectory, + appName, + ); +} + +Future _buildApp( + String flutterBin, + String workingDirectory, { + required List options, + List? expectedLines, + List? unexpectedLines, +}) async { + final List remainingExpectedLines = expectedLines ?? []; + final List unexpectedLinesFound = []; + final List command = [ + flutterBin, + ...getLocalEngineArguments(), + 'build', + ...options, + ]; + + final ProcessResult result = await processManager.run( + command, + workingDirectory: workingDirectory, + ); + + final List stdout = LineSplitter.split(result.stdout.toString()).toList(); + final List stderr = LineSplitter.split(result.stderr.toString()).toList(); + final List output = stdout + stderr; + for (final String line in output) { + // Remove "[ +3 ms] " prefix + String trimmedLine = line.trim(); + if (trimmedLine.startsWith('[')) { + final int prefixEndIndex = trimmedLine.indexOf(']'); + if (prefixEndIndex > 0) { + trimmedLine = trimmedLine + .substring(prefixEndIndex + 1, trimmedLine.length) + .trim(); + } + } + remainingExpectedLines.remove(trimmedLine); + remainingExpectedLines.removeWhere((Pattern expectedLine) => trimmedLine.contains(expectedLine)); + if (unexpectedLines != null && unexpectedLines.contains(trimmedLine)) { + unexpectedLinesFound.add(trimmedLine); + } + } + expect( + result.exitCode, + 0, + reason: 'Failed to build app for "${command.join(' ')}":\n' + 'stdout: \n${result.stdout}\n' + 'stderr: \n${result.stderr}\n', + ); + expect( + remainingExpectedLines, + isEmpty, + reason: 'Did not find expected lines for "${command.join(' ')}":\n' + 'stdout: \n${result.stdout}\n' + 'stderr: \n${result.stderr}\n', + ); + expect( + unexpectedLinesFound, + isEmpty, + reason: 'Found unexpected lines for "${command.join(' ')}":\n' + 'stdout: \n${result.stdout}\n' + 'stderr: \n${result.stderr}\n', + ); +} + +Future _cleanApp(String flutterBin, String workingDirectory) async { + final ProcessResult result = await processManager.run( + [ + flutterBin, + ...getLocalEngineArguments(), + 'clean', + ], + workingDirectory: workingDirectory, + ); + expect( + result.exitCode, + 0, + reason: 'Failed to clean app: \n' + 'stdout: \n${result.stdout}\n' + 'stderr: \n${result.stderr}\n', + ); +} + +Future<_Plugin> _createPlugin( + String flutterBin, + String workingDirectory, { + required String platform, + required String iosLanguage, + bool usesSwiftPackageManager = false, +}) async { + final String dependencyManager = usesSwiftPackageManager ? 'spm' : 'cocoapods'; + + // Create plugin + final String pluginName = '${platform}_${iosLanguage}_${dependencyManager}_plugin'; + final ProcessResult result = await processManager.run( + [ + flutterBin, + ...getLocalEngineArguments(), + 'create', + '--org', + 'io.flutter.devicelab', + '--template=plugin', + '--platforms=$platform', + '-i', + iosLanguage, + pluginName, + ], + workingDirectory: workingDirectory, + ); + + expect( + result.exitCode, + 0, + reason: 'Failed to create plugin: \n' + 'stdout: \n${result.stdout}\n' + 'stderr: \n${result.stderr}\n', + ); + + final Directory pluginDirectory = fileSystem.directory( + fileSystem.path.join(workingDirectory, pluginName), + ); + + return _Plugin( + pluginName: pluginName, + pluginPath: pluginDirectory.path, + platform: platform, + ); +} + +void _addDependency({ + required _Plugin plugin, + required String appDirectoryPath, +}) { + final File pubspec = fileSystem.file( + fileSystem.path.join(appDirectoryPath, 'pubspec.yaml'), + ); + final String pubspecContent = pubspec.readAsStringSync(); + pubspec.writeAsStringSync( + pubspecContent.replaceFirst( + '\ndependencies:\n', + '\ndependencies:\n ${plugin.pluginName}:\n path: ${plugin.pluginPath}\n', + ), + ); +} + +void _disableSwiftPackageManagerByPubspec({ + required String appDirectoryPath, +}) { + final File pubspec = fileSystem.file( + fileSystem.path.join(appDirectoryPath, 'pubspec.yaml'), + ); + final String pubspecContent = pubspec.readAsStringSync(); + pubspec.writeAsStringSync( + pubspecContent.replaceFirst( + '\n# The following section is specific to Flutter packages.\nflutter:\n', + '\n# The following section is specific to Flutter packages.\nflutter:\n disable-swift-package-manager: true', + ), + ); +} + +_Plugin _integrationTestPlugin(String platform) { + final String flutterRoot = getFlutterRoot(); + return _Plugin( + platform: platform, + pluginName: + (platform == 'ios') ? 'integration_test' : 'integration_test_macos', + pluginPath: (platform == 'ios') + ? fileSystem.path.join(flutterRoot, 'packages', 'integration_test') + : fileSystem.path.join(flutterRoot, 'packages', 'integration_test', 'integration_test_macos'), + ); +} + +List _expectedLines({ + required String platform, + required String appDirectoryPath, + _Plugin? cococapodsPlugin, + _Plugin? swiftPackagePlugin, + bool swiftPackageMangerEnabled = false, +}) { + final String frameworkName = platform == 'ios' ? 'Flutter' : 'FlutterMacOS'; + final String appPlatformDirectoryPath = fileSystem.path.join( + appDirectoryPath, + platform, + ); + + final List expectedLines = []; + if (swiftPackageMangerEnabled) { + expectedLines.addAll([ + 'FlutterGeneratedPluginSwiftPackage: $appPlatformDirectoryPath/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage', + "➜ Explicit dependency on target 'FlutterGeneratedPluginSwiftPackage' in project 'FlutterGeneratedPluginSwiftPackage'", + ]); + } + if (swiftPackagePlugin != null) { + // If using a Swift Package plugin, but Swift Package Manager is not enabled, it falls back to being used as a CocoaPods plugin. + if (swiftPackageMangerEnabled) { + expectedLines.addAll([ + RegExp('${swiftPackagePlugin.pluginName}: [/private]*${swiftPackagePlugin.pluginPath}/$platform/${swiftPackagePlugin.pluginName} @ local'), + "➜ Explicit dependency on target '${swiftPackagePlugin.pluginName}' in project '${swiftPackagePlugin.pluginName}'", + ]); + } else { + expectedLines.addAll([ + '-> Installing ${swiftPackagePlugin.pluginName} (0.0.1)', + "➜ Explicit dependency on target '${swiftPackagePlugin.pluginName}' in project 'Pods'", + ]); + } + } + if (cococapodsPlugin != null) { + expectedLines.addAll([ + 'Running pod install...', + '-> Installing $frameworkName (1.0.0)', + '-> Installing ${cococapodsPlugin.pluginName} (0.0.1)', + "Target 'Pods-Runner' in project 'Pods'", + "➜ Explicit dependency on target '$frameworkName' in project 'Pods'", + "➜ Explicit dependency on target '${cococapodsPlugin.pluginName}' in project 'Pods'", + ]); + } + return expectedLines; +} + +List _unexpectedLines({ + required String platform, + required String appDirectoryPath, + _Plugin? cococapodsPlugin, + _Plugin? swiftPackagePlugin, + bool swiftPackageMangerEnabled = false, +}) { + final String frameworkName = platform == 'ios' ? 'Flutter' : 'FlutterMacOS'; + final List unexpectedLines = []; + if (cococapodsPlugin == null) { + unexpectedLines.addAll([ + 'Running pod install...', + '-> Installing $frameworkName (1.0.0)', + "Target 'Pods-Runner' in project 'Pods'", + ]); + } + if (swiftPackagePlugin != null) { + if (swiftPackageMangerEnabled) { + unexpectedLines.addAll([ + '-> Installing ${swiftPackagePlugin.pluginName} (0.0.1)', + "➜ Explicit dependency on target '${swiftPackagePlugin.pluginName}' in project 'Pods'", + ]); + } else { + unexpectedLines.addAll([ + '${swiftPackagePlugin.pluginName}: ${swiftPackagePlugin.pluginPath}/$platform/${swiftPackagePlugin.pluginName} @ local', + "➜ Explicit dependency on target '${swiftPackagePlugin.pluginName}' in project '${swiftPackagePlugin.pluginName}'", + ]); + } + } + return unexpectedLines; +} + +class _Plugin { + _Plugin({ + required this.pluginName, + required this.pluginPath, + required this.platform, + }); + + final String pluginName; + final String pluginPath; + final String platform; + String get exampleAppPath => fileSystem.path.join(pluginPath, 'example'); + String get exampleAppPlatformPath => fileSystem.path.join(exampleAppPath, platform); +} diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart index aa874cfedda..b812679f986 100644 --- a/packages/flutter_tools/test/src/fakes.dart +++ b/packages/flutter_tools/test/src/fakes.dart @@ -315,6 +315,11 @@ class FakePlistParser implements PlistParser { @override String? plistXmlContent(String plistFilePath) => throw UnimplementedError(); + @override + String? plistJsonContent(String filePath, {bool sorted = false}) { + throw UnimplementedError(); + } + @override Map parseFile(String plistFilePath) { return _underlyingValues; @@ -472,6 +477,7 @@ class TestFeatureFlags implements FeatureFlags { this.isCliAnimationEnabled = true, this.isNativeAssetsEnabled = false, this.isPreviewDeviceEnabled = false, + this.isSwiftPackageManagerEnabled = false, }); @override @@ -507,6 +513,9 @@ class TestFeatureFlags implements FeatureFlags { @override final bool isPreviewDeviceEnabled; + @override + final bool isSwiftPackageManagerEnabled; + @override bool isEnabled(Feature feature) { return switch (feature) { diff --git a/packages/integration_test/integration_test_macos/macos/integration_test_macos.podspec b/packages/integration_test/integration_test_macos/macos/integration_test_macos.podspec index e22fbf1e170..01ce5e85d0a 100644 --- a/packages/integration_test/integration_test_macos/macos/integration_test_macos.podspec +++ b/packages/integration_test/integration_test_macos/macos/integration_test_macos.podspec @@ -15,10 +15,10 @@ LICENSE } s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } s.source = { :http => 'https://github.com/flutter/flutter/tree/main/packages/integration_test/integration_test_macos' } - s.source_files = 'Classes/**/*' + s.source_files = 'integration_test_macos/Sources/integration_test_macos/**/*' s.dependency 'FlutterMacOS' - s.platform = :osx, '10.11' + s.platform = :osx, '10.14' s.swift_version = '5.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/integration_test/integration_test_macos/macos/integration_test_macos/Package.swift b/packages/integration_test/integration_test_macos/macos/integration_test_macos/Package.swift new file mode 100644 index 00000000000..9ef9c76b9b2 --- /dev/null +++ b/packages/integration_test/integration_test_macos/macos/integration_test_macos/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "integration_test_macos", + platforms: [ + .macOS("10.14"), + ], + products: [ + .library(name: "integration-test-macos", targets: ["integration_test_macos"]), + ], + targets: [ + .target( + name: "integration_test_macos", + resources: [ + .process("Resources"), + ] + ), + ] +) diff --git a/packages/integration_test/integration_test_macos/macos/Classes/IntegrationTestPlugin.swift b/packages/integration_test/integration_test_macos/macos/integration_test_macos/Sources/integration_test_macos/IntegrationTestPlugin.swift similarity index 100% rename from packages/integration_test/integration_test_macos/macos/Classes/IntegrationTestPlugin.swift rename to packages/integration_test/integration_test_macos/macos/integration_test_macos/Sources/integration_test_macos/IntegrationTestPlugin.swift diff --git a/packages/integration_test/integration_test_macos/macos/Assets/.gitkeep b/packages/integration_test/integration_test_macos/macos/integration_test_macos/Sources/integration_test_macos/Resources/.gitkeep similarity index 100% rename from packages/integration_test/integration_test_macos/macos/Assets/.gitkeep rename to packages/integration_test/integration_test_macos/macos/integration_test_macos/Sources/integration_test_macos/Resources/.gitkeep diff --git a/packages/integration_test/ios/integration_test.podspec b/packages/integration_test/ios/integration_test.podspec index 71500bce2db..c5848dd8524 100644 --- a/packages/integration_test/ios/integration_test.podspec +++ b/packages/integration_test/ios/integration_test.podspec @@ -15,8 +15,8 @@ LICENSE } s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } s.source = { :http => 'https://github.com/flutter/flutter/tree/main/packages/integration_test' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' + s.source_files = 'integration_test/Sources/integration_test/**/*.{h,m}' + s.public_header_files = 'integration_test/Sources/integration_test/**/*.h' s.dependency 'Flutter' s.ios.framework = 'UIKit' diff --git a/packages/integration_test/ios/integration_test/Package.swift b/packages/integration_test/ios/integration_test/Package.swift new file mode 100644 index 00000000000..b9bd1a9a450 --- /dev/null +++ b/packages/integration_test/ios/integration_test/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "integration_test", + platforms: [ + .iOS("12.0"), + ], + products: [ + .library(name: "integration-test", targets: ["integration_test"]), + ], + targets: [ + .target( + name: "integration_test", + resources: [ + .process("Resources"), + ], + cSettings: [ + .headerSearchPath("include/integration_test"), + ] + ), + ] +) diff --git a/packages/integration_test/ios/Classes/FLTIntegrationTestRunner.m b/packages/integration_test/ios/integration_test/Sources/integration_test/FLTIntegrationTestRunner.m similarity index 100% rename from packages/integration_test/ios/Classes/FLTIntegrationTestRunner.m rename to packages/integration_test/ios/integration_test/Sources/integration_test/FLTIntegrationTestRunner.m diff --git a/packages/integration_test/ios/Classes/IntegrationTestIosTest.m b/packages/integration_test/ios/integration_test/Sources/integration_test/IntegrationTestIosTest.m similarity index 100% rename from packages/integration_test/ios/Classes/IntegrationTestIosTest.m rename to packages/integration_test/ios/integration_test/Sources/integration_test/IntegrationTestIosTest.m diff --git a/packages/integration_test/ios/Classes/IntegrationTestPlugin.m b/packages/integration_test/ios/integration_test/Sources/integration_test/IntegrationTestPlugin.m similarity index 100% rename from packages/integration_test/ios/Classes/IntegrationTestPlugin.m rename to packages/integration_test/ios/integration_test/Sources/integration_test/IntegrationTestPlugin.m diff --git a/packages/integration_test/ios/Assets/.gitkeep b/packages/integration_test/ios/integration_test/Sources/integration_test/Resources/.gitkeep similarity index 100% rename from packages/integration_test/ios/Assets/.gitkeep rename to packages/integration_test/ios/integration_test/Sources/integration_test/Resources/.gitkeep diff --git a/packages/integration_test/ios/Classes/FLTIntegrationTestRunner.h b/packages/integration_test/ios/integration_test/Sources/integration_test/include/FLTIntegrationTestRunner.h similarity index 100% rename from packages/integration_test/ios/Classes/FLTIntegrationTestRunner.h rename to packages/integration_test/ios/integration_test/Sources/integration_test/include/FLTIntegrationTestRunner.h diff --git a/packages/integration_test/ios/Classes/IntegrationTestIosTest.h b/packages/integration_test/ios/integration_test/Sources/integration_test/include/IntegrationTestIosTest.h similarity index 100% rename from packages/integration_test/ios/Classes/IntegrationTestIosTest.h rename to packages/integration_test/ios/integration_test/Sources/integration_test/include/IntegrationTestIosTest.h diff --git a/packages/integration_test/ios/Classes/IntegrationTestPlugin.h b/packages/integration_test/ios/integration_test/Sources/integration_test/include/IntegrationTestPlugin.h similarity index 100% rename from packages/integration_test/ios/Classes/IntegrationTestPlugin.h rename to packages/integration_test/ios/integration_test/Sources/integration_test/include/IntegrationTestPlugin.h