From cb2a868724e65717981aaa5126469f73539c2b10 Mon Sep 17 00:00:00 2001 From: Victoria Ashworth Date: Tue, 3 Mar 2026 21:31:49 -0600 Subject: [PATCH 1/3] Build CocoaPod plugin frameworks for Add to App FlutterPluginRegistrant --- .../lib/src/commands/build_swift_package.dart | 362 +++++++ .../flutter_tools/lib/src/darwin/darwin.dart | 10 + .../hermetic/build_swift_package_test.dart | 922 ++++++++++++++++-- 3 files changed, 1204 insertions(+), 90 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/build_swift_package.dart b/packages/flutter_tools/lib/src/commands/build_swift_package.dart index a6c7addab4158..79c4d28ebac8d 100644 --- a/packages/flutter_tools/lib/src/commands/build_swift_package.dart +++ b/packages/flutter_tools/lib/src/commands/build_swift_package.dart @@ -26,6 +26,7 @@ import '../darwin/darwin.dart'; import '../features.dart'; import '../flutter_plugins.dart'; import '../ios/xcodeproj.dart'; +import '../macos/cocoapod_utils.dart'; import '../macos/swift_package_manager.dart'; import '../macos/swift_packages.dart'; import '../macos/xcode.dart'; @@ -41,6 +42,7 @@ const String _kFileAnIssue = const String _kFrameworks = 'Frameworks'; const String _kPackages = 'Packages'; const String _kPlugins = 'Plugins'; +const String _kCocoaPods = 'CocoaPods'; const String _kNativeAssets = 'NativeAssets'; const String kPluginSwiftPackageName = 'FlutterPluginRegistrant'; const String _kSources = 'Sources'; @@ -95,6 +97,24 @@ class BuildSwiftPackage extends BuildSubCommand { 'build-mode', allowed: ['debug', 'profile', 'release'], defaultsTo: ['debug', 'profile', 'release'], + ) + ..addFlag( + 'static', + help: + 'Build CocoaPods plugins as static frameworks. Consider using with ' + '--no-cocoapods-as-binary-targets to reduce bundle size.', + ) + ..addFlag( + // When an XCFramework binary SwiftPM target has static frameworks, it still embeds the + // framework, but with a stub binary. There will be a note in the build logs that says + // "Injecting stub binary for codeless framework". Developers can alternatively not include + // the CocoaPod XCFrameworks through SwiftPM and link them directly in their Xcode project. + 'cocoapods-as-binary-targets', + defaultsTo: true, + help: + 'Adds CocoaPod generated XCFrameworks as binary targets in the generated Swift package. ' + 'When using the --static flag, consider disabling this and linking the frameworks ' + 'manually to reduce bundle size.', ); } @@ -240,6 +260,10 @@ class BuildSwiftPackage extends BuildSubCommand { targetPlatform: _targetPlatform, utils: utils, ); + late final cocoapodDependencies = CocoaPodPluginDependencies( + targetPlatform: _targetPlatform, + utils: utils, + ); @override Future runCommand() async { @@ -328,6 +352,13 @@ class BuildSwiftPackage extends BuildSubCommand { targetFile: targetFile, xcframeworkOutput: xcframeworkOutput, ); + + await cocoapodDependencies.generateArtifacts( + buildInfo: buildInfo, + buildStatic: boolArg('static'), + cacheDirectory: cacheDirectory, + xcframeworkOutput: xcframeworkOutput, + ); } Future _generateSwiftPackages({ @@ -352,6 +383,8 @@ class BuildSwiftPackage extends BuildSubCommand { pluginSwiftDependencies: pluginSwiftDependencies, flutterFrameworkDependency: flutterFrameworkDependency, appAndNativeAssetsDependencies: appAndNativeAssetsDependencies, + cocoapodDependencies: cocoapodDependencies, + includeCocoaPodBinaryTargets: boolArg('cocoapods-as-binary-targets'), packagesForConfiguration: packagesForConfiguration, xcframeworkOutput: xcframeworkOutput, ); @@ -403,6 +436,8 @@ class FlutterPluginRegistrantSwiftPackage { required FlutterPluginSwiftDependencies pluginSwiftDependencies, required FlutterFrameworkDependency flutterFrameworkDependency, required AppFrameworkAndNativeAssetsDependencies appAndNativeAssetsDependencies, + required CocoaPodPluginDependencies cocoapodDependencies, + required bool includeCocoaPodBinaryTargets, required Directory xcframeworkOutput, }) async { final ( @@ -419,10 +454,18 @@ class FlutterPluginRegistrantSwiftPackage { xcframeworkOutput: xcframeworkOutput, ); + final ( + List cocoaPodDependencies, + List cocoaPodTargets, + ) = cocoapodDependencies.generateDependencies( + xcframeworkOutput: xcframeworkOutput, + ); + final targetDependencies = [ flutterFrameworkDependency.targetDependency, ...pluginTargetDependencies, ...flutterGeneratedDependencies, + if (includeCocoaPodBinaryTargets) ...cocoaPodDependencies, ]; final packageDependencies = [ flutterFrameworkDependency.packageDependency, @@ -441,6 +484,7 @@ class FlutterPluginRegistrantSwiftPackage { final targets = [ SwiftPackageTarget.defaultTarget(name: swiftPackageName, dependencies: targetDependencies), ...flutterGeneratedTargets, + if (includeCocoaPodBinaryTargets) ...cocoaPodTargets, ]; final pluginsPackage = SwiftPackage( @@ -1154,6 +1198,324 @@ class AppFrameworkAndNativeAssetsDependencies { } } +/// Class that encapsulates the logic for building CocoaPod plugins for every platform and sdk into +/// frameworks and then combines them into a single xcframework for each. +@visibleForTesting +class CocoaPodPluginDependencies { + CocoaPodPluginDependencies({ + required FlutterDarwinPlatform targetPlatform, + required BuildSwiftPackageUtils utils, + }) : _targetPlatform = targetPlatform, + _utils = utils; + + final FlutterDarwinPlatform _targetPlatform; + final BuildSwiftPackageUtils _utils; + + /// Builds CocoaPod plugins for every platform and sdk into frameworks and then combines them into + /// a single xcframework for each. + /// + /// Intermediate build files are put in the [cacheDirectory]. The final xcframeworks are copied to + /// the [xcframeworkOutput]. + Future generateArtifacts({ + required BuildInfo buildInfo, + required Directory cacheDirectory, + required Directory xcframeworkOutput, + required bool buildStatic, + }) async { + final String xcodeBuildConfiguration = buildInfo.mode.uppercaseName; + final XcodeBasedProject xcodeProject = _targetPlatform.xcodeProject(_utils.project); + final Directory podsDirectory = xcodeProject.hostAppRoot.childDirectory('Pods'); + if (!podsDirectory.existsSync() || !xcodeProject.podfile.existsSync()) { + return; + } + final Directory cocoapodXCFrameworkOutput = xcframeworkOutput.childDirectory(_kCocoaPods); + final Directory cocoapodCacheDirectory = cacheDirectory + .childDirectory(xcodeBuildConfiguration) + .childDirectory(_kCocoaPods); + + final Status status = _utils.logger.startProgress(' ├─Building CocoaPod frameworks...'); + var skipped = false; + try { + final bool dependenciesChanged = _hasDependenciesChanged( + cacheDirectory.path, + cocoapodXCFrameworkOutput, + buildInfo.mode.cliName, + buildStatic, + xcodeProject, + ); + if (!dependenciesChanged && cocoapodXCFrameworkOutput.existsSync()) { + skipped = true; + return; + } + if (dependenciesChanged) { + ErrorHandlingFileSystem.deleteIfExists(cocoapodCacheDirectory, recursive: true); + ErrorHandlingFileSystem.deleteIfExists(cocoapodXCFrameworkOutput, recursive: true); + } + + await processPods(xcodeProject, buildInfo); + + final frameworksPerPod = >{}; + for (final XcodeSdk sdk in _targetPlatform.sdks) { + final Directory outputBuildDirectory = cocoapodCacheDirectory.childDirectory( + sdk.platformName, + ); + final Map> sdkSpecificFrameworks = await _buildCocoaPodsForSdk( + sdk: sdk, + platform: _targetPlatform, + xcodeBuildConfiguration: xcodeBuildConfiguration, + buildStatic: buildStatic, + outputBuildDirectory: outputBuildDirectory, + podsDirectory: podsDirectory, + ); + sdkSpecificFrameworks.forEach((String name, List frameworks) { + frameworksPerPod.putIfAbsent(name, () => []).addAll(frameworks); + }); + } + + for (final MapEntry> entry in frameworksPerPod.entries) { + await _produceXCFramework( + frameworks: entry.value, + frameworkBinaryName: entry.key, + outputDirectory: cocoapodXCFrameworkOutput, + processManager: _utils.processManager, + ); + } + _writeFingerprint( + cacheDirectory.path, + cocoapodXCFrameworkOutput, + buildInfo.mode.cliName, + buildStatic, + ); + } finally { + status.stop(); + if (skipped) { + _utils.logger.printStatus( + ' │ └── Skipping building CocoaPod plugins. No change detected.', + ); + } + } + } + + @visibleForTesting + /// Wrap [processPodsIfNeeded] in a method to be overwritten in tests. + Future processPods(XcodeBasedProject xcodeProject, BuildInfo buildInfo) async { + await processPodsIfNeeded(xcodeProject, _targetPlatform.buildDirectory(), buildInfo.mode); + } + + /// The target dependencies and binary targets for the CocoaPod plugin xcframeworks. + /// + /// ```swift + /// .target( + /// name: "FlutterPluginRegistrant", + /// dependencies: [ + /// .target(name: "cocoapod_plugin_a"), + /// + /// ... + /// + /// .binaryTarget( + /// name: "cocoapod_plugin_a", + /// path: "Frameworks/CocoaPods/cocoapod_plugin_a.xcframework" + /// ) + /// ``` + (List, List) generateDependencies({ + required Directory xcframeworkOutput, + }) { + return generateDependenciesFromDirectory( + fileSystem: _utils.fileSystem, + directoryName: _kCocoaPods, + xcframeworkDirectory: xcframeworkOutput.childDirectory(_kCocoaPods), + ); + } + + /// Builds CocoaPod plugins into frameworks for the given [xcodeBuildConfiguration], [platform], + /// and [sdk]. + Future>> _buildCocoaPodsForSdk({ + required XcodeSdk sdk, + required FlutterDarwinPlatform platform, + required String xcodeBuildConfiguration, + required bool buildStatic, + required Directory outputBuildDirectory, + required Directory podsDirectory, + }) async { + final String configuration = _configurationForSdkType(sdk, xcodeBuildConfiguration); + final ProcessResult buildPluginsResult = await _utils.processManager.run([ + ..._utils.xcode.xcrunCommand(), + 'xcodebuild', + '-alltargets', + '-sdk', + sdk.platformName, + '-configuration', + configuration, + 'SYMROOT=${outputBuildDirectory.path}', + 'ONLY_ACTIVE_ARCH=NO', // No device targeted, so build all valid architectures. + 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES', + if (buildStatic) 'MACH_O_TYPE=staticlib', + ], workingDirectory: podsDirectory.path); + if (buildPluginsResult.exitCode != 0) { + throwToolExit('Unable to build CocoaPod plugin frameworks: ${buildPluginsResult.stderr}'); + } + + final Directory configurationBuildDir; + switch (platform) { + case FlutterDarwinPlatform.macos: + configurationBuildDir = outputBuildDirectory.childDirectory(configuration); + case FlutterDarwinPlatform.ios: + configurationBuildDir = outputBuildDirectory.childDirectory( + '$configuration-${sdk.platformName}', + ); + } + return _findFrameworks(configurationBuildDir); + } + + /// Iterates through the build files and find .frameworks + /// + /// ex. + /// ```text + /// > Debug-iphoneos + /// > plugin_a + /// > plugin_a.framework + /// ``` + Future>> _findFrameworks(Directory configurationBuildDir) async { + final frameworks = >{}; + + final Iterable products = configurationBuildDir + .listSync(followLinks: false) + .whereType(); + for (final builtProduct in products) { + for (final Directory podProduct + in builtProduct.listSync(followLinks: false).whereType()) { + final String podFrameworkName = podProduct.basename; + if (_utils.fileSystem.path.extension(podFrameworkName) != '.framework') { + continue; + } + final String binaryName = _utils.fileSystem.path.basenameWithoutExtension(podFrameworkName); + frameworks.putIfAbsent(binaryName, () => []).add(podProduct); + } + } + return frameworks; + } + + bool _hasDependenciesChanged( + String cacheDirectoryPath, + Directory cocoapodXCFrameworkDirectory, + String xcodeBuildConfiguration, + bool buildStatic, + XcodeBasedProject xcodeProject, + ) { + final Fingerprinter fingerprinter = _cocoapodsFingerprinter( + cacheDirectoryPath, + cocoapodXCFrameworkDirectory, + xcodeBuildConfiguration, + buildStatic, + ); + if (!fingerprinter.doesFingerprintMatch()) { + return true; + } + + final File podfileFile = xcodeProject.podfile; + final File podfileLockFile = xcodeProject.podfileLock; + final File manifestLockFile = xcodeProject.podManifestLock; + + return !podfileLockFile.existsSync() || + !manifestLockFile.existsSync() || + podfileLockFile.statSync().modified.isBefore(podfileFile.statSync().modified) || + podfileLockFile.readAsStringSync() != manifestLockFile.readAsStringSync(); + } + + void _writeFingerprint( + String cacheDirectoryPath, + Directory cocoapodXCFrameworkDirectory, + String xcodeBuildConfiguration, + bool buildStatic, + ) { + final Fingerprinter fingerprinter = _cocoapodsFingerprinter( + cacheDirectoryPath, + cocoapodXCFrameworkDirectory, + xcodeBuildConfiguration, + buildStatic, + ); + fingerprinter.writeFingerprint(); + } + + Fingerprinter _cocoapodsFingerprinter( + String cacheDirectoryPath, + Directory cocoapodXCFrameworkDirectory, + String xcodeBuildConfiguration, + bool buildStatic, + ) { + final fingerprintedFiles = []; + + final File staticStatus = + _utils.fileSystem.file( + _utils.fileSystem.path.join( + cacheDirectoryPath, + 'build_${xcodeBuildConfiguration}_static_status', + ), + ) + ..createSync(recursive: true) + ..writeAsStringSync('$buildStatic'); + fingerprintedFiles.add(staticStatus.path); + + // Add already created XCFrameworks + if (cocoapodXCFrameworkDirectory.existsSync()) { + for (final FileSystemEntity entity in cocoapodXCFrameworkDirectory.listSync( + recursive: true, + )) { + if (entity is File) { + fingerprintedFiles.add(entity.path); + } + } + } + + // If the Xcode project, Podfile, generated plugin Swift Package, or podhelper + // have changed since last run, pods should be updated. + final XcodeBasedProject xcodeProject = _targetPlatform.xcodeProject(_utils.project); + fingerprintedFiles.add(xcodeProject.xcodeProjectInfoFile.path); + fingerprintedFiles.add(xcodeProject.podfile.path); + if (xcodeProject.flutterPluginSwiftPackageManifest.existsSync()) { + fingerprintedFiles.add(xcodeProject.flutterPluginSwiftPackageManifest.path); + } + + final fingerprinter = Fingerprinter( + fingerprintPath: _utils.fileSystem.path.join( + cacheDirectoryPath, + 'build_${xcodeBuildConfiguration}_pod_inputs.fingerprint', + ), + paths: [ + _utils.fileSystem.path.join( + _utils.flutterRoot, + 'packages', + 'flutter_tools', + 'bin', + 'podhelper.rb', + ), + _utils.fileSystem.path.join( + _utils.flutterRoot, + 'packages', + 'flutter_tools', + 'lib', + 'src', + 'commands', + 'build_swift_package.dart', + ), + ...fingerprintedFiles, + ], + fileSystem: _utils.fileSystem, + logger: _utils.logger, + ); + return fingerprinter; + } + + String _configurationForSdkType(XcodeSdk sdk, String configuration) { + if (sdk.sdkType == EnvironmentType.simulator) { + // Always build debug for simulator. + return BuildMode.debug.uppercaseName; + } else { + return configuration; + } + } +} + /// Create an XCFramework from a list of frameworks. Future _produceXCFramework({ required Iterable frameworks, diff --git a/packages/flutter_tools/lib/src/darwin/darwin.dart b/packages/flutter_tools/lib/src/darwin/darwin.dart index 43e5a60cc0a20..14d818d2bfabe 100644 --- a/packages/flutter_tools/lib/src/darwin/darwin.dart +++ b/packages/flutter_tools/lib/src/darwin/darwin.dart @@ -124,4 +124,14 @@ enum FlutterDarwinPlatform { macos => project.macos, }; } + + /// Returns the corresponding build directory for the platform. + String buildDirectory() { + switch (this) { + case FlutterDarwinPlatform.ios: + return getIosBuildDirectory(); + case FlutterDarwinPlatform.macos: + return getMacOSBuildDirectory(); + } + } } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_swift_package_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_swift_package_test.dart index 0d59a3e56c3e6..5888afcc1b24d 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_swift_package_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_swift_package_test.dart @@ -28,15 +28,16 @@ import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; import '../../src/context.dart'; +import '../../src/fake_process_manager.dart'; +const _flutterAppPath = '/path/to/my_flutter_app'; +const _flutterRoot = '/path/to/flutter'; const String _engineVersion = '1234567890abcdef1234567890abcdef12345678'; const String _iosSdkRoot = '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS26.2.sdk'; void main() { - const flutterRoot = '/path/to/flutter'; - const flutterAppPath = '/path/to/my_flutter_app'; - const flutterAppDartToolPath = '$flutterAppPath/.dart_tool'; + const flutterAppDartToolPath = '$_flutterAppPath/.dart_tool'; const flutterAppBuildPath = '$flutterAppDartToolPath/flutter_build'; const commandFilePath = '/path/to/flutter/packages/flutter_tools/lib/src/commands/build_swift_package.dart'; @@ -44,11 +45,13 @@ void main() { const debugModeDirectoryPath = '$pluginRegistrantSwiftPackagePath/Debug'; const debugFrameworksDirectoryPath = '$debugModeDirectoryPath/Frameworks'; const debugNativeAssetsDirectoryPath = '$debugFrameworksDirectoryPath/NativeAssets'; + const debugCocoaPodsDirectoryPath = '$debugFrameworksDirectoryPath/CocoaPods'; const debugPackagesDirectoryPath = '$debugModeDirectoryPath/Packages'; const releaseModeDirectoryPath = '$pluginRegistrantSwiftPackagePath/Release'; const releaseFrameworksDirectoryPath = '$releaseModeDirectoryPath/Frameworks'; const releaseNativeAssetsDirectoryPath = '$releaseFrameworksDirectoryPath/NativeAssets'; const cacheDirectoryPath = 'output/.cache'; + const debugCocoapodCache = '$cacheDirectoryPath/Debug/CocoaPods'; const pluginsDirectoryPath = '$pluginRegistrantSwiftPackagePath/Plugins'; const flutterCachePath = '/path/to/flutter/bin/cache'; const engineArtifactPath = '$flutterCachePath/artifacts/engine/ios/Flutter.xcframework'; @@ -66,7 +69,7 @@ void main() { analytics: FakeAnalytics(), artifacts: FakeArtifacts(engineArtifactPath), buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, flutterVersion: FakeFlutterVersion(), logger: logger, @@ -101,14 +104,14 @@ void main() { analytics: FakeAnalytics(), artifacts: FakeArtifacts(engineArtifactPath), buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -128,17 +131,39 @@ void main() { targetPlatform: targetPlatform, utils: testUtils, ); + late final cocoapodDependencies = CocoaPodPluginDependencies( + targetPlatform: targetPlatform, + utils: testUtils, + ); + // Plugin A represents a SwiftPM plugin final Directory modeDirectory = fs.directory(debugModeDirectoryPath); final pluginA = FakePlugin(name: 'PluginA', darwinPlatform: targetPlatform); pluginSwiftDependencies.copiedPlugins.add((pluginA, '$pluginsDirectoryPath/PluginA')); + // Plugin B represents a CocoaPod plugin + final pluginB = FakePlugin( + name: 'PluginB', + darwinPlatform: targetPlatform, + supportsSwiftPM: false, + ); + fs + .directory('$debugCocoaPodsDirectoryPath/PluginB.xcframework') + .createSync(recursive: true); + + // Plugin C represents a Native Asset + fs + .directory('$debugNativeAssetsDirectoryPath/PluginC.xcframework') + .createSync(recursive: true); + await package.generateSwiftPackage( modeDirectory: modeDirectory, - plugins: [pluginA], + plugins: [pluginA, pluginB], xcodeBuildConfiguration: 'Debug', pluginSwiftDependencies: pluginSwiftDependencies, flutterFrameworkDependency: flutterFrameworkDependency, appAndNativeAssetsDependencies: appAndNativeAssetsDependencies, + cocoapodDependencies: cocoapodDependencies, + includeCocoaPodBinaryTargets: true, packagesForConfiguration: fs.directory(debugPackagesDirectoryPath), xcframeworkOutput: fs.directory(debugFrameworksDirectoryPath), ); @@ -176,12 +201,22 @@ let package = Package( dependencies: [ .product(name: "FlutterFramework", package: "FlutterFramework"), .product(name: "PluginA", package: "PluginA"), - .target(name: "App") + .target(name: "PluginC"), + .target(name: "App"), + .target(name: "PluginB") ] ), + .binaryTarget( + name: "PluginC", + path: "Sources/Frameworks/NativeAssets/PluginC.xcframework" + ), .binaryTarget( name: "App", path: "Sources/Frameworks/App.xcframework" + ), + .binaryTarget( + name: "PluginB", + path: "Sources/Frameworks/CocoaPods/PluginB.xcframework" ) ] ) @@ -198,16 +233,171 @@ import Flutter import UIKit import PluginA +import PluginB @objc public class GeneratedPluginRegistrant: NSObject { @objc public static func register(with registry: FlutterPluginRegistry) { if let pluginAPlugin = registry.registrar(forPlugin: "PluginAPlugin") { PluginAPlugin.register(with: pluginAPlugin) } + if let pluginBPlugin = registry.registrar(forPlugin: "PluginBPlugin") { + PluginBPlugin.register(with: pluginBPlugin) + } } } '''); }); + + testWithoutContext( + 'generateSwiftPackage when includeCocoaPodBinaryTargets is false', + () async { + final fs = MemoryFileSystem.test(); + final logger = BufferLogger.test(); + final processManager = FakeProcessManager.list([]); + const FlutterDarwinPlatform targetPlatform = .ios; + final testUtils = BuildSwiftPackageUtils( + analytics: FakeAnalytics(), + artifacts: FakeArtifacts(engineArtifactPath), + buildSystem: FakeBuildSystem(), + cache: FakeCache(fs, _flutterRoot), + fileSystem: fs, + flutterRoot: _flutterRoot, + flutterVersion: FakeFlutterVersion(), + logger: logger, + platform: FakePlatform(), + processManager: processManager, + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), + templateRenderer: const MustacheTemplateRenderer(), + xcode: FakeXcode(), + ); + final package = FlutterPluginRegistrantSwiftPackage( + targetPlatform: targetPlatform, + utils: testUtils, + ); + final pluginSwiftDependencies = FlutterPluginSwiftDependencies( + targetPlatform: targetPlatform, + utils: testUtils, + ); + final flutterFrameworkDependency = FlutterFrameworkDependency( + targetPlatform: targetPlatform, + utils: testUtils, + ); + final appAndNativeAssetsDependencies = AppFrameworkAndNativeAssetsDependencies( + targetPlatform: targetPlatform, + utils: testUtils, + ); + late final cocoapodDependencies = CocoaPodPluginDependencies( + targetPlatform: targetPlatform, + utils: testUtils, + ); + // Plugin A represents a SwiftPM plugin + final Directory modeDirectory = fs.directory(debugModeDirectoryPath); + final pluginA = FakePlugin(name: 'PluginA', darwinPlatform: targetPlatform); + pluginSwiftDependencies.copiedPlugins.add((pluginA, '$pluginsDirectoryPath/PluginA')); + + // Plugin B represents a CocoaPod plugin + final pluginB = FakePlugin( + name: 'PluginB', + darwinPlatform: targetPlatform, + supportsSwiftPM: false, + ); + fs + .directory('$debugCocoaPodsDirectoryPath/PluginB.xcframework') + .createSync(recursive: true); + + // Plugin C represents a Native Asset + fs + .directory('$debugNativeAssetsDirectoryPath/PluginC.xcframework') + .createSync(recursive: true); + + await package.generateSwiftPackage( + modeDirectory: modeDirectory, + plugins: [pluginA, pluginB], + xcodeBuildConfiguration: 'Debug', + pluginSwiftDependencies: pluginSwiftDependencies, + flutterFrameworkDependency: flutterFrameworkDependency, + appAndNativeAssetsDependencies: appAndNativeAssetsDependencies, + cocoapodDependencies: cocoapodDependencies, + includeCocoaPodBinaryTargets: false, + packagesForConfiguration: fs.directory(debugPackagesDirectoryPath), + xcframeworkOutput: fs.directory(debugFrameworksDirectoryPath), + ); + + expect(logger.traceText, isEmpty); + expect(processManager.hasRemainingExpectations, false); + final File generatedPackageManifest = modeDirectory.childFile('Package.swift'); + expect(generatedPackageManifest, exists); + expect(generatedPackageManifest.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 + +// Debug + +let package = Package( + name: "FlutterPluginRegistrant", + platforms: [ + .iOS("13.0") + ], + products: [ + .library(name: "FlutterPluginRegistrant", type: .static, targets: ["FlutterPluginRegistrant"]) + ], + dependencies: [ + .package(name: "FlutterFramework", path: "Sources/Packages/FlutterFramework"), + .package(name: "PluginA", path: "Sources/Packages/PluginA") + ], + targets: [ + .target( + name: "FlutterPluginRegistrant", + dependencies: [ + .product(name: "FlutterFramework", package: "FlutterFramework"), + .product(name: "PluginA", package: "PluginA"), + .target(name: "PluginC"), + .target(name: "App") + ] + ), + .binaryTarget( + name: "PluginC", + path: "Sources/Frameworks/NativeAssets/PluginC.xcframework" + ), + .binaryTarget( + name: "App", + path: "Sources/Frameworks/App.xcframework" + ) + ] +) +'''); + final File generatedSource = modeDirectory + .childDirectory('FlutterPluginRegistrant') + .childFile('GeneratedPluginRegistrant.swift'); + expect(generatedSource, exists); + expect(generatedSource.readAsStringSync(), ''' +// +// Generated file. Do not edit. +// +import Flutter +import UIKit + +import PluginA +import PluginB + +@objc public class GeneratedPluginRegistrant: NSObject { + @objc public static func register(with registry: FlutterPluginRegistry) { + if let pluginAPlugin = registry.registrar(forPlugin: "PluginAPlugin") { + PluginAPlugin.register(with: pluginAPlugin) + } + if let pluginBPlugin = registry.registrar(forPlugin: "PluginBPlugin") { + PluginBPlugin.register(with: pluginBPlugin) + } + } +} +'''); + }, + ); }); group('FlutterFrameworkDependency', () { @@ -234,14 +424,14 @@ import PluginA analytics: FakeAnalytics(), artifacts: FakeArtifacts(engineArtifactPath), buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -267,14 +457,14 @@ import PluginA analytics: FakeAnalytics(), artifacts: FakeArtifacts(engineArtifactPath), buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -341,14 +531,14 @@ let package = Package( analytics: FakeAnalytics(), artifacts: FakeArtifacts(engineArtifactPath), buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -393,14 +583,14 @@ let package = Package( analytics: FakeAnalytics(), artifacts: FakeArtifacts(engineArtifactPath), buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -468,14 +658,14 @@ let package = Package( analytics: FakeAnalytics(), artifacts: FakeArtifacts(engineArtifactPath), buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -510,14 +700,14 @@ let package = Package( analytics: FakeAnalytics(), artifacts: FakeArtifacts(engineArtifactPath), buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -571,7 +761,7 @@ let package = Package( final logger = BufferLogger.test(); final Directory xcframeworkOutput = fs.directory(debugFrameworksDirectoryPath); final Directory cacheDirectory = fs.directory(cacheDirectoryPath); - final Directory appDirectory = fs.directory(flutterAppPath)..createSync(recursive: true); + final Directory appDirectory = fs.directory(_flutterAppPath)..createSync(recursive: true); fs.currentDirectory = appDirectory; @@ -619,12 +809,12 @@ let package = Package( expectations: [ BuildExpectations( expectedTargetName: 'debug_macos_bundle_flutter_assets', - expectedProjectDirPath: flutterAppPath, + expectedProjectDirPath: _flutterAppPath, expectedPackageConfigPath: '$flutterAppDartToolPath/package_config.json', expectedOutputDirPath: '$cacheDirectoryPath/Debug/macosx', expectedBuildDirPath: '$flutterAppBuildPath/', expectedCacheDirPath: flutterCachePath, - expectedFlutterRootDirPath: flutterRoot, + expectedFlutterRootDirPath: _flutterRoot, expectedEngineVersion: _engineVersion, expectedDefines: { 'TargetFile': 'lib/main.dart', @@ -640,14 +830,14 @@ let package = Package( ), ], ), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -673,7 +863,7 @@ let package = Package( final logger = BufferLogger.test(); final Directory xcframeworkOutput = fs.directory(releaseFrameworksDirectoryPath); final Directory cacheDirectory = fs.directory(cacheDirectoryPath); - final Directory appDirectory = fs.directory(flutterAppPath)..createSync(recursive: true); + final Directory appDirectory = fs.directory(_flutterAppPath)..createSync(recursive: true); fs.currentDirectory = appDirectory; @@ -726,12 +916,12 @@ let package = Package( expectations: [ BuildExpectations( expectedTargetName: 'release_macos_bundle_flutter_assets', - expectedProjectDirPath: flutterAppPath, + expectedProjectDirPath: _flutterAppPath, expectedPackageConfigPath: '$flutterAppDartToolPath/package_config.json', expectedOutputDirPath: '$cacheDirectoryPath/Release/macosx', expectedBuildDirPath: '$flutterAppBuildPath/', expectedCacheDirPath: flutterCachePath, - expectedFlutterRootDirPath: flutterRoot, + expectedFlutterRootDirPath: _flutterRoot, expectedEngineVersion: _engineVersion, expectedDefines: { 'TargetFile': 'lib/main.dart', @@ -747,14 +937,14 @@ let package = Package( ), ], ), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -780,7 +970,7 @@ let package = Package( final logger = BufferLogger.test(); final Directory xcframeworkOutput = fs.directory(debugFrameworksDirectoryPath); final Directory cacheDirectory = fs.directory(cacheDirectoryPath); - final Directory appDirectory = fs.directory(flutterAppPath)..createSync(recursive: true); + final Directory appDirectory = fs.directory(_flutterAppPath)..createSync(recursive: true); fs.currentDirectory = appDirectory; @@ -836,12 +1026,12 @@ let package = Package( expectations: [ BuildExpectations( expectedTargetName: 'debug_ios_bundle_flutter_assets', - expectedProjectDirPath: flutterAppPath, + expectedProjectDirPath: _flutterAppPath, expectedPackageConfigPath: '$flutterAppDartToolPath/package_config.json', expectedOutputDirPath: '$cacheDirectoryPath/Debug/iphoneos', expectedBuildDirPath: '$flutterAppBuildPath/', expectedCacheDirPath: flutterCachePath, - expectedFlutterRootDirPath: flutterRoot, + expectedFlutterRootDirPath: _flutterRoot, expectedEngineVersion: _engineVersion, expectedDefines: { 'TargetFile': 'lib/main.dart', @@ -858,12 +1048,12 @@ let package = Package( ), BuildExpectations( expectedTargetName: 'debug_ios_bundle_flutter_assets', - expectedProjectDirPath: flutterAppPath, + expectedProjectDirPath: _flutterAppPath, expectedPackageConfigPath: '$flutterAppDartToolPath/package_config.json', expectedOutputDirPath: '$cacheDirectoryPath/Debug/iphonesimulator', expectedBuildDirPath: '$flutterAppBuildPath/', expectedCacheDirPath: flutterCachePath, - expectedFlutterRootDirPath: flutterRoot, + expectedFlutterRootDirPath: _flutterRoot, expectedEngineVersion: _engineVersion, expectedDefines: { 'TargetFile': 'lib/main.dart', @@ -880,14 +1070,14 @@ let package = Package( ), ], ), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -919,7 +1109,7 @@ let package = Package( final logger = BufferLogger.test(); final Directory xcframeworkOutput = fs.directory(debugFrameworksDirectoryPath); final Directory cacheDirectory = fs.directory(cacheDirectoryPath); - final Directory appDirectory = fs.directory(flutterAppPath)..createSync(recursive: true); + final Directory appDirectory = fs.directory(_flutterAppPath)..createSync(recursive: true); fs.currentDirectory = appDirectory; @@ -969,12 +1159,12 @@ let package = Package( expectations: [ BuildExpectations( expectedTargetName: 'debug_ios_bundle_flutter_assets', - expectedProjectDirPath: flutterAppPath, + expectedProjectDirPath: _flutterAppPath, expectedPackageConfigPath: '$flutterAppDartToolPath/package_config.json', expectedOutputDirPath: '$cacheDirectoryPath/Debug/iphoneos', expectedBuildDirPath: '$flutterAppBuildPath/', expectedCacheDirPath: flutterCachePath, - expectedFlutterRootDirPath: flutterRoot, + expectedFlutterRootDirPath: _flutterRoot, expectedEngineVersion: _engineVersion, expectedDefines: { 'TargetFile': 'lib/main.dart', @@ -991,12 +1181,12 @@ let package = Package( ), BuildExpectations( expectedTargetName: 'debug_ios_bundle_flutter_assets', - expectedProjectDirPath: flutterAppPath, + expectedProjectDirPath: _flutterAppPath, expectedPackageConfigPath: '$flutterAppDartToolPath/package_config.json', expectedOutputDirPath: '$cacheDirectoryPath/Debug/iphonesimulator', expectedBuildDirPath: '$flutterAppBuildPath/', expectedCacheDirPath: flutterCachePath, - expectedFlutterRootDirPath: flutterRoot, + expectedFlutterRootDirPath: _flutterRoot, expectedEngineVersion: _engineVersion, expectedDefines: { 'TargetFile': 'lib/main.dart', @@ -1013,14 +1203,14 @@ let package = Package( ), ], ), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -1061,14 +1251,14 @@ let package = Package( analytics: FakeAnalytics(), artifacts: FakeArtifacts(engineArtifactPath), buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -1107,14 +1297,14 @@ let package = Package( analytics: FakeAnalytics(), artifacts: FakeArtifacts(engineArtifactPath), buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -1160,6 +1350,324 @@ let package = Package( expect(simulatorReleaseTarget.name, 'debug_ios_bundle_flutter_assets'); }); }); + + group('CocoaPodPluginDependencies', () { + testWithoutContext('generateArtifacts', () async { + final fs = MemoryFileSystem.test(); + final logger = BufferLogger.test(); + const FlutterDarwinPlatform targetPlatform = .ios; + final Directory podsDirectory = fs.directory('$_flutterAppPath/${targetPlatform.name}/Pods') + ..createSync(recursive: true); + _createPodFingerprintFiles(fs: fs, platformName: targetPlatform.name); + final Directory xcframeworkOutput = fs.directory(debugFrameworksDirectoryPath); + const iphoneosDirPath = '$debugCocoapodCache/iphoneos'; + const iphoneosCocoapodPluginPath = + '$iphoneosDirPath/Debug-iphoneos/cocoapod_plugin/cocoapod_plugin.framework'; + const simulatorDirPath = '$debugCocoapodCache/iphonesimulator'; + const simulatorCocoapodPluginPath = + '$simulatorDirPath/Debug-iphonesimulator/cocoapod_plugin/cocoapod_plugin.framework'; + const cocoapodPluginXCFrameworkPath = + '$debugCocoaPodsDirectoryPath/cocoapod_plugin.xcframework'; + + final processManager = FakeProcessManager.list([ + FakeCommand( + command: const [ + 'xcrun', + 'xcodebuild', + '-alltargets', + '-sdk', + 'iphoneos', + '-configuration', + 'Debug', + 'SYMROOT=$iphoneosDirPath', + 'ONLY_ACTIVE_ARCH=NO', + 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES', + ], + onRun: (command) { + fs.directory(iphoneosCocoapodPluginPath).createSync(recursive: true); + }, + workingDirectory: podsDirectory.path, + ), + FakeCommand( + command: const [ + 'xcrun', + 'xcodebuild', + '-alltargets', + '-sdk', + 'iphonesimulator', + '-configuration', + 'Debug', + 'SYMROOT=$simulatorDirPath', + 'ONLY_ACTIVE_ARCH=NO', + 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES', + ], + onRun: (command) { + fs.directory(simulatorCocoapodPluginPath).createSync(recursive: true); + }, + workingDirectory: podsDirectory.path, + ), + FakeCommand( + command: const [ + 'xcrun', + 'xcodebuild', + '-create-xcframework', + '-framework', + iphoneosCocoapodPluginPath, + '-framework', + simulatorCocoapodPluginPath, + '-output', + cocoapodPluginXCFrameworkPath, + ], + onRun: (command) { + fs.directory(cocoapodPluginXCFrameworkPath).createSync(recursive: true); + }, + ), + ]); + + final testUtils = BuildSwiftPackageUtils( + analytics: FakeAnalytics(), + artifacts: FakeArtifacts(engineArtifactPath), + buildSystem: FakeBuildSystem(), + cache: FakeCache(fs, _flutterRoot), + fileSystem: fs, + flutterRoot: _flutterRoot, + flutterVersion: FakeFlutterVersion(), + logger: logger, + platform: FakePlatform(), + processManager: processManager, + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), + templateRenderer: const MustacheTemplateRenderer(), + xcode: FakeXcode(), + ); + final cocoapodDependencies = CocoaPodPluginDependenciesSkipPodProcessing( + targetPlatform: targetPlatform, + utils: testUtils, + ); + + await cocoapodDependencies.generateArtifacts( + buildInfo: BuildInfo.debug, + cacheDirectory: fs.directory(cacheDirectoryPath), + xcframeworkOutput: xcframeworkOutput, + buildStatic: false, + ); + + // Run again to verify fingerprinter caches + await cocoapodDependencies.generateArtifacts( + buildInfo: BuildInfo.debug, + cacheDirectory: fs.directory(cacheDirectoryPath), + xcframeworkOutput: xcframeworkOutput, + buildStatic: false, + ); + expect(processManager, hasNoRemainingExpectations); + }); + + testWithoutContext('generateArtifacts static', () async { + final fs = MemoryFileSystem.test(); + final logger = BufferLogger.test(); + const FlutterDarwinPlatform targetPlatform = .ios; + final Directory podsDirectory = fs.directory('$_flutterAppPath/${targetPlatform.name}/Pods') + ..createSync(recursive: true); + _createPodFingerprintFiles(fs: fs, platformName: targetPlatform.name); + final Directory xcframeworkOutput = fs.directory(debugFrameworksDirectoryPath); + const iphoneosDirPath = '$debugCocoapodCache/iphoneos'; + const iphoneosCocoapodPluginPath = + '$iphoneosDirPath/Debug-iphoneos/cocoapod_plugin/cocoapod_plugin.framework'; + const simulatorDirPath = '$debugCocoapodCache/iphonesimulator'; + const simulatorCocoapodPluginPath = + '$simulatorDirPath/Debug-iphonesimulator/cocoapod_plugin/cocoapod_plugin.framework'; + const cocoapodPluginXCFrameworkPath = + '$debugCocoaPodsDirectoryPath/cocoapod_plugin.xcframework'; + final processManager = FakeProcessManager.list([ + FakeCommand( + command: const [ + 'xcrun', + 'xcodebuild', + '-alltargets', + '-sdk', + 'iphoneos', + '-configuration', + 'Debug', + 'SYMROOT=$iphoneosDirPath', + 'ONLY_ACTIVE_ARCH=NO', + 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES', + 'MACH_O_TYPE=staticlib', + ], + onRun: (command) { + fs.directory(iphoneosCocoapodPluginPath).createSync(recursive: true); + }, + workingDirectory: podsDirectory.path, + ), + FakeCommand( + command: const [ + 'xcrun', + 'xcodebuild', + '-alltargets', + '-sdk', + 'iphonesimulator', + '-configuration', + 'Debug', + 'SYMROOT=$simulatorDirPath', + 'ONLY_ACTIVE_ARCH=NO', + 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES', + 'MACH_O_TYPE=staticlib', + ], + onRun: (command) { + fs.directory(simulatorCocoapodPluginPath).createSync(recursive: true); + }, + workingDirectory: podsDirectory.path, + ), + FakeCommand( + command: const [ + 'xcrun', + 'xcodebuild', + '-create-xcframework', + '-framework', + iphoneosCocoapodPluginPath, + '-framework', + simulatorCocoapodPluginPath, + '-output', + cocoapodPluginXCFrameworkPath, + ], + onRun: (command) { + fs.directory(cocoapodPluginXCFrameworkPath).createSync(recursive: true); + }, + ), + FakeCommand( + command: const [ + 'xcrun', + 'xcodebuild', + '-alltargets', + '-sdk', + 'iphoneos', + '-configuration', + 'Debug', + 'SYMROOT=$iphoneosDirPath', + 'ONLY_ACTIVE_ARCH=NO', + 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES', + ], + onRun: (command) { + fs.directory(iphoneosCocoapodPluginPath).createSync(recursive: true); + }, + workingDirectory: podsDirectory.path, + ), + FakeCommand( + command: const [ + 'xcrun', + 'xcodebuild', + '-alltargets', + '-sdk', + 'iphonesimulator', + '-configuration', + 'Debug', + 'SYMROOT=$simulatorDirPath', + 'ONLY_ACTIVE_ARCH=NO', + 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES', + ], + onRun: (command) { + fs.directory(simulatorCocoapodPluginPath).createSync(recursive: true); + }, + workingDirectory: podsDirectory.path, + ), + FakeCommand( + command: const [ + 'xcrun', + 'xcodebuild', + '-create-xcframework', + '-framework', + iphoneosCocoapodPluginPath, + '-framework', + simulatorCocoapodPluginPath, + '-output', + cocoapodPluginXCFrameworkPath, + ], + onRun: (command) { + fs.directory(cocoapodPluginXCFrameworkPath).createSync(recursive: true); + }, + ), + ]); + + final testUtils = BuildSwiftPackageUtils( + analytics: FakeAnalytics(), + artifacts: FakeArtifacts(engineArtifactPath), + buildSystem: FakeBuildSystem(), + cache: FakeCache(fs, _flutterRoot), + fileSystem: fs, + flutterRoot: _flutterRoot, + flutterVersion: FakeFlutterVersion(), + logger: logger, + platform: FakePlatform(), + processManager: processManager, + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), + templateRenderer: const MustacheTemplateRenderer(), + xcode: FakeXcode(), + ); + final cocoapodDependencies = CocoaPodPluginDependenciesSkipPodProcessing( + targetPlatform: targetPlatform, + utils: testUtils, + ); + + await cocoapodDependencies.generateArtifacts( + buildInfo: BuildInfo.debug, + cacheDirectory: fs.directory(cacheDirectoryPath), + xcframeworkOutput: xcframeworkOutput, + buildStatic: true, + ); + + // Run again to verify fingerprinter does not match when static changes + await cocoapodDependencies.generateArtifacts( + buildInfo: BuildInfo.debug, + cacheDirectory: fs.directory(cacheDirectoryPath), + xcframeworkOutput: xcframeworkOutput, + buildStatic: false, + ); + expect(processManager, hasNoRemainingExpectations); + }); + + testWithoutContext('generateDependencies', () async { + final fs = MemoryFileSystem.test(); + final logger = BufferLogger.test(); + fs + .directory('$debugCocoaPodsDirectoryPath/cocoapod_plugin.xcframework') + .createSync(recursive: true); + const FlutterDarwinPlatform targetPlatform = .ios; + final processManager = FakeProcessManager.list([]); + final testUtils = BuildSwiftPackageUtils( + analytics: FakeAnalytics(), + artifacts: FakeArtifacts(engineArtifactPath), + buildSystem: FakeBuildSystem(), + cache: FakeCache(fs, _flutterRoot), + fileSystem: fs, + flutterRoot: _flutterRoot, + flutterVersion: FakeFlutterVersion(), + logger: logger, + platform: FakePlatform(), + processManager: processManager, + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), + templateRenderer: const MustacheTemplateRenderer(), + xcode: FakeXcode(), + ); + final cocoapodDependencies = CocoaPodPluginDependenciesSkipPodProcessing( + targetPlatform: targetPlatform, + utils: testUtils, + ); + + final ( + List targetDependencies, + List packageTargets, + ) = cocoapodDependencies.generateDependencies( + xcframeworkOutput: fs.directory(debugFrameworksDirectoryPath), + ); + expect(targetDependencies.length, 1); + expect(packageTargets.length, 1); + expect(targetDependencies[0].format(), contains('.target(name: "cocoapod_plugin")')); + expect(packageTargets[0].format(), ''' +.binaryTarget( + name: "cocoapod_plugin", + path: "Sources/Frameworks/CocoaPods/cocoapod_plugin.xcframework" + )'''); + expect(processManager, hasNoRemainingExpectations); + }); + }); }); group('macos', () { @@ -1173,14 +1681,14 @@ let package = Package( analytics: FakeAnalytics(), artifacts: FakeArtifacts(engineArtifactPath), buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -1200,6 +1708,10 @@ let package = Package( targetPlatform: targetPlatform, utils: testUtils, ); + late final cocoapodDependencies = CocoaPodPluginDependencies( + targetPlatform: targetPlatform, + utils: testUtils, + ); final Directory modeDirectory = fs.directory(debugModeDirectoryPath); final pluginA = FakePlugin(name: 'PluginA', darwinPlatform: targetPlatform); pluginSwiftDependencies.copiedPlugins.add((pluginA, '$pluginsDirectoryPath/PluginA')); @@ -1211,6 +1723,8 @@ let package = Package( pluginSwiftDependencies: pluginSwiftDependencies, flutterFrameworkDependency: flutterFrameworkDependency, appAndNativeAssetsDependencies: appAndNativeAssetsDependencies, + cocoapodDependencies: cocoapodDependencies, + includeCocoaPodBinaryTargets: true, packagesForConfiguration: fs.directory(debugPackagesDirectoryPath), xcframeworkOutput: fs.directory(debugFrameworksDirectoryPath), ); @@ -1289,14 +1803,14 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { analytics: FakeAnalytics(), artifacts: FakeArtifacts(engineArtifactPath), buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -1341,14 +1855,14 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { analytics: FakeAnalytics(), artifacts: FakeArtifacts(engineArtifactPath), buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -1421,14 +1935,14 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { analytics: FakeAnalytics(), artifacts: FakeArtifacts(engineArtifactPath), buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -1463,14 +1977,14 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { analytics: FakeAnalytics(), artifacts: FakeArtifacts(engineArtifactPath), buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -1524,7 +2038,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { final logger = BufferLogger.test(); final Directory xcframeworkOutput = fs.directory(debugFrameworksDirectoryPath); final Directory cacheDirectory = fs.directory(cacheDirectoryPath); - final Directory appDirectory = fs.directory(flutterAppPath)..createSync(recursive: true); + final Directory appDirectory = fs.directory(_flutterAppPath)..createSync(recursive: true); fs.currentDirectory = appDirectory; @@ -1587,12 +2101,12 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { expectations: [ BuildExpectations( expectedTargetName: 'debug_ios_bundle_flutter_assets', - expectedProjectDirPath: flutterAppPath, + expectedProjectDirPath: _flutterAppPath, expectedPackageConfigPath: '$flutterAppDartToolPath/package_config.json', expectedOutputDirPath: '$cacheDirectoryPath/Debug/iphoneos', expectedBuildDirPath: '$flutterAppBuildPath/', expectedCacheDirPath: flutterCachePath, - expectedFlutterRootDirPath: flutterRoot, + expectedFlutterRootDirPath: _flutterRoot, expectedEngineVersion: _engineVersion, expectedDefines: { 'TargetFile': 'lib/main.dart', @@ -1609,12 +2123,12 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ), BuildExpectations( expectedTargetName: 'debug_ios_bundle_flutter_assets', - expectedProjectDirPath: flutterAppPath, + expectedProjectDirPath: _flutterAppPath, expectedPackageConfigPath: '$flutterAppDartToolPath/package_config.json', expectedOutputDirPath: '$cacheDirectoryPath/Debug/iphonesimulator', expectedBuildDirPath: '$flutterAppBuildPath/', expectedCacheDirPath: flutterCachePath, - expectedFlutterRootDirPath: flutterRoot, + expectedFlutterRootDirPath: _flutterRoot, expectedEngineVersion: _engineVersion, expectedDefines: { 'TargetFile': 'lib/main.dart', @@ -1631,14 +2145,14 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ), ], ), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -1670,14 +2184,14 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { analytics: FakeAnalytics(), artifacts: FakeArtifacts(engineArtifactPath), buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -1716,14 +2230,14 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { analytics: FakeAnalytics(), artifacts: FakeArtifacts(engineArtifactPath), buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, flutterRoot), + cache: FakeCache(fs, _flutterRoot), fileSystem: fs, - flutterRoot: flutterRoot, + flutterRoot: _flutterRoot, flutterVersion: FakeFlutterVersion(), logger: logger, platform: FakePlatform(), processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(flutterAppPath)), + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), templateRenderer: const MustacheTemplateRenderer(), xcode: FakeXcode(), ); @@ -1752,6 +2266,139 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { expect(macosReleaseTarget.name, 'release_macos_bundle_flutter_assets'); }); }); + + group('CocoaPodPluginDependencies', () { + testWithoutContext('generateArtifacts', () async { + final fs = MemoryFileSystem.test(); + final logger = BufferLogger.test(); + const FlutterDarwinPlatform targetPlatform = .macos; + final Directory podsDirectory = fs.directory('$_flutterAppPath/${targetPlatform.name}/Pods') + ..createSync(recursive: true); + _createPodFingerprintFiles(fs: fs, platformName: targetPlatform.name); + final Directory xcframeworkOutput = fs.directory(debugFrameworksDirectoryPath); + const macosCacheDirPath = '$debugCocoapodCache/macosx'; + const cocoapodPluginPath = + '$macosCacheDirPath/Debug/cocoapod_plugin/cocoapod_plugin.framework'; + const cocoapodPluginXCFrameworkPath = + '$debugCocoaPodsDirectoryPath/cocoapod_plugin.xcframework'; + final processManager = FakeProcessManager.list([ + FakeCommand( + command: const [ + 'xcrun', + 'xcodebuild', + '-alltargets', + '-sdk', + 'macosx', + '-configuration', + 'Debug', + 'SYMROOT=$macosCacheDirPath', + 'ONLY_ACTIVE_ARCH=NO', + 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES', + ], + onRun: (command) { + fs.directory(cocoapodPluginPath).createSync(recursive: true); + }, + workingDirectory: podsDirectory.path, + ), + + FakeCommand( + command: const [ + 'xcrun', + 'xcodebuild', + '-create-xcframework', + '-framework', + cocoapodPluginPath, + '-output', + cocoapodPluginXCFrameworkPath, + ], + onRun: (command) { + fs.directory(cocoapodPluginXCFrameworkPath).createSync(recursive: true); + }, + ), + ]); + + final testUtils = BuildSwiftPackageUtils( + analytics: FakeAnalytics(), + artifacts: FakeArtifacts(engineArtifactPath), + buildSystem: FakeBuildSystem(), + cache: FakeCache(fs, _flutterRoot), + fileSystem: fs, + flutterRoot: _flutterRoot, + flutterVersion: FakeFlutterVersion(), + logger: logger, + platform: FakePlatform(), + processManager: processManager, + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), + templateRenderer: const MustacheTemplateRenderer(), + xcode: FakeXcode(), + ); + final cocoapodDependencies = CocoaPodPluginDependenciesSkipPodProcessing( + targetPlatform: targetPlatform, + utils: testUtils, + ); + + await cocoapodDependencies.generateArtifacts( + buildInfo: BuildInfo.debug, + cacheDirectory: fs.directory(cacheDirectoryPath), + xcframeworkOutput: xcframeworkOutput, + buildStatic: false, + ); + + // Run again to verify fingerprinter caches + await cocoapodDependencies.generateArtifacts( + buildInfo: BuildInfo.debug, + cacheDirectory: fs.directory(cacheDirectoryPath), + xcframeworkOutput: xcframeworkOutput, + buildStatic: false, + ); + expect(processManager, hasNoRemainingExpectations); + }); + + testWithoutContext('generateDependencies', () async { + final fs = MemoryFileSystem.test(); + final logger = BufferLogger.test(); + fs + .directory('$debugCocoaPodsDirectoryPath/cocoapod_plugin.xcframework') + .createSync(recursive: true); + const FlutterDarwinPlatform targetPlatform = .macos; + final processManager = FakeProcessManager.list([]); + final testUtils = BuildSwiftPackageUtils( + analytics: FakeAnalytics(), + artifacts: FakeArtifacts(engineArtifactPath), + buildSystem: FakeBuildSystem(), + cache: FakeCache(fs, _flutterRoot), + fileSystem: fs, + flutterRoot: _flutterRoot, + flutterVersion: FakeFlutterVersion(), + logger: logger, + platform: FakePlatform(), + processManager: processManager, + project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), + templateRenderer: const MustacheTemplateRenderer(), + xcode: FakeXcode(), + ); + final cocoapodDependencies = CocoaPodPluginDependenciesSkipPodProcessing( + targetPlatform: targetPlatform, + utils: testUtils, + ); + + final ( + List targetDependencies, + List packageTargets, + ) = cocoapodDependencies.generateDependencies( + xcframeworkOutput: fs.directory(debugFrameworksDirectoryPath), + ); + expect(targetDependencies.length, 1); + expect(packageTargets.length, 1); + expect(targetDependencies[0].format(), contains('.target(name: "cocoapod_plugin")')); + expect(packageTargets[0].format(), ''' +.binaryTarget( + name: "cocoapod_plugin", + path: "Sources/Frameworks/CocoaPods/cocoapod_plugin.xcframework" + )'''); + expect(processManager, hasNoRemainingExpectations); + }); + }); }); } @@ -1784,6 +2431,20 @@ let package = Package( '''; } +/// Creates Pod directory, Podfile, Podfile.lock, and Manifest.lock. Returns the Pods directory. +void _createPodFingerprintFiles({required FileSystem fs, required String platformName}) { + fs.file('$_flutterAppPath/$platformName/Podfile').createSync(recursive: true); + fs.file('$_flutterAppPath/$platformName/Podfile.lock').createSync(recursive: true); + fs.file('$_flutterAppPath/$platformName/Pods/Manifest.lock').createSync(recursive: true); + fs.file('$_flutterRoot/packages/flutter_tools/bin/podhelper.rb').createSync(recursive: true); + fs + .file('$_flutterRoot/packages/flutter_tools/lib/src/commands/build_swift_package.dart') + .createSync(recursive: true); + fs + .file('$_flutterAppPath/$platformName/Runner.xcodeproj/project.pbxproj') + .createSync(recursive: true); +} + class FakeAnalytics extends Fake implements Analytics {} class FakeXcode extends Fake implements Xcode { @@ -1791,6 +2452,9 @@ class FakeXcode extends Fake implements Xcode { Future sdkLocation(EnvironmentType environmentType) async { return _iosSdkRoot; } + + @override + List xcrunCommand() => ['xcrun']; } class FakeFlutterVersion extends Fake implements FlutterVersion { @@ -1900,6 +2564,74 @@ class FakeFlutterProject extends Fake implements FlutterProject { @override Directory get dartTool => directory.childDirectory('.dart_tool'); + + @override + late final ios = FakeIosProject(directory: directory); + + @override + late final macos = FakeMacosProject(directory: directory); +} + +class FakeIosProject extends Fake implements IosProject { + FakeIosProject({required this.directory}); + + final Directory directory; + + @override + Directory get hostAppRoot { + return directory.childDirectory('ios'); + } + + @override + File get podfile => hostAppRoot.childFile('Podfile'); + + @override + File get podfileLock => hostAppRoot.childFile('Podfile.lock'); + + @override + File get podManifestLock => hostAppRoot.childDirectory('Pods').childFile('Manifest.lock'); + + @override + Directory get xcodeProject => hostAppRoot.childDirectory('Runner.xcodeproj'); + + @override + File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj'); + + @override + File get flutterPluginSwiftPackageManifest => hostAppRoot.childFile( + 'Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage/Package.swift', + ); +} + +class FakeMacosProject extends Fake implements MacOSProject { + FakeMacosProject({required this.directory}); + + final Directory directory; + + @override + Directory get hostAppRoot { + return directory.childDirectory('macos'); + } + + @override + File get podfile => hostAppRoot.childFile('Podfile'); + + @override + File get podfileLock => hostAppRoot.childFile('Podfile.lock'); + + @override + File get podManifestLock => hostAppRoot.childDirectory('Pods').childFile('Manifest.lock'); + + @override + Directory get xcodeProject => hostAppRoot.childDirectory('Runner.xcodeproj'); + + @override + File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj'); + + @override + File get flutterPluginSwiftPackageManifest => hostAppRoot.childFile( + 'Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage/Package.swift', + ); } class FakePlugin extends Fake implements Plugin { @@ -1933,3 +2665,13 @@ class FakePlugin extends Fake implements Plugin { } class FakeFeatureFlags extends Fake implements FeatureFlags {} + +class CocoaPodPluginDependenciesSkipPodProcessing extends CocoaPodPluginDependencies { + CocoaPodPluginDependenciesSkipPodProcessing({ + required super.targetPlatform, + required super.utils, + }); + + @override + Future processPods(XcodeBasedProject xcodeProject, BuildInfo buildInfo) async {} +} From 4f035b6a837185c5e921e24df732ca79c1eb95b4 Mon Sep 17 00:00:00 2001 From: Victoria Ashworth Date: Wed, 4 Mar 2026 12:53:13 -0600 Subject: [PATCH 2/3] ++ --- .../lib/src/commands/build_swift_package.dart | 102 +++++------- .../lib/src/macos/cocoapods.dart | 4 + .../hermetic/build_swift_package_test.dart | 153 ------------------ 3 files changed, 47 insertions(+), 212 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/build_swift_package.dart b/packages/flutter_tools/lib/src/commands/build_swift_package.dart index 79c4d28ebac8d..1841e51bbc5da 100644 --- a/packages/flutter_tools/lib/src/commands/build_swift_package.dart +++ b/packages/flutter_tools/lib/src/commands/build_swift_package.dart @@ -27,6 +27,7 @@ import '../features.dart'; import '../flutter_plugins.dart'; import '../ios/xcodeproj.dart'; import '../macos/cocoapod_utils.dart'; +import '../macos/cocoapods.dart'; import '../macos/swift_package_manager.dart'; import '../macos/swift_packages.dart'; import '../macos/xcode.dart'; @@ -98,24 +99,7 @@ class BuildSwiftPackage extends BuildSubCommand { allowed: ['debug', 'profile', 'release'], defaultsTo: ['debug', 'profile', 'release'], ) - ..addFlag( - 'static', - help: - 'Build CocoaPods plugins as static frameworks. Consider using with ' - '--no-cocoapods-as-binary-targets to reduce bundle size.', - ) - ..addFlag( - // When an XCFramework binary SwiftPM target has static frameworks, it still embeds the - // framework, but with a stub binary. There will be a note in the build logs that says - // "Injecting stub binary for codeless framework". Developers can alternatively not include - // the CocoaPod XCFrameworks through SwiftPM and link them directly in their Xcode project. - 'cocoapods-as-binary-targets', - defaultsTo: true, - help: - 'Adds CocoaPod generated XCFrameworks as binary targets in the generated Swift package. ' - 'When using the --static flag, consider disabling this and linking the frameworks ' - 'manually to reduce bundle size.', - ); + ..addFlag('static', help: 'Build CocoaPods plugins as static frameworks.'); } @override @@ -384,7 +368,6 @@ class BuildSwiftPackage extends BuildSubCommand { flutterFrameworkDependency: flutterFrameworkDependency, appAndNativeAssetsDependencies: appAndNativeAssetsDependencies, cocoapodDependencies: cocoapodDependencies, - includeCocoaPodBinaryTargets: boolArg('cocoapods-as-binary-targets'), packagesForConfiguration: packagesForConfiguration, xcframeworkOutput: xcframeworkOutput, ); @@ -437,7 +420,6 @@ class FlutterPluginRegistrantSwiftPackage { required FlutterFrameworkDependency flutterFrameworkDependency, required AppFrameworkAndNativeAssetsDependencies appAndNativeAssetsDependencies, required CocoaPodPluginDependencies cocoapodDependencies, - required bool includeCocoaPodBinaryTargets, required Directory xcframeworkOutput, }) async { final ( @@ -465,7 +447,7 @@ class FlutterPluginRegistrantSwiftPackage { flutterFrameworkDependency.targetDependency, ...pluginTargetDependencies, ...flutterGeneratedDependencies, - if (includeCocoaPodBinaryTargets) ...cocoaPodDependencies, + ...cocoaPodDependencies, ]; final packageDependencies = [ flutterFrameworkDependency.packageDependency, @@ -484,7 +466,7 @@ class FlutterPluginRegistrantSwiftPackage { final targets = [ SwiftPackageTarget.defaultTarget(name: swiftPackageName, dependencies: targetDependencies), ...flutterGeneratedTargets, - if (includeCocoaPodBinaryTargets) ...cocoaPodTargets, + ...cocoaPodTargets, ]; final pluginsPackage = SwiftPackage( @@ -1199,7 +1181,7 @@ class AppFrameworkAndNativeAssetsDependencies { } /// Class that encapsulates the logic for building CocoaPod plugins for every platform and sdk into -/// frameworks and then combines them into a single xcframework for each. +/// frameworks and then combines them into a single XCFramework for each. @visibleForTesting class CocoaPodPluginDependencies { CocoaPodPluginDependencies({ @@ -1212,9 +1194,9 @@ class CocoaPodPluginDependencies { final BuildSwiftPackageUtils _utils; /// Builds CocoaPod plugins for every platform and sdk into frameworks and then combines them into - /// a single xcframework for each. + /// a single XCFramework for each. /// - /// Intermediate build files are put in the [cacheDirectory]. The final xcframeworks are copied to + /// Intermediate build files are put in the [cacheDirectory]. The final XCFramework are copied to /// the [xcframeworkOutput]. Future generateArtifacts({ required BuildInfo buildInfo, @@ -1302,33 +1284,11 @@ class CocoaPodPluginDependencies { await processPodsIfNeeded(xcodeProject, _targetPlatform.buildDirectory(), buildInfo.mode); } - /// The target dependencies and binary targets for the CocoaPod plugin xcframeworks. - /// - /// ```swift - /// .target( - /// name: "FlutterPluginRegistrant", - /// dependencies: [ - /// .target(name: "cocoapod_plugin_a"), - /// - /// ... - /// - /// .binaryTarget( - /// name: "cocoapod_plugin_a", - /// path: "Frameworks/CocoaPods/cocoapod_plugin_a.xcframework" - /// ) - /// ``` - (List, List) generateDependencies({ - required Directory xcframeworkOutput, - }) { - return generateDependenciesFromDirectory( - fileSystem: _utils.fileSystem, - directoryName: _kCocoaPods, - xcframeworkDirectory: xcframeworkOutput.childDirectory(_kCocoaPods), - ); - } - /// Builds CocoaPod plugins into frameworks for the given [xcodeBuildConfiguration], [platform], /// and [sdk]. + /// + /// Returns a Map where the key is the name of the plugin and the value is a list of [Directory]s + /// containing the plugin's frameworks. Future>> _buildCocoaPodsForSdk({ required XcodeSdk sdk, required FlutterDarwinPlatform platform, @@ -1395,6 +1355,8 @@ class CocoaPodPluginDependencies { return frameworks; } + /// Return true if CocoaPod fingerprinter has changed, or if the pod lock files + /// are outdated. bool _hasDependenciesChanged( String cacheDirectoryPath, Directory cocoapodXCFrameworkDirectory, @@ -1411,15 +1373,7 @@ class CocoaPodPluginDependencies { if (!fingerprinter.doesFingerprintMatch()) { return true; } - - final File podfileFile = xcodeProject.podfile; - final File podfileLockFile = xcodeProject.podfileLock; - final File manifestLockFile = xcodeProject.podManifestLock; - - return !podfileLockFile.existsSync() || - !manifestLockFile.existsSync() || - podfileLockFile.statSync().modified.isBefore(podfileFile.statSync().modified) || - podfileLockFile.readAsStringSync() != manifestLockFile.readAsStringSync(); + return CocoaPods.podLockFilesOutdated(xcodeProject); } void _writeFingerprint( @@ -1437,6 +1391,11 @@ class CocoaPodPluginDependencies { fingerprinter.writeFingerprint(); } + /// Returns a [Fingerprinter] for the CocoaPod plugins. + /// + /// The [Fingerprinter] is used to check if the CocoaPod output, static status, build + /// configuration, this file, Xcode project, Podfile, generated plugin Swift Package, or + /// podhelper have changed since the last build. Fingerprinter _cocoapodsFingerprinter( String cacheDirectoryPath, Directory cocoapodXCFrameworkDirectory, @@ -1514,6 +1473,31 @@ class CocoaPodPluginDependencies { return configuration; } } + + /// The target dependencies and binary targets for the CocoaPod plugin xcframeworks. + /// + /// ```swift + /// .target( + /// name: "FlutterPluginRegistrant", + /// dependencies: [ + /// .target(name: "cocoapod_plugin_a"), + /// + /// ... + /// + /// .binaryTarget( + /// name: "cocoapod_plugin_a", + /// path: "Frameworks/CocoaPods/cocoapod_plugin_a.xcframework" + /// ) + /// ``` + (List, List) generateDependencies({ + required Directory xcframeworkOutput, + }) { + return generateDependenciesFromDirectory( + fileSystem: _utils.fileSystem, + directoryName: _kCocoaPods, + xcframeworkDirectory: xcframeworkOutput.childDirectory(_kCocoaPods), + ); + } } /// Create an XCFramework from a list of frameworks. diff --git a/packages/flutter_tools/lib/src/macos/cocoapods.dart b/packages/flutter_tools/lib/src/macos/cocoapods.dart index a2b7018f526e3..757ff3467b72c 100644 --- a/packages/flutter_tools/lib/src/macos/cocoapods.dart +++ b/packages/flutter_tools/lib/src/macos/cocoapods.dart @@ -327,6 +327,10 @@ class CocoaPods { return true; } + return podLockFilesOutdated(xcodeProject); + } + + static bool podLockFilesOutdated(XcodeBasedProject xcodeProject) { final File podfileFile = xcodeProject.podfile; final File podfileLockFile = xcodeProject.podfileLock; final File manifestLockFile = xcodeProject.podManifestLock; diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_swift_package_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_swift_package_test.dart index 5888afcc1b24d..e10c67d491c51 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_swift_package_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_swift_package_test.dart @@ -163,7 +163,6 @@ void main() { flutterFrameworkDependency: flutterFrameworkDependency, appAndNativeAssetsDependencies: appAndNativeAssetsDependencies, cocoapodDependencies: cocoapodDependencies, - includeCocoaPodBinaryTargets: true, packagesForConfiguration: fs.directory(debugPackagesDirectoryPath), xcframeworkOutput: fs.directory(debugFrameworksDirectoryPath), ); @@ -247,157 +246,6 @@ import PluginB } '''); }); - - testWithoutContext( - 'generateSwiftPackage when includeCocoaPodBinaryTargets is false', - () async { - final fs = MemoryFileSystem.test(); - final logger = BufferLogger.test(); - final processManager = FakeProcessManager.list([]); - const FlutterDarwinPlatform targetPlatform = .ios; - final testUtils = BuildSwiftPackageUtils( - analytics: FakeAnalytics(), - artifacts: FakeArtifacts(engineArtifactPath), - buildSystem: FakeBuildSystem(), - cache: FakeCache(fs, _flutterRoot), - fileSystem: fs, - flutterRoot: _flutterRoot, - flutterVersion: FakeFlutterVersion(), - logger: logger, - platform: FakePlatform(), - processManager: processManager, - project: FakeFlutterProject(directory: fs.directory(_flutterAppPath)), - templateRenderer: const MustacheTemplateRenderer(), - xcode: FakeXcode(), - ); - final package = FlutterPluginRegistrantSwiftPackage( - targetPlatform: targetPlatform, - utils: testUtils, - ); - final pluginSwiftDependencies = FlutterPluginSwiftDependencies( - targetPlatform: targetPlatform, - utils: testUtils, - ); - final flutterFrameworkDependency = FlutterFrameworkDependency( - targetPlatform: targetPlatform, - utils: testUtils, - ); - final appAndNativeAssetsDependencies = AppFrameworkAndNativeAssetsDependencies( - targetPlatform: targetPlatform, - utils: testUtils, - ); - late final cocoapodDependencies = CocoaPodPluginDependencies( - targetPlatform: targetPlatform, - utils: testUtils, - ); - // Plugin A represents a SwiftPM plugin - final Directory modeDirectory = fs.directory(debugModeDirectoryPath); - final pluginA = FakePlugin(name: 'PluginA', darwinPlatform: targetPlatform); - pluginSwiftDependencies.copiedPlugins.add((pluginA, '$pluginsDirectoryPath/PluginA')); - - // Plugin B represents a CocoaPod plugin - final pluginB = FakePlugin( - name: 'PluginB', - darwinPlatform: targetPlatform, - supportsSwiftPM: false, - ); - fs - .directory('$debugCocoaPodsDirectoryPath/PluginB.xcframework') - .createSync(recursive: true); - - // Plugin C represents a Native Asset - fs - .directory('$debugNativeAssetsDirectoryPath/PluginC.xcframework') - .createSync(recursive: true); - - await package.generateSwiftPackage( - modeDirectory: modeDirectory, - plugins: [pluginA, pluginB], - xcodeBuildConfiguration: 'Debug', - pluginSwiftDependencies: pluginSwiftDependencies, - flutterFrameworkDependency: flutterFrameworkDependency, - appAndNativeAssetsDependencies: appAndNativeAssetsDependencies, - cocoapodDependencies: cocoapodDependencies, - includeCocoaPodBinaryTargets: false, - packagesForConfiguration: fs.directory(debugPackagesDirectoryPath), - xcframeworkOutput: fs.directory(debugFrameworksDirectoryPath), - ); - - expect(logger.traceText, isEmpty); - expect(processManager.hasRemainingExpectations, false); - final File generatedPackageManifest = modeDirectory.childFile('Package.swift'); - expect(generatedPackageManifest, exists); - expect(generatedPackageManifest.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 - -// Debug - -let package = Package( - name: "FlutterPluginRegistrant", - platforms: [ - .iOS("13.0") - ], - products: [ - .library(name: "FlutterPluginRegistrant", type: .static, targets: ["FlutterPluginRegistrant"]) - ], - dependencies: [ - .package(name: "FlutterFramework", path: "Sources/Packages/FlutterFramework"), - .package(name: "PluginA", path: "Sources/Packages/PluginA") - ], - targets: [ - .target( - name: "FlutterPluginRegistrant", - dependencies: [ - .product(name: "FlutterFramework", package: "FlutterFramework"), - .product(name: "PluginA", package: "PluginA"), - .target(name: "PluginC"), - .target(name: "App") - ] - ), - .binaryTarget( - name: "PluginC", - path: "Sources/Frameworks/NativeAssets/PluginC.xcframework" - ), - .binaryTarget( - name: "App", - path: "Sources/Frameworks/App.xcframework" - ) - ] -) -'''); - final File generatedSource = modeDirectory - .childDirectory('FlutterPluginRegistrant') - .childFile('GeneratedPluginRegistrant.swift'); - expect(generatedSource, exists); - expect(generatedSource.readAsStringSync(), ''' -// -// Generated file. Do not edit. -// -import Flutter -import UIKit - -import PluginA -import PluginB - -@objc public class GeneratedPluginRegistrant: NSObject { - @objc public static func register(with registry: FlutterPluginRegistry) { - if let pluginAPlugin = registry.registrar(forPlugin: "PluginAPlugin") { - PluginAPlugin.register(with: pluginAPlugin) - } - if let pluginBPlugin = registry.registrar(forPlugin: "PluginBPlugin") { - PluginBPlugin.register(with: pluginBPlugin) - } - } -} -'''); - }, - ); }); group('FlutterFrameworkDependency', () { @@ -1724,7 +1572,6 @@ let package = Package( flutterFrameworkDependency: flutterFrameworkDependency, appAndNativeAssetsDependencies: appAndNativeAssetsDependencies, cocoapodDependencies: cocoapodDependencies, - includeCocoaPodBinaryTargets: true, packagesForConfiguration: fs.directory(debugPackagesDirectoryPath), xcframeworkOutput: fs.directory(debugFrameworksDirectoryPath), ); From b23e731ef272689258fbffa8d79d8837ad4cb57d Mon Sep 17 00:00:00 2001 From: Victoria Ashworth Date: Thu, 5 Mar 2026 12:33:40 -0600 Subject: [PATCH 3/3] small tweaks --- .../flutter_tools/lib/src/commands/build_swift_package.dart | 4 ++-- packages/flutter_tools/lib/src/macos/cocoapods.dart | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/build_swift_package.dart b/packages/flutter_tools/lib/src/commands/build_swift_package.dart index 1841e51bbc5da..1a6857e515458 100644 --- a/packages/flutter_tools/lib/src/commands/build_swift_package.dart +++ b/packages/flutter_tools/lib/src/commands/build_swift_package.dart @@ -1218,7 +1218,7 @@ class CocoaPodPluginDependencies { final Status status = _utils.logger.startProgress(' ├─Building CocoaPod frameworks...'); var skipped = false; try { - final bool dependenciesChanged = _hasDependenciesChanged( + final bool dependenciesChanged = _haveDependenciesChanged( cacheDirectory.path, cocoapodXCFrameworkOutput, buildInfo.mode.cliName, @@ -1357,7 +1357,7 @@ class CocoaPodPluginDependencies { /// Return true if CocoaPod fingerprinter has changed, or if the pod lock files /// are outdated. - bool _hasDependenciesChanged( + bool _haveDependenciesChanged( String cacheDirectoryPath, Directory cocoapodXCFrameworkDirectory, String xcodeBuildConfiguration, diff --git a/packages/flutter_tools/lib/src/macos/cocoapods.dart b/packages/flutter_tools/lib/src/macos/cocoapods.dart index 757ff3467b72c..0e14719799848 100644 --- a/packages/flutter_tools/lib/src/macos/cocoapods.dart +++ b/packages/flutter_tools/lib/src/macos/cocoapods.dart @@ -330,6 +330,12 @@ class CocoaPods { return podLockFilesOutdated(xcodeProject); } + /// Return true if the pod lock files are outdated. + /// + /// This is true if: + /// - Podfile.lock doesn't exist or is older than Podfile + /// - Pods/Manifest.lock doesn't exist + /// - Podfile.lock doesn't match Pods/Manifest.lock static bool podLockFilesOutdated(XcodeBasedProject xcodeProject) { final File podfileFile = xcodeProject.podfile; final File podfileLockFile = xcodeProject.podfileLock;