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
556 lines
18 KiB
Dart
556 lines
18 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/logger.dart';
|
|
import 'package:flutter_tools/src/build_info.dart';
|
|
import 'package:flutter_tools/src/flutter_manifest.dart';
|
|
import 'package:flutter_tools/src/macos/cocoapod_utils.dart';
|
|
import 'package:flutter_tools/src/macos/cocoapods.dart';
|
|
import 'package:flutter_tools/src/project.dart';
|
|
import 'package:test/fake.dart';
|
|
|
|
import '../../src/common.dart';
|
|
import '../../src/context.dart';
|
|
|
|
void main() {
|
|
group('processPodsIfNeeded', () {
|
|
late MemoryFileSystem fs;
|
|
late FakeCocoaPods cocoaPods;
|
|
late BufferLogger logger;
|
|
|
|
// Adds basic properties to the flutterProject and its subprojects.
|
|
void setUpProject(FakeFlutterProject flutterProject, MemoryFileSystem fileSystem) {
|
|
flutterProject
|
|
..manifest = FakeFlutterManifest()
|
|
..directory = fileSystem.systemTempDirectory.childDirectory('app')
|
|
..flutterPluginsFile = flutterProject.directory.childFile('.flutter-plugins')
|
|
..flutterPluginsDependenciesFile = flutterProject.directory.childFile('.flutter-plugins-dependencies')
|
|
..ios = FakeIosProject(fileSystem: fileSystem, parent: flutterProject)
|
|
..macos = FakeMacOSProject(fileSystem: fileSystem, parent: flutterProject)
|
|
..android = FakeAndroidProject()
|
|
..web = FakeWebProject()
|
|
..windows = FakeWindowsProject()
|
|
..linux = FakeLinuxProject();
|
|
flutterProject.directory.childFile('.packages').createSync(recursive: true);
|
|
}
|
|
|
|
setUp(() async {
|
|
fs = MemoryFileSystem.test();
|
|
cocoaPods = FakeCocoaPods();
|
|
logger = BufferLogger.test();
|
|
});
|
|
|
|
void createFakePlugins(
|
|
FlutterProject flutterProject,
|
|
FileSystem fileSystem,
|
|
List<String> pluginNames,
|
|
) {
|
|
const String pluginYamlTemplate = '''
|
|
flutter:
|
|
plugin:
|
|
platforms:
|
|
ios:
|
|
pluginClass: PLUGIN_CLASS
|
|
macos:
|
|
pluginClass: PLUGIN_CLASS
|
|
''';
|
|
|
|
final Directory fakePubCache = fileSystem.systemTempDirectory.childDirectory('cache');
|
|
final File packagesFile = flutterProject.directory.childFile('.packages')
|
|
..createSync(recursive: true);
|
|
for (final String name in pluginNames) {
|
|
final Directory pluginDirectory = fakePubCache.childDirectory(name);
|
|
packagesFile.writeAsStringSync(
|
|
'$name:${pluginDirectory.childFile('lib').uri}\n',
|
|
mode: FileMode.writeOnlyAppend);
|
|
pluginDirectory.childFile('pubspec.yaml')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync(pluginYamlTemplate.replaceAll('PLUGIN_CLASS', name));
|
|
}
|
|
}
|
|
|
|
group('for iOS', () {
|
|
group('using CocoaPods only', () {
|
|
testUsingContext('processes when there are plugins', () async {
|
|
final FakeFlutterProject flutterProject = FakeFlutterProject();
|
|
setUpProject(flutterProject, fs);
|
|
createFakePlugins(flutterProject, fs, <String>[
|
|
'plugin_one',
|
|
'plugin_two'
|
|
]);
|
|
|
|
await processPodsIfNeeded(
|
|
flutterProject.ios,
|
|
fs.currentDirectory.childDirectory('build').path,
|
|
BuildMode.debug,
|
|
);
|
|
expect(cocoaPods.processedPods, isTrue);
|
|
}, overrides: <Type, Generator>{
|
|
FileSystem: () => fs,
|
|
ProcessManager: () => FakeProcessManager.any(),
|
|
CocoaPods: () => cocoaPods,
|
|
});
|
|
|
|
testUsingContext('processes when no plugins but the project is a module and podfile exists', () async {
|
|
final FakeFlutterProject flutterProject = FakeFlutterProject();
|
|
setUpProject(flutterProject, fs);
|
|
flutterProject.isModule = true;
|
|
flutterProject.ios.podfile.createSync(recursive: true);
|
|
|
|
await processPodsIfNeeded(
|
|
flutterProject.ios,
|
|
fs.currentDirectory.childDirectory('build').path,
|
|
BuildMode.debug,
|
|
);
|
|
expect(cocoaPods.processedPods, isTrue);
|
|
}, overrides: <Type, Generator>{
|
|
FileSystem: () => fs,
|
|
ProcessManager: () => FakeProcessManager.any(),
|
|
CocoaPods: () => cocoaPods,
|
|
});
|
|
|
|
testUsingContext("skips when no plugins and the project is a module but podfile doesn't exist", () async {
|
|
final FakeFlutterProject flutterProject = FakeFlutterProject();
|
|
setUpProject(flutterProject, fs);
|
|
flutterProject.isModule = true;
|
|
|
|
await processPodsIfNeeded(
|
|
flutterProject.ios,
|
|
fs.currentDirectory.childDirectory('build').path,
|
|
BuildMode.debug,
|
|
);
|
|
expect(cocoaPods.processedPods, isFalse);
|
|
}, overrides: <Type, Generator>{
|
|
FileSystem: () => fs,
|
|
ProcessManager: () => FakeProcessManager.any(),
|
|
CocoaPods: () => cocoaPods,
|
|
});
|
|
|
|
testUsingContext('skips when no plugins and project is not a module', () async {
|
|
final FakeFlutterProject flutterProject = FakeFlutterProject();
|
|
setUpProject(flutterProject, fs);
|
|
|
|
await processPodsIfNeeded(
|
|
flutterProject.ios,
|
|
fs.currentDirectory.childDirectory('build').path,
|
|
BuildMode.debug,
|
|
);
|
|
expect(cocoaPods.processedPods, isFalse);
|
|
}, overrides: <Type, Generator>{
|
|
FileSystem: () => fs,
|
|
ProcessManager: () => FakeProcessManager.any(),
|
|
CocoaPods: () => cocoaPods,
|
|
});
|
|
});
|
|
|
|
group('using Swift Package Manager', () {
|
|
testUsingContext('processes if podfile exists', () async {
|
|
final FakeFlutterProject flutterProject = FakeFlutterProject();
|
|
setUpProject(flutterProject, fs);
|
|
createFakePlugins(flutterProject, fs, <String>[
|
|
'plugin_one',
|
|
'plugin_two'
|
|
]);
|
|
flutterProject.usesSwiftPackageManager = true;
|
|
flutterProject.ios.podfile.createSync(recursive: true);
|
|
|
|
await processPodsIfNeeded(
|
|
flutterProject.ios,
|
|
fs.currentDirectory.childDirectory('build').path,
|
|
BuildMode.debug,
|
|
);
|
|
expect(cocoaPods.processedPods, isTrue);
|
|
}, overrides: <Type, Generator>{
|
|
FileSystem: () => fs,
|
|
ProcessManager: () => FakeProcessManager.any(),
|
|
CocoaPods: () => cocoaPods,
|
|
});
|
|
|
|
testUsingContext('skip if podfile does not exists', () async {
|
|
final FakeFlutterProject flutterProject = FakeFlutterProject();
|
|
setUpProject(flutterProject, fs);
|
|
createFakePlugins(flutterProject, fs, <String>[
|
|
'plugin_one',
|
|
'plugin_two'
|
|
]);
|
|
flutterProject.usesSwiftPackageManager = true;
|
|
|
|
await processPodsIfNeeded(
|
|
flutterProject.ios,
|
|
fs.currentDirectory.childDirectory('build').path,
|
|
BuildMode.debug,
|
|
);
|
|
expect(cocoaPods.processedPods, isFalse);
|
|
}, overrides: <Type, Generator>{
|
|
FileSystem: () => fs,
|
|
ProcessManager: () => FakeProcessManager.any(),
|
|
CocoaPods: () => cocoaPods,
|
|
});
|
|
|
|
testUsingContext('process if podfile does not exists but forceCocoaPodsOnly is true', () async {
|
|
final FakeFlutterProject flutterProject = FakeFlutterProject();
|
|
setUpProject(flutterProject, fs);
|
|
createFakePlugins(flutterProject, fs, <String>[
|
|
'plugin_one',
|
|
'plugin_two'
|
|
]);
|
|
flutterProject.usesSwiftPackageManager = true;
|
|
flutterProject.ios.flutterPluginSwiftPackageManifest.createSync(recursive: true);
|
|
|
|
await processPodsIfNeeded(
|
|
flutterProject.ios,
|
|
fs.currentDirectory.childDirectory('build').path,
|
|
BuildMode.debug,
|
|
forceCocoaPodsOnly: true,
|
|
);
|
|
expect(cocoaPods.processedPods, isTrue);
|
|
expect(cocoaPods.podfileSetup, isTrue);
|
|
expect(
|
|
logger.warningText,
|
|
'Swift Package Manager does not yet support this command. '
|
|
'CocoaPods will be used instead.\n');
|
|
expect(
|
|
flutterProject.ios.flutterPluginSwiftPackageManifest.existsSync(),
|
|
isFalse,
|
|
);
|
|
}, overrides: <Type, Generator>{
|
|
FileSystem: () => fs,
|
|
ProcessManager: () => FakeProcessManager.any(),
|
|
CocoaPods: () => cocoaPods,
|
|
Logger: () => logger,
|
|
});
|
|
});
|
|
});
|
|
|
|
group('for macOS', () {
|
|
group('using CocoaPods only', () {
|
|
testUsingContext('processes when there are plugins', () async {
|
|
final FakeFlutterProject flutterProject = FakeFlutterProject();
|
|
setUpProject(flutterProject, fs);
|
|
createFakePlugins(flutterProject, fs, <String>[
|
|
'plugin_one',
|
|
'plugin_two'
|
|
]);
|
|
|
|
await processPodsIfNeeded(
|
|
flutterProject.macos,
|
|
fs.currentDirectory.childDirectory('build').path,
|
|
BuildMode.debug,
|
|
);
|
|
expect(cocoaPods.processedPods, isTrue);
|
|
}, overrides: <Type, Generator>{
|
|
FileSystem: () => fs,
|
|
ProcessManager: () => FakeProcessManager.any(),
|
|
CocoaPods: () => cocoaPods,
|
|
});
|
|
|
|
testUsingContext('processes when no plugins but the project is a module and podfile exists', () async {
|
|
final FakeFlutterProject flutterProject = FakeFlutterProject();
|
|
setUpProject(flutterProject, fs);
|
|
flutterProject.isModule = true;
|
|
flutterProject.macos.podfile.createSync(recursive: true);
|
|
|
|
await processPodsIfNeeded(
|
|
flutterProject.macos,
|
|
fs.currentDirectory.childDirectory('build').path,
|
|
BuildMode.debug,
|
|
);
|
|
expect(cocoaPods.processedPods, isTrue);
|
|
}, overrides: <Type, Generator>{
|
|
FileSystem: () => fs,
|
|
ProcessManager: () => FakeProcessManager.any(),
|
|
CocoaPods: () => cocoaPods,
|
|
});
|
|
|
|
testUsingContext("skips when no plugins and the project is a module but podfile doesn't exist", () async {
|
|
final FakeFlutterProject flutterProject = FakeFlutterProject();
|
|
setUpProject(flutterProject, fs);
|
|
flutterProject.isModule = true;
|
|
|
|
await processPodsIfNeeded(
|
|
flutterProject.macos,
|
|
fs.currentDirectory.childDirectory('build').path,
|
|
BuildMode.debug,
|
|
);
|
|
expect(cocoaPods.processedPods, isFalse);
|
|
}, overrides: <Type, Generator>{
|
|
FileSystem: () => fs,
|
|
ProcessManager: () => FakeProcessManager.any(),
|
|
CocoaPods: () => cocoaPods,
|
|
});
|
|
|
|
testUsingContext('skips when no plugins and project is not a module', () async {
|
|
final FakeFlutterProject flutterProject = FakeFlutterProject();
|
|
setUpProject(flutterProject, fs);
|
|
|
|
await processPodsIfNeeded(
|
|
flutterProject.macos,
|
|
fs.currentDirectory.childDirectory('build').path,
|
|
BuildMode.debug,
|
|
);
|
|
expect(cocoaPods.processedPods, isFalse);
|
|
}, overrides: <Type, Generator>{
|
|
FileSystem: () => fs,
|
|
ProcessManager: () => FakeProcessManager.any(),
|
|
CocoaPods: () => cocoaPods,
|
|
});
|
|
});
|
|
|
|
group('using Swift Package Manager', () {
|
|
testUsingContext('processes if podfile exists', () async {
|
|
final FakeFlutterProject flutterProject = FakeFlutterProject();
|
|
setUpProject(flutterProject, fs);
|
|
createFakePlugins(flutterProject, fs, <String>[
|
|
'plugin_one',
|
|
'plugin_two'
|
|
]);
|
|
flutterProject.usesSwiftPackageManager = true;
|
|
flutterProject.macos.podfile.createSync(recursive: true);
|
|
|
|
await processPodsIfNeeded(
|
|
flutterProject.macos,
|
|
fs.currentDirectory.childDirectory('build').path,
|
|
BuildMode.debug,
|
|
);
|
|
expect(cocoaPods.processedPods, isTrue);
|
|
}, overrides: <Type, Generator>{
|
|
FileSystem: () => fs,
|
|
ProcessManager: () => FakeProcessManager.any(),
|
|
CocoaPods: () => cocoaPods,
|
|
});
|
|
|
|
testUsingContext('skip if podfile does not exists', () async {
|
|
final FakeFlutterProject flutterProject = FakeFlutterProject();
|
|
setUpProject(flutterProject, fs);
|
|
createFakePlugins(flutterProject, fs, <String>[
|
|
'plugin_one',
|
|
'plugin_two'
|
|
]);
|
|
flutterProject.usesSwiftPackageManager = true;
|
|
|
|
await processPodsIfNeeded(
|
|
flutterProject.macos,
|
|
fs.currentDirectory.childDirectory('build').path,
|
|
BuildMode.debug,
|
|
);
|
|
expect(cocoaPods.processedPods, isFalse);
|
|
}, overrides: <Type, Generator>{
|
|
FileSystem: () => fs,
|
|
ProcessManager: () => FakeProcessManager.any(),
|
|
CocoaPods: () => cocoaPods,
|
|
});
|
|
|
|
testUsingContext('process if podfile does not exists but forceCocoaPodsOnly is true', () async {
|
|
final FakeFlutterProject flutterProject = FakeFlutterProject();
|
|
setUpProject(flutterProject, fs);
|
|
createFakePlugins(flutterProject, fs, <String>[
|
|
'plugin_one',
|
|
'plugin_two'
|
|
]);
|
|
flutterProject.usesSwiftPackageManager = true;
|
|
flutterProject.macos.flutterPluginSwiftPackageManifest.createSync(recursive: true);
|
|
|
|
await processPodsIfNeeded(
|
|
flutterProject.macos,
|
|
fs.currentDirectory.childDirectory('build').path,
|
|
BuildMode.debug,
|
|
forceCocoaPodsOnly: true,
|
|
);
|
|
expect(cocoaPods.processedPods, isTrue);
|
|
expect(cocoaPods.podfileSetup, isTrue);
|
|
expect(
|
|
logger.warningText,
|
|
'Swift Package Manager does not yet support this command. '
|
|
'CocoaPods will be used instead.\n');
|
|
expect(
|
|
flutterProject.macos.flutterPluginSwiftPackageManifest.existsSync(),
|
|
isFalse,
|
|
);
|
|
}, overrides: <Type, Generator>{
|
|
FileSystem: () => fs,
|
|
ProcessManager: () => FakeProcessManager.any(),
|
|
CocoaPods: () => cocoaPods,
|
|
Logger: () => logger,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
class FakeFlutterManifest extends Fake implements FlutterManifest {
|
|
@override
|
|
Set<String> get dependencies => <String>{};
|
|
}
|
|
|
|
class FakeFlutterProject extends Fake implements FlutterProject {
|
|
@override
|
|
bool isModule = false;
|
|
|
|
@override
|
|
bool usesSwiftPackageManager = false;
|
|
|
|
@override
|
|
late FlutterManifest manifest;
|
|
|
|
@override
|
|
late Directory directory;
|
|
|
|
@override
|
|
late File flutterPluginsFile;
|
|
|
|
@override
|
|
late File flutterPluginsDependenciesFile;
|
|
|
|
@override
|
|
late IosProject ios;
|
|
|
|
@override
|
|
late MacOSProject macos;
|
|
|
|
@override
|
|
late AndroidProject android;
|
|
|
|
@override
|
|
late WebProject web;
|
|
|
|
@override
|
|
late LinuxProject linux;
|
|
|
|
@override
|
|
late WindowsProject windows;
|
|
}
|
|
|
|
class FakeMacOSProject extends Fake implements MacOSProject {
|
|
FakeMacOSProject({
|
|
required MemoryFileSystem fileSystem,
|
|
required this.parent,
|
|
}) : hostAppRoot = fileSystem.directory('app_name').childDirectory('ios');
|
|
|
|
@override
|
|
String pluginConfigKey = 'macos';
|
|
|
|
@override
|
|
final FlutterProject parent;
|
|
|
|
@override
|
|
Directory hostAppRoot;
|
|
|
|
bool exists = true;
|
|
|
|
@override
|
|
bool existsSync() => exists;
|
|
|
|
@override
|
|
File get podfile => hostAppRoot.childFile('Podfile');
|
|
|
|
@override
|
|
File get xcodeProjectInfoFile => hostAppRoot
|
|
.childDirectory('Runner.xcodeproj')
|
|
.childFile('project.pbxproj');
|
|
|
|
@override
|
|
File get flutterPluginSwiftPackageManifest => hostAppRoot
|
|
.childDirectory('Flutter')
|
|
.childDirectory('ephemeral')
|
|
.childDirectory('Packages')
|
|
.childDirectory('FlutterGeneratedPluginSwiftPackage')
|
|
.childFile('Package.swift');
|
|
}
|
|
|
|
class FakeIosProject extends Fake implements IosProject {
|
|
FakeIosProject({
|
|
required MemoryFileSystem fileSystem,
|
|
required this.parent,
|
|
}) : hostAppRoot = fileSystem.directory('app_name').childDirectory('ios');
|
|
|
|
@override
|
|
String pluginConfigKey = 'ios';
|
|
|
|
@override
|
|
final FlutterProject parent;
|
|
|
|
@override
|
|
Directory hostAppRoot;
|
|
|
|
@override
|
|
bool exists = true;
|
|
|
|
@override
|
|
bool existsSync() => exists;
|
|
|
|
@override
|
|
File get podfile => hostAppRoot.childFile('Podfile');
|
|
|
|
@override
|
|
File get xcodeProjectInfoFile => hostAppRoot
|
|
.childDirectory('Runner.xcodeproj')
|
|
.childFile('project.pbxproj');
|
|
|
|
@override
|
|
File get flutterPluginSwiftPackageManifest => hostAppRoot
|
|
.childDirectory('Flutter')
|
|
.childDirectory('ephemeral')
|
|
.childDirectory('Packages')
|
|
.childDirectory('FlutterGeneratedPluginSwiftPackage')
|
|
.childFile('Package.swift');
|
|
}
|
|
|
|
class FakeAndroidProject extends Fake implements AndroidProject {
|
|
@override
|
|
String pluginConfigKey = 'android';
|
|
|
|
@override
|
|
bool existsSync() => false;
|
|
}
|
|
|
|
class FakeWebProject extends Fake implements WebProject {
|
|
@override
|
|
String pluginConfigKey = 'web';
|
|
|
|
@override
|
|
bool existsSync() => false;
|
|
}
|
|
|
|
class FakeWindowsProject extends Fake implements WindowsProject {
|
|
@override
|
|
String pluginConfigKey = 'windows';
|
|
|
|
@override
|
|
bool existsSync() => false;
|
|
}
|
|
|
|
class FakeLinuxProject extends Fake implements LinuxProject {
|
|
@override
|
|
String pluginConfigKey = 'linux';
|
|
|
|
@override
|
|
bool existsSync() => false;
|
|
}
|
|
|
|
class FakeCocoaPods extends Fake implements CocoaPods {
|
|
bool podfileSetup = false;
|
|
bool processedPods = false;
|
|
|
|
@override
|
|
Future<bool> processPods({
|
|
required XcodeBasedProject xcodeProject,
|
|
required BuildMode buildMode,
|
|
bool dependenciesChanged = true,
|
|
}) async {
|
|
processedPods = true;
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Future<void> setupPodfile(XcodeBasedProject xcodeProject) async {
|
|
podfileSetup = true;
|
|
}
|
|
|
|
@override
|
|
void invalidatePodInstallOutput(XcodeBasedProject xcodeProject) {}
|
|
}
|