From 46b03c352936f5746952ce53dcbb47949a68587a Mon Sep 17 00:00:00 2001 From: Victoria Ashworth <15619084+vashworth@users.noreply.github.com> Date: Mon, 2 Jun 2025 11:11:06 -0500 Subject: [PATCH] Symlink SwiftPM plugins in the same directory (#168932) This PR symlinks SwiftPM plugins in the same directory so that they're relative to each other and the `FlutterGeneratedPluginSwiftPackage`. This allows plugins to depend on each other via relative paths. For example, > Flutter/ephemeral/Packages/.packages/plugin_a --> symlink --> /local/path/to/plugin_a > Flutter/ephemeral/Packages/.packages/plugin_b --> symlink --> /path/to/.pub-cache/plugin_b Then in plugin_b's Package.swift, you can do the following to add a dependency on plugin_a: ```swift dependencies: [ .package(name: "plugin_a", path: "../plugin_a"), ], ``` Addresses https://github.com/flutter/flutter/issues/166528 and incremental change toward https://github.com/flutter/flutter/issues/166489. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../lib/src/macos/swift_package_manager.dart | 38 +++++-- packages/flutter_tools/lib/src/plugins.dart | 23 +++- .../flutter_tools/lib/src/xcode_project.dart | 19 ++-- .../macos/cocoapod_utils_test.dart | 40 ++++--- .../macos/swift_package_manager_test.dart | 92 ++++++++++------ .../test/general.shard/plugins_test.dart | 102 ++++++------------ .../general.shard/xcode_project_test.dart | 33 ++++++ .../swift_package_manager_test.dart | 73 ++++++++++++- .../swift_package_manager_utils.dart | 15 ++- 9 files changed, 294 insertions(+), 141 deletions(-) diff --git a/packages/flutter_tools/lib/src/macos/swift_package_manager.dart b/packages/flutter_tools/lib/src/macos/swift_package_manager.dart index a77bd78b506..7b4a3f14c8d 100644 --- a/packages/flutter_tools/lib/src/macos/swift_package_manager.dart +++ b/packages/flutter_tools/lib/src/macos/swift_package_manager.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import '../base/common.dart'; +import '../base/error_handling_io.dart'; import '../base/file_system.dart'; import '../base/template.dart'; import '../base/version.dart'; @@ -54,10 +55,19 @@ class SwiftPackageManager { ) async { _validatePlatform(platform); + final Directory symlinkDirectory = project.relativeSwiftPackagesDirectory; + ErrorHandlingFileSystem.deleteIfExists(symlinkDirectory, recursive: true); + symlinkDirectory.createSync(recursive: true); + final ( List packageDependencies, List targetDependencies, - ) = _dependenciesForPlugins(plugins, platform); + ) = _dependenciesForPlugins( + plugins: plugins, + platform: platform, + symlinkDirectory: symlinkDirectory, + pathRelativeTo: project.flutterPluginSwiftPackageDirectory.path, + ); // 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 @@ -100,10 +110,13 @@ class SwiftPackageManager { pluginsPackage.createSwiftPackage(); } - (List, List) _dependenciesForPlugins( - List plugins, - SupportedPlatform platform, - ) { + (List, List) + _dependenciesForPlugins({ + required List plugins, + required SupportedPlatform platform, + required Directory symlinkDirectory, + required String pathRelativeTo, + }) { final List packageDependencies = []; final List targetDependencies = []; @@ -113,18 +126,21 @@ class SwiftPackageManager { _fileSystem, platform.name, ); + String? packagePath = plugin.pluginSwiftPackagePath(_fileSystem, platform.name); if (plugin.platforms[platform.name] == null || pluginSwiftPackageManifestPath == null || + packagePath == null || !_fileSystem.file(pluginSwiftPackageManifestPath).existsSync()) { continue; } - packageDependencies.add( - SwiftPackagePackageDependency( - name: plugin.name, - path: _fileSystem.file(pluginSwiftPackageManifestPath).parent.path, - ), - ); + final Link pluginSymlink = symlinkDirectory.childLink(plugin.name); + ErrorHandlingFileSystem.deleteIfExists(pluginSymlink); + pluginSymlink.createSync(packagePath); + packagePath = pluginSymlink.path; + packagePath = _fileSystem.path.relative(packagePath, from: pathRelativeTo); + + packageDependencies.add(SwiftPackagePackageDependency(name: plugin.name, path: packagePath)); // The target dependency product name is hyphen separated because it's // the dependency's library name, which Swift Package Manager will diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart index 0cbea601c4f..af86b7043be 100644 --- a/packages/flutter_tools/lib/src/plugins.dart +++ b/packages/flutter_tools/lib/src/plugins.dart @@ -438,14 +438,29 @@ class Plugin { /// Dev dependencies are intended to be stripped out in release builds. final bool isDevDependency; - /// 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) { + /// Expected path to the plugin's swift package, which contains the Package.swift. + /// + /// This path should be `/path/to/[package_name]/[platform]/[package_name]` + /// (e.g. `/path/to/my_plugin/ios/my_plugin`). + /// + /// Returns null if the plugin does not support the [platform] or the + /// [platform] is not iOS or macOS. + String? pluginSwiftPackagePath(FileSystem fileSystem, String platform) { final String? platformDirectoryName = _darwinPluginDirectoryName(platform); if (platformDirectoryName == null) { return null; } - return fileSystem.path.join(path, platformDirectoryName, name, 'Package.swift'); + return fileSystem.path.join(path, platformDirectoryName, name); + } + + /// 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? packagePath = pluginSwiftPackagePath(fileSystem, platform); + if (packagePath == null) { + return null; + } + return fileSystem.path.join(packagePath, 'Package.swift'); } /// Expected path to the plugin's podspec. Returns null if the plugin does diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart index c7a44824886..433ee972077 100644 --- a/packages/flutter_tools/lib/src/xcode_project.dart +++ b/packages/flutter_tools/lib/src/xcode_project.dart @@ -139,13 +139,20 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform { /// 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(kFlutterGeneratedPluginSwiftPackageName); + /// The Flutter generated directory for generated Swift packages. + Directory get flutterSwiftPackagesDirectory => ephemeralDirectory.childDirectory('Packages'); - /// The Flutter generated Swift Package manifest (Package.swift) for plugin + /// Flutter plugins that support SwiftPM will be symlinked in this directory to keep all + /// Swift packages relative to each other. + Directory get relativeSwiftPackagesDirectory => + flutterSwiftPackagesDirectory.childDirectory('.packages'); + + /// The Flutter generated directory for the Swift package handling plugin + /// dependencies. + Directory get flutterPluginSwiftPackageDirectory => + flutterSwiftPackagesDirectory.childDirectory(kFlutterGeneratedPluginSwiftPackageName); + + /// The Flutter generated Swift package manifest (Package.swift) for plugin /// dependencies. File get flutterPluginSwiftPackageManifest => flutterPluginSwiftPackageDirectory.childFile('Package.swift'); 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 index d2425df4161..9bf40cb3360 100644 --- a/packages/flutter_tools/test/general.shard/macos/cocoapod_utils_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/cocoapod_utils_test.dart @@ -506,12 +506,20 @@ class FakeMacOSProject extends Fake implements MacOSProject { hostAppRoot.childDirectory('Runner.xcodeproj').childFile('project.pbxproj'); @override - File get flutterPluginSwiftPackageManifest => hostAppRoot - .childDirectory('Flutter') - .childDirectory('ephemeral') - .childDirectory('Packages') - .childDirectory('FlutterGeneratedPluginSwiftPackage') - .childFile('Package.swift'); + Directory get flutterSwiftPackagesDirectory => + hostAppRoot.childDirectory('Flutter').childDirectory('ephemeral').childDirectory('Packages'); + + @override + Directory get relativeSwiftPackagesDirectory => + flutterSwiftPackagesDirectory.childDirectory('.packages'); + + @override + Directory get flutterPluginSwiftPackageDirectory => + flutterSwiftPackagesDirectory.childDirectory('FlutterGeneratedPluginSwiftPackage'); + + @override + File get flutterPluginSwiftPackageManifest => + flutterPluginSwiftPackageDirectory.childFile('Package.swift'); @override bool usesSwiftPackageManager = false; @@ -547,12 +555,20 @@ class FakeIosProject extends Fake implements IosProject { hostAppRoot.childDirectory('Runner.xcodeproj').childFile('project.pbxproj'); @override - File get flutterPluginSwiftPackageManifest => hostAppRoot - .childDirectory('Flutter') - .childDirectory('ephemeral') - .childDirectory('Packages') - .childDirectory('FlutterGeneratedPluginSwiftPackage') - .childFile('Package.swift'); + Directory get flutterSwiftPackagesDirectory => + hostAppRoot.childDirectory('Flutter').childDirectory('ephemeral').childDirectory('Packages'); + + @override + Directory get relativeSwiftPackagesDirectory => + flutterSwiftPackagesDirectory.childDirectory('.packages'); + + @override + Directory get flutterPluginSwiftPackageDirectory => + flutterSwiftPackagesDirectory.childDirectory('FlutterGeneratedPluginSwiftPackage'); + + @override + File get flutterPluginSwiftPackageManifest => + flutterPluginSwiftPackageDirectory.childFile('Package.swift'); @override bool usesSwiftPackageManager = false; 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 index 5c15b095547..143a595c781 100644 --- 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 @@ -4,6 +4,7 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; +import 'package:file_testing/file_testing.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'; @@ -118,13 +119,15 @@ $_doubleIndent fileSystem: fs, ); - final File validPlugin1Manifest = fs.file( - '/local/path/to/plugins/valid_plugin_1/Package.swift', - )..createSync(recursive: true); + final Directory validPlugin1Directory = fs.directory( + '/local/path/to/plugins/valid_plugin_1', + ); + validPlugin1Directory.childFile('Package.swift').createSync(recursive: true); + final FakePlugin validPlugin1 = FakePlugin( name: 'valid_plugin_1', platforms: {platform.name: FakePluginPlatform()}, - pluginSwiftPackageManifestPath: validPlugin1Manifest.path, + pluginSwiftPackagePath: validPlugin1Directory.path, ); final SwiftPackageManager spm = SwiftPackageManager( fileSystem: fs, @@ -135,6 +138,11 @@ $_doubleIndent final String supportedPlatform = platform == SupportedPlatform.ios ? '.iOS("13.0")' : '.macOS("10.15")'; expect(project.flutterPluginSwiftPackageManifest.existsSync(), isTrue); + expect(project.relativeSwiftPackagesDirectory.childLink('valid_plugin_1'), exists); + expect( + project.relativeSwiftPackagesDirectory.childLink('valid_plugin_1').targetSync(), + validPlugin1Directory.path, + ); expect(project.flutterPluginSwiftPackageManifest.readAsStringSync(), ''' // swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. @@ -153,7 +161,7 @@ let package = Package( .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_1", path: "../.packages/valid_plugin_1") ], targets: [ .target( @@ -176,34 +184,38 @@ let package = Package( final FakePlugin nonPlatformCompatiblePlugin = FakePlugin( name: 'invalid_plugin_due_to_incompatible_platform', platforms: {}, - pluginSwiftPackageManifestPath: '/some/path', + pluginSwiftPackagePath: '/some/path', ); final FakePlugin pluginSwiftPackageManifestIsNull = FakePlugin( name: 'invalid_plugin_due_to_null_plugin_swift_package_path', platforms: {platform.name: FakePluginPlatform()}, - pluginSwiftPackageManifestPath: null, + pluginSwiftPackagePath: null, ); final FakePlugin pluginSwiftPackageManifestNotExists = FakePlugin( name: 'invalid_plugin_due_to_plugin_swift_package_path_does_not_exist', platforms: {platform.name: FakePluginPlatform()}, - pluginSwiftPackageManifestPath: '/some/path', + pluginSwiftPackagePath: '/some/path', ); - final File validPlugin1Manifest = fs.file( - '/local/path/to/plugins/valid_plugin_1/Package.swift', - )..createSync(recursive: true); + final Directory validPlugin1Directory = fs.directory( + '/local/path/to/plugins/valid_plugin_1', + ); + validPlugin1Directory.childFile('Package.swift').createSync(recursive: true); final FakePlugin validPlugin1 = FakePlugin( name: 'valid_plugin_1', platforms: {platform.name: FakePluginPlatform()}, - pluginSwiftPackageManifestPath: validPlugin1Manifest.path, + pluginSwiftPackagePath: validPlugin1Directory.path, ); - final File validPlugin2Manifest = fs.file( - '/.pub-cache/plugins/valid_plugin_2/Package.swift', - )..createSync(recursive: true); + + final Directory validPlugin2Directory = fs.directory( + '/.pub-cache/plugins/valid_plugin_2', + ); + validPlugin2Directory.childFile('Package.swift').createSync(recursive: true); + final FakePlugin validPlugin2 = FakePlugin( name: 'valid_plugin_2', platforms: {platform.name: FakePluginPlatform()}, - pluginSwiftPackageManifestPath: validPlugin2Manifest.path, + pluginSwiftPackagePath: validPlugin2Directory.path, ); final SwiftPackageManager spm = SwiftPackageManager( @@ -225,6 +237,16 @@ let package = Package( final String supportedPlatform = platform == SupportedPlatform.ios ? '.iOS("13.0")' : '.macOS("10.15")'; expect(project.flutterPluginSwiftPackageManifest.existsSync(), isTrue); + expect(project.relativeSwiftPackagesDirectory.childLink('valid_plugin_1'), exists); + expect( + project.relativeSwiftPackagesDirectory.childLink('valid_plugin_1').targetSync(), + validPlugin1Directory.path, + ); + expect(project.relativeSwiftPackagesDirectory.childLink('valid_plugin_2'), exists); + expect( + project.relativeSwiftPackagesDirectory.childLink('valid_plugin_2').targetSync(), + validPlugin2Directory.path, + ); expect(project.flutterPluginSwiftPackageManifest.readAsStringSync(), ''' // swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. @@ -243,8 +265,8 @@ let package = Package( .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") + .package(name: "valid_plugin_1", path: "../.packages/valid_plugin_1"), + .package(name: "valid_plugin_2", path: "../.packages/valid_plugin_2") ], targets: [ .target( @@ -373,11 +395,16 @@ class FakeXcodeProject extends Fake implements IosProject { String hostAppProjectName = 'Runner'; @override - Directory get flutterPluginSwiftPackageDirectory => hostAppRoot - .childDirectory('Flutter') - .childDirectory('ephemeral') - .childDirectory('Packages') - .childDirectory('FlutterGeneratedPluginSwiftPackage'); + Directory get flutterSwiftPackagesDirectory => + hostAppRoot.childDirectory('Flutter').childDirectory('ephemeral').childDirectory('Packages'); + + @override + Directory get relativeSwiftPackagesDirectory => + flutterSwiftPackagesDirectory.childDirectory('.packages'); + + @override + Directory get flutterPluginSwiftPackageDirectory => + flutterSwiftPackagesDirectory.childDirectory('FlutterGeneratedPluginSwiftPackage'); @override File get flutterPluginSwiftPackageManifest => @@ -391,13 +418,10 @@ class FakeXcodeProject extends Fake implements IosProject { } class FakePlugin extends Fake implements Plugin { - FakePlugin({ - required this.name, - required this.platforms, - required String? pluginSwiftPackageManifestPath, - }) : _pluginSwiftPackageManifestPath = pluginSwiftPackageManifestPath; + FakePlugin({required this.name, required this.platforms, required String? pluginSwiftPackagePath}) + : _pluginSwiftPackagePath = pluginSwiftPackagePath; - final String? _pluginSwiftPackageManifestPath; + final String? _pluginSwiftPackagePath; @override final String name; @@ -405,9 +429,17 @@ class FakePlugin extends Fake implements Plugin { @override final Map platforms; + @override + String? pluginSwiftPackagePath(FileSystem fileSystem, String platform) { + return _pluginSwiftPackagePath; + } + @override String? pluginSwiftPackageManifestPath(FileSystem fileSystem, String platform) { - return _pluginSwiftPackageManifestPath; + if (_pluginSwiftPackagePath == null) { + return null; + } + return '$_pluginSwiftPackagePath/Package.swift'; } } diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart index abb8eb77269..845858c7cbf 100644 --- a/packages/flutter_tools/test/general.shard/plugins_test.dart +++ b/packages/flutter_tools/test/general.shard/plugins_test.dart @@ -2153,7 +2153,7 @@ flutter: }); group('Plugin files', () { - testWithoutContext('pluginSwiftPackageManifestPath for iOS and macOS plugins', () async { + testWithoutContext('for SwiftPM and podspec paths for iOS and macOS plugins', () async { final MemoryFileSystem fs = MemoryFileSystem.test(); final Plugin plugin = Plugin( name: 'test', @@ -2168,7 +2168,11 @@ flutter: isDirectDependency: true, isDevDependency: false, ); - + expect(plugin.pluginSwiftPackagePath(fs, IOSPlugin.kConfigKey), '/path/to/test/ios/test'); + expect( + plugin.pluginSwiftPackagePath(fs, MacOSPlugin.kConfigKey), + '/path/to/test/macos/test', + ); expect( plugin.pluginSwiftPackageManifestPath(fs, IOSPlugin.kConfigKey), '/path/to/test/ios/test/Package.swift', @@ -2177,74 +2181,6 @@ flutter: 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, - isDevDependency: false, - ); - - 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, - isDevDependency: false, - ); - - 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, - isDevDependency: false, - ); - expect( plugin.pluginPodspecPath(fs, IOSPlugin.kConfigKey), '/path/to/test/ios/test.podspec', @@ -2255,7 +2191,7 @@ flutter: ); }); - testWithoutContext('pluginPodspecPath for darwin plugins', () async { + testWithoutContext('for SwiftPM and podspec paths for darwin plugins', () async { final MemoryFileSystem fs = MemoryFileSystem.test(); final Plugin plugin = Plugin( name: 'test', @@ -2275,6 +2211,22 @@ flutter: isDevDependency: false, ); + expect( + plugin.pluginSwiftPackagePath(fs, IOSPlugin.kConfigKey), + '/path/to/test/darwin/test', + ); + expect( + plugin.pluginSwiftPackagePath(fs, MacOSPlugin.kConfigKey), + '/path/to/test/darwin/test', + ); + 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', + ); expect( plugin.pluginPodspecPath(fs, IOSPlugin.kConfigKey), '/path/to/test/darwin/test.podspec', @@ -2285,7 +2237,7 @@ flutter: ); }); - testWithoutContext('pluginPodspecPath for non darwin plugins', () async { + testWithoutContext('for SwiftPM and podspec paths for non darwin plugins', () async { final MemoryFileSystem fs = MemoryFileSystem.test(); final Plugin plugin = Plugin( name: 'test', @@ -2300,6 +2252,12 @@ flutter: isDevDependency: false, ); + expect(plugin.pluginSwiftPackagePath(fs, IOSPlugin.kConfigKey), isNull); + expect(plugin.pluginSwiftPackagePath(fs, MacOSPlugin.kConfigKey), isNull); + expect(plugin.pluginSwiftPackagePath(fs, WindowsPlugin.kConfigKey), isNull); + expect(plugin.pluginSwiftPackageManifestPath(fs, IOSPlugin.kConfigKey), isNull); + expect(plugin.pluginSwiftPackageManifestPath(fs, MacOSPlugin.kConfigKey), isNull); + expect(plugin.pluginSwiftPackageManifestPath(fs, WindowsPlugin.kConfigKey), isNull); expect(plugin.pluginPodspecPath(fs, IOSPlugin.kConfigKey), isNull); expect(plugin.pluginPodspecPath(fs, MacOSPlugin.kConfigKey), isNull); expect(plugin.pluginPodspecPath(fs, WindowsPlugin.kConfigKey), isNull); diff --git a/packages/flutter_tools/test/general.shard/xcode_project_test.dart b/packages/flutter_tools/test/general.shard/xcode_project_test.dart index 65f1cf8af0f..fe524177474 100644 --- a/packages/flutter_tools/test/general.shard/xcode_project_test.dart +++ b/packages/flutter_tools/test/general.shard/xcode_project_test.dart @@ -51,6 +51,21 @@ void main() { expect(project.ephemeralDirectory.path, 'app_name/.ios/Flutter/ephemeral'); }); + testWithoutContext('flutterSwiftPackagesDirectory', () { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final IosProject project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + expect(project.flutterSwiftPackagesDirectory.path, 'app_name/ios/Flutter/ephemeral/Packages'); + }); + + testWithoutContext('relativeSwiftPackagesDirectory', () { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final IosProject project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + expect( + project.relativeSwiftPackagesDirectory.path, + 'app_name/ios/Flutter/ephemeral/Packages/.packages', + ); + }); + testWithoutContext('flutterPluginSwiftPackageDirectory', () { final MemoryFileSystem fs = MemoryFileSystem.test(); final IosProject project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); @@ -440,6 +455,24 @@ void main() { expect(project.ephemeralDirectory.path, 'app_name/macos/Flutter/ephemeral'); }); + testWithoutContext('flutterSwiftPackagesDirectory', () { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final MacOSProject project = MacOSProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + expect( + project.flutterSwiftPackagesDirectory.path, + 'app_name/macos/Flutter/ephemeral/Packages', + ); + }); + + testWithoutContext('relativeSwiftPackagesDirectory', () { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final MacOSProject project = MacOSProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + expect( + project.relativeSwiftPackagesDirectory.path, + 'app_name/macos/Flutter/ephemeral/Packages/.packages', + ); + }); + testWithoutContext('flutterPluginSwiftPackageDirectory', () { final MemoryFileSystem fs = MemoryFileSystem.test(); final MacOSProject project = MacOSProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); 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 index 45ba3b1453d..c9a77ae3745 100644 --- a/packages/flutter_tools/test/integration.shard/swift_package_manager_test.dart +++ b/packages/flutter_tools/test/integration.shard/swift_package_manager_test.dart @@ -101,6 +101,71 @@ void main() { flutterBin, workingDirectoryPath, ); + + // Create a SwiftPM plugin that depends on native code from another SwiftPM plugin + final SwiftPackageManagerPlugin createdSwiftPMPlugin = + await SwiftPackageManagerUtils.createPlugin( + flutterBin, + workingDirectoryPath, + platform: platformName, + iosLanguage: iosLanguage, + usesSwiftPackageManager: true, + ); + final File swiftPMPluginPackageManifest = fileSystem + .directory(createdSwiftPMPlugin.pluginPath) + .childDirectory(platformName) + .childDirectory(createdSwiftPMPlugin.pluginName) + .childFile('Package.swift'); + final String manifestContents = swiftPMPluginPackageManifest.readAsStringSync(); + swiftPMPluginPackageManifest.writeAsStringSync( + manifestContents + .replaceFirst( + 'dependencies: []', + 'dependencies: [.package(name: "${integrationTestPlugin.pluginName}", path: "../${integrationTestPlugin.pluginName}")]', + ) + .replaceFirst( + 'dependencies: []', + 'dependencies: [.product(name: "${integrationTestPlugin.pluginName.replaceAll('_', '-')}", package: "${integrationTestPlugin.pluginName}")]', + ), + ); + final File swiftPMPluginPodspec = fileSystem + .directory(createdSwiftPMPlugin.pluginPath) + .childDirectory(platformName) + .childFile('${createdSwiftPMPlugin.pluginName}.podspec'); + final String podspecContents = swiftPMPluginPodspec.readAsStringSync(); + swiftPMPluginPodspec.writeAsStringSync( + podspecContents.replaceFirst( + '\nend', + "\n s.dependency '${integrationTestPlugin.pluginName}'\n\nend", + ), + ); + final String pluginClassFileName = + iosLanguage == 'swift' + ? '${createdSwiftPMPlugin.className}.swift' + : '${createdSwiftPMPlugin.className}.m'; + final String pluginClassFileImport = + iosLanguage == 'swift' + ? 'import ${integrationTestPlugin.pluginName}' + : '@import ${integrationTestPlugin.pluginName};'; + final File pluginClassFile = fileSystem + .directory(createdSwiftPMPlugin.pluginPath) + .childDirectory(platformName) + .childDirectory(createdSwiftPMPlugin.pluginName) + .childDirectory('Sources') + .childDirectory(createdSwiftPMPlugin.pluginName) + .childFile(pluginClassFileName); + final String pluginClassFileContent = pluginClassFile.readAsStringSync(); + pluginClassFile.writeAsStringSync('$pluginClassFileImport\n$pluginClassFileContent'); + SwiftPackageManagerUtils.addDependency( + appDirectoryPath: createdSwiftPMPlugin.pluginPath, + plugin: integrationTestPlugin, + ); + + SwiftPackageManagerUtils.addDependency( + appDirectoryPath: appDirectoryPath, + plugin: createdSwiftPMPlugin, + ); + await SwiftPackageManagerUtils.buildApp( flutterBin, appDirectoryPath, @@ -731,9 +796,9 @@ void main() { expect(generatedManifestFile, exists); String generatedManifest = generatedManifestFile.readAsStringSync(); - final String generatedSwiftDependency = ''' + const String generatedSwiftDependency = ''' dependencies: [ - .package(name: "integration_test", path: "${integrationTestPlugin.swiftPackagePlatformPath}") + .package(name: "integration_test", path: "../.packages/integration_test") ], '''; @@ -828,9 +893,9 @@ void main() { String xcodeProject = xcodeProjectFile.readAsStringSync(); String generatedManifest = generatedManifestFile.readAsStringSync(); - final String generatedSwiftDependency = ''' + const String generatedSwiftDependency = ''' dependencies: [ - .package(name: "integration_test", path: "${integrationTestPlugin.swiftPackagePlatformPath}") + .package(name: "integration_test", path: "../.packages/integration_test") ], '''; diff --git a/packages/flutter_tools/test/integration.shard/swift_package_manager_utils.dart b/packages/flutter_tools/test/integration.shard/swift_package_manager_utils.dart index 365558f54ec..515b262b1a7 100644 --- a/packages/flutter_tools/test/integration.shard/swift_package_manager_utils.dart +++ b/packages/flutter_tools/test/integration.shard/swift_package_manager_utils.dart @@ -211,9 +211,15 @@ class SwiftPackageManagerUtils { pluginName: pluginName, pluginPath: pluginDirectory.path, platform: platform, + className: + '${_capitalize(platform)}${_capitalize(iosLanguage)}${_capitalize(dependencyManager)}Plugin', ); } + static String _capitalize(String str) { + return str[0].toUpperCase() + str.substring(1); + } + static void addDependency({ required SwiftPackageManagerPlugin plugin, required String appDirectoryPath, @@ -269,6 +275,7 @@ class SwiftPackageManagerUtils { 'integration_test', 'integration_test_macos', ), + className: 'IntegrationTestPlugin', ); } @@ -295,7 +302,7 @@ class SwiftPackageManagerUtils { if (swiftPackageMangerEnabled) { expectedLines.addAll([ RegExp( - '${swiftPackagePlugin.pluginName}: [/private]*${swiftPackagePlugin.pluginPath}/$platform/${swiftPackagePlugin.pluginName} @ local', + '${swiftPackagePlugin.pluginName}: [/private]*$appPlatformDirectoryPath/Flutter/ephemeral/Packages/.packages/${swiftPackagePlugin.pluginName} @ local', ), "➜ Explicit dependency on target '${swiftPackagePlugin.pluginName}' in project '${swiftPackagePlugin.pluginName}'", ]); @@ -335,6 +342,8 @@ class SwiftPackageManagerUtils { bool migrated = false, }) { final String frameworkName = platform == 'ios' ? 'Flutter' : 'FlutterMacOS'; + final String appPlatformDirectoryPath = fileSystem.path.join(appDirectoryPath, platform); + final List unexpectedLines = []; if (cocoaPodsPlugin == null && !migrated) { unexpectedLines.addAll([ @@ -351,7 +360,7 @@ class SwiftPackageManagerUtils { ]); } else { unexpectedLines.addAll([ - '${swiftPackagePlugin.pluginName}: ${swiftPackagePlugin.pluginPath}/$platform/${swiftPackagePlugin.pluginName} @ local', + '${swiftPackagePlugin.pluginName}: $appPlatformDirectoryPath/Flutter/ephemeral/Packages/.packages/${swiftPackagePlugin.pluginName} @ local', "➜ Explicit dependency on target '${swiftPackagePlugin.pluginName}' in project '${swiftPackagePlugin.pluginName}'", ]); } @@ -368,11 +377,13 @@ class SwiftPackageManagerPlugin { required this.pluginName, required this.pluginPath, required this.platform, + required this.className, }); final String pluginName; final String pluginPath; final String platform; + final String className; String get exampleAppPath => fileSystem.path.join(pluginPath, 'example'); String get exampleAppPlatformPath => fileSystem.path.join(exampleAppPath, platform); String get swiftPackagePlatformPath => fileSystem.path.join(pluginPath, platform, pluginName);