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].

<!-- Links -->
[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
This commit is contained in:
Victoria Ashworth 2025-06-02 11:11:06 -05:00 committed by GitHub
parent a63836aa2b
commit 46b03c3529
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 294 additions and 141 deletions

View File

@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import '../base/common.dart'; import '../base/common.dart';
import '../base/error_handling_io.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/template.dart'; import '../base/template.dart';
import '../base/version.dart'; import '../base/version.dart';
@ -54,10 +55,19 @@ class SwiftPackageManager {
) async { ) async {
_validatePlatform(platform); _validatePlatform(platform);
final Directory symlinkDirectory = project.relativeSwiftPackagesDirectory;
ErrorHandlingFileSystem.deleteIfExists(symlinkDirectory, recursive: true);
symlinkDirectory.createSync(recursive: true);
final ( final (
List<SwiftPackagePackageDependency> packageDependencies, List<SwiftPackagePackageDependency> packageDependencies,
List<SwiftPackageTargetDependency> targetDependencies, List<SwiftPackageTargetDependency> 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 // 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 // migrated yet, don't generate a Swift package or migrate the app since
@ -100,10 +110,13 @@ class SwiftPackageManager {
pluginsPackage.createSwiftPackage(); pluginsPackage.createSwiftPackage();
} }
(List<SwiftPackagePackageDependency>, List<SwiftPackageTargetDependency>) _dependenciesForPlugins( (List<SwiftPackagePackageDependency>, List<SwiftPackageTargetDependency>)
List<Plugin> plugins, _dependenciesForPlugins({
SupportedPlatform platform, required List<Plugin> plugins,
) { required SupportedPlatform platform,
required Directory symlinkDirectory,
required String pathRelativeTo,
}) {
final List<SwiftPackagePackageDependency> packageDependencies = final List<SwiftPackagePackageDependency> packageDependencies =
<SwiftPackagePackageDependency>[]; <SwiftPackagePackageDependency>[];
final List<SwiftPackageTargetDependency> targetDependencies = <SwiftPackageTargetDependency>[]; final List<SwiftPackageTargetDependency> targetDependencies = <SwiftPackageTargetDependency>[];
@ -113,18 +126,21 @@ class SwiftPackageManager {
_fileSystem, _fileSystem,
platform.name, platform.name,
); );
String? packagePath = plugin.pluginSwiftPackagePath(_fileSystem, platform.name);
if (plugin.platforms[platform.name] == null || if (plugin.platforms[platform.name] == null ||
pluginSwiftPackageManifestPath == null || pluginSwiftPackageManifestPath == null ||
packagePath == null ||
!_fileSystem.file(pluginSwiftPackageManifestPath).existsSync()) { !_fileSystem.file(pluginSwiftPackageManifestPath).existsSync()) {
continue; continue;
} }
packageDependencies.add( final Link pluginSymlink = symlinkDirectory.childLink(plugin.name);
SwiftPackagePackageDependency( ErrorHandlingFileSystem.deleteIfExists(pluginSymlink);
name: plugin.name, pluginSymlink.createSync(packagePath);
path: _fileSystem.file(pluginSwiftPackageManifestPath).parent.path, 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 target dependency product name is hyphen separated because it's
// the dependency's library name, which Swift Package Manager will // the dependency's library name, which Swift Package Manager will

View File

@ -438,14 +438,29 @@ class Plugin {
/// Dev dependencies are intended to be stripped out in release builds. /// Dev dependencies are intended to be stripped out in release builds.
final bool isDevDependency; final bool isDevDependency;
/// Expected path to the plugin's Package.swift. Returns null if the plugin /// Expected path to the plugin's swift package, which contains the Package.swift.
/// does not support the [platform] or the [platform] is not iOS or macOS. ///
String? pluginSwiftPackageManifestPath(FileSystem fileSystem, String platform) { /// 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); final String? platformDirectoryName = _darwinPluginDirectoryName(platform);
if (platformDirectoryName == null) { if (platformDirectoryName == null) {
return 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 /// Expected path to the plugin's podspec. Returns null if the plugin does

View File

@ -139,13 +139,20 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform {
/// checked in should live here. /// checked in should live here.
Directory get ephemeralDirectory => managedDirectory.childDirectory('ephemeral'); Directory get ephemeralDirectory => managedDirectory.childDirectory('ephemeral');
/// The Flutter generated directory for the Swift Package handling plugin /// The Flutter generated directory for generated Swift packages.
/// dependencies. Directory get flutterSwiftPackagesDirectory => ephemeralDirectory.childDirectory('Packages');
Directory get flutterPluginSwiftPackageDirectory => ephemeralDirectory
.childDirectory('Packages')
.childDirectory(kFlutterGeneratedPluginSwiftPackageName);
/// 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. /// dependencies.
File get flutterPluginSwiftPackageManifest => File get flutterPluginSwiftPackageManifest =>
flutterPluginSwiftPackageDirectory.childFile('Package.swift'); flutterPluginSwiftPackageDirectory.childFile('Package.swift');

View File

@ -506,12 +506,20 @@ class FakeMacOSProject extends Fake implements MacOSProject {
hostAppRoot.childDirectory('Runner.xcodeproj').childFile('project.pbxproj'); hostAppRoot.childDirectory('Runner.xcodeproj').childFile('project.pbxproj');
@override @override
File get flutterPluginSwiftPackageManifest => hostAppRoot Directory get flutterSwiftPackagesDirectory =>
.childDirectory('Flutter') hostAppRoot.childDirectory('Flutter').childDirectory('ephemeral').childDirectory('Packages');
.childDirectory('ephemeral')
.childDirectory('Packages') @override
.childDirectory('FlutterGeneratedPluginSwiftPackage') Directory get relativeSwiftPackagesDirectory =>
.childFile('Package.swift'); flutterSwiftPackagesDirectory.childDirectory('.packages');
@override
Directory get flutterPluginSwiftPackageDirectory =>
flutterSwiftPackagesDirectory.childDirectory('FlutterGeneratedPluginSwiftPackage');
@override
File get flutterPluginSwiftPackageManifest =>
flutterPluginSwiftPackageDirectory.childFile('Package.swift');
@override @override
bool usesSwiftPackageManager = false; bool usesSwiftPackageManager = false;
@ -547,12 +555,20 @@ class FakeIosProject extends Fake implements IosProject {
hostAppRoot.childDirectory('Runner.xcodeproj').childFile('project.pbxproj'); hostAppRoot.childDirectory('Runner.xcodeproj').childFile('project.pbxproj');
@override @override
File get flutterPluginSwiftPackageManifest => hostAppRoot Directory get flutterSwiftPackagesDirectory =>
.childDirectory('Flutter') hostAppRoot.childDirectory('Flutter').childDirectory('ephemeral').childDirectory('Packages');
.childDirectory('ephemeral')
.childDirectory('Packages') @override
.childDirectory('FlutterGeneratedPluginSwiftPackage') Directory get relativeSwiftPackagesDirectory =>
.childFile('Package.swift'); flutterSwiftPackagesDirectory.childDirectory('.packages');
@override
Directory get flutterPluginSwiftPackageDirectory =>
flutterSwiftPackagesDirectory.childDirectory('FlutterGeneratedPluginSwiftPackage');
@override
File get flutterPluginSwiftPackageManifest =>
flutterPluginSwiftPackageDirectory.childFile('Package.swift');
@override @override
bool usesSwiftPackageManager = false; bool usesSwiftPackageManager = false;

View File

@ -4,6 +4,7 @@
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:file/memory.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/isolated/mustache_template.dart';
import 'package:flutter_tools/src/macos/swift_package_manager.dart'; import 'package:flutter_tools/src/macos/swift_package_manager.dart';
import 'package:flutter_tools/src/platform_plugins.dart'; import 'package:flutter_tools/src/platform_plugins.dart';
@ -118,13 +119,15 @@ $_doubleIndent
fileSystem: fs, fileSystem: fs,
); );
final File validPlugin1Manifest = fs.file( final Directory validPlugin1Directory = fs.directory(
'/local/path/to/plugins/valid_plugin_1/Package.swift', '/local/path/to/plugins/valid_plugin_1',
)..createSync(recursive: true); );
validPlugin1Directory.childFile('Package.swift').createSync(recursive: true);
final FakePlugin validPlugin1 = FakePlugin( final FakePlugin validPlugin1 = FakePlugin(
name: 'valid_plugin_1', name: 'valid_plugin_1',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()}, platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginSwiftPackageManifestPath: validPlugin1Manifest.path, pluginSwiftPackagePath: validPlugin1Directory.path,
); );
final SwiftPackageManager spm = SwiftPackageManager( final SwiftPackageManager spm = SwiftPackageManager(
fileSystem: fs, fileSystem: fs,
@ -135,6 +138,11 @@ $_doubleIndent
final String supportedPlatform = final String supportedPlatform =
platform == SupportedPlatform.ios ? '.iOS("13.0")' : '.macOS("10.15")'; platform == SupportedPlatform.ios ? '.iOS("13.0")' : '.macOS("10.15")';
expect(project.flutterPluginSwiftPackageManifest.existsSync(), isTrue); 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(), ''' expect(project.flutterPluginSwiftPackageManifest.readAsStringSync(), '''
// swift-tools-version: 5.9 // swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package. // 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"]) .library(name: "FlutterGeneratedPluginSwiftPackage", type: .static, targets: ["FlutterGeneratedPluginSwiftPackage"])
], ],
dependencies: [ 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: [ targets: [
.target( .target(
@ -176,34 +184,38 @@ let package = Package(
final FakePlugin nonPlatformCompatiblePlugin = FakePlugin( final FakePlugin nonPlatformCompatiblePlugin = FakePlugin(
name: 'invalid_plugin_due_to_incompatible_platform', name: 'invalid_plugin_due_to_incompatible_platform',
platforms: <String, PluginPlatform>{}, platforms: <String, PluginPlatform>{},
pluginSwiftPackageManifestPath: '/some/path', pluginSwiftPackagePath: '/some/path',
); );
final FakePlugin pluginSwiftPackageManifestIsNull = FakePlugin( final FakePlugin pluginSwiftPackageManifestIsNull = FakePlugin(
name: 'invalid_plugin_due_to_null_plugin_swift_package_path', name: 'invalid_plugin_due_to_null_plugin_swift_package_path',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()}, platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginSwiftPackageManifestPath: null, pluginSwiftPackagePath: null,
); );
final FakePlugin pluginSwiftPackageManifestNotExists = FakePlugin( final FakePlugin pluginSwiftPackageManifestNotExists = FakePlugin(
name: 'invalid_plugin_due_to_plugin_swift_package_path_does_not_exist', name: 'invalid_plugin_due_to_plugin_swift_package_path_does_not_exist',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()}, platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginSwiftPackageManifestPath: '/some/path', pluginSwiftPackagePath: '/some/path',
); );
final File validPlugin1Manifest = fs.file( final Directory validPlugin1Directory = fs.directory(
'/local/path/to/plugins/valid_plugin_1/Package.swift', '/local/path/to/plugins/valid_plugin_1',
)..createSync(recursive: true); );
validPlugin1Directory.childFile('Package.swift').createSync(recursive: true);
final FakePlugin validPlugin1 = FakePlugin( final FakePlugin validPlugin1 = FakePlugin(
name: 'valid_plugin_1', name: 'valid_plugin_1',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()}, platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginSwiftPackageManifestPath: validPlugin1Manifest.path, pluginSwiftPackagePath: validPlugin1Directory.path,
); );
final File validPlugin2Manifest = fs.file(
'/.pub-cache/plugins/valid_plugin_2/Package.swift', final Directory validPlugin2Directory = fs.directory(
)..createSync(recursive: true); '/.pub-cache/plugins/valid_plugin_2',
);
validPlugin2Directory.childFile('Package.swift').createSync(recursive: true);
final FakePlugin validPlugin2 = FakePlugin( final FakePlugin validPlugin2 = FakePlugin(
name: 'valid_plugin_2', name: 'valid_plugin_2',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()}, platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginSwiftPackageManifestPath: validPlugin2Manifest.path, pluginSwiftPackagePath: validPlugin2Directory.path,
); );
final SwiftPackageManager spm = SwiftPackageManager( final SwiftPackageManager spm = SwiftPackageManager(
@ -225,6 +237,16 @@ let package = Package(
final String supportedPlatform = final String supportedPlatform =
platform == SupportedPlatform.ios ? '.iOS("13.0")' : '.macOS("10.15")'; platform == SupportedPlatform.ios ? '.iOS("13.0")' : '.macOS("10.15")';
expect(project.flutterPluginSwiftPackageManifest.existsSync(), isTrue); 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(), ''' expect(project.flutterPluginSwiftPackageManifest.readAsStringSync(), '''
// swift-tools-version: 5.9 // swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package. // 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"]) .library(name: "FlutterGeneratedPluginSwiftPackage", type: .static, targets: ["FlutterGeneratedPluginSwiftPackage"])
], ],
dependencies: [ dependencies: [
.package(name: "valid_plugin_1", path: "/local/path/to/plugins/valid_plugin_1"), .package(name: "valid_plugin_1", path: "../.packages/valid_plugin_1"),
.package(name: "valid_plugin_2", path: "/.pub-cache/plugins/valid_plugin_2") .package(name: "valid_plugin_2", path: "../.packages/valid_plugin_2")
], ],
targets: [ targets: [
.target( .target(
@ -373,11 +395,16 @@ class FakeXcodeProject extends Fake implements IosProject {
String hostAppProjectName = 'Runner'; String hostAppProjectName = 'Runner';
@override @override
Directory get flutterPluginSwiftPackageDirectory => hostAppRoot Directory get flutterSwiftPackagesDirectory =>
.childDirectory('Flutter') hostAppRoot.childDirectory('Flutter').childDirectory('ephemeral').childDirectory('Packages');
.childDirectory('ephemeral')
.childDirectory('Packages') @override
.childDirectory('FlutterGeneratedPluginSwiftPackage'); Directory get relativeSwiftPackagesDirectory =>
flutterSwiftPackagesDirectory.childDirectory('.packages');
@override
Directory get flutterPluginSwiftPackageDirectory =>
flutterSwiftPackagesDirectory.childDirectory('FlutterGeneratedPluginSwiftPackage');
@override @override
File get flutterPluginSwiftPackageManifest => File get flutterPluginSwiftPackageManifest =>
@ -391,13 +418,10 @@ class FakeXcodeProject extends Fake implements IosProject {
} }
class FakePlugin extends Fake implements Plugin { class FakePlugin extends Fake implements Plugin {
FakePlugin({ FakePlugin({required this.name, required this.platforms, required String? pluginSwiftPackagePath})
required this.name, : _pluginSwiftPackagePath = pluginSwiftPackagePath;
required this.platforms,
required String? pluginSwiftPackageManifestPath,
}) : _pluginSwiftPackageManifestPath = pluginSwiftPackageManifestPath;
final String? _pluginSwiftPackageManifestPath; final String? _pluginSwiftPackagePath;
@override @override
final String name; final String name;
@ -405,9 +429,17 @@ class FakePlugin extends Fake implements Plugin {
@override @override
final Map<String, PluginPlatform> platforms; final Map<String, PluginPlatform> platforms;
@override
String? pluginSwiftPackagePath(FileSystem fileSystem, String platform) {
return _pluginSwiftPackagePath;
}
@override @override
String? pluginSwiftPackageManifestPath(FileSystem fileSystem, String platform) { String? pluginSwiftPackageManifestPath(FileSystem fileSystem, String platform) {
return _pluginSwiftPackageManifestPath; if (_pluginSwiftPackagePath == null) {
return null;
}
return '$_pluginSwiftPackagePath/Package.swift';
} }
} }

View File

@ -2153,7 +2153,7 @@ flutter:
}); });
group('Plugin files', () { 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 MemoryFileSystem fs = MemoryFileSystem.test();
final Plugin plugin = Plugin( final Plugin plugin = Plugin(
name: 'test', name: 'test',
@ -2168,7 +2168,11 @@ flutter:
isDirectDependency: true, isDirectDependency: true,
isDevDependency: false, 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( expect(
plugin.pluginSwiftPackageManifestPath(fs, IOSPlugin.kConfigKey), plugin.pluginSwiftPackageManifestPath(fs, IOSPlugin.kConfigKey),
'/path/to/test/ios/test/Package.swift', '/path/to/test/ios/test/Package.swift',
@ -2177,74 +2181,6 @@ flutter:
plugin.pluginSwiftPackageManifestPath(fs, MacOSPlugin.kConfigKey), plugin.pluginSwiftPackageManifestPath(fs, MacOSPlugin.kConfigKey),
'/path/to/test/macos/test/Package.swift', '/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 <String, String>{},
pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{},
platforms: const <String, PluginPlatform>{
IOSPlugin.kConfigKey: IOSPlugin(
name: 'test',
classPrefix: '',
sharedDarwinSource: true,
),
MacOSPlugin.kConfigKey: MacOSPlugin(name: 'test', sharedDarwinSource: true),
},
dependencies: <String>[],
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 <String, String>{},
pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{},
platforms: const <String, PluginPlatform>{
WindowsPlugin.kConfigKey: WindowsPlugin(name: 'test', pluginClass: ''),
},
dependencies: <String>[],
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 <String, String>{},
pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{},
platforms: const <String, PluginPlatform>{
IOSPlugin.kConfigKey: IOSPlugin(name: 'test', classPrefix: ''),
MacOSPlugin.kConfigKey: MacOSPlugin(name: 'test'),
},
dependencies: <String>[],
isDirectDependency: true,
isDevDependency: false,
);
expect( expect(
plugin.pluginPodspecPath(fs, IOSPlugin.kConfigKey), plugin.pluginPodspecPath(fs, IOSPlugin.kConfigKey),
'/path/to/test/ios/test.podspec', '/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 MemoryFileSystem fs = MemoryFileSystem.test();
final Plugin plugin = Plugin( final Plugin plugin = Plugin(
name: 'test', name: 'test',
@ -2275,6 +2211,22 @@ flutter:
isDevDependency: false, 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( expect(
plugin.pluginPodspecPath(fs, IOSPlugin.kConfigKey), plugin.pluginPodspecPath(fs, IOSPlugin.kConfigKey),
'/path/to/test/darwin/test.podspec', '/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 MemoryFileSystem fs = MemoryFileSystem.test();
final Plugin plugin = Plugin( final Plugin plugin = Plugin(
name: 'test', name: 'test',
@ -2300,6 +2252,12 @@ flutter:
isDevDependency: false, 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, IOSPlugin.kConfigKey), isNull);
expect(plugin.pluginPodspecPath(fs, MacOSPlugin.kConfigKey), isNull); expect(plugin.pluginPodspecPath(fs, MacOSPlugin.kConfigKey), isNull);
expect(plugin.pluginPodspecPath(fs, WindowsPlugin.kConfigKey), isNull); expect(plugin.pluginPodspecPath(fs, WindowsPlugin.kConfigKey), isNull);

View File

@ -51,6 +51,21 @@ void main() {
expect(project.ephemeralDirectory.path, 'app_name/.ios/Flutter/ephemeral'); 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', () { testWithoutContext('flutterPluginSwiftPackageDirectory', () {
final MemoryFileSystem fs = MemoryFileSystem.test(); final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); final IosProject project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs));
@ -440,6 +455,24 @@ void main() {
expect(project.ephemeralDirectory.path, 'app_name/macos/Flutter/ephemeral'); 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', () { testWithoutContext('flutterPluginSwiftPackageDirectory', () {
final MemoryFileSystem fs = MemoryFileSystem.test(); final MemoryFileSystem fs = MemoryFileSystem.test();
final MacOSProject project = MacOSProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); final MacOSProject project = MacOSProject.fromFlutter(FakeFlutterProject(fileSystem: fs));

View File

@ -101,6 +101,71 @@ void main() {
flutterBin, flutterBin,
workingDirectoryPath, 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( await SwiftPackageManagerUtils.buildApp(
flutterBin, flutterBin,
appDirectoryPath, appDirectoryPath,
@ -731,9 +796,9 @@ void main() {
expect(generatedManifestFile, exists); expect(generatedManifestFile, exists);
String generatedManifest = generatedManifestFile.readAsStringSync(); String generatedManifest = generatedManifestFile.readAsStringSync();
final String generatedSwiftDependency = ''' const String generatedSwiftDependency = '''
dependencies: [ 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 xcodeProject = xcodeProjectFile.readAsStringSync();
String generatedManifest = generatedManifestFile.readAsStringSync(); String generatedManifest = generatedManifestFile.readAsStringSync();
final String generatedSwiftDependency = ''' const String generatedSwiftDependency = '''
dependencies: [ dependencies: [
.package(name: "integration_test", path: "${integrationTestPlugin.swiftPackagePlatformPath}") .package(name: "integration_test", path: "../.packages/integration_test")
], ],
'''; ''';

View File

@ -211,9 +211,15 @@ class SwiftPackageManagerUtils {
pluginName: pluginName, pluginName: pluginName,
pluginPath: pluginDirectory.path, pluginPath: pluginDirectory.path,
platform: platform, 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({ static void addDependency({
required SwiftPackageManagerPlugin plugin, required SwiftPackageManagerPlugin plugin,
required String appDirectoryPath, required String appDirectoryPath,
@ -269,6 +275,7 @@ class SwiftPackageManagerUtils {
'integration_test', 'integration_test',
'integration_test_macos', 'integration_test_macos',
), ),
className: 'IntegrationTestPlugin',
); );
} }
@ -295,7 +302,7 @@ class SwiftPackageManagerUtils {
if (swiftPackageMangerEnabled) { if (swiftPackageMangerEnabled) {
expectedLines.addAll(<Pattern>[ expectedLines.addAll(<Pattern>[
RegExp( 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}'", "➜ Explicit dependency on target '${swiftPackagePlugin.pluginName}' in project '${swiftPackagePlugin.pluginName}'",
]); ]);
@ -335,6 +342,8 @@ class SwiftPackageManagerUtils {
bool migrated = false, bool migrated = false,
}) { }) {
final String frameworkName = platform == 'ios' ? 'Flutter' : 'FlutterMacOS'; final String frameworkName = platform == 'ios' ? 'Flutter' : 'FlutterMacOS';
final String appPlatformDirectoryPath = fileSystem.path.join(appDirectoryPath, platform);
final List<String> unexpectedLines = <String>[]; final List<String> unexpectedLines = <String>[];
if (cocoaPodsPlugin == null && !migrated) { if (cocoaPodsPlugin == null && !migrated) {
unexpectedLines.addAll(<String>[ unexpectedLines.addAll(<String>[
@ -351,7 +360,7 @@ class SwiftPackageManagerUtils {
]); ]);
} else { } else {
unexpectedLines.addAll(<String>[ unexpectedLines.addAll(<String>[
'${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}'", "➜ Explicit dependency on target '${swiftPackagePlugin.pluginName}' in project '${swiftPackagePlugin.pluginName}'",
]); ]);
} }
@ -368,11 +377,13 @@ class SwiftPackageManagerPlugin {
required this.pluginName, required this.pluginName,
required this.pluginPath, required this.pluginPath,
required this.platform, required this.platform,
required this.className,
}); });
final String pluginName; final String pluginName;
final String pluginPath; final String pluginPath;
final String platform; final String platform;
final String className;
String get exampleAppPath => fileSystem.path.join(pluginPath, 'example'); String get exampleAppPath => fileSystem.path.join(pluginPath, 'example');
String get exampleAppPlatformPath => fileSystem.path.join(exampleAppPath, platform); String get exampleAppPlatformPath => fileSystem.path.join(exampleAppPath, platform);
String get swiftPackagePlatformPath => fileSystem.path.join(pluginPath, platform, pluginName); String get swiftPackagePlatformPath => fileSystem.path.join(pluginPath, platform, pluginName);