flutter/packages/flutter_tools/test/general.shard/macos/swift_packages_test.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

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")');
});
});
}