
<!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> ### Overview This splits the Swift Package Manager feature in two: 1. **SwiftPM feature**: This builds plugins using SwiftPM (if the app supports it), and, uses the new app & plugin templates that support SwiftPM 2. **SwiftPM app migration feature**: this updates an existing Flutter iOS or macOS app to support Swift Package Manager. This feature requires the SwiftPM feature - if SwiftPM is off, the app migration is also off. For now, both features are off by default. We plan to turn on the first feature in the next stable release. The app migration feature will be stay off by default until we have higher confidence in the migration. See this mini design doc: https://github.com/flutter/flutter/issues/151567#issuecomment-2455941279 Here's the PR that updates the SwiftPM docs: https://github.com/flutter/website/pull/11495 Part of https://github.com/flutter/flutter/issues/151567 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Andrew Kolos <andrewrkolos@gmail.com>
417 lines
13 KiB
Dart
417 lines
13 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 'dart:convert';
|
|
|
|
import 'package:flutter_tools/src/base/file_system.dart';
|
|
import 'package:flutter_tools/src/base/io.dart';
|
|
import 'package:flutter_tools/src/features.dart';
|
|
|
|
import '../src/common.dart';
|
|
import 'test_utils.dart';
|
|
|
|
class SwiftPackageManagerUtils {
|
|
static Future<void> enableSwiftPackageManager(
|
|
String flutterBin,
|
|
String workingDirectory, {
|
|
bool enableMigration = true,
|
|
}) async {
|
|
await _enableFeature(flutterBin, workingDirectory, swiftPackageManager);
|
|
|
|
if (enableMigration) {
|
|
await _enableFeature(flutterBin, workingDirectory, swiftPackageManagerMigration);
|
|
}
|
|
}
|
|
|
|
static Future<void> disableSwiftPackageManager(
|
|
String flutterBin,
|
|
String workingDirectory, {
|
|
bool disableMigration = true,
|
|
}) async {
|
|
if (disableMigration) {
|
|
await _disableFeature(flutterBin, workingDirectory, swiftPackageManagerMigration);
|
|
}
|
|
|
|
await _disableFeature(flutterBin, workingDirectory, swiftPackageManager);
|
|
}
|
|
|
|
static Future<void> _enableFeature(
|
|
String flutterBin,
|
|
String workingDirectory,
|
|
Feature feature,
|
|
) async {
|
|
final ProcessResult result = await processManager.run(<String>[
|
|
flutterBin,
|
|
...getLocalEngineArguments(),
|
|
'config',
|
|
'--${feature.configSetting}',
|
|
'-v',
|
|
], workingDirectory: workingDirectory);
|
|
|
|
expect(
|
|
result.exitCode,
|
|
0,
|
|
reason:
|
|
'Failed to enable feature "${feature.name}": \n'
|
|
'stdout: \n${result.stdout}\n'
|
|
'stderr: \n${result.stderr}\n',
|
|
verbose: true,
|
|
);
|
|
}
|
|
|
|
static Future<void> _disableFeature(
|
|
String flutterBin,
|
|
String workingDirectory,
|
|
Feature feature,
|
|
) async {
|
|
final ProcessResult result = await processManager.run(<String>[
|
|
flutterBin,
|
|
...getLocalEngineArguments(),
|
|
'config',
|
|
'--no-${feature.configSetting}',
|
|
'-v',
|
|
], workingDirectory: workingDirectory);
|
|
|
|
expect(
|
|
result.exitCode,
|
|
0,
|
|
reason:
|
|
'Failed to disable feature "${feature.name}": \n'
|
|
'stdout: \n${result.stdout}\n'
|
|
'stderr: \n${result.stderr}\n',
|
|
verbose: true,
|
|
);
|
|
}
|
|
|
|
static Future<String> createApp(
|
|
String flutterBin,
|
|
String workingDirectory, {
|
|
required String platform,
|
|
required String iosLanguage,
|
|
required List<String> options,
|
|
bool usesSwiftPackageManager = false,
|
|
}) async {
|
|
final String appTemplateType = usesSwiftPackageManager ? 'spm' : 'default';
|
|
|
|
final String appName = '${platform}_${iosLanguage}_${appTemplateType}_app';
|
|
final ProcessResult result = await processManager.run(<String>[
|
|
flutterBin,
|
|
...getLocalEngineArguments(),
|
|
'create',
|
|
'--org',
|
|
'io.flutter.devicelab',
|
|
'-i',
|
|
iosLanguage,
|
|
...options,
|
|
appName,
|
|
], workingDirectory: workingDirectory);
|
|
|
|
expect(
|
|
result.exitCode,
|
|
0,
|
|
reason:
|
|
'Failed to create app: \n'
|
|
'stdout: \n${result.stdout}\n'
|
|
'stderr: \n${result.stderr}\n',
|
|
);
|
|
|
|
return fileSystem.path.join(workingDirectory, appName);
|
|
}
|
|
|
|
static Future<void> buildApp(
|
|
String flutterBin,
|
|
String workingDirectory, {
|
|
required List<String> options,
|
|
List<Pattern>? expectedLines,
|
|
List<String>? unexpectedLines,
|
|
}) async {
|
|
final List<Pattern> remainingExpectedLines = expectedLines ?? <Pattern>[];
|
|
final List<String> unexpectedLinesFound = <String>[];
|
|
final List<String> command = <String>[
|
|
flutterBin,
|
|
...getLocalEngineArguments(),
|
|
'build',
|
|
...options,
|
|
];
|
|
|
|
final ProcessResult result = await processManager.run(
|
|
command,
|
|
workingDirectory: workingDirectory,
|
|
);
|
|
|
|
final List<String> stdout = LineSplitter.split(result.stdout.toString()).toList();
|
|
final List<String> stderr = LineSplitter.split(result.stderr.toString()).toList();
|
|
final List<String> output = stdout + stderr;
|
|
for (final String line in output) {
|
|
// Remove "[ +3 ms] " prefix
|
|
String trimmedLine = line.trim();
|
|
if (trimmedLine.startsWith('[')) {
|
|
final int prefixEndIndex = trimmedLine.indexOf(']');
|
|
if (prefixEndIndex > 0) {
|
|
trimmedLine = trimmedLine.substring(prefixEndIndex + 1, trimmedLine.length).trim();
|
|
}
|
|
}
|
|
remainingExpectedLines.remove(trimmedLine);
|
|
remainingExpectedLines.removeWhere(
|
|
(Pattern expectedLine) => trimmedLine.contains(expectedLine),
|
|
);
|
|
if (unexpectedLines != null) {
|
|
if (unexpectedLines
|
|
.where((String unexpectedLine) => trimmedLine.contains(unexpectedLine))
|
|
.firstOrNull !=
|
|
null) {
|
|
unexpectedLinesFound.add(trimmedLine);
|
|
}
|
|
}
|
|
}
|
|
expect(
|
|
result.exitCode,
|
|
0,
|
|
reason:
|
|
'Failed to build app for "${command.join(' ')}":\n'
|
|
'stdout: \n${result.stdout}\n'
|
|
'stderr: \n${result.stderr}\n',
|
|
);
|
|
expect(
|
|
remainingExpectedLines,
|
|
isEmpty,
|
|
reason:
|
|
'Did not find expected lines for "${command.join(' ')}":\n'
|
|
'stdout: \n${result.stdout}\n'
|
|
'stderr: \n${result.stderr}\n',
|
|
);
|
|
expect(
|
|
unexpectedLinesFound,
|
|
isEmpty,
|
|
reason:
|
|
'Found unexpected lines for "${command.join(' ')}":\n'
|
|
'stdout: \n${result.stdout}\n'
|
|
'stderr: \n${result.stderr}\n',
|
|
);
|
|
}
|
|
|
|
static Future<void> cleanApp(String flutterBin, String workingDirectory) async {
|
|
final ProcessResult result = await processManager.run(<String>[
|
|
flutterBin,
|
|
...getLocalEngineArguments(),
|
|
'clean',
|
|
], workingDirectory: workingDirectory);
|
|
expect(
|
|
result.exitCode,
|
|
0,
|
|
reason:
|
|
'Failed to clean app: \n'
|
|
'stdout: \n${result.stdout}\n'
|
|
'stderr: \n${result.stderr}\n',
|
|
);
|
|
}
|
|
|
|
static Future<SwiftPackageManagerPlugin> createPlugin(
|
|
String flutterBin,
|
|
String workingDirectory, {
|
|
required String platform,
|
|
required String iosLanguage,
|
|
bool usesSwiftPackageManager = false,
|
|
}) async {
|
|
final String dependencyManager = usesSwiftPackageManager ? 'spm' : 'cocoapods';
|
|
|
|
// Create plugin
|
|
final String pluginName = '${platform}_${iosLanguage}_${dependencyManager}_plugin';
|
|
final ProcessResult result = await processManager.run(<String>[
|
|
flutterBin,
|
|
...getLocalEngineArguments(),
|
|
'create',
|
|
'--org',
|
|
'io.flutter.devicelab',
|
|
'--template=plugin',
|
|
'--platforms=$platform',
|
|
'-i',
|
|
iosLanguage,
|
|
pluginName,
|
|
], workingDirectory: workingDirectory);
|
|
|
|
expect(
|
|
result.exitCode,
|
|
0,
|
|
reason:
|
|
'Failed to create plugin: \n'
|
|
'stdout: \n${result.stdout}\n'
|
|
'stderr: \n${result.stderr}\n',
|
|
);
|
|
|
|
final Directory pluginDirectory = fileSystem.directory(
|
|
fileSystem.path.join(workingDirectory, pluginName),
|
|
);
|
|
|
|
return SwiftPackageManagerPlugin(
|
|
pluginName: pluginName,
|
|
pluginPath: pluginDirectory.path,
|
|
platform: platform,
|
|
);
|
|
}
|
|
|
|
static void addDependency({
|
|
required SwiftPackageManagerPlugin plugin,
|
|
required String appDirectoryPath,
|
|
}) {
|
|
final File pubspec = fileSystem.file(fileSystem.path.join(appDirectoryPath, 'pubspec.yaml'));
|
|
final String pubspecContent = pubspec.readAsStringSync();
|
|
pubspec.writeAsStringSync(
|
|
pubspecContent.replaceFirst(
|
|
'\ndependencies:\n',
|
|
'\ndependencies:\n ${plugin.pluginName}:\n path: ${plugin.pluginPath}\n',
|
|
),
|
|
);
|
|
}
|
|
|
|
static void removeDependency({
|
|
required SwiftPackageManagerPlugin plugin,
|
|
required String appDirectoryPath,
|
|
}) {
|
|
final File pubspec = fileSystem.file(fileSystem.path.join(appDirectoryPath, 'pubspec.yaml'));
|
|
final String pubspecContent = pubspec.readAsStringSync();
|
|
final String updatedPubspecContent = pubspecContent.replaceFirst(
|
|
'\n ${plugin.pluginName}:\n path: ${plugin.pluginPath}\n',
|
|
'\n',
|
|
);
|
|
|
|
expect(updatedPubspecContent, isNot(pubspecContent));
|
|
|
|
pubspec.writeAsStringSync(updatedPubspecContent);
|
|
}
|
|
|
|
static void disableSwiftPackageManagerByPubspec({required String appDirectoryPath}) {
|
|
final File pubspec = fileSystem.file(fileSystem.path.join(appDirectoryPath, 'pubspec.yaml'));
|
|
final String pubspecContent = pubspec.readAsStringSync();
|
|
pubspec.writeAsStringSync(
|
|
pubspecContent.replaceFirst(
|
|
'\n# The following section is specific to Flutter packages.\nflutter:\n',
|
|
'\n# The following section is specific to Flutter packages.\nflutter:\n disable-swift-package-manager: true',
|
|
),
|
|
);
|
|
}
|
|
|
|
static SwiftPackageManagerPlugin integrationTestPlugin(String platform) {
|
|
final String flutterRoot = getFlutterRoot();
|
|
return SwiftPackageManagerPlugin(
|
|
platform: platform,
|
|
pluginName: (platform == 'ios') ? 'integration_test' : 'integration_test_macos',
|
|
pluginPath:
|
|
(platform == 'ios')
|
|
? fileSystem.path.join(flutterRoot, 'packages', 'integration_test')
|
|
: fileSystem.path.join(
|
|
flutterRoot,
|
|
'packages',
|
|
'integration_test',
|
|
'integration_test_macos',
|
|
),
|
|
);
|
|
}
|
|
|
|
static List<Pattern> expectedLines({
|
|
required String platform,
|
|
required String appDirectoryPath,
|
|
SwiftPackageManagerPlugin? cocoaPodsPlugin,
|
|
SwiftPackageManagerPlugin? swiftPackagePlugin,
|
|
bool swiftPackageMangerEnabled = false,
|
|
bool migrated = false,
|
|
}) {
|
|
final String frameworkName = platform == 'ios' ? 'Flutter' : 'FlutterMacOS';
|
|
final String appPlatformDirectoryPath = fileSystem.path.join(appDirectoryPath, platform);
|
|
|
|
final List<Pattern> expectedLines = <Pattern>[];
|
|
if (swiftPackageMangerEnabled) {
|
|
expectedLines.addAll(<String>[
|
|
'FlutterGeneratedPluginSwiftPackage: $appPlatformDirectoryPath/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage',
|
|
"➜ Explicit dependency on target 'FlutterGeneratedPluginSwiftPackage' in project 'FlutterGeneratedPluginSwiftPackage'",
|
|
]);
|
|
}
|
|
if (swiftPackagePlugin != null) {
|
|
// If using a Swift Package plugin, but Swift Package Manager is not enabled, it falls back to being used as a CocoaPods plugin.
|
|
if (swiftPackageMangerEnabled) {
|
|
expectedLines.addAll(<Pattern>[
|
|
RegExp(
|
|
'${swiftPackagePlugin.pluginName}: [/private]*${swiftPackagePlugin.pluginPath}/$platform/${swiftPackagePlugin.pluginName} @ local',
|
|
),
|
|
"➜ Explicit dependency on target '${swiftPackagePlugin.pluginName}' in project '${swiftPackagePlugin.pluginName}'",
|
|
]);
|
|
} else {
|
|
expectedLines.addAll(<String>[
|
|
'-> Installing ${swiftPackagePlugin.pluginName} (0.0.1)',
|
|
"➜ Explicit dependency on target '${swiftPackagePlugin.pluginName}' in project 'Pods'",
|
|
]);
|
|
}
|
|
}
|
|
if (cocoaPodsPlugin != null) {
|
|
expectedLines.addAll(<String>[
|
|
'Running pod install...',
|
|
'-> Installing $frameworkName (1.0.0)',
|
|
'-> Installing ${cocoaPodsPlugin.pluginName} (0.0.1)',
|
|
"Target 'Pods-Runner' in project 'Pods'",
|
|
"➜ Explicit dependency on target '$frameworkName' in project 'Pods'",
|
|
"➜ Explicit dependency on target '${cocoaPodsPlugin.pluginName}' in project 'Pods'",
|
|
]);
|
|
}
|
|
if (migrated) {
|
|
expectedLines.addAll(<String>[
|
|
'Adding Swift Package Manager integration...',
|
|
'Running pod install...',
|
|
"Target 'Pods-Runner' in project 'Pods'",
|
|
]);
|
|
}
|
|
return expectedLines;
|
|
}
|
|
|
|
static List<String> unexpectedLines({
|
|
required String platform,
|
|
required String appDirectoryPath,
|
|
SwiftPackageManagerPlugin? cocoaPodsPlugin,
|
|
SwiftPackageManagerPlugin? swiftPackagePlugin,
|
|
bool swiftPackageMangerEnabled = false,
|
|
bool migrated = false,
|
|
}) {
|
|
final String frameworkName = platform == 'ios' ? 'Flutter' : 'FlutterMacOS';
|
|
final List<String> unexpectedLines = <String>[];
|
|
if (cocoaPodsPlugin == null && !migrated) {
|
|
unexpectedLines.addAll(<String>[
|
|
'Running pod install...',
|
|
'-> Installing $frameworkName (1.0.0)',
|
|
"Target 'Pods-Runner' in project 'Pods'",
|
|
]);
|
|
}
|
|
if (swiftPackagePlugin != null) {
|
|
if (swiftPackageMangerEnabled) {
|
|
unexpectedLines.addAll(<String>[
|
|
'-> Installing ${swiftPackagePlugin.pluginName} (0.0.1)',
|
|
"➜ Explicit dependency on target '${swiftPackagePlugin.pluginName}' in project 'Pods'",
|
|
]);
|
|
} else {
|
|
unexpectedLines.addAll(<String>[
|
|
'${swiftPackagePlugin.pluginName}: ${swiftPackagePlugin.pluginPath}/$platform/${swiftPackagePlugin.pluginName} @ local',
|
|
"➜ Explicit dependency on target '${swiftPackagePlugin.pluginName}' in project '${swiftPackagePlugin.pluginName}'",
|
|
]);
|
|
}
|
|
}
|
|
if (!migrated) {
|
|
unexpectedLines.addAll(<String>['Adding Swift Package Manager integration...']);
|
|
}
|
|
return unexpectedLines;
|
|
}
|
|
}
|
|
|
|
class SwiftPackageManagerPlugin {
|
|
SwiftPackageManagerPlugin({
|
|
required this.pluginName,
|
|
required this.pluginPath,
|
|
required this.platform,
|
|
});
|
|
|
|
final String pluginName;
|
|
final String pluginPath;
|
|
final String platform;
|
|
String get exampleAppPath => fileSystem.path.join(pluginPath, 'example');
|
|
String get exampleAppPlatformPath => fileSystem.path.join(exampleAppPath, platform);
|
|
String get swiftPackagePlatformPath => fileSystem.path.join(pluginPath, platform, pluginName);
|
|
}
|