
This PR adds initial support for Swift Package Manager (SPM). Users must opt in. Only compatible with Xcode 15+. Fixes https://github.com/flutter/flutter/issues/146369. ## Included Features This PR includes the following features: * Enabling SPM via config `flutter config --enable-swift-package-manager` * Disabling SPM via config (will disable for all projects) `flutter config --no-enable-swift-package-manager` * Disabling SPM via pubspec.yaml (will disable for the specific project) ``` flutter: disable-swift-package-manager: true ``` * Migrating existing apps to add SPM integration if using a Flutter plugin with a Package.swift * Generates a Swift Package (named `FlutterGeneratedPluginSwiftPackage`) that handles Flutter SPM-compatible plugin dependencies. Generated package is added to the Xcode project. * Error parsing of common errors that may occur due to using CocoaPods and Swift Package Manager together * Tool will print warnings when using all Swift Package plugins and encourage you to remove CocoaPods This PR also converts `integration_test` and `integration_test_macos` plugins to be both Swift Packages and CocoaPod Pods. ## How it Works The Flutter CLI will generate a Swift Package called `FlutterGeneratedPluginSwiftPackage`, which will have local dependencies on all Swift Package compatible Flutter plugins. The `FlutterGeneratedPluginSwiftPackage` package will be added to the Xcode project via altering of the `project.pbxproj`. In addition, a "Pre-action" script will be added via altering of the `Runner.xcscheme`. This script will invoke the flutter tool to copy the Flutter/FlutterMacOS framework to the `BUILT_PRODUCTS_DIR` directory before the build starts. This is needed because plugins need to be linked to the Flutter framework and fortunately Swift Package Manager automatically uses `BUILT_PRODUCTS_DIR` as a framework search path. CocoaPods will continue to run and be used to support non-Swift Package compatible Flutter plugins. ## Not Included Features It does not include the following (will be added in future PRs): * Create plugin template * Create app template * Add-to-App integration
197 lines
7.8 KiB
Dart
197 lines
7.8 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||
// Use of this source code is governed by a BSD-style license that can be
|
||
// found in the LICENSE file.
|
||
|
||
import '../base/common.dart';
|
||
import '../base/file_system.dart';
|
||
import '../base/template.dart';
|
||
import '../base/version.dart';
|
||
import '../plugins.dart';
|
||
import '../project.dart';
|
||
import 'swift_packages.dart';
|
||
|
||
/// Swift Package Manager is a dependency management solution for iOS and macOS
|
||
/// applications.
|
||
///
|
||
/// See also:
|
||
/// * https://www.swift.org/documentation/package-manager/ - documentation on
|
||
/// Swift Package Manager.
|
||
/// * https://developer.apple.com/documentation/packagedescription/package -
|
||
/// documentation on Swift Package Manager manifest file, Package.swift.
|
||
class SwiftPackageManager {
|
||
const SwiftPackageManager({
|
||
required FileSystem fileSystem,
|
||
required TemplateRenderer templateRenderer,
|
||
}) : _fileSystem = fileSystem,
|
||
_templateRenderer = templateRenderer;
|
||
|
||
final FileSystem _fileSystem;
|
||
final TemplateRenderer _templateRenderer;
|
||
|
||
static const String _defaultFlutterPluginsSwiftPackageName = 'FlutterGeneratedPluginSwiftPackage';
|
||
|
||
static final SwiftPackageSupportedPlatform _iosSwiftPackageSupportedPlatform = SwiftPackageSupportedPlatform(
|
||
platform: SwiftPackagePlatform.ios,
|
||
version: Version(12, 0, null),
|
||
);
|
||
|
||
static final SwiftPackageSupportedPlatform _macosSwiftPackageSupportedPlatform = SwiftPackageSupportedPlatform(
|
||
platform: SwiftPackagePlatform.macos,
|
||
version: Version(10, 14, null),
|
||
);
|
||
|
||
/// Creates a Swift Package called 'FlutterGeneratedPluginSwiftPackage' that
|
||
/// has dependencies on Flutter plugins that are compatible with Swift
|
||
/// Package Manager.
|
||
Future<void> generatePluginsSwiftPackage(
|
||
List<Plugin> plugins,
|
||
SupportedPlatform platform,
|
||
XcodeBasedProject project,
|
||
) async {
|
||
_validatePlatform(platform);
|
||
|
||
final (
|
||
List<SwiftPackagePackageDependency> packageDependencies,
|
||
List<SwiftPackageTargetDependency> targetDependencies
|
||
) = _dependenciesForPlugins(plugins, platform);
|
||
|
||
// If there aren't any Swift Package plugins and the project hasn't been
|
||
// migrated yet, don't generate a Swift package or migrate the app since
|
||
// it's not needed. If the project has already been migrated, regenerate
|
||
// the Package.swift even if there are no dependencies in case there
|
||
// were dependencies previously.
|
||
if (packageDependencies.isEmpty && !project.flutterPluginSwiftPackageInProjectSettings) {
|
||
return;
|
||
}
|
||
|
||
final SwiftPackageSupportedPlatform swiftSupportedPlatform;
|
||
if (platform == SupportedPlatform.ios) {
|
||
swiftSupportedPlatform = _iosSwiftPackageSupportedPlatform;
|
||
} else {
|
||
swiftSupportedPlatform = _macosSwiftPackageSupportedPlatform;
|
||
}
|
||
|
||
// FlutterGeneratedPluginSwiftPackage must be statically linked to ensure
|
||
// any dynamic dependencies are linked to Runner and prevent undefined symbols.
|
||
final SwiftPackageProduct generatedProduct = SwiftPackageProduct(
|
||
name: _defaultFlutterPluginsSwiftPackageName,
|
||
targets: <String>[_defaultFlutterPluginsSwiftPackageName],
|
||
libraryType: SwiftPackageLibraryType.static,
|
||
);
|
||
|
||
final SwiftPackageTarget generatedTarget = SwiftPackageTarget.defaultTarget(
|
||
name: _defaultFlutterPluginsSwiftPackageName,
|
||
dependencies: targetDependencies,
|
||
);
|
||
|
||
final SwiftPackage pluginsPackage = SwiftPackage(
|
||
manifest: project.flutterPluginSwiftPackageManifest,
|
||
name: _defaultFlutterPluginsSwiftPackageName,
|
||
platforms: <SwiftPackageSupportedPlatform>[swiftSupportedPlatform],
|
||
products: <SwiftPackageProduct>[generatedProduct],
|
||
dependencies: packageDependencies,
|
||
targets: <SwiftPackageTarget>[generatedTarget],
|
||
templateRenderer: _templateRenderer,
|
||
);
|
||
pluginsPackage.createSwiftPackage();
|
||
}
|
||
|
||
(List<SwiftPackagePackageDependency>, List<SwiftPackageTargetDependency>) _dependenciesForPlugins(
|
||
List<Plugin> plugins,
|
||
SupportedPlatform platform,
|
||
) {
|
||
final List<SwiftPackagePackageDependency> packageDependencies =
|
||
<SwiftPackagePackageDependency>[];
|
||
final List<SwiftPackageTargetDependency> targetDependencies =
|
||
<SwiftPackageTargetDependency>[];
|
||
|
||
for (final Plugin plugin in plugins) {
|
||
final String? pluginSwiftPackageManifestPath = plugin.pluginSwiftPackageManifestPath(
|
||
_fileSystem,
|
||
platform.name,
|
||
);
|
||
if (plugin.platforms[platform.name] == null ||
|
||
pluginSwiftPackageManifestPath == null ||
|
||
!_fileSystem.file(pluginSwiftPackageManifestPath).existsSync()) {
|
||
continue;
|
||
}
|
||
|
||
packageDependencies.add(SwiftPackagePackageDependency(
|
||
name: plugin.name,
|
||
path: _fileSystem.file(pluginSwiftPackageManifestPath).parent.path,
|
||
));
|
||
|
||
// The target dependency product name is hyphen separated because it's
|
||
// the dependency's library name, which Swift Package Manager will
|
||
// automatically use as the CFBundleIdentifier if linked dynamically. The
|
||
// CFBundleIdentifier cannot contain underscores.
|
||
targetDependencies.add(SwiftPackageTargetDependency.product(
|
||
name: plugin.name.replaceAll('_', '-'),
|
||
packageName: plugin.name,
|
||
));
|
||
}
|
||
return (packageDependencies, targetDependencies);
|
||
}
|
||
|
||
/// Validates the platform is either iOS or macOS, otherwise throw an error.
|
||
static void _validatePlatform(SupportedPlatform platform) {
|
||
if (platform != SupportedPlatform.ios &&
|
||
platform != SupportedPlatform.macos) {
|
||
throwToolExit(
|
||
'The platform ${platform.name} is not compatible with Swift Package Manager. '
|
||
'Only iOS and macOS are allowed.',
|
||
);
|
||
}
|
||
}
|
||
|
||
/// If the project's IPHONEOS_DEPLOYMENT_TARGET/MACOSX_DEPLOYMENT_TARGET is
|
||
/// higher than the FlutterGeneratedPluginSwiftPackage's default
|
||
/// SupportedPlatform, increase the SupportedPlatform to match the project's
|
||
/// deployment target.
|
||
///
|
||
/// This is done for the use case of a plugin requiring a higher iOS/macOS
|
||
/// version than FlutterGeneratedPluginSwiftPackage.
|
||
///
|
||
/// Swift Package Manager emits an error if a dependency isn’t compatible
|
||
/// with the top-level package’s deployment version. The deployment target of
|
||
/// a package’s dependencies must be lower than or equal to the top-level
|
||
/// package’s deployment target version for a particular platform.
|
||
///
|
||
/// To still be able to use the plugin, the user can increase the Xcode
|
||
/// project's iOS/macOS deployment target and this will then increase the
|
||
/// deployment target for FlutterGeneratedPluginSwiftPackage.
|
||
static void updateMinimumDeployment({
|
||
required XcodeBasedProject project,
|
||
required SupportedPlatform platform,
|
||
required String deploymentTarget,
|
||
}) {
|
||
final Version? projectDeploymentTargetVersion = Version.parse(deploymentTarget);
|
||
final SwiftPackageSupportedPlatform defaultPlatform;
|
||
final SwiftPackagePlatform packagePlatform;
|
||
if (platform == SupportedPlatform.ios) {
|
||
defaultPlatform = _iosSwiftPackageSupportedPlatform;
|
||
packagePlatform = SwiftPackagePlatform.ios;
|
||
} else {
|
||
defaultPlatform = _macosSwiftPackageSupportedPlatform;
|
||
packagePlatform = SwiftPackagePlatform.macos;
|
||
}
|
||
|
||
if (projectDeploymentTargetVersion == null ||
|
||
projectDeploymentTargetVersion <= defaultPlatform.version ||
|
||
!project.flutterPluginSwiftPackageManifest.existsSync()) {
|
||
return;
|
||
}
|
||
|
||
final String manifestContents = project.flutterPluginSwiftPackageManifest.readAsStringSync();
|
||
final String oldSupportedPlatform = defaultPlatform.format();
|
||
final String newSupportedPlatform = SwiftPackageSupportedPlatform(
|
||
platform: packagePlatform,
|
||
version: projectDeploymentTargetVersion,
|
||
).format();
|
||
|
||
project.flutterPluginSwiftPackageManifest.writeAsStringSync(
|
||
manifestContents.replaceFirst(oldSupportedPlatform, newSupportedPlatform),
|
||
);
|
||
}
|
||
}
|