mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

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
376 lines
15 KiB
Dart
376 lines
15 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 'package:file/file.dart';
|
|
import 'package:file/memory.dart';
|
|
import 'package:flutter_tools/src/base/version.dart';
|
|
import 'package:flutter_tools/src/isolated/mustache_template.dart';
|
|
import 'package:flutter_tools/src/macos/swift_packages.dart';
|
|
|
|
import '../../src/common.dart';
|
|
|
|
const String _doubleIndent = ' ';
|
|
|
|
void main() {
|
|
group('SwiftPackage', () {
|
|
testWithoutContext('createSwiftPackage also creates source file for each default target', () {
|
|
final MemoryFileSystem fs = MemoryFileSystem();
|
|
final File swiftPackageFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Package.swift');
|
|
const String target1Name = 'Target1';
|
|
const String target2Name = 'Target2';
|
|
final File target1SourceFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Sources/$target1Name/$target1Name.swift');
|
|
final File target2SourceFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Sources/$target2Name/$target2Name.swift');
|
|
final SwiftPackage swiftPackage = SwiftPackage(
|
|
manifest: swiftPackageFile,
|
|
name: 'FlutterGeneratedPluginSwiftPackage',
|
|
platforms: <SwiftPackageSupportedPlatform>[],
|
|
products: <SwiftPackageProduct>[],
|
|
dependencies: <SwiftPackagePackageDependency>[],
|
|
targets: <SwiftPackageTarget>[
|
|
SwiftPackageTarget.defaultTarget(name: target1Name),
|
|
SwiftPackageTarget.defaultTarget(name: 'Target2'),
|
|
],
|
|
templateRenderer: const MustacheTemplateRenderer(),
|
|
);
|
|
swiftPackage.createSwiftPackage();
|
|
expect(swiftPackageFile.existsSync(), isTrue);
|
|
expect(target1SourceFile.existsSync(), isTrue);
|
|
expect(target2SourceFile.existsSync(), isTrue);
|
|
});
|
|
|
|
testWithoutContext('createSwiftPackage also creates source file for binary target', () {
|
|
final MemoryFileSystem fs = MemoryFileSystem();
|
|
final File swiftPackageFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Package.swift');
|
|
final SwiftPackage swiftPackage = SwiftPackage(
|
|
manifest: swiftPackageFile,
|
|
name: 'FlutterGeneratedPluginSwiftPackage',
|
|
platforms: <SwiftPackageSupportedPlatform>[],
|
|
products: <SwiftPackageProduct>[],
|
|
dependencies: <SwiftPackagePackageDependency>[],
|
|
targets: <SwiftPackageTarget>[
|
|
SwiftPackageTarget.binaryTarget(name: 'BinaryTarget', relativePath: ''),
|
|
],
|
|
templateRenderer: const MustacheTemplateRenderer(),
|
|
);
|
|
swiftPackage.createSwiftPackage();
|
|
expect(swiftPackageFile.existsSync(), isTrue);
|
|
expect(fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Sources/BinaryTarget/BinaryTarget.swift').existsSync(), isFalse);
|
|
});
|
|
|
|
testWithoutContext('createSwiftPackage does not creates source file if already exists', () {
|
|
final MemoryFileSystem fs = MemoryFileSystem();
|
|
final File swiftPackageFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Package.swift');
|
|
const String target1Name = 'Target1';
|
|
const String target2Name = 'Target2';
|
|
final File target1SourceFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Sources/$target1Name/$target1Name.swift');
|
|
final File target2SourceFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Sources/$target2Name/$target2Name.swift');
|
|
|
|
fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Sources/$target1Name/SomeSourceFile.swift').createSync(recursive: true);
|
|
fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Sources/$target2Name/SomeSourceFile.swift').createSync(recursive: true);
|
|
|
|
final SwiftPackage swiftPackage = SwiftPackage(
|
|
manifest: swiftPackageFile,
|
|
name: 'FlutterGeneratedPluginSwiftPackage',
|
|
platforms: <SwiftPackageSupportedPlatform>[],
|
|
products: <SwiftPackageProduct>[],
|
|
dependencies: <SwiftPackagePackageDependency>[],
|
|
targets: <SwiftPackageTarget>[
|
|
SwiftPackageTarget.defaultTarget(name: target1Name),
|
|
SwiftPackageTarget.defaultTarget(name: 'Target2'),
|
|
],
|
|
templateRenderer: const MustacheTemplateRenderer(),
|
|
);
|
|
swiftPackage.createSwiftPackage();
|
|
expect(swiftPackageFile.existsSync(), isTrue);
|
|
expect(target1SourceFile.existsSync(), isFalse);
|
|
expect(target2SourceFile.existsSync(), isFalse);
|
|
});
|
|
|
|
group('create Package.swift from template', () {
|
|
testWithoutContext('with none in each field', () {
|
|
final MemoryFileSystem fs = MemoryFileSystem();
|
|
final File swiftPackageFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Package.swift');
|
|
final SwiftPackage swiftPackage = SwiftPackage(
|
|
manifest: swiftPackageFile,
|
|
name: 'FlutterGeneratedPluginSwiftPackage',
|
|
platforms: <SwiftPackageSupportedPlatform>[],
|
|
products: <SwiftPackageProduct>[],
|
|
dependencies: <SwiftPackagePackageDependency>[],
|
|
targets: <SwiftPackageTarget>[],
|
|
templateRenderer: const MustacheTemplateRenderer(),
|
|
);
|
|
swiftPackage.createSwiftPackage();
|
|
expect(swiftPackageFile.readAsStringSync(), '''
|
|
// swift-tools-version: 5.9
|
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
|
//
|
|
// Generated file. Do not edit.
|
|
//
|
|
|
|
import PackageDescription
|
|
|
|
let package = Package(
|
|
name: "FlutterGeneratedPluginSwiftPackage",
|
|
products: [
|
|
$_doubleIndent
|
|
],
|
|
dependencies: [
|
|
$_doubleIndent
|
|
],
|
|
targets: [
|
|
$_doubleIndent
|
|
]
|
|
)
|
|
''');
|
|
});
|
|
|
|
testWithoutContext('with single in each field', () {
|
|
final MemoryFileSystem fs = MemoryFileSystem();
|
|
final File swiftPackageFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Package.swift');
|
|
final SwiftPackage swiftPackage = SwiftPackage(
|
|
manifest: swiftPackageFile,
|
|
name: 'FlutterGeneratedPluginSwiftPackage',
|
|
platforms: <SwiftPackageSupportedPlatform>[
|
|
SwiftPackageSupportedPlatform(platform: SwiftPackagePlatform.ios, version: Version(12, 0, null)),
|
|
],
|
|
products: <SwiftPackageProduct>[
|
|
SwiftPackageProduct(name: 'Product1', targets: <String>['Target1']),
|
|
],
|
|
dependencies: <SwiftPackagePackageDependency>[
|
|
SwiftPackagePackageDependency(name: 'Dependency1', path: '/path/to/dependency1'),
|
|
],
|
|
targets: <SwiftPackageTarget>[
|
|
SwiftPackageTarget.defaultTarget(
|
|
name: 'Target1',
|
|
dependencies: <SwiftPackageTargetDependency>[
|
|
SwiftPackageTargetDependency.product(name: 'TargetDependency1', packageName: 'TargetDependency1Package'),
|
|
],
|
|
)
|
|
],
|
|
templateRenderer: const MustacheTemplateRenderer(),
|
|
);
|
|
swiftPackage.createSwiftPackage();
|
|
expect(swiftPackageFile.readAsStringSync(), '''
|
|
// swift-tools-version: 5.9
|
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
|
//
|
|
// Generated file. Do not edit.
|
|
//
|
|
|
|
import PackageDescription
|
|
|
|
let package = Package(
|
|
name: "FlutterGeneratedPluginSwiftPackage",
|
|
platforms: [
|
|
.iOS("12.0")
|
|
],
|
|
products: [
|
|
.library(name: "Product1", targets: ["Target1"])
|
|
],
|
|
dependencies: [
|
|
.package(name: "Dependency1", path: "/path/to/dependency1")
|
|
],
|
|
targets: [
|
|
.target(
|
|
name: "Target1",
|
|
dependencies: [
|
|
.product(name: "TargetDependency1", package: "TargetDependency1Package")
|
|
]
|
|
)
|
|
]
|
|
)
|
|
''');
|
|
});
|
|
|
|
testWithoutContext('with multiple in each field', () {
|
|
final MemoryFileSystem fs = MemoryFileSystem();
|
|
final File swiftPackageFile = fs.systemTempDirectory.childFile('Packages/FlutterGeneratedPluginSwiftPackage/Package.swift');
|
|
final SwiftPackage swiftPackage = SwiftPackage(
|
|
manifest: swiftPackageFile,
|
|
name: 'FlutterGeneratedPluginSwiftPackage',
|
|
platforms: <SwiftPackageSupportedPlatform>[
|
|
SwiftPackageSupportedPlatform(platform: SwiftPackagePlatform.ios, version: Version(12, 0, null)),
|
|
SwiftPackageSupportedPlatform(platform: SwiftPackagePlatform.macos, version: Version(10, 14, null)),
|
|
],
|
|
products: <SwiftPackageProduct>[
|
|
SwiftPackageProduct(name: 'Product1', targets: <String>['Target1']),
|
|
SwiftPackageProduct(name: 'Product2', targets: <String>['Target2'])
|
|
],
|
|
dependencies: <SwiftPackagePackageDependency>[
|
|
SwiftPackagePackageDependency(name: 'Dependency1', path: '/path/to/dependency1'),
|
|
SwiftPackagePackageDependency(name: 'Dependency2', path: '/path/to/dependency2'),
|
|
],
|
|
targets: <SwiftPackageTarget>[
|
|
SwiftPackageTarget.binaryTarget(name: 'Target1', relativePath: '/path/to/target1'),
|
|
SwiftPackageTarget.defaultTarget(
|
|
name: 'Target2',
|
|
dependencies: <SwiftPackageTargetDependency>[
|
|
SwiftPackageTargetDependency.target(name: 'TargetDependency1'),
|
|
SwiftPackageTargetDependency.product(name: 'TargetDependency2', packageName: 'TargetDependency2Package'),
|
|
],
|
|
)
|
|
],
|
|
templateRenderer: const MustacheTemplateRenderer(),
|
|
);
|
|
swiftPackage.createSwiftPackage();
|
|
expect(swiftPackageFile.readAsStringSync(), '''
|
|
// swift-tools-version: 5.9
|
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
|
//
|
|
// Generated file. Do not edit.
|
|
//
|
|
|
|
import PackageDescription
|
|
|
|
let package = Package(
|
|
name: "FlutterGeneratedPluginSwiftPackage",
|
|
platforms: [
|
|
.iOS("12.0"),
|
|
.macOS("10.14")
|
|
],
|
|
products: [
|
|
.library(name: "Product1", targets: ["Target1"]),
|
|
.library(name: "Product2", targets: ["Target2"])
|
|
],
|
|
dependencies: [
|
|
.package(name: "Dependency1", path: "/path/to/dependency1"),
|
|
.package(name: "Dependency2", path: "/path/to/dependency2")
|
|
],
|
|
targets: [
|
|
.binaryTarget(
|
|
name: "Target1",
|
|
path: "/path/to/target1"
|
|
),
|
|
.target(
|
|
name: "Target2",
|
|
dependencies: [
|
|
.target(name: "TargetDependency1"),
|
|
.product(name: "TargetDependency2", package: "TargetDependency2Package")
|
|
]
|
|
)
|
|
]
|
|
)
|
|
''');
|
|
});
|
|
});
|
|
});
|
|
|
|
testWithoutContext('Format SwiftPackageSupportedPlatform', () {
|
|
final SwiftPackageSupportedPlatform supportedPlatform = SwiftPackageSupportedPlatform(
|
|
platform: SwiftPackagePlatform.ios,
|
|
version: Version(17, 0, null),
|
|
);
|
|
expect(supportedPlatform.format(), '.iOS("17.0")');
|
|
});
|
|
|
|
group('Format SwiftPackageProduct', () {
|
|
testWithoutContext('without targets and libraryType', () {
|
|
final SwiftPackageProduct product = SwiftPackageProduct(
|
|
name: 'ProductName',
|
|
targets: <String>[],
|
|
);
|
|
expect(product.format(), '.library(name: "ProductName")');
|
|
});
|
|
|
|
testWithoutContext('with targets', () {
|
|
final SwiftPackageProduct singleProduct = SwiftPackageProduct(
|
|
name: 'ProductName',
|
|
targets: <String>['Target1'],
|
|
);
|
|
expect(singleProduct.format(), '.library(name: "ProductName", targets: ["Target1"])');
|
|
|
|
final SwiftPackageProduct multipleProducts = SwiftPackageProduct(
|
|
name: 'ProductName',
|
|
targets: <String>['Target1', 'Target2'],
|
|
);
|
|
expect(multipleProducts.format(), '.library(name: "ProductName", targets: ["Target1", "Target2"])');
|
|
});
|
|
|
|
testWithoutContext('with libraryType', () {
|
|
final SwiftPackageProduct product = SwiftPackageProduct(
|
|
name: 'ProductName',
|
|
targets: <String>[],
|
|
libraryType: SwiftPackageLibraryType.dynamic,
|
|
);
|
|
expect(product.format(), '.library(name: "ProductName", type: .dynamic)');
|
|
});
|
|
|
|
testWithoutContext('with targets and libraryType', () {
|
|
final SwiftPackageProduct product = SwiftPackageProduct(
|
|
name: 'ProductName',
|
|
targets: <String>['Target1', 'Target2'],
|
|
libraryType: SwiftPackageLibraryType.dynamic,
|
|
);
|
|
expect(product.format(), '.library(name: "ProductName", type: .dynamic, targets: ["Target1", "Target2"])');
|
|
});
|
|
});
|
|
|
|
testWithoutContext('Format SwiftPackagePackageDependency', () {
|
|
final SwiftPackagePackageDependency supportedPlatform = SwiftPackagePackageDependency(
|
|
name: 'DependencyName',
|
|
path: '/path/to/dependency',
|
|
);
|
|
expect(supportedPlatform.format(), '.package(name: "DependencyName", path: "/path/to/dependency")');
|
|
});
|
|
|
|
group('Format SwiftPackageTarget', () {
|
|
testWithoutContext('as default target with multiple SwiftPackageTargetDependency', () {
|
|
final SwiftPackageTarget product = SwiftPackageTarget.defaultTarget(
|
|
name: 'ProductName',
|
|
dependencies: <SwiftPackageTargetDependency>[
|
|
SwiftPackageTargetDependency.target(name: 'Dependency1'),
|
|
SwiftPackageTargetDependency.product(name: 'Dependency2', packageName: 'Dependency2Package'),
|
|
],
|
|
);
|
|
expect(product.format(), '''
|
|
.target(
|
|
name: "ProductName",
|
|
dependencies: [
|
|
.target(name: "Dependency1"),
|
|
.product(name: "Dependency2", package: "Dependency2Package")
|
|
]
|
|
)''');
|
|
});
|
|
|
|
testWithoutContext('as default target with no SwiftPackageTargetDependency', () {
|
|
final SwiftPackageTarget product = SwiftPackageTarget.defaultTarget(
|
|
name: 'ProductName',
|
|
);
|
|
expect(product.format(), '''
|
|
.target(
|
|
name: "ProductName"
|
|
)''');
|
|
});
|
|
|
|
testWithoutContext('as binaryTarget', () {
|
|
final SwiftPackageTarget product = SwiftPackageTarget.binaryTarget(
|
|
name: 'ProductName',
|
|
relativePath: '/path/to/target',
|
|
);
|
|
expect(product.format(), '''
|
|
.binaryTarget(
|
|
name: "ProductName",
|
|
path: "/path/to/target"
|
|
)''');
|
|
});
|
|
});
|
|
|
|
group('Format SwiftPackageTargetDependency', () {
|
|
testWithoutContext('with only name', () {
|
|
final SwiftPackageTargetDependency targetDependency = SwiftPackageTargetDependency.target(
|
|
name: 'DependencyName',
|
|
);
|
|
expect(targetDependency.format(), ' .target(name: "DependencyName")');
|
|
});
|
|
|
|
testWithoutContext('with name and package', () {
|
|
final SwiftPackageTargetDependency targetDependency = SwiftPackageTargetDependency.product(
|
|
name: 'DependencyName',
|
|
packageName: 'PackageName',
|
|
);
|
|
expect(targetDependency.format(), ' .product(name: "DependencyName", package: "PackageName")');
|
|
});
|
|
});
|
|
}
|