flutter/packages/flutter_tools/test/integration.shard/swift_package_manager_utils.dart
Loïc Sharma e7a1b68086
[SwiftPM] Add separate feature flag for the app migration (#158897)
<!--
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>
2025-01-07 19:46:03 +00:00

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