flutter/packages/flutter_tools/lib/src/macos/swift_package_manager.dart
Victoria Ashworth 6d19fa3bfa
Add Swift Package Manager as new opt-in feature for iOS and macOS (#146256)
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
2024-04-18 21:12:36 +00:00

197 lines
7.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 isnt compatible
/// with the top-level packages deployment version. The deployment target of
/// a packages dependencies must be lower than or equal to the top-level
/// packages 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),
);
}
}