Skip to content

Commit 6d19fa3

Browse files
authored
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 flutter/flutter#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
1 parent f0fc419 commit 6d19fa3

File tree

56 files changed

+10015
-137
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+10015
-137
lines changed

dev/bots/analyze.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,9 @@ Future<void> _verifyNoMissingLicenseForExtension(
793793
if (contents.isEmpty) {
794794
continue; // let's not go down the /bin/true rabbit hole
795795
}
796+
if (path.basename(file.path) == 'Package.swift') {
797+
continue;
798+
}
796799
if (!contents.startsWith(RegExp(header + licensePattern))) {
797800
errors.add(file.path);
798801
}

packages/flutter_tools/bin/macos_assemble.sh

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,23 @@ BuildApp() {
144144
if [[ -n "$CODE_SIZE_DIRECTORY" ]]; then
145145
flutter_args+=("-dCodeSizeDirectory=${CODE_SIZE_DIRECTORY}")
146146
fi
147-
flutter_args+=("${build_mode}_macos_bundle_flutter_assets")
147+
148+
# Run flutter assemble with the build mode specific target that was passed in.
149+
# If no target was passed it, default to build mode specific
150+
# macos_bundle_flutter_assets target.
151+
if [[ -n "$1" ]]; then
152+
flutter_args+=("${build_mode}$1")
153+
else
154+
flutter_args+=("${build_mode}_macos_bundle_flutter_assets")
155+
fi
148156

149157
RunCommand "${flutter_args[@]}"
150158
}
151159

160+
PrepareFramework() {
161+
BuildApp "_unpack_macos"
162+
}
163+
152164
# Adds the App.framework as an embedded binary, the flutter_assets as
153165
# resources, and the native assets.
154166
EmbedFrameworks() {
@@ -192,5 +204,7 @@ else
192204
BuildApp ;;
193205
"embed")
194206
EmbedFrameworks ;;
207+
"prepare")
208+
PrepareFramework ;;
195209
esac
196210
fi

