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.
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<SwiftPackagePackageDependency> packageDependencies,
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
// migrated yet, don't generate a Swift package or migrate the app since
@ -100,10 +110,13 @@ class SwiftPackageManager {
pluginsPackage.createSwiftPackage();
}
(List<SwiftPackagePackageDependency>, List<SwiftPackageTargetDependency>) _dependenciesForPlugins(
List<Plugin> plugins,
SupportedPlatform platform,
) {
(List<SwiftPackagePackageDependency>, List<SwiftPackageTargetDependency>)
_dependenciesForPlugins({
required List<Plugin> plugins,
required SupportedPlatform platform,
required Directory symlinkDirectory,
required String pathRelativeTo,
}) {
final List<SwiftPackagePackageDependency> packageDependencies =
<SwiftPackagePackageDependency>[];
final List<SwiftPackageTargetDependency> targetDependencies = <SwiftPackageTargetDependency>[];
@ -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

View File

@ -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

View File

@ -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');

View File

@ -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;

View File

@ -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: <String, PluginPlatform>{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: <String, PluginPlatform>{},
pluginSwiftPackageManifestPath: '/some/path',
pluginSwiftPackagePath: '/some/path',
);
final FakePlugin pluginSwiftPackageManifestIsNull = FakePlugin(
name: 'invalid_plugin_due_to_null_plugin_swift_package_path',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginSwiftPackageManifestPath: null,
pluginSwiftPackagePath: null,
);
final FakePlugin pluginSwiftPackageManifestNotExists = FakePlugin(
name: 'invalid_plugin_due_to_plugin_swift_package_path_does_not_exist',
platforms: <String, PluginPlatform>{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: <String, PluginPlatform>{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: <String, PluginPlatform>{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<String, PluginPlatform> 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';
}
}

View File

@ -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 <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(
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);

View File

@ -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));

View File

@ -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")
],
''';

View File

@ -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(<Pattern>[
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<String> unexpectedLines = <String>[];
if (cocoaPodsPlugin == null && !migrated) {
unexpectedLines.addAll(<String>[
@ -351,7 +360,7 @@ class SwiftPackageManagerUtils {
]);
} else {
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}'",
]);
}
@ -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);