packages/flutter_tools/bin/podhelper.rb

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,10 @@ def flutter_install_plugin_pods(application_path = nil, relative_symlink_dir, pl
302302
system('mkdir', '-p', symlink_plugins_dir)
303303

304304
plugins_file = File.join(application_path, '..', '.flutter-plugins-dependencies')
305-
plugin_pods = flutter_parse_plugins_file(plugins_file, platform)
305+
dependencies_hash = flutter_parse_plugins_file(plugins_file)
306+
plugin_pods = flutter_get_plugins_list(dependencies_hash, platform)
307+
swift_package_manager_enabled = flutter_get_swift_package_manager_enabled(dependencies_hash)
308+
306309
plugin_pods.each do |plugin_hash|
307310
plugin_name = plugin_hash['name']
308311
plugin_path = plugin_hash['path']
@@ -319,25 +322,43 @@ def flutter_install_plugin_pods(application_path = nil, relative_symlink_dir, pl
319322
# Keep pod path relative so it can be checked into Podfile.lock.
320323
relative = flutter_relative_path_from_podfile(symlink)
321324

325+
# If Swift Package Manager is enabled and the plugin has a Package.swift,
326+
# skip from installing as a pod.
327+
swift_package_exists = File.exists?(File.join(relative, platform_directory, plugin_name, "Package.swift"))
328+
next if swift_package_manager_enabled && swift_package_exists
329+
330+
# If a plugin is Swift Package Manager compatible but not CocoaPods compatible, skip it.
331+
# The tool will print an error about it.
332+
next if swift_package_exists && !File.exists?(File.join(relative, platform_directory, plugin_name + ".podspec"))
333+
322334
pod plugin_name, path: File.join(relative, platform_directory)
323335
end
324336
end
325337

326-
# .flutter-plugins-dependencies format documented at
327-
# https://flutter.dev/go/plugins-list-migration
328-
def flutter_parse_plugins_file(file, platform)
338+
def flutter_parse_plugins_file(file)
329339
file_path = File.expand_path(file)
330340
return [] unless File.exist? file_path
331341

332342
dependencies_file = File.read(file)
333-
dependencies_hash = JSON.parse(dependencies_file)
343+
JSON.parse(dependencies_file)
344+
end
334345

346+
# .flutter-plugins-dependencies format documented at
347+
# https://flutter.dev/go/plugins-list-migration
348+
def flutter_get_plugins_list(dependencies_hash, platform)
335349
# dependencies_hash.dig('plugins', 'ios') not available until Ruby 2.3
350+
return [] unless dependencies_hash.any?
336351
return [] unless dependencies_hash.has_key?('plugins')
337352
return [] unless dependencies_hash['plugins'].has_key?(platform)
338353
dependencies_hash['plugins'][platform] || []
339354
end
340355

356+
def flutter_get_swift_package_manager_enabled(dependencies_hash)
357+
return false unless dependencies_hash.any?
358+
return false unless dependencies_hash.has_key?('swift_package_manager_enabled')
359+
dependencies_hash['swift_package_manager_enabled'] == true
360+
end
361+
341362
def flutter_relative_path_from_podfile(path)
342363
# defined_in_file is set by CocoaPods and is a Pathname to the Podfile.
343364
project_directory_pathname = defined_in_file.dirname

packages/flutter_tools/bin/xcode_backend.dart

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ class Context {
4949
switch (subCommand) {
5050
case 'build':
5151
buildApp();
52+
case 'prepare':
53+
prepare();
5254
case 'thin':
5355
// No-op, thinning is handled during the bundle asset assemble build target.
5456
break;
@@ -351,21 +353,67 @@ class Context {
351353
}
352354
}
353355

356+
void prepare() {
357+
final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty;
358+
final String sourceRoot = environment['SOURCE_ROOT'] ?? '';
359+
final String projectPath = environment['FLUTTER_APPLICATION_PATH'] ?? '$sourceRoot/..';
360+
361+
final String buildMode = parseFlutterBuildMode();
362+
363+
final List<String> flutterArgs = _generateFlutterArgsForAssemble(buildMode, verbose);
364+
365+
flutterArgs.add('${buildMode}_unpack_ios');
366+
367+
final ProcessResult result = runSync(
368+
'${environmentEnsure('FLUTTER_ROOT')}/bin/flutter',
369+
flutterArgs,
370+
verbose: verbose,
371+
allowFail: true,
372+
workingDirectory: projectPath, // equivalent of RunCommand pushd "${project_path}"
373+
);
374+
375+
if (result.exitCode != 0) {
376+
echoError('Failed to copy Flutter framework.');
377+
exitApp(-1);
378+
}
379+
}
380+
354381
void buildApp() {
355-
final bool verbose = environment['VERBOSE_SCRIPT_LOGGING'] != null && environment['VERBOSE_SCRIPT_LOGGING'] != '';
382+
final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty;
356383
final String sourceRoot = environment['SOURCE_ROOT'] ?? '';
357-
String projectPath = '$sourceRoot/..';
358-
if (environment['FLUTTER_APPLICATION_PATH'] != null) {
359-
projectPath = environment['FLUTTER_APPLICATION_PATH']!;
384+
final String projectPath = environment['FLUTTER_APPLICATION_PATH'] ?? '$sourceRoot/..';
385+
386+
final String buildMode = parseFlutterBuildMode();
387+
388+
final List<String> flutterArgs = _generateFlutterArgsForAssemble(buildMode, verbose);
389+
390+
flutterArgs.add('${buildMode}_ios_bundle_flutter_assets');
391+
392+
final ProcessResult result = runSync(
393+
'${environmentEnsure('FLUTTER_ROOT')}/bin/flutter',
394+
flutterArgs,
395+
verbose: verbose,
396+
allowFail: true,
397+
workingDirectory: projectPath, // equivalent of RunCommand pushd "${project_path}"
398+
);
399+
400+
if (result.exitCode != 0) {
401+
echoError('Failed to package $projectPath.');
402+
exitApp(-1);
360403
}
361404

405+
streamOutput('done');
406+
streamOutput(' └─Compiling, linking and signing...');
407+
408+
echo('Project $projectPath built and packaged successfully.');
409+
}
410+
411+
List<String> _generateFlutterArgsForAssemble(String buildMode, bool verbose) {
362412
String targetPath = 'lib/main.dart';
363413
if (environment['FLUTTER_TARGET'] != null) {
364414
targetPath = environment['FLUTTER_TARGET']!;
365415
}
366416

367-
final String buildMode = parseFlutterBuildMode();
368-
369417
// Warn the user if not archiving (ACTION=install) in release mode.
370418
final String? action = environment['ACTION'];
371419
if (action == 'install' && buildMode != 'release') {
@@ -432,24 +480,6 @@ class Context {
432480
flutterArgs.add('-dCodeSizeDirectory=${environment['CODE_SIZE_DIRECTORY']}');
433481
}
434482

435-
flutterArgs.add('${buildMode}_ios_bundle_flutter_assets');
436-
437-
final ProcessResult result = runSync(
438-
'${environmentEnsure('FLUTTER_ROOT')}/bin/flutter',
439-
flutterArgs,
440-
verbose: verbose,
441-
allowFail: true,
442-
workingDirectory: projectPath, // equivalent of RunCommand pushd "${project_path}"
443-
);
444-
445-
if (result.exitCode != 0) {
446-
echoError('Failed to package $projectPath.');
447-
exitApp(-1);
448-
}
449-
450-
streamOutput('done');
451-
streamOutput(' └─Compiling, linking and signing...');
452-
453-
echo('Project $projectPath built and packaged successfully.');
483+
return flutterArgs;
454484
}
455485
}

packages/flutter_tools/lib/src/commands/assemble.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ List<Target> _kDefaultTargets = <Target>[
4141
const DebugMacOSBundleFlutterAssets(),
4242
const ProfileMacOSBundleFlutterAssets(),
4343
const ReleaseMacOSBundleFlutterAssets(),
44+
const DebugUnpackMacOS(),
45+
const ProfileUnpackMacOS(),
46+
const ReleaseUnpackMacOS(),
4447
// Linux targets
4548
const DebugBundleLinuxAssets(TargetPlatform.linux_x64),
4649
const DebugBundleLinuxAssets(TargetPlatform.linux_arm64),
@@ -72,6 +75,9 @@ List<Target> _kDefaultTargets = <Target>[
7275
const DebugIosApplicationBundle(),
7376
const ProfileIosApplicationBundle(),
7477
const ReleaseIosApplicationBundle(),
78+
const DebugUnpackIOS(),
79+
const ProfileUnpackIOS(),
80+
const ReleaseUnpackIOS(),
7581
// Windows targets
7682
const UnpackWindows(TargetPlatform.windows_x64),
7783
const UnpackWindows(TargetPlatform.windows_arm64),

packages/flutter_tools/lib/src/commands/build_ios.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import '../globals.dart' as globals;
2323
import '../ios/application_package.dart';
2424
import '../ios/mac.dart';
2525
import '../ios/plist_parser.dart';
26+
import '../project.dart';
2627
import '../reporting/reporting.dart';
2728
import '../runner/flutter_command.dart';
2829
import 'build.dart';
@@ -686,7 +687,15 @@ abstract class _BuildIOSSubCommand extends BuildSubCommand {
686687
xcodeBuildResult = result;
687688

688689
if (!result.success) {
689-
await diagnoseXcodeBuildFailure(result, globals.flutterUsage, globals.logger, globals.analytics);
690+
await diagnoseXcodeBuildFailure(
691+
result,
692+
analytics: globals.analytics,
693+
fileSystem: globals.fs,
694+
flutterUsage: globals.flutterUsage,
695+
logger: globals.logger,
696+
platform: SupportedPlatform.ios,
697+
project: app.project.parent,
698+
);
690699
final String presentParticiple = xcodeBuildAction == XcodeBuildAction.build ? 'building' : 'archiving';
691700
throwToolExit('Encountered error while $presentParticiple for $logTarget.');
692701
}

packages/flutter_tools/lib/src/commands/build_ios_framework.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,12 @@ class BuildIOSFrameworkCommand extends BuildFrameworkCommand {
272272
buildInfo, modeDirectory, iPhoneBuildOutput, simulatorBuildOutput);
273273

274274
// Build and copy plugins.
275-
await processPodsIfNeeded(project.ios, getIosBuildDirectory(), buildInfo.mode);
275+
await processPodsIfNeeded(
276+
project.ios,
277+
getIosBuildDirectory(),
278+
buildInfo.mode,
279+
forceCocoaPodsOnly: true,
280+
);
276281
if (boolArg('plugins') && hasPlugins(project)) {
277282
await _producePlugins(buildInfo.mode, xcodeBuildConfiguration, iPhoneBuildOutput, simulatorBuildOutput, modeDirectory);
278283
}

packages/flutter_tools/lib/src/commands/build_macos_framework.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,12 @@ class BuildMacOSFrameworkCommand extends BuildFrameworkCommand {
9494
await _produceAppFramework(buildInfo, modeDirectory, buildOutput);
9595

9696
// Build and copy plugins.
97-
await processPodsIfNeeded(project.macos, getMacOSBuildDirectory(), buildInfo.mode);
97+
await processPodsIfNeeded(
98+
project.macos,
99+
getMacOSBuildDirectory(),
100+
buildInfo.mode,
101+
forceCocoaPodsOnly: true,
102+
);
98103
if (boolArg('plugins') && hasPlugins(project)) {
99104
await _producePlugins(xcodeBuildConfiguration, buildOutput, modeDirectory);
100105
}

packages/flutter_tools/lib/src/features.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ abstract class FeatureFlags {
5151
/// Whether native assets compilation and bundling is enabled.
5252
bool get isPreviewDeviceEnabled => true;
5353

54+
/// Whether Swift Package Manager dependency management is enabled.
55+
bool get isSwiftPackageManagerEnabled => false;
56+
5457
/// Whether a particular feature is enabled for the current channel.
5558
///
5659
/// Prefer using one of the specific getters above instead of this API.
@@ -70,6 +73,7 @@ const List<Feature> allFeatures = <Feature>[
7073
cliAnimation,
7174
nativeAssets,
7275
previewDevice,
76+
swiftPackageManager,
7377
];
7478

7579
/// All current Flutter feature flags that can be configured.
@@ -175,6 +179,16 @@ const Feature previewDevice = Feature(
175179
),
176180
);
177181

182+
/// Enable Swift Package Mangaer as a darwin dependency manager.
183+
const Feature swiftPackageManager = Feature(
184+
name: 'support for Swift Package Manager for iOS and macOS',
185+
configSetting: 'enable-swift-package-manager',
186+
environmentOverride: 'SWIFT_PACKAGE_MANAGER',
187+
master: FeatureChannelSetting(
188+
available: true,
189+
),
190+
);
191+
178192
/// A [Feature] is a process for conditionally enabling tool features.
179193
///
180194
/// All settings are optional, and if not provided will generally default to

packages/flutter_tools/lib/src/flutter_features.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ class FlutterFeatureFlags implements FeatureFlags {
5858
@override
5959
bool get isPreviewDeviceEnabled => isEnabled(previewDevice);
6060

61+
@override
62+
bool get isSwiftPackageManagerEnabled => isEnabled(swiftPackageManager);
63+
6164
@override
6265
bool isEnabled(Feature feature) {
6366
final String currentChannel = _flutterVersion.channel;

packages/flutter_tools/lib/src/flutter_manifest.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ class FlutterManifest {
136136
return _flutterDescriptor['uses-material-design'] as bool? ?? false;
137137
}
138138

139+
/// If true, does not use Swift Package Manager as a dependency manager.
140+
/// CocoaPods will be used instead.
141+
bool get disabledSwiftPackageManager {
142+
return _flutterDescriptor['disable-swift-package-manager'] as bool? ?? false;
143+
}
144+
139145
/// True if this Flutter module should use AndroidX dependencies.
140146
///
141147
/// If false the deprecated Android Support library will be used.
@@ -547,6 +553,10 @@ void _validateFlutter(YamlMap? yaml, List<String> errors) {
547553
break;
548554
case 'deferred-components':
549555
_validateDeferredComponents(kvp, errors);
556+
case 'disable-swift-package-manager':
557+
if (yamlValue is! bool) {
558+
errors.add('Expected "$yamlKey" to be a bool, but got $yamlValue (${yamlValue.runtimeType}).');
559+
}
550560
default:
551561
errors.add('Unexpected child "$yamlKey" found under "flutter".');
552562
break;

0 commit comments

Comments
 (0)