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
This commit is contained in:
Victoria Ashworth 2024-04-18 16:12:36 -05:00 committed by GitHub
parent f0fc419a6c
commit 6d19fa3bfa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 10017 additions and 139 deletions

View File

@ -793,6 +793,9 @@ Future<void> _verifyNoMissingLicenseForExtension(
if (contents.isEmpty) {
continue; // let's not go down the /bin/true rabbit hole
}
if (path.basename(file.path) == 'Package.swift') {
continue;
}
if (!contents.startsWith(RegExp(header + licensePattern))) {
errors.add(file.path);
}

View File

@ -144,11 +144,23 @@ BuildApp() {
if [[ -n "$CODE_SIZE_DIRECTORY" ]]; then
flutter_args+=("-dCodeSizeDirectory=${CODE_SIZE_DIRECTORY}")
fi
flutter_args+=("${build_mode}_macos_bundle_flutter_assets")
# Run flutter assemble with the build mode specific target that was passed in.
# If no target was passed it, default to build mode specific
# macos_bundle_flutter_assets target.
if [[ -n "$1" ]]; then
flutter_args+=("${build_mode}$1")
else
flutter_args+=("${build_mode}_macos_bundle_flutter_assets")
fi
RunCommand "${flutter_args[@]}"
}
PrepareFramework() {
BuildApp "_unpack_macos"
}
# Adds the App.framework as an embedded binary, the flutter_assets as
# resources, and the native assets.
EmbedFrameworks() {
@ -192,5 +204,7 @@ else
BuildApp ;;
"embed")
EmbedFrameworks ;;
"prepare")
PrepareFramework ;;
esac
fi

View File

@ -302,7 +302,10 @@ def flutter_install_plugin_pods(application_path = nil, relative_symlink_dir, pl
system('mkdir', '-p', symlink_plugins_dir)
plugins_file = File.join(application_path, '..', '.flutter-plugins-dependencies')
plugin_pods = flutter_parse_plugins_file(plugins_file, platform)
dependencies_hash = flutter_parse_plugins_file(plugins_file)
plugin_pods = flutter_get_plugins_list(dependencies_hash, platform)
swift_package_manager_enabled = flutter_get_swift_package_manager_enabled(dependencies_hash)
plugin_pods.each do |plugin_hash|
plugin_name = plugin_hash['name']
plugin_path = plugin_hash['path']
@ -319,25 +322,43 @@ def flutter_install_plugin_pods(application_path = nil, relative_symlink_dir, pl
# Keep pod path relative so it can be checked into Podfile.lock.
relative = flutter_relative_path_from_podfile(symlink)
# If Swift Package Manager is enabled and the plugin has a Package.swift,
# skip from installing as a pod.
swift_package_exists = File.exists?(File.join(relative, platform_directory, plugin_name, "Package.swift"))
next if swift_package_manager_enabled && swift_package_exists
# If a plugin is Swift Package Manager compatible but not CocoaPods compatible, skip it.
# The tool will print an error about it.
next if swift_package_exists && !File.exists?(File.join(relative, platform_directory, plugin_name + ".podspec"))
pod plugin_name, path: File.join(relative, platform_directory)
end
end
# .flutter-plugins-dependencies format documented at
# https://flutter.dev/go/plugins-list-migration
def flutter_parse_plugins_file(file, platform)
def flutter_parse_plugins_file(file)
file_path = File.expand_path(file)
return [] unless File.exist? file_path
dependencies_file = File.read(file)
dependencies_hash = JSON.parse(dependencies_file)
JSON.parse(dependencies_file)
end
# .flutter-plugins-dependencies format documented at
# https://flutter.dev/go/plugins-list-migration
def flutter_get_plugins_list(dependencies_hash, platform)
# dependencies_hash.dig('plugins', 'ios') not available until Ruby 2.3
return [] unless dependencies_hash.any?
return [] unless dependencies_hash.has_key?('plugins')
return [] unless dependencies_hash['plugins'].has_key?(platform)
dependencies_hash['plugins'][platform] || []
end
def flutter_get_swift_package_manager_enabled(dependencies_hash)
return false unless dependencies_hash.any?
return false unless dependencies_hash.has_key?('swift_package_manager_enabled')
dependencies_hash['swift_package_manager_enabled'] == true
end
def flutter_relative_path_from_podfile(path)
# defined_in_file is set by CocoaPods and is a Pathname to the Podfile.
project_directory_pathname = defined_in_file.dirname

View File

@ -49,6 +49,8 @@ class Context {
switch (subCommand) {
case 'build':
buildApp();
case 'prepare':
prepare();
case 'thin':
// No-op, thinning is handled during the bundle asset assemble build target.
break;
@ -351,21 +353,67 @@ class Context {
}
}
void buildApp() {
final bool verbose = environment['VERBOSE_SCRIPT_LOGGING'] != null && environment['VERBOSE_SCRIPT_LOGGING'] != '';
void prepare() {
final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty;
final String sourceRoot = environment['SOURCE_ROOT'] ?? '';
String projectPath = '$sourceRoot/..';
if (environment['FLUTTER_APPLICATION_PATH'] != null) {
projectPath = environment['FLUTTER_APPLICATION_PATH']!;
final String projectPath = environment['FLUTTER_APPLICATION_PATH'] ?? '$sourceRoot/..';
final String buildMode = parseFlutterBuildMode();
final List<String> flutterArgs = _generateFlutterArgsForAssemble(buildMode, verbose);
flutterArgs.add('${buildMode}_unpack_ios');
final ProcessResult result = runSync(
'${environmentEnsure('FLUTTER_ROOT')}/bin/flutter',
flutterArgs,
verbose: verbose,
allowFail: true,
workingDirectory: projectPath, // equivalent of RunCommand pushd "${project_path}"
);
if (result.exitCode != 0) {
echoError('Failed to copy Flutter framework.');
exitApp(-1);
}
}
void buildApp() {
final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty;
final String sourceRoot = environment['SOURCE_ROOT'] ?? '';
final String projectPath = environment['FLUTTER_APPLICATION_PATH'] ?? '$sourceRoot/..';
final String buildMode = parseFlutterBuildMode();
final List<String> flutterArgs = _generateFlutterArgsForAssemble(buildMode, verbose);
flutterArgs.add('${buildMode}_ios_bundle_flutter_assets');
final ProcessResult result = runSync(
'${environmentEnsure('FLUTTER_ROOT')}/bin/flutter',
flutterArgs,
verbose: verbose,
allowFail: true,
workingDirectory: projectPath, // equivalent of RunCommand pushd "${project_path}"
);
if (result.exitCode != 0) {
echoError('Failed to package $projectPath.');
exitApp(-1);
}
streamOutput('done');
streamOutput(' └─Compiling, linking and signing...');
echo('Project $projectPath built and packaged successfully.');
}
List<String> _generateFlutterArgsForAssemble(String buildMode, bool verbose) {
String targetPath = 'lib/main.dart';
if (environment['FLUTTER_TARGET'] != null) {
targetPath = environment['FLUTTER_TARGET']!;
}
final String buildMode = parseFlutterBuildMode();
// Warn the user if not archiving (ACTION=install) in release mode.
final String? action = environment['ACTION'];
if (action == 'install' && buildMode != 'release') {
@ -432,24 +480,6 @@ class Context {
flutterArgs.add('-dCodeSizeDirectory=${environment['CODE_SIZE_DIRECTORY']}');
}
flutterArgs.add('${buildMode}_ios_bundle_flutter_assets');
final ProcessResult result = runSync(
'${environmentEnsure('FLUTTER_ROOT')}/bin/flutter',
flutterArgs,
verbose: verbose,
allowFail: true,
workingDirectory: projectPath, // equivalent of RunCommand pushd "${project_path}"
);
if (result.exitCode != 0) {
echoError('Failed to package $projectPath.');
exitApp(-1);
}
streamOutput('done');
streamOutput(' └─Compiling, linking and signing...');
echo('Project $projectPath built and packaged successfully.');
return flutterArgs;
}
}

View File

@ -41,6 +41,9 @@ List<Target> _kDefaultTargets = <Target>[
const DebugMacOSBundleFlutterAssets(),
const ProfileMacOSBundleFlutterAssets(),
const ReleaseMacOSBundleFlutterAssets(),
const DebugUnpackMacOS(),
const ProfileUnpackMacOS(),
const ReleaseUnpackMacOS(),
// Linux targets
const DebugBundleLinuxAssets(TargetPlatform.linux_x64),
const DebugBundleLinuxAssets(TargetPlatform.linux_arm64),
@ -72,6 +75,9 @@ List<Target> _kDefaultTargets = <Target>[
const DebugIosApplicationBundle(),
const ProfileIosApplicationBundle(),
const ReleaseIosApplicationBundle(),
const DebugUnpackIOS(),
const ProfileUnpackIOS(),
const ReleaseUnpackIOS(),
// Windows targets
const UnpackWindows(TargetPlatform.windows_x64),
const UnpackWindows(TargetPlatform.windows_arm64),

View File

@ -23,6 +23,7 @@ import '../globals.dart' as globals;
import '../ios/application_package.dart';
import '../ios/mac.dart';
import '../ios/plist_parser.dart';
import '../project.dart';
import '../reporting/reporting.dart';
import '../runner/flutter_command.dart';
import 'build.dart';
@ -686,7 +687,15 @@ abstract class _BuildIOSSubCommand extends BuildSubCommand {
xcodeBuildResult = result;
if (!result.success) {
await diagnoseXcodeBuildFailure(result, globals.flutterUsage, globals.logger, globals.analytics);
await diagnoseXcodeBuildFailure(
result,
analytics: globals.analytics,
fileSystem: globals.fs,
flutterUsage: globals.flutterUsage,
logger: globals.logger,
platform: SupportedPlatform.ios,
project: app.project.parent,
);
final String presentParticiple = xcodeBuildAction == XcodeBuildAction.build ? 'building' : 'archiving';
throwToolExit('Encountered error while $presentParticiple for $logTarget.');
}

View File

@ -272,7 +272,12 @@ class BuildIOSFrameworkCommand extends BuildFrameworkCommand {
buildInfo, modeDirectory, iPhoneBuildOutput, simulatorBuildOutput);
// Build and copy plugins.
await processPodsIfNeeded(project.ios, getIosBuildDirectory(), buildInfo.mode);
await processPodsIfNeeded(
project.ios,
getIosBuildDirectory(),
buildInfo.mode,
forceCocoaPodsOnly: true,
);
if (boolArg('plugins') && hasPlugins(project)) {
await _producePlugins(buildInfo.mode, xcodeBuildConfiguration, iPhoneBuildOutput, simulatorBuildOutput, modeDirectory);
}

View File

@ -94,7 +94,12 @@ class BuildMacOSFrameworkCommand extends BuildFrameworkCommand {
await _produceAppFramework(buildInfo, modeDirectory, buildOutput);
// Build and copy plugins.
await processPodsIfNeeded(project.macos, getMacOSBuildDirectory(), buildInfo.mode);
await processPodsIfNeeded(
project.macos,
getMacOSBuildDirectory(),
buildInfo.mode,
forceCocoaPodsOnly: true,
);
if (boolArg('plugins') && hasPlugins(project)) {
await _producePlugins(xcodeBuildConfiguration, buildOutput, modeDirectory);
}

View File

@ -51,6 +51,9 @@ abstract class FeatureFlags {
/// Whether native assets compilation and bundling is enabled.
bool get isPreviewDeviceEnabled => true;
/// Whether Swift Package Manager dependency management is enabled.
bool get isSwiftPackageManagerEnabled => false;
/// Whether a particular feature is enabled for the current channel.
///
/// Prefer using one of the specific getters above instead of this API.
@ -70,6 +73,7 @@ const List<Feature> allFeatures = <Feature>[
cliAnimation,
nativeAssets,
previewDevice,
swiftPackageManager,
];
/// All current Flutter feature flags that can be configured.
@ -175,6 +179,16 @@ const Feature previewDevice = Feature(
),
);
/// Enable Swift Package Mangaer as a darwin dependency manager.
const Feature swiftPackageManager = Feature(
name: 'support for Swift Package Manager for iOS and macOS',
configSetting: 'enable-swift-package-manager',
environmentOverride: 'SWIFT_PACKAGE_MANAGER',
master: FeatureChannelSetting(
available: true,
),
);
/// A [Feature] is a process for conditionally enabling tool features.
///
/// All settings are optional, and if not provided will generally default to

View File

@ -58,6 +58,9 @@ class FlutterFeatureFlags implements FeatureFlags {
@override
bool get isPreviewDeviceEnabled => isEnabled(previewDevice);
@override
bool get isSwiftPackageManagerEnabled => isEnabled(swiftPackageManager);
@override
bool isEnabled(Feature feature) {
final String currentChannel = _flutterVersion.channel;

View File

@ -136,6 +136,12 @@ class FlutterManifest {
return _flutterDescriptor['uses-material-design'] as bool? ?? false;
}
/// If true, does not use Swift Package Manager as a dependency manager.
/// CocoaPods will be used instead.
bool get disabledSwiftPackageManager {
return _flutterDescriptor['disable-swift-package-manager'] as bool? ?? false;
}
/// True if this Flutter module should use AndroidX dependencies.
///
/// If false the deprecated Android Support library will be used.
@ -547,6 +553,10 @@ void _validateFlutter(YamlMap? yaml, List<String> errors) {
break;
case 'deferred-components':
_validateDeferredComponents(kvp, errors);
case 'disable-swift-package-manager':
if (yamlValue is! bool) {
errors.add('Expected "$yamlKey" to be a bool, but got $yamlValue (${yamlValue.runtimeType}).');
}
default:
errors.add('Unexpected child "$yamlKey" found under "flutter".');
break;

View File

@ -22,6 +22,8 @@ import 'dart/language_version.dart';
import 'dart/package_map.dart';
import 'features.dart';
import 'globals.dart' as globals;
import 'macos/darwin_dependency_management.dart';
import 'macos/swift_package_manager.dart';
import 'platform_plugins.dart';
import 'plugins.dart';
import 'project.dart';
@ -160,7 +162,11 @@ const String _kFlutterPluginsSharedDarwinSource = 'shared_darwin_source';
///
///
/// Finally, returns [true] if the plugins list has changed, otherwise returns [false].
bool _writeFlutterPluginsList(FlutterProject project, List<Plugin> plugins) {
bool _writeFlutterPluginsList(
FlutterProject project,
List<Plugin> plugins, {
bool forceCocoaPodsOnly = false,
}) {
final File pluginsFile = project.flutterPluginsDependenciesFile;
if (plugins.isEmpty) {
return ErrorHandlingFileSystem.deleteIfExists(pluginsFile);
@ -190,6 +196,7 @@ bool _writeFlutterPluginsList(FlutterProject project, List<Plugin> plugins) {
result['dependencyGraph'] = _createPluginLegacyDependencyGraph(plugins);
result['date_created'] = globals.systemClock.now().toString();
result['version'] = globals.flutterVersion.frameworkVersion;
result['swift_package_manager_enabled'] = !forceCocoaPodsOnly && project.usesSwiftPackageManager;
// Only notify if the plugins list has changed. [date_created] will always be different,
// [version] is not relevant for this check.
@ -1000,6 +1007,7 @@ Future<void> refreshPluginsList(
FlutterProject project, {
bool iosPlatform = false,
bool macOSPlatform = false,
bool forceCocoaPodsOnly = false,
}) async {
final List<Plugin> plugins = await findPlugins(project);
// Sort the plugins by name to keep ordering stable in generated files.
@ -1008,8 +1016,12 @@ Future<void> refreshPluginsList(
// Write the legacy plugin files to avoid breaking existing apps.
final bool legacyChanged = _writeFlutterPluginsListLegacy(project, plugins);
final bool changed = _writeFlutterPluginsList(project, plugins);
if (changed || legacyChanged) {
final bool changed = _writeFlutterPluginsList(
project,
plugins,
forceCocoaPodsOnly: forceCocoaPodsOnly,
);
if (changed || legacyChanged || forceCocoaPodsOnly) {
createPluginSymlinks(project, force: true);
if (iosPlatform) {
globals.cocoaPods?.invalidatePodInstallOutput(project.ios);
@ -1069,6 +1081,7 @@ Future<void> injectPlugins(
bool macOSPlatform = false,
bool windowsPlatform = false,
Iterable<String>? allowedPlugins,
DarwinDependencyManagement? darwinDependencyManagement,
}) async {
final List<Plugin> plugins = await findPlugins(project);
// Sort the plugins by name to keep ordering stable in generated files.
@ -1088,20 +1101,27 @@ Future<void> injectPlugins(
if (windowsPlatform) {
await writeWindowsPluginFiles(project, plugins, globals.templateRenderer, allowedPlugins: allowedPlugins);
}
if (!project.isModule) {
final List<XcodeBasedProject> darwinProjects = <XcodeBasedProject>[
if (iosPlatform) project.ios,
if (macOSPlatform) project.macos,
];
for (final XcodeBasedProject subproject in darwinProjects) {
if (plugins.isNotEmpty) {
await globals.cocoaPods?.setupPodfile(subproject);
}
/// The user may have a custom maintained Podfile that they're running `pod install`
/// on themselves.
else if (subproject.podfile.existsSync() && subproject.podfileLock.existsSync()) {
globals.cocoaPods?.addPodsDependencyToFlutterXcconfig(subproject);
}
if (iosPlatform || macOSPlatform) {
final DarwinDependencyManagement darwinDependencyManagerSetup = darwinDependencyManagement ?? DarwinDependencyManagement(
project: project,
plugins: plugins,
cocoapods: globals.cocoaPods!,
swiftPackageManager: SwiftPackageManager(
fileSystem: globals.fs,
templateRenderer: globals.templateRenderer,
),
fileSystem: globals.fs,
logger: globals.logger,
);
if (iosPlatform) {
await darwinDependencyManagerSetup.setUp(
platform: SupportedPlatform.ios,
);
}
if (macOSPlatform) {
await darwinDependencyManagerSetup.setUp(
platform: SupportedPlatform.macos,
);
}
}
}

View File

@ -496,7 +496,15 @@ class IOSDevice extends Device {
);
if (!buildResult.success) {
_logger.printError('Could not build the precompiled application for the device.');
await diagnoseXcodeBuildFailure(buildResult, globals.flutterUsage, _logger, globals.analytics);
await diagnoseXcodeBuildFailure(
buildResult,
analytics: globals.analytics,
fileSystem: globals.fs,
flutterUsage: globals.flutterUsage,
logger: globals.logger,
platform: SupportedPlatform.ios,
project: package.project.parent,
);
_logger.printError('');
return LaunchResult.failed();
}

View File

@ -19,12 +19,16 @@ import '../build_info.dart';
import '../cache.dart';
import '../device.dart';
import '../flutter_manifest.dart';
import '../flutter_plugins.dart';
import '../globals.dart' as globals;
import '../macos/cocoapod_utils.dart';
import '../macos/swift_package_manager.dart';
import '../macos/xcode.dart';
import '../migrations/swift_package_manager_integration_migration.dart';
import '../migrations/xcode_project_object_version_migration.dart';
import '../migrations/xcode_script_build_phase_migration.dart';
import '../migrations/xcode_thin_binary_build_phase_input_paths_migration.dart';
import '../plugins.dart';
import '../project.dart';
import '../reporting/reporting.dart';
import 'application_package.dart';
@ -148,6 +152,8 @@ Future<XcodeBuildResult> buildXcodeProject({
return XcodeBuildResult(success: false);
}
final FlutterProject project = FlutterProject.current();
final List<ProjectMigrator> migrators = <ProjectMigrator>[
RemoveFrameworkLinkAndEmbeddingMigration(app.project, globals.logger, globals.flutterUsage, globals.analytics),
XcodeBuildSystemMigration(app.project, globals.logger),
@ -160,6 +166,16 @@ Future<XcodeBuildResult> buildXcodeProject({
RemoveBitcodeMigration(app.project, globals.logger),
XcodeThinBinaryBuildPhaseInputPathsMigration(app.project, globals.logger),
UIApplicationMainDeprecationMigration(app.project, globals.logger),
if (project.usesSwiftPackageManager && app.project.flutterPluginSwiftPackageManifest.existsSync())
SwiftPackageManagerIntegrationMigration(
app.project,
SupportedPlatform.ios,
buildInfo,
xcodeProjectInterpreter: globals.xcodeProjectInterpreter!,
logger: globals.logger,
fileSystem: globals.fs,
plistParser: globals.plistParser,
),
];
final ProjectMigration migration = ProjectMigration(migrators);
@ -245,12 +261,21 @@ Future<XcodeBuildResult> buildXcodeProject({
);
}
final FlutterProject project = FlutterProject.current();
await updateGeneratedXcodeProperties(
project: project,
targetOverride: targetOverride,
buildInfo: buildInfo,
);
if (project.usesSwiftPackageManager) {
final String? iosDeploymentTarget = buildSettings['IPHONEOS_DEPLOYMENT_TARGET'];
if (iosDeploymentTarget != null) {
SwiftPackageManager.updateMinimumDeployment(
platform: SupportedPlatform.ios,
project: project.ios,
deploymentTarget: iosDeploymentTarget,
);
}
}
await processPodsIfNeeded(project.ios, getIosBuildDirectory(), buildInfo.mode);
if (configOnly) {
return XcodeBuildResult(success: true);
@ -596,11 +621,14 @@ return result.exitCode != 0 &&
}
Future<void> diagnoseXcodeBuildFailure(
XcodeBuildResult result,
Usage flutterUsage,
Logger logger,
Analytics analytics,
) async {
XcodeBuildResult result, {
required Analytics analytics,
required Logger logger,
required FileSystem fileSystem,
required Usage flutterUsage,
required SupportedPlatform platform,
required FlutterProject project,
}) async {
final XcodeBuildExecution? xcodeBuildExecution = result.xcodeBuildExecution;
if (xcodeBuildExecution != null
&& xcodeBuildExecution.environmentType == EnvironmentType.physical
@ -627,7 +655,14 @@ Future<void> diagnoseXcodeBuildFailure(
}
// Handle errors.
final bool issueDetected = _handleIssues(result.xcResult, logger, xcodeBuildExecution);
final bool issueDetected = await _handleIssues(
result,
xcodeBuildExecution,
project: project,
platform: platform,
logger: logger,
fileSystem: fileSystem,
);
if (!issueDetected && xcodeBuildExecution != null) {
// Fallback to use stdout to detect and print issues.
@ -728,7 +763,11 @@ bool upgradePbxProjWithFlutterAssets(IosProject project, Logger logger) {
return true;
}
_XCResultIssueHandlingResult _handleXCResultIssue({required XCResultIssue issue, required Logger logger}) {
_XCResultIssueHandlingResult _handleXCResultIssue({
required XCResultIssue issue,
required XcodeBuildResult result,
required Logger logger,
}) {
// Issue summary from xcresult.
final StringBuffer issueSummaryBuffer = StringBuffer();
issueSummaryBuffer.write(issue.subType ?? 'Unknown');
@ -761,20 +800,61 @@ _XCResultIssueHandlingResult _handleXCResultIssue({required XCResultIssue issue,
if (missingPlatform != null) {
return _XCResultIssueHandlingResult(requiresProvisioningProfile: false, hasProvisioningProfileIssue: false, missingPlatform: missingPlatform);
}
} else if (message.toLowerCase().contains('redefinition of module')) {
final String? duplicateModule = _parseModuleRedefinition(message);
return _XCResultIssueHandlingResult(
requiresProvisioningProfile: false,
hasProvisioningProfileIssue: false,
duplicateModule: duplicateModule,
);
} else if (message.toLowerCase().contains('duplicate symbols')) {
// The message does not contain the plugin name, must parse the stdout.
String? duplicateModule;
if (result.stdout != null) {
duplicateModule = _parseDuplicateSymbols(result.stdout!);
}
return _XCResultIssueHandlingResult(
requiresProvisioningProfile: false,
hasProvisioningProfileIssue: false,
duplicateModule: duplicateModule,
);
} else if (message.toLowerCase().contains('not found')) {
final String? missingModule = _parseMissingModule(message);
if (missingModule != null) {
return _XCResultIssueHandlingResult(
requiresProvisioningProfile: false,
hasProvisioningProfileIssue: false,
missingModule: missingModule,
);
}
}
return _XCResultIssueHandlingResult(requiresProvisioningProfile: false, hasProvisioningProfileIssue: false);
}
// Returns `true` if at least one issue is detected.
bool _handleIssues(XCResult? xcResult, Logger logger, XcodeBuildExecution? xcodeBuildExecution) {
Future<bool> _handleIssues(
XcodeBuildResult result,
XcodeBuildExecution? xcodeBuildExecution, {
required FlutterProject project,
required SupportedPlatform platform,
required Logger logger,
required FileSystem fileSystem,
}) async {
bool requiresProvisioningProfile = false;
bool hasProvisioningProfileIssue = false;
bool issueDetected = false;
String? missingPlatform;
final List<String> duplicateModules = <String>[];
final List<String> missingModules = <String>[];
final XCResult? xcResult = result.xcResult;
if (xcResult != null && xcResult.parseSuccess) {
for (final XCResultIssue issue in xcResult.issues) {
final _XCResultIssueHandlingResult handlingResult = _handleXCResultIssue(issue: issue, logger: logger);
final _XCResultIssueHandlingResult handlingResult = _handleXCResultIssue(
issue: issue,
result: result,
logger: logger,
);
if (handlingResult.hasProvisioningProfileIssue) {
hasProvisioningProfileIssue = true;
}
@ -782,12 +862,20 @@ bool _handleIssues(XCResult? xcResult, Logger logger, XcodeBuildExecution? xcode
requiresProvisioningProfile = true;
}
missingPlatform = handlingResult.missingPlatform;
if (handlingResult.duplicateModule != null) {
duplicateModules.add(handlingResult.duplicateModule!);
}
if (handlingResult.missingModule != null) {
missingModules.add(handlingResult.missingModule!);
}
issueDetected = true;
}
} else if (xcResult != null) {
globals.printTrace('XCResult parsing error: ${xcResult.parsingErrorMessage}');
}
final XcodeBasedProject xcodeProject = platform == SupportedPlatform.ios ? project.ios : project.macos;
if (requiresProvisioningProfile) {
logger.printError(noProvisioningProfileInstruction, emphasis: true);
} else if ((!issueDetected || hasProvisioningProfileIssue) && _missingDevelopmentTeam(xcodeBuildExecution)) {
@ -803,11 +891,84 @@ bool _handleIssues(XCResult? xcResult, Logger logger, XcodeBuildExecution? xcode
logger.printError("Also try selecting 'Product > Build' to fix the problem.");
} else if (missingPlatform != null) {
logger.printError(missingPlatformInstructions(missingPlatform), emphasis: true);
} else if (duplicateModules.isNotEmpty) {
final bool usesCocoapods = xcodeProject.podfile.existsSync();
final bool usesSwiftPackageManager = project.usesSwiftPackageManager;
if (usesCocoapods && usesSwiftPackageManager) {
logger.printError(
'Your project uses both CocoaPods and Swift Package Manager, which can '
'cause the above error. It may be caused by there being both a CocoaPod '
'and Swift Package Manager dependency for the following module(s): '
'${duplicateModules.join(', ')}.\n\n'
'You can try to identify which Pod the conflicting module is from by '
'looking at your "ios/Podfile.lock" dependency tree and requesting the '
'author add Swift Package Manager compatibility. See https://stackoverflow.com/a/27955017 '
'to learn more about understanding Podlock dependency tree. \n\n'
'You can also disable Swift Package Manager for the project by adding the '
'following in the project\'s pubspec.yaml under the "flutter" section:\n'
' "disable-swift-package-manager: true"\n',
);
}
} else if (missingModules.isNotEmpty) {
final bool usesCocoapods = xcodeProject.podfile.existsSync();
final bool usesSwiftPackageManager = project.usesSwiftPackageManager;
if (usesCocoapods && !usesSwiftPackageManager) {
final List<String> swiftPackageOnlyPlugins = <String>[];
for (final String module in missingModules) {
if (await _isPluginSwiftPackageOnly(
platform: platform,
project: project,
pluginName: module,
fileSystem: fileSystem,
)) {
swiftPackageOnlyPlugins.add(module);
}
}
if (swiftPackageOnlyPlugins.isNotEmpty) {
logger.printError(
'Your project uses CocoaPods as a dependency manager, but the following '
'plugin(s) only support Swift Package Manager: ${swiftPackageOnlyPlugins.join(', ')}.\n'
'Try enabling Swift Package Manager with "flutter config --enable-swift-package-manager".',
);
}
}
}
return issueDetected;
}
/// Returns true if a Package.swift is found for the plugin and a podspec is not.
Future<bool> _isPluginSwiftPackageOnly({
required SupportedPlatform platform,
required FlutterProject project,
required String pluginName,
required FileSystem fileSystem,
}) async {
final List<Plugin> plugins = await findPlugins(project);
final Plugin? matched = plugins
.where((Plugin plugin) =>
plugin.name.toLowerCase() == pluginName.toLowerCase() &&
plugin.platforms[platform.name] != null)
.firstOrNull;
if (matched == null) {
return false;
}
final String? swiftPackagePath = matched.pluginSwiftPackageManifestPath(
fileSystem,
platform.name,
);
final bool swiftPackageExists = swiftPackagePath != null &&
fileSystem.file(swiftPackagePath).existsSync();
final String? podspecPath = matched.pluginPodspecPath(
fileSystem,
platform.name,
);
final bool podspecExists = podspecPath != null &&
fileSystem.file(podspecPath).existsSync();
return swiftPackageExists && !podspecExists;
}
// Return 'true' a missing development team issue is detected.
bool _missingDevelopmentTeam(XcodeBuildExecution? xcodeBuildExecution) {
// Make sure the user has specified one of:
@ -852,22 +1013,68 @@ String? _parseMissingPlatform(String message) {
return pattern.firstMatch(message)?.group(1);
}
String? _parseModuleRedefinition(String message) {
// Example: "Redefinition of module 'plugin_1_name'"
final RegExp pattern = RegExp(r"Redefinition of module '(.*?)'");
final RegExpMatch? match = pattern.firstMatch(message);
if (match != null && match.groupCount > 0) {
final String? version = match.group(1);
return version;
}
return null;
}
String? _parseDuplicateSymbols(String message) {
// Example: "duplicate symbol '_$s29plugin_1_name23PluginNamePluginC9setDouble3key5valueySS_SdtF' in:
// /Users/username/path/to/app/build/ios/Debug-iphonesimulator/plugin_1_name/plugin_1_name.framework/plugin_1_name[arm64][5](PluginNamePlugin.o)
final RegExp pattern = RegExp(r'duplicate symbol [\s|\S]*?\/(.*)\.o');
final RegExpMatch? match = pattern.firstMatch(message);
if (match != null && match.groupCount > 0) {
final String? version = match.group(1);
if (version != null) {
return version.split('/').last.split('[').first.split('(').first;
}
return version;
}
return null;
}
String? _parseMissingModule(String message) {
// Example: "Module 'plugin_1_name' not found"
final RegExp pattern = RegExp(r"Module '(.*?)' not found");
final RegExpMatch? match = pattern.firstMatch(message);
if (match != null && match.groupCount > 0) {
final String? version = match.group(1);
return version;
}
return null;
}
// The result of [_handleXCResultIssue].
class _XCResultIssueHandlingResult {
_XCResultIssueHandlingResult({
required this.requiresProvisioningProfile,
required this.hasProvisioningProfileIssue,
this.missingPlatform,
this.duplicateModule,
this.missingModule,
});
// An issue indicates that user didn't provide the provisioning profile.
/// An issue indicates that user didn't provide the provisioning profile.
final bool requiresProvisioningProfile;
// An issue indicates that there is a provisioning profile issue.
/// An issue indicates that there is a provisioning profile issue.
final bool hasProvisioningProfileIssue;
final String? missingPlatform;
/// An issue indicates a module is declared twice, potentially due to being
/// used in both Swift Package Manager and CocoaPods.
final String? duplicateModule;
/// An issue indicates a module was imported but not found, potentially due
/// to it being Swift Package Manager compatible only.
final String? missingModule;
}
const String _kResultBundlePath = 'temporary_xcresult_bundle';

View File

@ -64,6 +64,35 @@ class PlistParser {
}
}
/// Returns the content, converted to JSON, of the plist file located at
/// [filePath].
///
/// If [filePath] points to a non-existent file or a file that's not a
/// valid property list file, this will return null.
String? plistJsonContent(String filePath) {
if (!_fileSystem.isFileSync(_plutilExecutable)) {
throw const FileNotFoundException(_plutilExecutable);
}
final List<String> args = <String>[
_plutilExecutable,
'-convert',
'json',
'-o',
'-',
filePath,
];
try {
final String jsonContent = _processUtils.runSync(
args,
throwOnError: true,
).stdout.trim();
return jsonContent;
} on ProcessException catch (error) {
_logger.printError('$error');
return null;
}
}
/// Replaces the string key in the given plist file with the given value.
///
/// If the value is null, then the key will be removed.

View File

@ -571,7 +571,15 @@ class IOSSimulator extends Device {
deviceID: id,
);
if (!buildResult.success) {
await diagnoseXcodeBuildFailure(buildResult, globals.flutterUsage, globals.logger, globals.analytics);
await diagnoseXcodeBuildFailure(
buildResult,
analytics: globals.analytics,
fileSystem: globals.fs,
flutterUsage: globals.flutterUsage,
logger: globals.logger,
platform: SupportedPlatform.ios,
project: app.project.parent,
);
throwToolExit('Could not build the application for the simulator.');
}

View File

@ -16,6 +16,7 @@ import '../convert.dart';
import '../globals.dart' as globals;
import '../ios/xcode_build_settings.dart';
import '../ios/xcodeproj.dart';
import '../migrations/swift_package_manager_integration_migration.dart';
import '../migrations/xcode_project_object_version_migration.dart';
import '../migrations/xcode_script_build_phase_migration.dart';
import '../migrations/xcode_thin_binary_build_phase_input_paths_migration.dart';
@ -85,6 +86,16 @@ Future<void> buildMacOS({
XcodeThinBinaryBuildPhaseInputPathsMigration(flutterProject.macos, globals.logger),
FlutterApplicationMigration(flutterProject.macos, globals.logger),
NSApplicationMainDeprecationMigration(flutterProject.macos, globals.logger),
if (flutterProject.usesSwiftPackageManager && flutterProject.macos.flutterPluginSwiftPackageManifest.existsSync())
SwiftPackageManagerIntegrationMigration(
flutterProject.macos,
SupportedPlatform.macos,
buildInfo,
xcodeProjectInterpreter: globals.xcodeProjectInterpreter!,
logger: globals.logger,
fileSystem: globals.fs,
plistParser: globals.plistParser,
),
];
final ProjectMigration migration = ProjectMigration(migrators);
@ -101,6 +112,11 @@ Future<void> buildMacOS({
targetOverride: targetOverride,
useMacOSConfig: true,
);
// TODO(vashworth): Call `SwiftPackageManager.updateMinimumDeployment`
// using MACOSX_DEPLOYMENT_TARGET once https://github.com/flutter/flutter/issues/146204
// is fixed.
await processPodsIfNeeded(flutterProject.macos, getMacOSBuildDirectory(), buildInfo.mode);
// If the xcfilelists do not exist, create empty version.
if (!flutterProject.macos.inputFileList.existsSync()) {

View File

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../base/error_handling_io.dart';
import '../base/fingerprint.dart';
import '../build_info.dart';
import '../cache.dart';
@ -14,20 +15,57 @@ import '../project.dart';
Future<void> processPodsIfNeeded(
XcodeBasedProject xcodeProject,
String buildDirectory,
BuildMode buildMode) async {
BuildMode buildMode, {
bool forceCocoaPodsOnly = false,
}) async {
final FlutterProject project = xcodeProject.parent;
// Ensure that the plugin list is up to date, since hasPlugins relies on it.
await refreshPluginsList(project, macOSPlatform: project.macos.existsSync());
if (!(hasPlugins(project) || (project.isModule && xcodeProject.podfile.existsSync()))) {
// When using Swift Package Manager, the Podfile may not exist so if there
// isn't a Podfile, skip processing pods.
if (project.usesSwiftPackageManager && !xcodeProject.podfile.existsSync() && !forceCocoaPodsOnly) {
return;
}
// If the Xcode project, Podfile, or generated xcconfig have changed since
// last run, pods should be updated.
// Ensure that the plugin list is up to date, since hasPlugins relies on it.
await refreshPluginsList(
project,
iosPlatform: project.ios.existsSync(),
macOSPlatform: project.macos.existsSync(),
forceCocoaPodsOnly: forceCocoaPodsOnly,
);
// If there are no plugins and if the project is a not module with an existing
// podfile, skip processing pods
if (!hasPlugins(project) && !(project.isModule && xcodeProject.podfile.existsSync())) {
return;
}
// If forcing the use of only CocoaPods, but the project is using Swift
// Package Manager, print a warning that CocoaPods will be used.
if (forceCocoaPodsOnly && project.usesSwiftPackageManager) {
globals.logger.printWarning(
'Swift Package Manager does not yet support this command. '
'CocoaPods will be used instead.');
// If CocoaPods has been deintegrated, add it back.
if (!xcodeProject.podfile.existsSync()) {
await globals.cocoaPods?.setupPodfile(xcodeProject);
}
// Delete Swift Package Manager manifest to invalidate fingerprinter
ErrorHandlingFileSystem.deleteIfExists(
xcodeProject.flutterPluginSwiftPackageManifest,
);
}
// If the Xcode project, Podfile, generated plugin Swift Package, or podhelper
// have changed since last run, pods should be updated.
final Fingerprinter fingerprinter = Fingerprinter(
fingerprintPath: globals.fs.path.join(buildDirectory, 'pod_inputs.fingerprint'),
paths: <String>[
xcodeProject.xcodeProjectInfoFile.path,
xcodeProject.podfile.path,
if (xcodeProject.flutterPluginSwiftPackageManifest.existsSync())
xcodeProject.flutterPluginSwiftPackageManifest.path,
globals.fs.path.join(
Cache.flutterRoot!,
'packages',

View File

@ -21,8 +21,8 @@ import '../cache.dart';
import '../ios/xcodeproj.dart';
import '../migrations/cocoapods_script_symlink.dart';
import '../migrations/cocoapods_toolchain_directory_migration.dart';
import '../project.dart';
import '../reporting/reporting.dart';
import '../xcode_project.dart';
const String noCocoaPodsConsequence = '''
CocoaPods is a package manager for iOS or macOS platform code.
@ -166,6 +166,10 @@ class CocoaPods {
bool dependenciesChanged = true,
}) async {
if (!xcodeProject.podfile.existsSync()) {
// Swift Package Manager doesn't need Podfile, so don't error.
if (xcodeProject.parent.usesSwiftPackageManager) {
return false;
}
throwToolExit('Podfile missing');
}
_warnIfPodfileOutOfDate(xcodeProject);
@ -258,6 +262,18 @@ class CocoaPods {
addPodsDependencyToFlutterXcconfig(xcodeProject);
return;
}
final File podfileTemplate = await getPodfileTemplate(
xcodeProject,
runnerProject,
);
podfileTemplate.copySync(podfile.path);
addPodsDependencyToFlutterXcconfig(xcodeProject);
}
Future<File> getPodfileTemplate(
XcodeBasedProject xcodeProject,
Directory runnerProject,
) async {
String podfileTemplateName;
if (xcodeProject is MacOSProject) {
podfileTemplateName = 'Podfile-macos';
@ -268,7 +284,7 @@ class CocoaPods {
)).containsKey('SWIFT_VERSION');
podfileTemplateName = isSwift ? 'Podfile-ios-swift' : 'Podfile-ios-objc';
}
final File podfileTemplate = _fileSystem.file(_fileSystem.path.join(
return _fileSystem.file(_fileSystem.path.join(
Cache.flutterRoot!,
'packages',
'flutter_tools',
@ -276,8 +292,6 @@ class CocoaPods {
'cocoapods',
podfileTemplateName,
));
podfileTemplate.copySync(podfile.path);
addPodsDependencyToFlutterXcconfig(xcodeProject);
}
/// Ensures all `Flutter/Xxx.xcconfig` files for the given Xcode-based
@ -287,12 +301,24 @@ class CocoaPods {
_addPodsDependencyToFlutterXcconfig(xcodeProject, 'Release');
}
String includePodsXcconfig(String mode) {
return 'Pods/Target Support Files/Pods-Runner/Pods-Runner.${mode
.toLowerCase()}.xcconfig';
}
bool xcconfigIncludesPods(File xcodeConfig) {
if (xcodeConfig.existsSync()) {
final String content = xcodeConfig.readAsStringSync();
return content.contains('Pods/Target Support Files/Pods-');
}
return false;
}
void _addPodsDependencyToFlutterXcconfig(XcodeBasedProject xcodeProject, String mode) {
final File file = xcodeProject.xcodeConfigFor(mode);
if (file.existsSync()) {
final String content = file.readAsStringSync();
final String includeFile = 'Pods/Target Support Files/Pods-Runner/Pods-Runner.${mode
.toLowerCase()}.xcconfig';
final String includeFile = includePodsXcconfig(mode);
final String include = '#include? "$includeFile"';
if (!content.contains('Pods/Target Support Files/Pods-')) {
file.writeAsStringSync('$include\n$content', flush: true);

View File

@ -0,0 +1,257 @@
// 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 '../base/common.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../plugins.dart';
import '../project.dart';
import 'cocoapods.dart';
import 'swift_package_manager.dart';
/// Flutter has two dependency management solutions for iOS and macOS
/// applications: CocoaPods and Swift Package Manager. They may be used
/// individually or together. This class handles setting up required files and
/// project settings for the dependency manager(s) being used.
class DarwinDependencyManagement {
DarwinDependencyManagement({
required FlutterProject project,
required List<Plugin> plugins,
required CocoaPods cocoapods,
required SwiftPackageManager swiftPackageManager,
required FileSystem fileSystem,
required Logger logger,
}) : _project = project,
_plugins = plugins,
_cocoapods = cocoapods,
_swiftPackageManager = swiftPackageManager,
_fileSystem = fileSystem,
_logger = logger;
final FlutterProject _project;
final List<Plugin> _plugins;
final CocoaPods _cocoapods;
final SwiftPackageManager _swiftPackageManager;
final FileSystem _fileSystem;
final Logger _logger;
/// Generates/updates required files and project settings for Darwin
/// Dependency Managers (CocoaPods and Swift Package Manager). Projects may
/// use only CocoaPods (if no SPM compatible dependencies or SPM has been
/// disabled), only Swift Package Manager (if no CocoaPod dependencies), or
/// both. This only generates files for the manager(s) being used.
///
/// CocoaPods requires a Podfile and certain values in the Flutter xcconfig
/// files.
///
/// Swift Package Manager requires a generated Package.swift and certain
/// settings in the Xcode project's project.pbxproj and xcscheme (done later
/// before build).
Future<void> setUp({
required SupportedPlatform platform,
}) async {
if (platform != SupportedPlatform.ios &&
platform != SupportedPlatform.macos) {
throwToolExit(
'The platform ${platform.name} is incompatible with Darwin Dependency Managers. Only iOS and macOS are allowed.',
);
}
final XcodeBasedProject xcodeProject = platform == SupportedPlatform.ios
? _project.ios
: _project.macos;
if (_project.usesSwiftPackageManager) {
await _swiftPackageManager.generatePluginsSwiftPackage(
_plugins,
platform,
xcodeProject,
);
} else if (xcodeProject.flutterPluginSwiftPackageInProjectSettings) {
// If Swift Package Manager is not enabled but the project is already
// integrated for Swift Package Manager, pass no plugins to the generator.
// This will still generate the required Package.swift, but it will have
// no dependencies.
await _swiftPackageManager.generatePluginsSwiftPackage(
<Plugin>[],
platform,
xcodeProject,
);
}
// Skip updating Podfile if project is a module, since it will use a
// different module-specific Podfile.
if (_project.isModule) {
return;
}
final (:int totalCount, :int swiftPackageCount, :int podCount) = await _evaluatePluginsAndPrintWarnings(
platform: platform,
xcodeProject: xcodeProject,
);
final bool useCocoapods;
if (_project.usesSwiftPackageManager) {
useCocoapods = _usingCocoaPodsPlugin(
pluginCount: totalCount,
swiftPackageCount: swiftPackageCount,
cocoapodCount: podCount,
);
} else {
// When Swift Package Manager is not enabled, set up Podfile if plugins
// is not empty, regardless of if plugins are CocoaPod compatible. This
// is done because `processPodsIfNeeded` uses `hasPlugins` to determine
// whether to run.
useCocoapods = _plugins.isNotEmpty;
}
if (useCocoapods) {
await _cocoapods.setupPodfile(xcodeProject);
}
/// The user may have a custom maintained Podfile that they're running `pod install`
/// on themselves.
else if (xcodeProject.podfile.existsSync() && xcodeProject.podfileLock.existsSync()) {
_cocoapods.addPodsDependencyToFlutterXcconfig(xcodeProject);
}
}
bool _usingCocoaPodsPlugin({
required int pluginCount,
required int swiftPackageCount,
required int cocoapodCount,
}) {
if (_project.usesSwiftPackageManager) {
if (pluginCount == swiftPackageCount) {
return false;
}
}
return cocoapodCount > 0;
}
/// Returns count of total number of plugins, number of Swift Package Manager
/// compatible plugins, and number of CocoaPods compatible plugins. A plugin
/// can be both Swift Package Manager and CocoaPods compatible.
///
/// Prints warnings when using a plugin incompatible with the available Darwin
/// Dependency Manager (Swift Package Manager or CocoaPods).
///
/// Prints message prompting the user to deintegrate CocoaPods if using all
/// Swift Package plugins.
Future<({int totalCount, int swiftPackageCount, int podCount})> _evaluatePluginsAndPrintWarnings({
required SupportedPlatform platform,
required XcodeBasedProject xcodeProject,
}) async {
int pluginCount = 0;
int swiftPackageCount = 0;
int cocoapodCount = 0;
for (final Plugin plugin in _plugins) {
if (plugin.platforms[platform.name] == null) {
continue;
}
final String? swiftPackagePath = plugin.pluginSwiftPackageManifestPath(
_fileSystem,
platform.name,
);
final bool swiftPackageManagerCompatible = swiftPackagePath != null &&
_fileSystem.file(swiftPackagePath).existsSync();
final String? podspecPath = plugin.pluginPodspecPath(
_fileSystem,
platform.name,
);
final bool cocoaPodsCompatible =
podspecPath != null && _fileSystem.file(podspecPath).existsSync();
// If a plugin is missing both a Package.swift and Podspec, it won't be
// included by either Swift Package Manager or Cocoapods. This can happen
// when a plugin doesn't have native platform code.
// For example, image_picker_macos only uses dart code.
if (!swiftPackageManagerCompatible && !cocoaPodsCompatible) {
continue;
}
pluginCount += 1;
if (swiftPackageManagerCompatible) {
swiftPackageCount += 1;
}
if (cocoaPodsCompatible) {
cocoapodCount += 1;
}
// If not using Swift Package Manager and plugin does not have podspec
// but does have a Package.swift, throw an error. Otherwise, it'll error
// when it builds.
if (!_project.usesSwiftPackageManager &&
!cocoaPodsCompatible &&
swiftPackageManagerCompatible) {
throwToolExit(
'Plugin ${plugin.name} is only Swift Package Manager compatible. Try '
'enabling Swift Package Manager by running '
'"flutter config --enable-swift-package-manager" or remove the '
'plugin as a dependency.');
}
}
// Only show warnings to remove CocoaPods if the project is using Swift
// Package Manager, has already been migrated to have SPM integration, and
// all plugins are Swift Packages.
if (_project.usesSwiftPackageManager &&
xcodeProject.flutterPluginSwiftPackageInProjectSettings &&
pluginCount == swiftPackageCount &&
swiftPackageCount != 0) {
final bool podfileExists = xcodeProject.podfile.existsSync();
if (podfileExists) {
// If all plugins are Swift Packages and the Podfile matches the
// default template, recommend pod deintegration.
final File podfileTemplate = await _cocoapods.getPodfileTemplate(
xcodeProject,
xcodeProject.xcodeProject,
);
final String configWarning = '${_podIncludeInConfigWarning(xcodeProject, 'Debug')}'
'${_podIncludeInConfigWarning(xcodeProject, 'Release')}';
if (xcodeProject.podfile.readAsStringSync() ==
podfileTemplate.readAsStringSync()) {
_logger.printWarning(
'All plugins found for ${platform.name} are Swift Packages, but your '
'project still has CocoaPods integration. To remove CocoaPods '
'integration, complete the following steps:\n'
' * In the ${platform.name}/ directory run "pod deintegrate"\n'
' * Also in the ${platform.name}/ directory, delete the Podfile\n'
'$configWarning\n'
"Removing CocoaPods integration will improve the project's build time.");
} else {
// If all plugins are Swift Packages, but the Podfile has custom logic,
// recommend migrating manually.
_logger.printWarning(
'All plugins found for ${platform.name} are Swift Packages, but your '
'project still has CocoaPods integration. Your project uses a '
'non-standard Podfile and will need to be migrated to Swift Package '
'Manager manually. Some steps you may need to complete include:\n'
' * In the ${platform.name}/ directory run "pod deintegrate"\n'
' * Transition any Pod dependencies to Swift Package equivalents. '
'See https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app\n'
' * Transition any custom logic\n'
'$configWarning\n'
"Removing CocoaPods integration will improve the project's build time.");
}
}
}
return (
totalCount: pluginCount,
swiftPackageCount: swiftPackageCount,
podCount: cocoapodCount,
);
}
String _podIncludeInConfigWarning(XcodeBasedProject xcodeProject, String mode) {
final File xcconfigFile = xcodeProject.xcodeConfigFor(mode);
final bool configIncludesPods = _cocoapods.xcconfigIncludesPods(xcconfigFile);
if (configIncludesPods) {
return ' * Remove the include to '
'"${_cocoapods.includePodsXcconfig(mode)}" in your '
'${xcconfigFile.parent.parent.basename}/${xcconfigFile.parent.basename}/${xcconfigFile.basename}\n';
}
return '';
}
}

View File

@ -0,0 +1,196 @@
// 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 '../base/common.dart';
import '../base/file_system.dart';
import '../base/template.dart';
import '../base/version.dart';
import '../plugins.dart';
import '../project.dart';
import 'swift_packages.dart';
/// Swift Package Manager is a dependency management solution for iOS and macOS
/// applications.
///
/// See also:
/// * https://www.swift.org/documentation/package-manager/ - documentation on
/// Swift Package Manager.
/// * https://developer.apple.com/documentation/packagedescription/package -
/// documentation on Swift Package Manager manifest file, Package.swift.
class SwiftPackageManager {
const SwiftPackageManager({
required FileSystem fileSystem,
required TemplateRenderer templateRenderer,
}) : _fileSystem = fileSystem,
_templateRenderer = templateRenderer;
final FileSystem _fileSystem;
final TemplateRenderer _templateRenderer;
static const String _defaultFlutterPluginsSwiftPackageName = 'FlutterGeneratedPluginSwiftPackage';
static final SwiftPackageSupportedPlatform _iosSwiftPackageSupportedPlatform = SwiftPackageSupportedPlatform(
platform: SwiftPackagePlatform.ios,
version: Version(12, 0, null),
);
static final SwiftPackageSupportedPlatform _macosSwiftPackageSupportedPlatform = SwiftPackageSupportedPlatform(
platform: SwiftPackagePlatform.macos,
version: Version(10, 14, null),
);
/// Creates a Swift Package called 'FlutterGeneratedPluginSwiftPackage' that
/// has dependencies on Flutter plugins that are compatible with Swift
/// Package Manager.
Future<void> generatePluginsSwiftPackage(
List<Plugin> plugins,
SupportedPlatform platform,
XcodeBasedProject project,
) async {
_validatePlatform(platform);
final (
List<SwiftPackagePackageDependency> packageDependencies,
List<SwiftPackageTargetDependency> targetDependencies
) = _dependenciesForPlugins(plugins, platform);
// If there aren't any Swift Package plugins and the project hasn't been
// migrated yet, don't generate a Swift package or migrate the app since
// it's not needed. If the project has already been migrated, regenerate
// the Package.swift even if there are no dependencies in case there
// were dependencies previously.
if (packageDependencies.isEmpty && !project.flutterPluginSwiftPackageInProjectSettings) {
return;
}
final SwiftPackageSupportedPlatform swiftSupportedPlatform;
if (platform == SupportedPlatform.ios) {
swiftSupportedPlatform = _iosSwiftPackageSupportedPlatform;
} else {
swiftSupportedPlatform = _macosSwiftPackageSupportedPlatform;
}
// FlutterGeneratedPluginSwiftPackage must be statically linked to ensure
// any dynamic dependencies are linked to Runner and prevent undefined symbols.
final SwiftPackageProduct generatedProduct = SwiftPackageProduct(
name: _defaultFlutterPluginsSwiftPackageName,
targets: <String>[_defaultFlutterPluginsSwiftPackageName],
libraryType: SwiftPackageLibraryType.static,
);
final SwiftPackageTarget generatedTarget = SwiftPackageTarget.defaultTarget(
name: _defaultFlutterPluginsSwiftPackageName,
dependencies: targetDependencies,
);
final SwiftPackage pluginsPackage = SwiftPackage(
manifest: project.flutterPluginSwiftPackageManifest,
name: _defaultFlutterPluginsSwiftPackageName,
platforms: <SwiftPackageSupportedPlatform>[swiftSupportedPlatform],
products: <SwiftPackageProduct>[generatedProduct],
dependencies: packageDependencies,
targets: <SwiftPackageTarget>[generatedTarget],
templateRenderer: _templateRenderer,
);
pluginsPackage.createSwiftPackage();
}
(List<SwiftPackagePackageDependency>, List<SwiftPackageTargetDependency>) _dependenciesForPlugins(
List<Plugin> plugins,
SupportedPlatform platform,
) {
final List<SwiftPackagePackageDependency> packageDependencies =
<SwiftPackagePackageDependency>[];
final List<SwiftPackageTargetDependency> targetDependencies =
<SwiftPackageTargetDependency>[];
for (final Plugin plugin in plugins) {
final String? pluginSwiftPackageManifestPath = plugin.pluginSwiftPackageManifestPath(
_fileSystem,
platform.name,
);
if (plugin.platforms[platform.name] == null ||
pluginSwiftPackageManifestPath == null ||
!_fileSystem.file(pluginSwiftPackageManifestPath).existsSync()) {
continue;
}
packageDependencies.add(SwiftPackagePackageDependency(
name: plugin.name,
path: _fileSystem.file(pluginSwiftPackageManifestPath).parent.path,
));
// The target dependency product name is hyphen separated because it's
// the dependency's library name, which Swift Package Manager will
// automatically use as the CFBundleIdentifier if linked dynamically. The
// CFBundleIdentifier cannot contain underscores.
targetDependencies.add(SwiftPackageTargetDependency.product(
name: plugin.name.replaceAll('_', '-'),
packageName: plugin.name,
));
}
return (packageDependencies, targetDependencies);
}
/// Validates the platform is either iOS or macOS, otherwise throw an error.
static void _validatePlatform(SupportedPlatform platform) {
if (platform != SupportedPlatform.ios &&
platform != SupportedPlatform.macos) {
throwToolExit(
'The platform ${platform.name} is not compatible with Swift Package Manager. '
'Only iOS and macOS are allowed.',
);
}
}
/// If the project's IPHONEOS_DEPLOYMENT_TARGET/MACOSX_DEPLOYMENT_TARGET is
/// higher than the FlutterGeneratedPluginSwiftPackage's default
/// SupportedPlatform, increase the SupportedPlatform to match the project's
/// deployment target.
///
/// This is done for the use case of a plugin requiring a higher iOS/macOS
/// version than FlutterGeneratedPluginSwiftPackage.
///
/// Swift Package Manager emits an error if a dependency isnt compatible
/// with the top-level packages deployment version. The deployment target of
/// a packages dependencies must be lower than or equal to the top-level
/// packages deployment target version for a particular platform.
///
/// To still be able to use the plugin, the user can increase the Xcode
/// project's iOS/macOS deployment target and this will then increase the
/// deployment target for FlutterGeneratedPluginSwiftPackage.
static void updateMinimumDeployment({
required XcodeBasedProject project,
required SupportedPlatform platform,
required String deploymentTarget,
}) {
final Version? projectDeploymentTargetVersion = Version.parse(deploymentTarget);
final SwiftPackageSupportedPlatform defaultPlatform;
final SwiftPackagePlatform packagePlatform;
if (platform == SupportedPlatform.ios) {
defaultPlatform = _iosSwiftPackageSupportedPlatform;
packagePlatform = SwiftPackagePlatform.ios;
} else {
defaultPlatform = _macosSwiftPackageSupportedPlatform;
packagePlatform = SwiftPackagePlatform.macos;
}
if (projectDeploymentTargetVersion == null ||
projectDeploymentTargetVersion <= defaultPlatform.version ||
!project.flutterPluginSwiftPackageManifest.existsSync()) {
return;
}
final String manifestContents = project.flutterPluginSwiftPackageManifest.readAsStringSync();
final String oldSupportedPlatform = defaultPlatform.format();
final String newSupportedPlatform = SwiftPackageSupportedPlatform(
platform: packagePlatform,
version: projectDeploymentTargetVersion,
).format();
project.flutterPluginSwiftPackageManifest.writeAsStringSync(
manifestContents.replaceFirst(oldSupportedPlatform, newSupportedPlatform),
);
}
}

View File

@ -0,0 +1,403 @@
// 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 '../base/file_system.dart';
import '../base/template.dart';
import '../base/version.dart';
/// Swift toolchain version included with Xcode 15.0.
const String minimumSwiftToolchainVersion = '5.9';
const String _swiftPackageTemplate = '''
// swift-tools-version: {{swiftToolsVersion}}
// 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: "{{packageName}}",
{{#platforms}}
platforms: [
{{platforms}}
],
{{/platforms}}
products: [
{{products}}
],
dependencies: [
{{dependencies}}
],
targets: [
{{targets}}
]
)
''';
const String _swiftPackageSourceTemplate = '''
//
// Generated file. Do not edit.
//
''';
const String _singleIndent = ' ';
const String _doubleIndent = '$_singleIndent$_singleIndent';
/// A Swift Package is reusable code that can be shared across projects and
/// with other developers in iOS and macOS applications. A Swift Package
/// requires a Package.swift. This class handles the formatting and creation of
/// a Package.swift.
///
/// See https://developer.apple.com/documentation/packagedescription/package
/// for more information about Swift Packages and Package.swift.
class SwiftPackage {
SwiftPackage({
required File manifest,
required String name,
required List<SwiftPackageSupportedPlatform> platforms,
required List<SwiftPackageProduct> products,
required List<SwiftPackagePackageDependency> dependencies,
required List<SwiftPackageTarget> targets,
required TemplateRenderer templateRenderer,
}) : _manifest = manifest,
_name = name,
_platforms = platforms,
_products = products,
_dependencies = dependencies,
_targets = targets,
_templateRenderer = templateRenderer;
/// [File] for Package.swift.
final File _manifest;
/// The name of the Swift package.
final String _name;
/// The list of minimum versions for platforms supported by the package.
final List<SwiftPackageSupportedPlatform> _platforms;
/// The list of products that this package vends and that clients can use.
final List<SwiftPackageProduct> _products;
/// The list of package dependencies.
final List<SwiftPackagePackageDependency> _dependencies;
/// The list of targets that are part of this package.
final List<SwiftPackageTarget> _targets;
final TemplateRenderer _templateRenderer;
/// Context for the [_swiftPackageTemplate] template.
Map<String, Object> get _templateContext {
return <String, Object>{
'swiftToolsVersion': minimumSwiftToolchainVersion,
'packageName': _name,
// Supported platforms can't be empty, so only include if not null.
'platforms': _formatPlatforms() ?? false,
'products': _formatProducts(),
'dependencies': _formatDependencies(),
'targets': _formatTargets(),
};
}
/// Create a Package.swift using settings from [_templateContext].
void createSwiftPackage() {
// Swift Packages require at least one source file per non-binary target,
// whether it be in Swift or Objective C. If the target does not have any
// files yet, create an empty Swift file.
for (final SwiftPackageTarget target in _targets) {
if (target.targetType == SwiftPackageTargetType.binaryTarget) {
continue;
}
final Directory targetDirectory = _manifest.parent
.childDirectory('Sources')
.childDirectory(target.name);
if (!targetDirectory.existsSync() || targetDirectory.listSync().isEmpty) {
final File requiredSwiftFile = targetDirectory.childFile(
'${target.name}.swift',
);
requiredSwiftFile.createSync(recursive: true);
requiredSwiftFile.writeAsStringSync(_swiftPackageSourceTemplate);
}
}
final String renderedTemplate = _templateRenderer.renderString(
_swiftPackageTemplate,
_templateContext,
);
_manifest.createSync(recursive: true);
_manifest.writeAsStringSync(renderedTemplate);
}
String? _formatPlatforms() {
if (_platforms.isEmpty) {
return null;
}
final List<String> platformStrings = _platforms
.map((SwiftPackageSupportedPlatform platform) => platform.format())
.toList();
return platformStrings.join(',\n$_doubleIndent');
}
String _formatProducts() {
if (_products.isEmpty) {
return '';
}
final List<String> libraries = _products
.map((SwiftPackageProduct product) => product.format())
.toList();
return libraries.join(',\n$_doubleIndent');
}
String _formatDependencies() {
if (_dependencies.isEmpty) {
return '';
}
final List<String> packages = _dependencies
.map((SwiftPackagePackageDependency dependency) => dependency.format())
.toList();
return packages.join(',\n$_doubleIndent');
}
String _formatTargets() {
if (_targets.isEmpty) {
return '';
}
final List<String> targetList =
_targets.map((SwiftPackageTarget target) => target.format()).toList();
return targetList.join(',\n$_doubleIndent');
}
}
enum SwiftPackagePlatform {
ios(name: '.iOS'),
macos(name: '.macOS'),
tvos(name: '.tvOS'),
watchos(name: '.watchOS');
const SwiftPackagePlatform({required this.name});
final String name;
}
/// A platform that the Swift package supports.
///
/// Representation of SupportedPlatform from
/// https://developer.apple.com/documentation/packagedescription/supportedplatform.
class SwiftPackageSupportedPlatform {
SwiftPackageSupportedPlatform({
required this.platform,
required this.version,
});
final SwiftPackagePlatform platform;
final Version version;
String format() {
// platforms: [
// .macOS("10.14"),
// .iOS("12.0"),
// ],
return '${platform.name}("$version")';
}
}
/// Types of library linking.
///
/// Representation of Product.Library.LibraryType from
/// https://developer.apple.com/documentation/packagedescription/product/library/librarytype.
enum SwiftPackageLibraryType {
dynamic(name: '.dynamic'),
static(name: '.static');
const SwiftPackageLibraryType({required this.name});
final String name;
}
/// An externally visible build artifact that's available to clients of the
/// package.
///
/// Representation of Product from
/// https://developer.apple.com/documentation/packagedescription/product.
class SwiftPackageProduct {
SwiftPackageProduct({
required this.name,
required this.targets,
this.libraryType,
});
final String name;
final SwiftPackageLibraryType? libraryType;
final List<String> targets;
String format() {
// products: [
// .library(name: "FlutterGeneratedPluginSwiftPackage", targets: ["FlutterGeneratedPluginSwiftPackage"]),
// .library(name: "FlutterDependenciesPackage", type: .dynamic, targets: ["FlutterDependenciesPackage"]),
// ],
String targetsString = '';
if (targets.isNotEmpty) {
final List<String> quotedTargets =
targets.map((String target) => '"$target"').toList();
targetsString = ', targets: [${quotedTargets.join(', ')}]';
}
String libraryTypeString = '';
if (libraryType != null) {
libraryTypeString = ', type: ${libraryType!.name}';
}
return '.library(name: "$name"$libraryTypeString$targetsString)';
}
}
/// A package dependency of a Swift package.
///
/// Representation of Package.Dependency from
/// https://developer.apple.com/documentation/packagedescription/package/dependency.
class SwiftPackagePackageDependency {
SwiftPackagePackageDependency({
required this.name,
required this.path,
});
final String name;
final String path;
String format() {
// dependencies: [
// .package(name: "image_picker_ios", path: "/path/to/packages/image_picker/image_picker_ios/ios/image_picker_ios"),
// ],
return '.package(name: "$name", path: "$path")';
}
}
/// Type of Target constructor.
///
/// See https://developer.apple.com/documentation/packagedescription/target for
/// more information.
enum SwiftPackageTargetType {
target(name: '.target'),
binaryTarget(name: '.binaryTarget');
const SwiftPackageTargetType({required this.name});
final String name;
}
/// A building block of a Swift Package that contains a set of source files
/// that Swift Package Manager compiles into a module.
///
/// Representation of Target from
/// https://developer.apple.com/documentation/packagedescription/target.
class SwiftPackageTarget {
SwiftPackageTarget.defaultTarget({
required this.name,
this.dependencies,
}) : path = null,
targetType = SwiftPackageTargetType.target;
SwiftPackageTarget.binaryTarget({
required this.name,
required String relativePath,
}) : path = relativePath,
dependencies = null,
targetType = SwiftPackageTargetType.binaryTarget;
final String name;
final String? path;
final List<SwiftPackageTargetDependency>? dependencies;
final SwiftPackageTargetType targetType;
String format() {
// targets: [
// .binaryTarget(
// name: "Flutter",
// path: "Flutter.xcframework"
// ),
// .target(
// name: "FlutterGeneratedPluginSwiftPackage",
// dependencies: [
// .target(name: "Flutter"),
// .product(name: "image_picker_ios", package: "image_picker_ios")
// ]
// ),
// ]
const String targetIndent = _doubleIndent;
const String targetDetailsIndent = '$_doubleIndent$_singleIndent';
final List<String> targetDetails = <String>[];
final String nameString = 'name: "$name"';
targetDetails.add(nameString);
if (path != null) {
final String pathString = 'path: "$path"';
targetDetails.add(pathString);
}
if (dependencies != null && dependencies!.isNotEmpty) {
final List<String> targetDependencies = dependencies!
.map((SwiftPackageTargetDependency dependency) => dependency.format())
.toList();
final String dependenciesString = '''
dependencies: [
${targetDependencies.join(",\n")}
$targetDetailsIndent]''';
targetDetails.add(dependenciesString);
}
return '''
${targetType.name}(
$targetDetailsIndent${targetDetails.join(",\n$targetDetailsIndent")}
$targetIndent)''';
}
}
/// Type of Target.Dependency constructor.
///
/// See https://developer.apple.com/documentation/packagedescription/target/dependency
/// for more information.
enum SwiftPackageTargetDependencyType {
product(name: '.product'),
target(name: '.target');
const SwiftPackageTargetDependencyType({required this.name});
final String name;
}
/// A dependency for the Target on a product from a package dependency or from
/// another Target in the same package.
///
/// Representation of Target.Dependency from
/// https://developer.apple.com/documentation/packagedescription/target/dependency.
class SwiftPackageTargetDependency {
SwiftPackageTargetDependency.product({
required this.name,
required String packageName,
}) : package = packageName,
dependencyType = SwiftPackageTargetDependencyType.product;
SwiftPackageTargetDependency.target({
required this.name,
}) : package = null,
dependencyType = SwiftPackageTargetDependencyType.target;
final String name;
final String? package;
final SwiftPackageTargetDependencyType dependencyType;
String format() {
// dependencies: [
// .target(name: "Flutter"),
// .product(name: "image_picker_ios", package: "image_picker_ios")
// ]
if (dependencyType == SwiftPackageTargetDependencyType.product) {
return '$_doubleIndent$_doubleIndent${dependencyType.name}(name: "$name", package: "$package")';
}
return '$_doubleIndent$_doubleIndent${dependencyType.name}(name: "$name")';
}
}

View File

@ -397,6 +397,51 @@ class Plugin {
/// Whether this plugin is a direct dependency of the app.
/// If [false], the plugin is a dependency of another plugin.
final bool isDirectDependency;
/// Expected path to the plugin's Package.swift. Returns null if the plugin
/// does not support the [platform] or the [platform] is not iOS or macOS.
String? pluginSwiftPackageManifestPath(
FileSystem fileSystem,
String platform,
) {
final String? platformDirectoryName = _darwinPluginDirectoryName(platform);
if (platformDirectoryName == null) {
return null;
}
return fileSystem.path.join(
path,
platformDirectoryName,
name,
'Package.swift',
);
}
/// Expected path to the plugin's podspec. Returns null if the plugin does
/// not support the [platform] or the [platform] is not iOS or macOS.
String? pluginPodspecPath(FileSystem fileSystem, String platform) {
final String? platformDirectoryName = _darwinPluginDirectoryName(platform);
if (platformDirectoryName == null) {
return null;
}
return fileSystem.path.join(path, platformDirectoryName, '$name.podspec');
}
String? _darwinPluginDirectoryName(String platform) {
final PluginPlatform? platformPlugin = platforms[platform];
if (platformPlugin == null ||
(platform != IOSPlugin.kConfigKey &&
platform != MacOSPlugin.kConfigKey)) {
return null;
}
// iOS and macOS code can be shared in "darwin" directory, otherwise
// respectively in "ios" or "macos" directories.
if (platformPlugin is DarwinPlugin &&
(platformPlugin as DarwinPlugin).sharedDarwinSource) {
return 'darwin';
}
return platform;
}
}
/// Metadata associated with the resolution of a platform interface of a plugin.

View File

@ -21,6 +21,7 @@ import 'features.dart';
import 'flutter_manifest.dart';
import 'flutter_plugins.dart';
import 'globals.dart' as globals;
import 'macos/xcode.dart';
import 'platform_plugins.dart';
import 'project_validator_result.dart';
import 'template.dart';
@ -31,14 +32,20 @@ export 'xcode_project.dart';
/// Enum for each officially supported platform.
enum SupportedPlatform {
android,
ios,
linux,
macos,
web,
windows,
fuchsia,
root, // Special platform to represent the root project directory
android(name: 'android'),
ios(name: 'ios'),
linux(name: 'linux'),
macos(name: 'macos'),
web(name: 'web'),
windows(name: 'windows'),
fuchsia(name: 'fuchsia'),
root(name: 'root'); // Special platform to represent the root project directory
const SupportedPlatform({
required this.name,
});
final String name;
}
class FlutterProjectFactory {
@ -261,6 +268,24 @@ class FlutterProject {
/// True if this project has an example application.
bool get hasExampleApp => _exampleDirectory(directory).existsSync();
/// True if this project doesn't have Swift Package Manager disabled in the
/// pubspec, has either an iOS or macOS platform implementation, is not a
/// module project, Xcode is 15 or greater, and the Swift Package Manager
/// feature is enabled.
bool get usesSwiftPackageManager {
if (!manifest.disabledSwiftPackageManager &&
(ios.existsSync() || macos.existsSync()) &&
!isModule) {
final Xcode? xcode = globals.xcode;
final Version? xcodeVersion = xcode?.currentVersion;
if (xcodeVersion == null || xcodeVersion.major < 15) {
return false;
}
return featureFlags.isSwiftPackageManagerEnabled;
}
return false;
}
/// Returns a list of platform names that are supported by the project.
List<SupportedPlatform> getSupportedPlatforms({bool includeRoot = false}) {
final List<SupportedPlatform> platforms = includeRoot ? <SupportedPlatform>[SupportedPlatform.root] : <SupportedPlatform>[];

View File

@ -115,6 +115,46 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform {
.childDirectory('Pods')
.childDirectory('Target Support Files')
.childDirectory('Pods-Runner');
/// The directory in the project that is managed by Flutter. As much as
/// possible, files that are edited by Flutter tooling after initial project
/// creation should live here.
Directory get managedDirectory => hostAppRoot.childDirectory('Flutter');
/// The subdirectory of [managedDirectory] that contains files that are
/// generated on the fly. All generated files that are not intended to be
/// checked in should live here.
Directory get ephemeralDirectory => managedDirectory
.childDirectory('ephemeral');
/// The Flutter generated directory for the Swift Package handling plugin
/// dependencies.
Directory get flutterPluginSwiftPackageDirectory => ephemeralDirectory
.childDirectory('Packages')
.childDirectory('FlutterGeneratedPluginSwiftPackage');
/// The Flutter generated Swift Package manifest (Package.swift) for plugin
/// dependencies.
File get flutterPluginSwiftPackageManifest =>
flutterPluginSwiftPackageDirectory.childFile('Package.swift');
/// Checks if FlutterGeneratedPluginSwiftPackage has been added to the
/// project's build settings by checking the contents of the pbxproj.
bool get flutterPluginSwiftPackageInProjectSettings {
return xcodeProjectInfoFile.existsSync() &&
xcodeProjectInfoFile
.readAsStringSync()
.contains('FlutterGeneratedPluginSwiftPackage');
}
Future<XcodeProjectInfo?> projectInfo() async {
final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter;
if (!xcodeProject.existsSync() || xcodeProjectInterpreter == null || !xcodeProjectInterpreter.isInstalled) {
return null;
}
return _projectInfo ??= await xcodeProjectInterpreter.getInfo(hostAppRoot.path);
}
XcodeProjectInfo? _projectInfo;
}
/// Represents the iOS sub-project of a Flutter project.
@ -167,16 +207,16 @@ class IosProject extends XcodeBasedProject {
/// Whether the Flutter application has an iOS project.
bool get exists => hostAppRoot.existsSync();
/// Put generated files here.
Directory get ephemeralDirectory => _flutterLibRoot.childDirectory('Flutter').childDirectory('ephemeral');
@override
Directory get managedDirectory => _flutterLibRoot.childDirectory('Flutter');
@override
File xcodeConfigFor(String mode) => _flutterLibRoot.childDirectory('Flutter').childFile('$mode.xcconfig');
File xcodeConfigFor(String mode) => managedDirectory.childFile('$mode.xcconfig');
@override
File get generatedEnvironmentVariableExportScript => _flutterLibRoot.childDirectory('Flutter').childFile('flutter_export_environment.sh');
File get generatedEnvironmentVariableExportScript => managedDirectory.childFile('flutter_export_environment.sh');
File get appFrameworkInfoPlist => _flutterLibRoot.childDirectory('Flutter').childFile('AppFrameworkInfo.plist');
File get appFrameworkInfoPlist => managedDirectory.childFile('AppFrameworkInfo.plist');
/// The 'AppDelegate.swift' file of the host app. This file might not exist if the app project uses Objective-C.
File get appDelegateSwift => _editableDirectory.childDirectory('Runner').childFile('AppDelegate.swift');
@ -444,15 +484,6 @@ class IosProject extends XcodeBasedProject {
final Map<XcodeProjectBuildContext, Map<String, String>> _buildSettingsByBuildContext = <XcodeProjectBuildContext, Map<String, String>>{};
Future<XcodeProjectInfo?> projectInfo() async {
final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter;
if (!xcodeProject.existsSync() || xcodeProjectInterpreter == null || !xcodeProjectInterpreter.isInstalled) {
return null;
}
return _projectInfo ??= await xcodeProjectInterpreter.getInfo(hostAppRoot.path);
}
XcodeProjectInfo? _projectInfo;
Future<Map<String, String>?> _xcodeProjectBuildSettings(XcodeProjectBuildContext buildContext) async {
final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter;
if (xcodeProjectInterpreter == null || !xcodeProjectInterpreter.isInstalled) {
@ -692,16 +723,6 @@ class MacOSProject extends XcodeBasedProject {
@override
Directory get hostAppRoot => parent.directory.childDirectory('macos');
/// The directory in the project that is managed by Flutter. As much as
/// possible, files that are edited by Flutter tooling after initial project
/// creation should live here.
Directory get managedDirectory => hostAppRoot.childDirectory('Flutter');
/// The subdirectory of [managedDirectory] that contains files that are
/// generated on the fly. All generated files that are not intended to be
/// checked in should live here.
Directory get ephemeralDirectory => managedDirectory.childDirectory('ephemeral');
/// The xcfilelist used to track the inputs for the Flutter script phase in
/// the Xcode build.
File get inputFileList => ephemeralDirectory.childFile('FlutterInputs.xcfilelist');

View File

@ -66,9 +66,11 @@ void main() {
expect(projectUnderTest.ios.deprecatedCompiledDartFramework, isNot(exists));
expect(projectUnderTest.ios.deprecatedProjectFlutterFramework, isNot(exists));
expect(projectUnderTest.ios.flutterPodspec, isNot(exists));
expect(projectUnderTest.ios.flutterPluginSwiftPackageDirectory, isNot(exists));
expect(projectUnderTest.linux.ephemeralDirectory, isNot(exists));
expect(projectUnderTest.macos.ephemeralDirectory, isNot(exists));
expect(projectUnderTest.macos.flutterPluginSwiftPackageDirectory, isNot(exists));
expect(projectUnderTest.windows.ephemeralDirectory, isNot(exists));
expect(projectUnderTest.flutterPluginsFile, isNot(exists));
@ -239,9 +241,11 @@ FlutterProject setupProjectUnderTest(Directory currentDirectory, bool setupXcode
projectUnderTest.ios.deprecatedCompiledDartFramework.createSync(recursive: true);
projectUnderTest.ios.deprecatedProjectFlutterFramework.createSync(recursive: true);
projectUnderTest.ios.flutterPodspec.createSync(recursive: true);
projectUnderTest.ios.flutterPluginSwiftPackageDirectory.createSync(recursive: true);
projectUnderTest.linux.ephemeralDirectory.createSync(recursive: true);
projectUnderTest.macos.ephemeralDirectory.createSync(recursive: true);
projectUnderTest.macos.flutterPluginSwiftPackageDirectory.createSync(recursive: true);
projectUnderTest.windows.ephemeralDirectory.createSync(recursive: true);
projectUnderTest.flutterPluginsFile.createSync(recursive: true);
projectUnderTest.flutterPluginsDependenciesFile.createSync(recursive: true);

View File

@ -402,5 +402,14 @@ void main() {
expect(nativeAssets.stable.enabledByDefault, false);
expect(nativeAssets.stable.available, false);
});
test('${swiftPackageManager.name} availability and default enabled', () {
expect(swiftPackageManager.master.enabledByDefault, false);
expect(swiftPackageManager.master.available, true);
expect(swiftPackageManager.beta.enabledByDefault, false);
expect(swiftPackageManager.beta.available, false);
expect(swiftPackageManager.stable.enabledByDefault, false);
expect(swiftPackageManager.stable.available, false);
});
});
}

View File

@ -1415,6 +1415,39 @@ name: test
expect(flutterManifest, isNotNull);
expect(flutterManifest!.dependencies, isEmpty);
});
testWithoutContext('FlutterManifest knows if Swift Package Manager is disabled', () async {
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
disable-swift-package-manager: true
''';
final FlutterManifest flutterManifest = FlutterManifest.createFromString(
manifest,
logger: logger,
)!;
expect(flutterManifest.disabledSwiftPackageManager, true);
});
testWithoutContext('FlutterManifest does not disable Swift Package Manager if missing', () async {
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
''';
final FlutterManifest flutterManifest = FlutterManifest.createFromString(
manifest,
logger: logger,
)!;
expect(flutterManifest.disabledSwiftPackageManager, false);
});
}
Matcher matchesManifest({

View File

@ -11,6 +11,7 @@ import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/flutter_manifest.dart';
import 'package:flutter_tools/src/ios/code_signing.dart';
import 'package:flutter_tools/src/ios/mac.dart';
import 'package:flutter_tools/src/ios/xcresult.dart';
@ -20,6 +21,7 @@ import 'package:test/fake.dart';
import 'package:unified_analytics/unified_analytics.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_process_manager.dart';
import '../../src/fakes.dart';
@ -217,8 +219,16 @@ void main() {
buildSettings: buildSettings,
),
);
await diagnoseXcodeBuildFailure(buildResult, testUsage, logger, fakeAnalytics);
final MemoryFileSystem fs = MemoryFileSystem.test();
await diagnoseXcodeBuildFailure(
buildResult,
flutterUsage: testUsage,
logger: logger,
analytics: fakeAnalytics,
fileSystem: fs,
platform: SupportedPlatform.ios,
project: FakeFlutterProject(fileSystem: fs),
);
expect(testUsage.events, contains(
TestUsageEvent(
'build',
@ -310,8 +320,16 @@ Error launching application on iPhone.''',
buildSettings: buildSettingsWithDevTeam,
),
);
await diagnoseXcodeBuildFailure(buildResult, testUsage, logger, fakeAnalytics);
final MemoryFileSystem fs = MemoryFileSystem.test();
await diagnoseXcodeBuildFailure(
buildResult,
flutterUsage: testUsage,
logger: logger,
analytics: fakeAnalytics,
fileSystem: fs,
platform: SupportedPlatform.ios,
project: FakeFlutterProject(fileSystem: fs),
);
expect(
logger.errorText,
contains(noProvisioningProfileInstruction),
@ -348,8 +366,16 @@ Error launching application on iPhone.''',
buildSettings: buildSettingsWithDevTeam,
),
);
await diagnoseXcodeBuildFailure(buildResult, testUsage, logger, fakeAnalytics);
final MemoryFileSystem fs = MemoryFileSystem.test();
await diagnoseXcodeBuildFailure(
buildResult,
flutterUsage: testUsage,
logger: logger,
analytics: fakeAnalytics,
fileSystem: fs,
platform: SupportedPlatform.ios,
project: FakeFlutterProject(fileSystem: fs),
);
expect(
logger.errorText,
contains(missingPlatformInstructions('iOS 17.0')),
@ -388,8 +414,16 @@ Could not build the precompiled application for the device.''',
buildSettings: buildSettings,
),
);
await diagnoseXcodeBuildFailure(buildResult, testUsage, logger, fakeAnalytics);
final MemoryFileSystem fs = MemoryFileSystem.test();
await diagnoseXcodeBuildFailure(
buildResult,
flutterUsage: testUsage,
logger: logger,
analytics: fakeAnalytics,
fileSystem: fs,
platform: SupportedPlatform.ios,
project: FakeFlutterProject(fileSystem: fs),
);
expect(
logger.errorText,
contains('Building a deployable iOS app requires a selected Development Team with a \nProvisioning Profile.'),
@ -432,10 +466,227 @@ Could not build the precompiled application for the device.''',
])
);
await diagnoseXcodeBuildFailure(buildResult, testUsage, logger, fakeAnalytics);
final MemoryFileSystem fs = MemoryFileSystem.test();
await diagnoseXcodeBuildFailure(
buildResult,
flutterUsage: testUsage,
logger: logger,
analytics: fakeAnalytics,
fileSystem: fs,
platform: SupportedPlatform.ios,
project: FakeFlutterProject(fileSystem: fs),
);
expect(logger.errorText, contains('Error (Xcode): Target aot_assembly_release failed'));
expect(logger.errorText, isNot(contains('Building a deployable iOS app requires a selected Development Team')));
});
testWithoutContext('parses redefinition of module error', () async{
const List<String> buildCommands = <String>['xcrun', 'cc', 'blah'];
final XcodeBuildResult buildResult = XcodeBuildResult(
success: false,
stdout: '',
xcodeBuildExecution: XcodeBuildExecution(
buildCommands: buildCommands,
appDirectory: '/blah/blah',
environmentType: EnvironmentType.physical,
buildSettings: buildSettings,
),
xcResult: XCResult.test(issues: <XCResultIssue>[
XCResultIssue.test(message: "Redefinition of module 'plugin_1_name'", subType: 'Error'),
XCResultIssue.test(message: "Redefinition of module 'plugin_2_name'", subType: 'Error'),
]),
);
final MemoryFileSystem fs = MemoryFileSystem.test();
final FakeFlutterProject project = FakeFlutterProject(
fileSystem: fs,
usesSwiftPackageManager: true,
);
project.ios.podfile.createSync(recursive: true);
await diagnoseXcodeBuildFailure(
buildResult,
flutterUsage: testUsage,
logger: logger,
analytics: fakeAnalytics,
fileSystem: fs,
platform: SupportedPlatform.ios,
project: project,
);
expect(logger.errorText, contains(
'Your project uses both CocoaPods and Swift Package Manager, which can '
'cause the above error. It may be caused by there being both a CocoaPod '
'and Swift Package Manager dependency for the following module(s): '
'plugin_1_name, plugin_2_name.'
));
});
testWithoutContext('parses duplicate symbols error with arch and number', () async{
const List<String> buildCommands = <String>['xcrun', 'cc', 'blah'];
final XcodeBuildResult buildResult = XcodeBuildResult(
success: false,
stdout: r'''
duplicate symbol '_$s29plugin_1_name23PluginNamePluginC9setDouble3key5valueySS_SdtF' in:
/Users/username/path/to/app/build/ios/Debug-iphonesimulator/plugin_1_name/plugin_1_name.framework/plugin_1_name[arm64][5](PluginNamePlugin.o)
/Users/username/path/to/app/build/ios/Debug-iphonesimulator/plugin_1_name.o
duplicate symbol '_$s29plugin_1_name23PluginNamePluginCAA15UserDefaultsApiAAWP' in:
/Users/username/path/to/app/build/ios/Debug-iphonesimulator/plugin_1_name/plugin_1_name.framework/plugin_1_name[arm64][5](PluginNamePlugin.o)
/Users/username/path/to/app/build/ios/Debug-iphonesimulator/plugin_1_name.o
''',
xcodeBuildExecution: XcodeBuildExecution(
buildCommands: buildCommands,
appDirectory: '/blah/blah',
environmentType: EnvironmentType.physical,
buildSettings: buildSettings,
),
xcResult: XCResult.test(issues: <XCResultIssue>[
XCResultIssue.test(message: '37 duplicate symbols', subType: 'Error'),
]),
);
final MemoryFileSystem fs = MemoryFileSystem.test();
final FakeFlutterProject project = FakeFlutterProject(
fileSystem: fs,
usesSwiftPackageManager: true,
);
project.ios.podfile.createSync(recursive: true);
await diagnoseXcodeBuildFailure(
buildResult,
flutterUsage: testUsage,
logger: logger,
analytics: fakeAnalytics,
fileSystem: fs,
platform: SupportedPlatform.ios,
project: project,
);
expect(logger.errorText, contains(
'Your project uses both CocoaPods and Swift Package Manager, which can '
'cause the above error. It may be caused by there being both a CocoaPod '
'and Swift Package Manager dependency for the following module(s): '
'plugin_1_name.'
));
});
testWithoutContext('parses duplicate symbols error with number', () async{
const List<String> buildCommands = <String>['xcrun', 'cc', 'blah'];
final XcodeBuildResult buildResult = XcodeBuildResult(
success: false,
stdout: r'''
duplicate symbol '_$s29plugin_1_name23PluginNamePluginC9setDouble3key5valueySS_SdtF' in:
/Users/username/path/to/app/build/ios/Debug-iphonesimulator/plugin_1_name/plugin_1_name.framework/plugin_1_name[5](PluginNamePlugin.o)
/Users/username/path/to/app/build/ios/Debug-iphonesimulator/plugin_1_name.o
''',
xcodeBuildExecution: XcodeBuildExecution(
buildCommands: buildCommands,
appDirectory: '/blah/blah',
environmentType: EnvironmentType.physical,
buildSettings: buildSettings,
),
xcResult: XCResult.test(issues: <XCResultIssue>[
XCResultIssue.test(message: '37 duplicate symbols', subType: 'Error'),
]),
);
final MemoryFileSystem fs = MemoryFileSystem.test();
final FakeFlutterProject project = FakeFlutterProject(
fileSystem: fs,
usesSwiftPackageManager: true,
);
project.ios.podfile.createSync(recursive: true);
await diagnoseXcodeBuildFailure(
buildResult,
flutterUsage: testUsage,
logger: logger,
analytics: fakeAnalytics,
fileSystem: fs,
platform: SupportedPlatform.ios,
project: project,
);
expect(logger.errorText, contains(
'Your project uses both CocoaPods and Swift Package Manager, which can '
'cause the above error. It may be caused by there being both a CocoaPod '
'and Swift Package Manager dependency for the following module(s): '
'plugin_1_name.'
));
});
testWithoutContext('parses duplicate symbols error without arch and number', () async{
const List<String> buildCommands = <String>['xcrun', 'cc', 'blah'];
final XcodeBuildResult buildResult = XcodeBuildResult(
success: false,
stdout: r'''
duplicate symbol '_$s29plugin_1_name23PluginNamePluginC9setDouble3key5valueySS_SdtF' in:
/Users/username/path/to/app/build/ios/Debug-iphonesimulator/plugin_1_name/plugin_1_name.framework/plugin_1_name(PluginNamePlugin.o)
''',
xcodeBuildExecution: XcodeBuildExecution(
buildCommands: buildCommands,
appDirectory: '/blah/blah',
environmentType: EnvironmentType.physical,
buildSettings: buildSettings,
),
xcResult: XCResult.test(issues: <XCResultIssue>[
XCResultIssue.test(message: '37 duplicate symbols', subType: 'Error'),
]),
);
final MemoryFileSystem fs = MemoryFileSystem.test();
final FakeFlutterProject project = FakeFlutterProject(
fileSystem: fs,
usesSwiftPackageManager: true,
);
project.ios.podfile.createSync(recursive: true);
await diagnoseXcodeBuildFailure(
buildResult,
flutterUsage: testUsage,
logger: logger,
analytics: fakeAnalytics,
fileSystem: fs,
platform: SupportedPlatform.ios,
project: project,
);
expect(logger.errorText, contains(
'Your project uses both CocoaPods and Swift Package Manager, which can '
'cause the above error. It may be caused by there being both a CocoaPod '
'and Swift Package Manager dependency for the following module(s): '
'plugin_1_name.'
));
});
testUsingContext('parses missing module error', () async{
const List<String> buildCommands = <String>['xcrun', 'cc', 'blah'];
final XcodeBuildResult buildResult = XcodeBuildResult(
success: false,
stdout: '',
xcodeBuildExecution: XcodeBuildExecution(
buildCommands: buildCommands,
appDirectory: '/blah/blah',
environmentType: EnvironmentType.physical,
buildSettings: buildSettings,
),
xcResult: XCResult.test(issues: <XCResultIssue>[
XCResultIssue.test(message: "Module 'plugin_1_name' not found", subType: 'Error'),
XCResultIssue.test(message: "Module 'plugin_2_name' not found", subType: 'Error'),
]),
);
final MemoryFileSystem fs = MemoryFileSystem.test();
final FakeFlutterProject project = FakeFlutterProject(fileSystem: fs);
project.ios.podfile.createSync(recursive: true);
project.directory.childFile('.packages').createSync(recursive: true);
project.manifest = FakeFlutterManifest();
createFakePlugins(project, fs, <String>['plugin_1_name', 'plugin_2_name']);
fs.systemTempDirectory.childFile('cache/plugin_1_name/ios/plugin_1_name/Package.swift')
.createSync(recursive: true);
fs.systemTempDirectory.childFile('cache/plugin_2_name/ios/plugin_2_name/Package.swift')
.createSync(recursive: true);
await diagnoseXcodeBuildFailure(
buildResult,
flutterUsage: testUsage,
logger: logger,
analytics: fakeAnalytics,
fileSystem: fs,
platform: SupportedPlatform.ios,
project: project,
);
expect(logger.errorText, contains(
'Your project uses CocoaPods as a dependency manager, but the following plugin(s) '
'only support Swift Package Manager: plugin_1_name, plugin_2_name.'
));
});
});
group('Upgrades project.pbxproj for old asset usage', () {
@ -454,9 +705,10 @@ Could not build the precompiled application for the device.''',
'another line';
testWithoutContext('upgradePbxProjWithFlutterAssets', () async {
final File pbxprojFile = MemoryFileSystem.test().file('project.pbxproj')
final FakeIosProject project = FakeIosProject(fileSystem: MemoryFileSystem.test());
final File pbxprojFile = project.xcodeProjectInfoFile
..createSync(recursive: true)
..writeAsStringSync(flutterAssetPbxProjLines);
final FakeIosProject project = FakeIosProject(pbxprojFile);
bool result = upgradePbxProjWithFlutterAssets(project, logger);
expect(result, true);
@ -528,14 +780,88 @@ Could not build the precompiled application for the device.''',
});
}
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));
}
}
class FakeIosProject extends Fake implements IosProject {
FakeIosProject(this.xcodeProjectInfoFile);
FakeIosProject({
required MemoryFileSystem fileSystem,
}) : hostAppRoot = fileSystem.directory('app_name').childDirectory('ios');
@override
final File xcodeProjectInfoFile;
Directory hostAppRoot;
@override
File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');
@override
Future<String> hostAppBundleName(BuildInfo? buildInfo) async => 'UnitTestRunner.app';
@override
Directory get xcodeProject => xcodeProjectInfoFile.fileSystem.directory('Runner.xcodeproj');
Directory get xcodeProject => hostAppRoot.childDirectory('Runner.xcodeproj');
@override
File get podfile => hostAppRoot.childFile('Podfile');
}
class FakeFlutterProject extends Fake implements FlutterProject {
FakeFlutterProject({
required this.fileSystem,
this.usesSwiftPackageManager = false,
this.isModule = false,
});
MemoryFileSystem fileSystem;
@override
late final Directory directory = fileSystem.directory('app_name');
@override
late FlutterManifest manifest;
@override
File get flutterPluginsFile => directory.childFile('.flutter-plugins');
@override
File get flutterPluginsDependenciesFile => directory.childFile('.flutter-plugins-dependencies');
@override
late final IosProject ios = FakeIosProject(fileSystem: fileSystem);
@override
final bool usesSwiftPackageManager;
@override
final bool isModule;
}
class FakeFlutterManifest extends Fake implements FlutterManifest {
@override
Set<String> get dependencies => <String>{};
}

View File

@ -0,0 +1,555 @@
// 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) {}
}

View File

@ -9,6 +9,7 @@ import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/flutter_plugins.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/macos/cocoapods.dart';
@ -457,6 +458,19 @@ void main() {
expect(fakeProcessManager, hasNoRemainingExpectations);
});
testUsingContext("doesn't throw, if using Swift Package Manager and Podfile is missing.", () async {
final FlutterProject projectUnderTest = setupProjectUnderTest();
final bool didInstall = await cocoaPodsUnderTest.processPods(
xcodeProject: projectUnderTest.ios,
buildMode: BuildMode.debug,
);
expect(didInstall, isFalse);
expect(fakeProcessManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)),
});
testUsingContext('throws, if specs repo is outdated.', () async {
final FlutterProject projectUnderTest = setupProjectUnderTest();
pretendPodIsInstalled();
@ -1380,7 +1394,11 @@ Specs satisfying the `$fakePluginName (from `Flutter/ephemeral/.symlinks/plugins
}
class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
FakeXcodeProjectInterpreter({this.isInstalled = true, this.buildSettings = const <String, String>{}});
FakeXcodeProjectInterpreter({
this.isInstalled = true,
this.buildSettings = const <String, String>{},
this.version,
});
@override
final bool isInstalled;
@ -1393,4 +1411,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete
}) async => buildSettings;
final Map<String, String> buildSettings;
@override
Version? version;
}

View File

@ -0,0 +1,692 @@
// 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/macos/cocoapods.dart';
import 'package:flutter_tools/src/macos/darwin_dependency_management.dart';
import 'package:flutter_tools/src/macos/swift_package_manager.dart';
import 'package:flutter_tools/src/platform_plugins.dart';
import 'package:flutter_tools/src/plugins.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:test/fake.dart';
import '../../src/common.dart';
void main() {
const List<SupportedPlatform> supportedPlatforms = <SupportedPlatform>[
SupportedPlatform.ios,
SupportedPlatform.macos,
];
group('DarwinDependencyManagement', () {
for (final SupportedPlatform platform in supportedPlatforms) {
group('for ${platform.name}', () {
group('generatePluginsSwiftPackage', () {
testWithoutContext('throw if invalid platform', () async {
final MemoryFileSystem fs = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement(
project: FakeFlutterProject(fileSystem: fs),
plugins: <Plugin>[],
cocoapods: FakeCocoaPods(),
swiftPackageManager: FakeSwiftPackageManager(),
fileSystem: fs,
logger: testLogger,
);
await expectLater(() => dependencyManagement.setUp(
platform: SupportedPlatform.android,
),
throwsToolExit(
message: 'The platform android is incompatible with Darwin Dependency Managers. Only iOS and macOS are allowed.',
),
);
});
group('when using Swift Package Manager', () {
testWithoutContext('with only CocoaPod plugins', () async {
final MemoryFileSystem fs = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final File cocoapodPluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1.podspec')
..createSync(recursive: true);
final List<Plugin> plugins = <Plugin>[
FakePlugin(
name: 'cocoapod_plugin_1',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginPodspecPath: cocoapodPluginPodspec.path,
),
];
final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager(
expectedPlugins: plugins,
);
final FakeCocoaPods cocoaPods = FakeCocoaPods();
final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement(
project: FakeFlutterProject(
usesSwiftPackageManager: true,
fileSystem: fs,
),
plugins: plugins,
cocoapods: cocoaPods,
swiftPackageManager: swiftPackageManager,
fileSystem: fs,
logger: testLogger,
);
await dependencyManagement.setUp(
platform: platform,
);
expect(swiftPackageManager.generated, isTrue);
expect(testLogger.warningText, isEmpty);
expect(testLogger.statusText, isEmpty);
expect(cocoaPods.podfileSetup, isTrue);
});
testWithoutContext('with only Swift Package Manager plugins and no pod integration', () async {
final MemoryFileSystem fs = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final File swiftPackagePluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1/Package.swift')
..createSync(recursive: true);
final List<Plugin> plugins = <Plugin>[
FakePlugin(
name: 'swift_package_plugin_1',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginSwiftPackageManifestPath: swiftPackagePluginPodspec.path,
),
];
final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager(
expectedPlugins: plugins,
);
final FakeCocoaPods cocoaPods = FakeCocoaPods();
final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement(
project: FakeFlutterProject(
usesSwiftPackageManager: true,
fileSystem: fs,
),
plugins: plugins,
cocoapods: cocoaPods,
swiftPackageManager: swiftPackageManager,
fileSystem: fs,
logger: testLogger,
);
await dependencyManagement.setUp(
platform: platform,
);
expect(swiftPackageManager.generated, isTrue);
expect(testLogger.warningText, isEmpty);
expect(testLogger.statusText, isEmpty);
expect(cocoaPods.podfileSetup, isFalse);
});
testWithoutContext('with only Swift Package Manager plugins but project not migrated', () async {
final MemoryFileSystem fs = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final File swiftPackagePluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1/Package.swift')
..createSync(recursive: true);
final List<Plugin> plugins = <Plugin>[
FakePlugin(
name: 'swift_package_plugin_1',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginSwiftPackageManifestPath: swiftPackagePluginPodspec.path,
),
];
final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager(
expectedPlugins: plugins,
);
final File projectPodfile = fs.file('/path/to/Podfile')..createSync(recursive: true);
projectPodfile.writeAsStringSync('Standard Podfile template');
final FakeCocoaPods cocoaPods = FakeCocoaPods(
podFile: projectPodfile,
);
final FakeFlutterProject project = FakeFlutterProject(
usesSwiftPackageManager: true,
fileSystem: fs,
);
final XcodeBasedProject xcodeProject = platform == SupportedPlatform.ios ? project.ios : project.macos;
xcodeProject.podfile.createSync(recursive: true);
xcodeProject.podfile.writeAsStringSync('Standard Podfile template');
final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement(
project: project,
plugins: plugins,
cocoapods: cocoaPods,
swiftPackageManager: swiftPackageManager,
fileSystem: fs,
logger: testLogger,
);
await dependencyManagement.setUp(
platform: platform,
);
expect(swiftPackageManager.generated, isTrue);
expect(testLogger.warningText, isEmpty);
expect(testLogger.statusText, isEmpty);
expect(cocoaPods.podfileSetup, isFalse);
});
testWithoutContext('with only Swift Package Manager plugins with preexisting standard CocoaPods Podfile', () async {
final MemoryFileSystem fs = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final File swiftPackagePluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1/Package.swift')
..createSync(recursive: true);
final List<Plugin> plugins = <Plugin>[
FakePlugin(
name: 'swift_package_plugin_1',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginSwiftPackageManifestPath: swiftPackagePluginPodspec.path,
),
];
final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager(
expectedPlugins: plugins,
);
final File projectPodfile = fs.file('/path/to/Podfile')..createSync(recursive: true);
projectPodfile.writeAsStringSync('Standard Podfile template');
final FakeCocoaPods cocoaPods = FakeCocoaPods(
podFile: projectPodfile,
);
final FakeFlutterProject project = FakeFlutterProject(
usesSwiftPackageManager: true,
fileSystem: fs,
);
final XcodeBasedProject xcodeProject = platform == SupportedPlatform.ios ? project.ios : project.macos;
xcodeProject.podfile.createSync(recursive: true);
xcodeProject.podfile.writeAsStringSync('Standard Podfile template');
xcodeProject.xcodeProjectInfoFile.createSync(recursive: true);
xcodeProject.xcodeProjectInfoFile.writeAsStringSync('FlutterGeneratedPluginSwiftPackage');
final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement(
project: project,
plugins: plugins,
cocoapods: cocoaPods,
swiftPackageManager: swiftPackageManager,
fileSystem: fs,
logger: testLogger,
);
await dependencyManagement.setUp(
platform: platform,
);
expect(swiftPackageManager.generated, isTrue);
final String xcconfigPrefix = platform == SupportedPlatform.macos ? 'Flutter-' : '';
expect(testLogger.warningText, contains(
'All plugins found for ${platform.name} are Swift Packages, '
'but your project still has CocoaPods integration. To remove '
'CocoaPods integration, complete the following steps:\n'
' * In the ${platform.name}/ directory run "pod deintegrate"\n'
' * Also in the ${platform.name}/ directory, delete the Podfile\n'
' * Remove the include to "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" '
'in your ${platform.name}/Flutter/${xcconfigPrefix}Debug.xcconfig\n'
' * Remove the include to "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" '
'in your ${platform.name}/Flutter/${xcconfigPrefix}Release.xcconfig\n\n'
"Removing CocoaPods integration will improve the project's build time.\n"
));
expect(testLogger.statusText, isEmpty);
expect(cocoaPods.podfileSetup, isFalse);
});
testWithoutContext('with only Swift Package Manager plugins with preexisting custom CocoaPods Podfile', () async {
final MemoryFileSystem fs = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final File swiftPackagePluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1/Package.swift')
..createSync(recursive: true);
final List<Plugin> plugins = <Plugin>[
FakePlugin(
name: 'swift_package_plugin_1',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginSwiftPackageManifestPath: swiftPackagePluginPodspec.path,
),
];
final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager(
expectedPlugins: plugins,
);
final File projectPodfile = fs.file('/path/to/Podfile')..createSync(recursive: true);
projectPodfile.writeAsStringSync('Standard Podfile template');
final FakeCocoaPods cocoaPods = FakeCocoaPods(
podFile: projectPodfile,
);
final FakeFlutterProject project = FakeFlutterProject(
usesSwiftPackageManager: true,
fileSystem: fs,
);
final XcodeBasedProject xcodeProject = platform == SupportedPlatform.ios ? project.ios : project.macos;
xcodeProject.podfile.createSync(recursive: true);
xcodeProject.podfile.writeAsStringSync('Non-Standard Podfile template');
xcodeProject.xcodeProjectInfoFile.createSync(recursive: true);
xcodeProject.xcodeProjectInfoFile.writeAsStringSync('FlutterGeneratedPluginSwiftPackage');
final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement(
project: project,
plugins: plugins,
cocoapods: cocoaPods,
swiftPackageManager: swiftPackageManager,
fileSystem: fs,
logger: testLogger,
);
await dependencyManagement.setUp(
platform: platform,
);
expect(swiftPackageManager.generated, isTrue);
final String xcconfigPrefix = platform == SupportedPlatform.macos ? 'Flutter-' : '';
expect(testLogger.warningText, contains(
'All plugins found for ${platform.name} are Swift Packages, '
'but your project still has CocoaPods integration. Your '
'project uses a non-standard Podfile and will need to be '
'migrated to Swift Package Manager manually. Some steps you '
'may need to complete include:\n'
' * In the ${platform.name}/ directory run "pod deintegrate"\n'
' * Transition any Pod dependencies to Swift Package equivalents. '
'See https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app\n'
' * Transition any custom logic\n'
' * Remove the include to "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" '
'in your ${platform.name}/Flutter/${xcconfigPrefix}Debug.xcconfig\n'
' * Remove the include to "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" '
'in your ${platform.name}/Flutter/${xcconfigPrefix}Release.xcconfig\n\n'
"Removing CocoaPods integration will improve the project's build time.\n"
));
expect(testLogger.statusText, isEmpty);
expect(cocoaPods.podfileSetup, isFalse);
});
testWithoutContext('with mixed plugins', () async {
final MemoryFileSystem fs = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final File cocoapodPluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1.podspec')
..createSync(recursive: true);
final File swiftPackagePluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1/Package.swift')
..createSync(recursive: true);
final List<Plugin> plugins = <Plugin>[
FakePlugin(
name: 'cocoapod_plugin_1',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginPodspecPath: cocoapodPluginPodspec.path,
),
FakePlugin(
name: 'swift_package_plugin_1',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginSwiftPackageManifestPath: swiftPackagePluginPodspec.path,
),
FakePlugin(
name: 'neither_plugin_1',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
),
];
final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager(
expectedPlugins: plugins,
);
final FakeCocoaPods cocoaPods = FakeCocoaPods();
final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement(
project: FakeFlutterProject(
usesSwiftPackageManager: true,
fileSystem: fs,
),
plugins: plugins,
cocoapods: cocoaPods,
swiftPackageManager: swiftPackageManager,
fileSystem: fs,
logger: testLogger,
);
await dependencyManagement.setUp(
platform: platform,
);
expect(swiftPackageManager.generated, isTrue);
expect(testLogger.warningText, isEmpty);
expect(testLogger.statusText, isEmpty);
expect(cocoaPods.podfileSetup, isTrue);
});
});
group('when not using Swift Package Manager', () {
testWithoutContext('but project already migrated', () async {
final MemoryFileSystem fs = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final File cocoapodPluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1.podspec')
..createSync(recursive: true);
final List<Plugin> plugins = <Plugin>[
FakePlugin(
name: 'cocoapod_plugin_1',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginPodspecPath: cocoapodPluginPodspec.path,
),
];
final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager(
expectedPlugins: plugins,
);
final FakeCocoaPods cocoaPods = FakeCocoaPods();
final FakeFlutterProject project = FakeFlutterProject(
usesSwiftPackageManager: true,
fileSystem: fs,
);
final XcodeBasedProject xcodeProject = platform == SupportedPlatform.ios ? project.ios : project.macos;
xcodeProject.xcodeProjectInfoFile.createSync(recursive: true);
xcodeProject.xcodeProjectInfoFile.writeAsStringSync(
'FlutterGeneratedPluginSwiftPackage',
);
final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement(
project: project,
plugins: plugins,
cocoapods: cocoaPods,
swiftPackageManager: swiftPackageManager,
fileSystem: fs,
logger: testLogger,
);
await dependencyManagement.setUp(
platform: platform,
);
expect(swiftPackageManager.generated, isTrue);
expect(testLogger.warningText, isEmpty);
expect(testLogger.statusText, isEmpty);
expect(cocoaPods.podfileSetup, isTrue);
});
testWithoutContext('with only CocoaPod plugins', () async {
final MemoryFileSystem fs = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final File cocoapodPluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1.podspec')
..createSync(recursive: true);
final List<Plugin> plugins = <Plugin>[
FakePlugin(
name: 'cocoapod_plugin_1',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginPodspecPath: cocoapodPluginPodspec.path,
),
];
final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager(
expectedPlugins: plugins,
);
final FakeCocoaPods cocoaPods = FakeCocoaPods();
final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement(
project: FakeFlutterProject(
fileSystem: fs,
),
plugins: plugins,
cocoapods: cocoaPods,
swiftPackageManager: swiftPackageManager,
fileSystem: fs,
logger: testLogger,
);
await dependencyManagement.setUp(
platform: platform,
);
expect(swiftPackageManager.generated, isFalse);
expect(testLogger.warningText, isEmpty);
expect(testLogger.statusText, isEmpty);
expect(cocoaPods.podfileSetup, isTrue);
});
testWithoutContext('with only Swift Package Manager plugins', () async {
final MemoryFileSystem fs = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final File swiftPackagePluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1/Package.swift')
..createSync(recursive: true);
final List<Plugin> plugins = <Plugin>[
FakePlugin(
name: 'swift_package_plugin_1',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginSwiftPackageManifestPath: swiftPackagePluginPodspec.path,
),
];
final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager(
expectedPlugins: plugins,
);
final FakeCocoaPods cocoaPods = FakeCocoaPods();
final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement(
project: FakeFlutterProject(
fileSystem: fs,
),
plugins: plugins,
cocoapods: cocoaPods,
swiftPackageManager: swiftPackageManager,
fileSystem: fs,
logger: testLogger,
);
await expectLater(() => dependencyManagement.setUp(
platform: platform,
),
throwsToolExit(
message: 'Plugin swift_package_plugin_1 is only Swift Package Manager compatible. Try '
'enabling Swift Package Manager by running '
'"flutter config --enable-swift-package-manager" or remove the '
'plugin as a dependency.',
),
);
expect(swiftPackageManager.generated, isFalse);
expect(cocoaPods.podfileSetup, isFalse);
});
testWithoutContext('when project is a module', () async {
final MemoryFileSystem fs = MemoryFileSystem();
final BufferLogger testLogger = BufferLogger.test();
final File cocoapodPluginPodspec = fs.file('/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1.podspec')
..createSync(recursive: true);
final List<Plugin> plugins = <Plugin>[
FakePlugin(
name: 'cocoapod_plugin_1',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginPodspecPath: cocoapodPluginPodspec.path,
),
];
final FakeSwiftPackageManager swiftPackageManager = FakeSwiftPackageManager(
expectedPlugins: plugins,
);
final FakeCocoaPods cocoaPods = FakeCocoaPods();
final DarwinDependencyManagement dependencyManagement = DarwinDependencyManagement(
project: FakeFlutterProject(
fileSystem: fs,
isModule: true,
),
plugins: plugins,
cocoapods: cocoaPods,
swiftPackageManager: swiftPackageManager,
fileSystem: fs,
logger: testLogger,
);
await dependencyManagement.setUp(
platform: platform,
);
expect(swiftPackageManager.generated, isFalse);
expect(testLogger.warningText, isEmpty);
expect(testLogger.statusText, isEmpty);
expect(cocoaPods.podfileSetup, isFalse);
});
});
});
});
}
});
}
class FakeIosProject extends Fake implements IosProject {
FakeIosProject({
required MemoryFileSystem fileSystem,
}) : hostAppRoot = fileSystem.directory('app_name').childDirectory('ios');
@override
Directory hostAppRoot;
@override
File get podfile => hostAppRoot.childFile('Podfile');
@override
File get podfileLock => hostAppRoot.childFile('Podfile.lock');
@override
Directory get xcodeProject => hostAppRoot.childDirectory('Runner.xcodeproj');
@override
File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');
@override
bool get flutterPluginSwiftPackageInProjectSettings {
return xcodeProjectInfoFile.existsSync() &&
xcodeProjectInfoFile
.readAsStringSync()
.contains('FlutterGeneratedPluginSwiftPackage');
}
@override
Directory get managedDirectory => hostAppRoot.childDirectory('Flutter');
@override
File xcodeConfigFor(String mode) => managedDirectory.childFile('$mode.xcconfig');
}
class FakeMacOSProject extends Fake implements MacOSProject {
FakeMacOSProject({
required MemoryFileSystem fileSystem,
}) : hostAppRoot = fileSystem.directory('app_name').childDirectory('macos');
@override
Directory hostAppRoot;
@override
File get podfile => hostAppRoot.childFile('Podfile');
@override
File get podfileLock => hostAppRoot.childFile('Podfile.lock');
@override
Directory get xcodeProject => hostAppRoot.childDirectory('Runner.xcodeproj');
@override
File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');
@override
bool get flutterPluginSwiftPackageInProjectSettings {
return xcodeProjectInfoFile.existsSync() &&
xcodeProjectInfoFile
.readAsStringSync()
.contains('FlutterGeneratedPluginSwiftPackage');
}
@override
Directory get managedDirectory => hostAppRoot.childDirectory('Flutter');
@override
File xcodeConfigFor(String mode) => managedDirectory.childFile('Flutter-$mode.xcconfig');
}
class FakeFlutterProject extends Fake implements FlutterProject {
FakeFlutterProject({
required this.fileSystem,
this.usesSwiftPackageManager = false,
this.isModule = false,
});
MemoryFileSystem fileSystem;
@override
late final IosProject ios = FakeIosProject(fileSystem: fileSystem);
@override
late final MacOSProject macos = FakeMacOSProject(fileSystem: fileSystem);
@override
final bool usesSwiftPackageManager;
@override
final bool isModule;
}
class FakeSwiftPackageManager extends Fake implements SwiftPackageManager {
FakeSwiftPackageManager({
this.expectedPlugins,
});
bool generated = false;
final List<Plugin>? expectedPlugins;
@override
Future<void> generatePluginsSwiftPackage(
List<Plugin> plugins,
SupportedPlatform platform,
XcodeBasedProject project,
) async {
generated = true;
expect(plugins, expectedPlugins);
}
}
class FakeCocoaPods extends Fake implements CocoaPods {
FakeCocoaPods({
this.podFile,
this.configIncludesPods = true,
});
File? podFile;
bool podfileSetup = false;
bool addedPodDependencyToFlutterXcconfig = false;
bool configIncludesPods;
@override
Future<void> setupPodfile(XcodeBasedProject xcodeProject) async {
podfileSetup = true;
}
@override
void addPodsDependencyToFlutterXcconfig(XcodeBasedProject xcodeProject) {
addedPodDependencyToFlutterXcconfig = true;
}
@override
Future<File> getPodfileTemplate(XcodeBasedProject xcodeProject, Directory runnerProject) async {
return podFile!;
}
@override
bool xcconfigIncludesPods(File xcodeConfig) {
return configIncludesPods;
}
@override
String includePodsXcconfig(String mode) {
return 'Pods/Target Support Files/Pods-Runner/Pods-Runner.${mode
.toLowerCase()}.xcconfig';
}
}
class FakePlugin extends Fake implements Plugin {
FakePlugin({
required this.name,
required this.platforms,
String? pluginSwiftPackageManifestPath,
String? pluginPodspecPath,
}) : _pluginSwiftPackageManifestPath = pluginSwiftPackageManifestPath,
_pluginPodspecPath = pluginPodspecPath;
final String? _pluginSwiftPackageManifestPath;
final String? _pluginPodspecPath;
@override
final String name;
@override
final Map<String, PluginPlatform> platforms;
@override
String? pluginSwiftPackageManifestPath(
FileSystem fileSystem,
String platform,
) {
return _pluginSwiftPackageManifestPath;
}
@override
String? pluginPodspecPath(
FileSystem fileSystem,
String platform,
) {
return _pluginPodspecPath;
}
}
class FakePluginPlatform extends Fake implements PluginPlatform {}

View File

@ -0,0 +1,424 @@
// 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/isolated/mustache_template.dart';
import 'package:flutter_tools/src/macos/swift_package_manager.dart';
import 'package:flutter_tools/src/platform_plugins.dart';
import 'package:flutter_tools/src/plugins.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:test/fake.dart';
import '../../src/common.dart';
const String _doubleIndent = ' ';
void main() {
const List<SupportedPlatform> supportedPlatforms = <SupportedPlatform>[
SupportedPlatform.ios,
SupportedPlatform.macos,
];
group('SwiftPackageManager', () {
for (final SupportedPlatform platform in supportedPlatforms) {
group('for ${platform.name}', () {
group('generatePluginsSwiftPackage', () {
testWithoutContext('throw if invalid platform', () async {
final MemoryFileSystem fs = MemoryFileSystem();
final FakeXcodeProject project = FakeXcodeProject(
platform: platform.name,
fileSystem: fs,
);
final SwiftPackageManager spm = SwiftPackageManager(
fileSystem: fs,
templateRenderer: const MustacheTemplateRenderer(),
);
await expectLater(() => spm.generatePluginsSwiftPackage(
<Plugin>[],
SupportedPlatform.android,
project,
),
throwsToolExit(message: 'The platform android is not compatible with Swift Package Manager. Only iOS and macOS are allowed.'),
);
});
testWithoutContext('skip if no dependencies and not already migrated', () async {
final MemoryFileSystem fs = MemoryFileSystem();
final FakeXcodeProject project = FakeXcodeProject(
platform: platform.name,
fileSystem: fs,
);
final SwiftPackageManager spm = SwiftPackageManager(
fileSystem: fs,
templateRenderer: const MustacheTemplateRenderer(),
);
await spm.generatePluginsSwiftPackage(
<Plugin>[],
platform,
project,
);
expect(project.flutterPluginSwiftPackageManifest.existsSync(), isFalse);
});
testWithoutContext('generate if no dependencies and already migrated', () async {
final MemoryFileSystem fs = MemoryFileSystem();
final FakeXcodeProject project = FakeXcodeProject(
platform: platform.name,
fileSystem: fs,
);
project.xcodeProjectInfoFile.createSync(recursive: true);
project.xcodeProjectInfoFile.writeAsStringSync('''
' 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };';
''');
final SwiftPackageManager spm = SwiftPackageManager(
fileSystem: fs,
templateRenderer: const MustacheTemplateRenderer(),
);
await spm.generatePluginsSwiftPackage(
<Plugin>[],
platform,
project,
);
final String supportedPlatform = platform == SupportedPlatform.ios ? '.iOS("12.0")' : '.macOS("10.14")';
expect(project.flutterPluginSwiftPackageManifest.existsSync(), isTrue);
expect(project.flutterPluginSwiftPackageManifest.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: [
$supportedPlatform
],
products: [
.library(name: "FlutterGeneratedPluginSwiftPackage", type: .static, targets: ["FlutterGeneratedPluginSwiftPackage"])
],
dependencies: [
$_doubleIndent
],
targets: [
.target(
name: "FlutterGeneratedPluginSwiftPackage"
)
]
)
''');
});
testWithoutContext('generate with single dependency', () async {
final MemoryFileSystem fs = MemoryFileSystem();
final FakeXcodeProject project = FakeXcodeProject(
platform: platform.name,
fileSystem: fs,
);
final File validPlugin1Manifest = fs.file('/local/path/to/plugins/valid_plugin_1/Package.swift')..createSync(recursive: true);
final FakePlugin validPlugin1 = FakePlugin(
name: 'valid_plugin_1',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginSwiftPackageManifestPath: validPlugin1Manifest.path,
);
final SwiftPackageManager spm = SwiftPackageManager(
fileSystem: fs,
templateRenderer: const MustacheTemplateRenderer(),
);
await spm.generatePluginsSwiftPackage(
<Plugin>[validPlugin1],
platform,
project,
);
final String supportedPlatform = platform == SupportedPlatform.ios ? '.iOS("12.0")' : '.macOS("10.14")';
expect(project.flutterPluginSwiftPackageManifest.existsSync(), isTrue);
expect(project.flutterPluginSwiftPackageManifest.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: [
$supportedPlatform
],
products: [
.library(name: "FlutterGeneratedPluginSwiftPackage", type: .static, targets: ["FlutterGeneratedPluginSwiftPackage"])
],
dependencies: [
.package(name: "valid_plugin_1", path: "/local/path/to/plugins/valid_plugin_1")
],
targets: [
.target(
name: "FlutterGeneratedPluginSwiftPackage",
dependencies: [
.product(name: "valid-plugin-1", package: "valid_plugin_1")
]
)
]
)
''');
});
testWithoutContext('generate with multiple dependencies', () async {
final MemoryFileSystem fs = MemoryFileSystem();
final FakeXcodeProject project = FakeXcodeProject(
platform: platform.name,
fileSystem: fs,
);
final FakePlugin nonPlatformCompatiblePlugin = FakePlugin(
name: 'invalid_plugin_due_to_incompatible_platform',
platforms: <String, PluginPlatform>{},
pluginSwiftPackageManifestPath: '/some/path',
);
final FakePlugin pluginSwiftPackageManifestIsNull = FakePlugin(
name: 'invalid_plugin_due_to_null_plugin_swift_package_path',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginSwiftPackageManifestPath: null,
);
final FakePlugin pluginSwiftPackageManifestNotExists = FakePlugin(
name: 'invalid_plugin_due_to_plugin_swift_package_path_does_not_exist',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginSwiftPackageManifestPath: '/some/path',
);
final File validPlugin1Manifest = fs.file('/local/path/to/plugins/valid_plugin_1/Package.swift')..createSync(recursive: true);
final FakePlugin validPlugin1 = FakePlugin(
name: 'valid_plugin_1',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginSwiftPackageManifestPath: validPlugin1Manifest.path,
);
final File validPlugin2Manifest = fs.file('/.pub-cache/plugins/valid_plugin_2/Package.swift')..createSync(recursive: true);
final FakePlugin validPlugin2 = FakePlugin(
name: 'valid_plugin_2',
platforms: <String, PluginPlatform>{platform.name: FakePluginPlatform()},
pluginSwiftPackageManifestPath: validPlugin2Manifest.path,
);
final SwiftPackageManager spm = SwiftPackageManager(
fileSystem: fs,
templateRenderer: const MustacheTemplateRenderer(),
);
await spm.generatePluginsSwiftPackage(
<Plugin>[
nonPlatformCompatiblePlugin,
pluginSwiftPackageManifestIsNull,
pluginSwiftPackageManifestNotExists,
validPlugin1,
validPlugin2,
],
platform,
project,
);
final String supportedPlatform = platform == SupportedPlatform.ios
? '.iOS("12.0")'
: '.macOS("10.14")';
expect(project.flutterPluginSwiftPackageManifest.existsSync(), isTrue);
expect(project.flutterPluginSwiftPackageManifest.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: [
$supportedPlatform
],
products: [
.library(name: "FlutterGeneratedPluginSwiftPackage", type: .static, targets: ["FlutterGeneratedPluginSwiftPackage"])
],
dependencies: [
.package(name: "valid_plugin_1", path: "/local/path/to/plugins/valid_plugin_1"),
.package(name: "valid_plugin_2", path: "/.pub-cache/plugins/valid_plugin_2")
],
targets: [
.target(
name: "FlutterGeneratedPluginSwiftPackage",
dependencies: [
.product(name: "valid-plugin-1", package: "valid_plugin_1"),
.product(name: "valid-plugin-2", package: "valid_plugin_2")
]
)
]
)
''');
});
});
group('updateMinimumDeployment', () {
testWithoutContext('return if invalid deploymentTarget', () {
final MemoryFileSystem fs = MemoryFileSystem();
final FakeXcodeProject project = FakeXcodeProject(
platform: platform.name,
fileSystem: fs,
);
final String supportedPlatform = platform == SupportedPlatform.ios ? '.iOS("12.0")' : '.macOS("10.14")';
project.flutterPluginSwiftPackageManifest.createSync(recursive: true);
project.flutterPluginSwiftPackageManifest.writeAsStringSync(supportedPlatform);
SwiftPackageManager.updateMinimumDeployment(
project: project,
platform: platform,
deploymentTarget: '',
);
expect(
project.flutterPluginSwiftPackageManifest.readAsLinesSync(),
contains(supportedPlatform),
);
});
testWithoutContext('return if deploymentTarget is lower than default', () {
final MemoryFileSystem fs = MemoryFileSystem();
final FakeXcodeProject project = FakeXcodeProject(
platform: platform.name,
fileSystem: fs,
);
final String supportedPlatform = platform == SupportedPlatform.ios ? '.iOS("12.0")' : '.macOS("10.14")';
project.flutterPluginSwiftPackageManifest.createSync(recursive: true);
project.flutterPluginSwiftPackageManifest.writeAsStringSync(supportedPlatform);
SwiftPackageManager.updateMinimumDeployment(
project: project,
platform: platform,
deploymentTarget: '9.0',
);
expect(
project.flutterPluginSwiftPackageManifest.readAsLinesSync(),
contains(supportedPlatform),
);
});
testWithoutContext('return if deploymentTarget is same than default', () {
final MemoryFileSystem fs = MemoryFileSystem();
final FakeXcodeProject project = FakeXcodeProject(
platform: platform.name,
fileSystem: fs,
);
final String supportedPlatform = platform == SupportedPlatform.ios ? '.iOS("12.0")' : '.macOS("10.14")';
project.flutterPluginSwiftPackageManifest.createSync(recursive: true);
project.flutterPluginSwiftPackageManifest.writeAsStringSync(supportedPlatform);
SwiftPackageManager.updateMinimumDeployment(
project: project,
platform: platform,
deploymentTarget: platform == SupportedPlatform.ios ? '12.0' : '10.14',
);
expect(
project.flutterPluginSwiftPackageManifest.readAsLinesSync(),
contains(supportedPlatform),
);
});
testWithoutContext('update if deploymentTarget is higher than default', () {
final MemoryFileSystem fs = MemoryFileSystem();
final FakeXcodeProject project = FakeXcodeProject(
platform: platform.name,
fileSystem: fs,
);
final String supportedPlatform = platform == SupportedPlatform.ios ? '.iOS("12.0")' : '.macOS("10.14")';
project.flutterPluginSwiftPackageManifest.createSync(recursive: true);
project.flutterPluginSwiftPackageManifest.writeAsStringSync(supportedPlatform);
SwiftPackageManager.updateMinimumDeployment(
project: project,
platform: platform,
deploymentTarget: '14.0',
);
expect(
project.flutterPluginSwiftPackageManifest
.readAsLinesSync()
.contains(supportedPlatform),
isFalse,
);
expect(
project.flutterPluginSwiftPackageManifest.readAsLinesSync(),
contains(platform == SupportedPlatform.ios ? '.iOS("14.0")' : '.macOS("14.0")'),
);
});
});
});
}
});
}
class FakeXcodeProject extends Fake implements IosProject {
FakeXcodeProject({
required MemoryFileSystem fileSystem,
required String platform,
}) : hostAppRoot = fileSystem.directory('app_name').childDirectory(platform);
@override
Directory hostAppRoot;
@override
Directory get xcodeProject => hostAppRoot.childDirectory('$hostAppProjectName.xcodeproj');
@override
File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');
@override
String hostAppProjectName = 'Runner';
@override
Directory get flutterPluginSwiftPackageDirectory => hostAppRoot
.childDirectory('Flutter')
.childDirectory('ephemeral')
.childDirectory('Packages')
.childDirectory('FlutterGeneratedPluginSwiftPackage');
@override
File get flutterPluginSwiftPackageManifest =>
flutterPluginSwiftPackageDirectory.childFile('Package.swift');
@override
bool get flutterPluginSwiftPackageInProjectSettings {
return xcodeProjectInfoFile.existsSync() &&
xcodeProjectInfoFile
.readAsStringSync()
.contains('FlutterGeneratedPluginSwiftPackage');
}
}
class FakePlugin extends Fake implements Plugin {
FakePlugin({
required this.name,
required this.platforms,
required String? pluginSwiftPackageManifestPath,
}) : _pluginSwiftPackageManifestPath = pluginSwiftPackageManifestPath;
final String? _pluginSwiftPackageManifestPath;
@override
final String name;
@override
final Map<String, PluginPlatform> platforms;
@override
String? pluginSwiftPackageManifestPath(
FileSystem fileSystem,
String platform,
) {
return _pluginSwiftPackageManifestPath;
}
}
class FakePluginPlatform extends Fake implements PluginPlatform {}

View File

@ -0,0 +1,375 @@
// 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")');
});
});
}

View File

@ -16,6 +16,8 @@ import 'package:flutter_tools/src/flutter_manifest.dart';
import 'package:flutter_tools/src/flutter_plugins.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/macos/darwin_dependency_management.dart';
import 'package:flutter_tools/src/platform_plugins.dart';
import 'package:flutter_tools/src/plugins.dart';
import 'package:flutter_tools/src/preview_device.dart';
import 'package:flutter_tools/src/project.dart';
@ -522,6 +524,7 @@ dependencies:
expect(jsonContent['dependencyGraph'], expectedDependencyGraph);
expect(jsonContent['date_created'], dateCreated.toString());
expect(jsonContent['version'], '1.0.0');
expect(jsonContent['swift_package_manager_enabled'], false);
// Make sure tests are updated if a new object is added/removed.
final List<String> expectedKeys = <String>[
@ -530,6 +533,7 @@ dependencies:
'dependencyGraph',
'date_created',
'version',
'swift_package_manager_enabled',
];
expect(jsonContent.keys, expectedKeys);
}, overrides: <Type, Generator>{
@ -609,6 +613,79 @@ dependencies:
FlutterVersion: () => flutterVersion,
});
testUsingContext(
'.flutter-plugins-dependencies contains swift_package_manager_enabled true when project is using Swift Package Manager', () async {
createPlugin(
name: 'plugin-a',
platforms: const <String, _PluginPlatformInfo>{
// Native-only; should include native build.
'android': _PluginPlatformInfo(pluginClass: 'Foo', androidPackage: 'bar.foo'),
// Hybrid native and Dart; should include native build.
'ios': _PluginPlatformInfo(pluginClass: 'Foo', dartPluginClass: 'Bar', sharedDarwinSource: true),
// Web; should not have the native build key at all since it doesn't apply.
'web': _PluginPlatformInfo(pluginClass: 'Foo', fileName: 'lib/foo.dart'),
// Dart-only; should not include native build.
'windows': _PluginPlatformInfo(dartPluginClass: 'Foo'),
});
iosProject.testExists = true;
final DateTime dateCreated = DateTime(1970);
systemClock.currentTime = dateCreated;
flutterProject.usesSwiftPackageManager = true;
await refreshPluginsList(flutterProject);
expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true);
final String pluginsString = flutterProject.flutterPluginsDependenciesFile
.readAsStringSync();
final Map<String, dynamic> jsonContent = json.decode(pluginsString) as Map<String, dynamic>;
expect(jsonContent['swift_package_manager_enabled'], true);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
SystemClock: () => systemClock,
FlutterVersion: () => flutterVersion,
});
testUsingContext(
'.flutter-plugins-dependencies contains swift_package_manager_enabled false when project is using Swift Package Manager but forceCocoaPodsOnly is true',
() async {
createPlugin(
name: 'plugin-a',
platforms: const <String, _PluginPlatformInfo>{
// Native-only; should include native build.
'android': _PluginPlatformInfo(pluginClass: 'Foo', androidPackage: 'bar.foo'),
// Hybrid native and Dart; should include native build.
'ios': _PluginPlatformInfo(pluginClass: 'Foo', dartPluginClass: 'Bar', sharedDarwinSource: true),
// Web; should not have the native build key at all since it doesn't apply.
'web': _PluginPlatformInfo(pluginClass: 'Foo', fileName: 'lib/foo.dart'),
// Dart-only; should not include native build.
'windows': _PluginPlatformInfo(dartPluginClass: 'Foo'),
});
iosProject.testExists = true;
final DateTime dateCreated = DateTime(1970);
systemClock.currentTime = dateCreated;
flutterProject.usesSwiftPackageManager = true;
await refreshPluginsList(flutterProject, forceCocoaPodsOnly: true);
expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true);
final String pluginsString = flutterProject.flutterPluginsDependenciesFile
.readAsStringSync();
final Map<String, dynamic> jsonContent = json.decode(pluginsString) as Map<String, dynamic>;
expect(jsonContent['swift_package_manager_enabled'], false);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
SystemClock: () => systemClock,
FlutterVersion: () => flutterVersion,
});
testUsingContext('Changes to the plugin list invalidates the Cocoapod lockfiles', () async {
simulatePodInstallRun(iosProject);
simulatePodInstallRun(macosProject);
@ -886,8 +963,12 @@ flutter:
ios:
dartPluginClass: SomePlugin
''');
await injectPlugins(flutterProject, iosPlatform: true);
final FakeDarwinDependencyManagement dependencyManagement = FakeDarwinDependencyManagement();
await injectPlugins(
flutterProject,
iosPlatform: true,
darwinDependencyManagement: dependencyManagement,
);
final File registrantFile = iosProject.pluginRegistrantImplementation;
@ -909,8 +990,12 @@ flutter:
macos:
dartPluginClass: SomePlugin
''');
await injectPlugins(flutterProject, macOSPlatform: true);
final FakeDarwinDependencyManagement dependencyManagement = FakeDarwinDependencyManagement();
await injectPlugins(
flutterProject,
macOSPlatform: true,
darwinDependencyManagement: dependencyManagement,
);
final File registrantFile = macosProject.managedDirectory.childFile('GeneratedPluginRegistrant.swift');
@ -933,8 +1018,12 @@ flutter:
pluginClass: none
dartPluginClass: SomePlugin
''');
await injectPlugins(flutterProject, macOSPlatform: true);
final FakeDarwinDependencyManagement dependencyManagement = FakeDarwinDependencyManagement();
await injectPlugins(
flutterProject,
macOSPlatform: true,
darwinDependencyManagement: dependencyManagement,
);
final File registrantFile = macosProject.managedDirectory.childFile('GeneratedPluginRegistrant.swift');
@ -953,8 +1042,12 @@ flutter:
pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(r'''
"aws ... \"Branch\": $BITBUCKET_BRANCH, \"Date\": $(date +"%m-%d-%y"), \"Time\": $(date +"%T")}\"
''');
await injectPlugins(flutterProject, macOSPlatform: true);
final FakeDarwinDependencyManagement dependencyManagement = FakeDarwinDependencyManagement();
await injectPlugins(
flutterProject,
macOSPlatform: true,
darwinDependencyManagement: dependencyManagement,
);
final File registrantFile = macosProject.managedDirectory.childFile('GeneratedPluginRegistrant.swift');
@ -1194,6 +1287,35 @@ The Flutter Preview device does not support the following plugins from your pubs
FileSystem: () => fsWindows,
ProcessManager: () => FakeProcessManager.empty(),
});
testUsingContext('iOS and macOS project setup up Darwin Dependency Management', () async {
final FakeDarwinDependencyManagement dependencyManagement = FakeDarwinDependencyManagement();
await injectPlugins(
flutterProject,
iosPlatform: true,
macOSPlatform: true,
darwinDependencyManagement: dependencyManagement,
);
expect(
dependencyManagement.setupPlatforms,
<SupportedPlatform>[SupportedPlatform.ios, SupportedPlatform.macos],
);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('non-iOS or macOS project does not setup up Darwin Dependency Management', () async {
final FakeDarwinDependencyManagement dependencyManagement = FakeDarwinDependencyManagement();
await injectPlugins(
flutterProject,
darwinDependencyManagement: dependencyManagement,
);
expect(dependencyManagement.setupPlatforms, <SupportedPlatform>[]);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
});
group('createPluginSymlinks', () {
@ -1390,6 +1512,154 @@ The Flutter Preview device does not support the following plugins from your pubs
});
group('Plugin files', () {
testWithoutContext('pluginSwiftPackageManifestPath for iOS and macOS plugins', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Plugin plugin = Plugin(
name: 'test',
path: '/path/to/test/',
defaultPackagePlatforms: const <String, String>{},
pluginDartClassPlatforms: const <String, String>{},
platforms: const <String, PluginPlatform>{
IOSPlugin.kConfigKey: IOSPlugin(name: 'test', classPrefix: ''),
MacOSPlugin.kConfigKey: MacOSPlugin(name: 'test'),
},
dependencies: <String>[],
isDirectDependency: true,
);
expect(
plugin.pluginSwiftPackageManifestPath(fs, IOSPlugin.kConfigKey),
'/path/to/test/ios/test/Package.swift',
);
expect(
plugin.pluginSwiftPackageManifestPath(fs, MacOSPlugin.kConfigKey),
'/path/to/test/macos/test/Package.swift',
);
});
testWithoutContext('pluginSwiftPackageManifestPath for darwin plugins', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Plugin plugin = Plugin(
name: 'test',
path: '/path/to/test/',
defaultPackagePlatforms: const <String, String>{},
pluginDartClassPlatforms: const <String, String>{},
platforms: const <String, PluginPlatform>{
IOSPlugin.kConfigKey: IOSPlugin(name: 'test', classPrefix: '', sharedDarwinSource: true),
MacOSPlugin.kConfigKey: MacOSPlugin(name: 'test', sharedDarwinSource: true),
},
dependencies: <String>[],
isDirectDependency: true,
);
expect(
plugin.pluginSwiftPackageManifestPath(fs, IOSPlugin.kConfigKey),
'/path/to/test/darwin/test/Package.swift',
);
expect(
plugin.pluginSwiftPackageManifestPath(fs, MacOSPlugin.kConfigKey),
'/path/to/test/darwin/test/Package.swift',
);
});
testWithoutContext('pluginSwiftPackageManifestPath for non darwin plugins', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Plugin plugin = Plugin(
name: 'test',
path: '/path/to/test/',
defaultPackagePlatforms: const <String, String>{},
pluginDartClassPlatforms: const <String, String>{},
platforms: const <String, PluginPlatform>{
WindowsPlugin.kConfigKey: WindowsPlugin(name: 'test', pluginClass: ''),
},
dependencies: <String>[],
isDirectDependency: true,
);
expect(
plugin.pluginSwiftPackageManifestPath(fs, IOSPlugin.kConfigKey),
isNull,
);
expect(
plugin.pluginSwiftPackageManifestPath(fs, MacOSPlugin.kConfigKey),
isNull,
);
expect(
plugin.pluginSwiftPackageManifestPath(fs, WindowsPlugin.kConfigKey),
isNull,
);
});
testWithoutContext('pluginPodspecPath for iOS and macOS plugins', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Plugin plugin = Plugin(
name: 'test',
path: '/path/to/test/',
defaultPackagePlatforms: const <String, String>{},
pluginDartClassPlatforms: const <String, String>{},
platforms: const <String, PluginPlatform>{
IOSPlugin.kConfigKey: IOSPlugin(name: 'test', classPrefix: ''),
MacOSPlugin.kConfigKey: MacOSPlugin(name: 'test'),
},
dependencies: <String>[],
isDirectDependency: true,
);
expect(
plugin.pluginPodspecPath(fs, IOSPlugin.kConfigKey),
'/path/to/test/ios/test.podspec',
);
expect(
plugin.pluginPodspecPath(fs, MacOSPlugin.kConfigKey),
'/path/to/test/macos/test.podspec',
);
});
testWithoutContext('pluginPodspecPath for darwin plugins', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Plugin plugin = Plugin(
name: 'test',
path: '/path/to/test/',
defaultPackagePlatforms: const <String, String>{},
pluginDartClassPlatforms: const <String, String>{},
platforms: const <String, PluginPlatform>{
IOSPlugin.kConfigKey: IOSPlugin(name: 'test', classPrefix: '', sharedDarwinSource: true),
MacOSPlugin.kConfigKey: MacOSPlugin(name: 'test', sharedDarwinSource: true),
},
dependencies: <String>[],
isDirectDependency: true,
);
expect(
plugin.pluginPodspecPath(fs, IOSPlugin.kConfigKey),
'/path/to/test/darwin/test.podspec',
);
expect(
plugin.pluginPodspecPath(fs, MacOSPlugin.kConfigKey),
'/path/to/test/darwin/test.podspec',
);
});
testWithoutContext('pluginPodspecPath for non darwin plugins', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Plugin plugin = Plugin(
name: 'test',
path: '/path/to/test/',
defaultPackagePlatforms: const <String, String>{},
pluginDartClassPlatforms: const <String, String>{},
platforms: const <String, PluginPlatform>{
WindowsPlugin.kConfigKey: WindowsPlugin(name: 'test', pluginClass: ''),
},
dependencies: <String>[],
isDirectDependency: true,
);
expect(plugin.pluginPodspecPath(fs, IOSPlugin.kConfigKey), isNull);
expect(plugin.pluginPodspecPath(fs, MacOSPlugin.kConfigKey), isNull);
expect(plugin.pluginPodspecPath(fs, WindowsPlugin.kConfigKey), isNull);
});
});
testWithoutContext('Symlink failures give developer mode instructions on recent versions of Windows', () async {
final Platform platform = FakePlatform(operatingSystem: 'windows');
final FakeOperatingSystemUtils os = FakeOperatingSystemUtils('Microsoft Windows [Version 10.0.14972.1]');
@ -1498,6 +1768,9 @@ class FakeFlutterProject extends Fake implements FlutterProject {
@override
bool isModule = false;
@override
bool usesSwiftPackageManager = false;
@override
late FlutterManifest manifest;
@ -1684,3 +1957,14 @@ class FakeSystemClock extends Fake implements SystemClock {
return currentTime;
}
}
class FakeDarwinDependencyManagement extends Fake implements DarwinDependencyManagement {
List<SupportedPlatform> setupPlatforms = <SupportedPlatform>[];
@override
Future<void> setUp({
required SupportedPlatform platform,
}) async {
setupPlatforms.add(platform);
}
}

View File

@ -375,6 +375,91 @@ void main() {
});
});
group('usesSwiftPackageManager', () {
testUsingContext('is true when iOS project exists', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory projectDirectory = fs.directory('path');
projectDirectory.childDirectory('ios').createSync(recursive: true);
final FlutterManifest manifest = FakeFlutterManifest();
final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest);
expect(project.usesSwiftPackageManager, isTrue);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)),
});
testUsingContext('is true when macOS project exists', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory projectDirectory = fs.directory('path');
projectDirectory.childDirectory('macos').createSync(recursive: true);
final FlutterManifest manifest = FakeFlutterManifest();
final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest);
expect(project.usesSwiftPackageManager, isTrue);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)),
});
testUsingContext('is false when disabled via manifest', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory projectDirectory = fs.directory('path');
projectDirectory.childDirectory('ios').createSync(recursive: true);
final FlutterManifest manifest = FakeFlutterManifest(disabledSwiftPackageManager: true);
final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest);
expect(project.usesSwiftPackageManager, isFalse);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)),
});
testUsingContext("is false when iOS and macOS project don't exist", () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory projectDirectory = fs.directory('path');
final FlutterManifest manifest = FakeFlutterManifest();
final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest);
expect(project.usesSwiftPackageManager, isFalse);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)),
});
testUsingContext('is false when Xcode is less than 15', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory projectDirectory = fs.directory('path');
projectDirectory.childDirectory('ios').createSync(recursive: true);
final FlutterManifest manifest = FakeFlutterManifest();
final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest);
expect(project.usesSwiftPackageManager, isFalse);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(14, 0, 0)),
});
testUsingContext('is false when Swift Package Manager feature is not enabled', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory projectDirectory = fs.directory('path');
projectDirectory.childDirectory('ios').createSync(recursive: true);
final FlutterManifest manifest = FakeFlutterManifest();
final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest);
expect(project.usesSwiftPackageManager, isFalse);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)),
});
testUsingContext('is false when project is a module', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory projectDirectory = fs.directory('path');
projectDirectory.childDirectory('ios').createSync(recursive: true);
final FlutterManifest manifest = FakeFlutterManifest(isModule: true);
final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest);
expect(project.usesSwiftPackageManager, isFalse);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)),
});
});
group('java gradle agp compatibility', () {
Future<FlutterProject?> configureGradleAgpForTest({
required String gradleV,
@ -1046,6 +1131,31 @@ plugins {
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
});
});
group('flutterSwiftPackageInProjectSettings', () {
testWithMocks('is false if pbxproj missing', () async {
final FlutterProject project = await someProject();
expect(project.ios.xcodeProjectInfoFile.existsSync(), isFalse);
expect(project.ios.flutterPluginSwiftPackageInProjectSettings, isFalse);
});
testWithMocks('is false if pbxproj does not contain FlutterGeneratedPluginSwiftPackage in build process', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProjectInfoFile.createSync(recursive: true);
expect(project.ios.xcodeProjectInfoFile.existsSync(), isTrue);
expect(project.ios.flutterPluginSwiftPackageInProjectSettings, isFalse);
});
testWithMocks('is true if pbxproj does contain FlutterGeneratedPluginSwiftPackage in build process', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProjectInfoFile.createSync(recursive: true);
project.ios.xcodeProjectInfoFile.writeAsStringSync('''
' 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };';
''');
expect(project.ios.xcodeProjectInfoFile.existsSync(), isTrue);
expect(project.ios.flutterPluginSwiftPackageInProjectSettings, isTrue);
});
});
});
group('application bundle name', () {
@ -1724,6 +1834,10 @@ File androidPluginRegistrant(Directory parent) {
}
class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
FakeXcodeProjectInterpreter({
this.version,
});
final Map<XcodeProjectBuildContext, Map<String, String>> buildSettingsByBuildContext = <XcodeProjectBuildContext, Map<String, String>>{};
late XcodeProjectInfo xcodeProjectInfo;
@ -1745,6 +1859,9 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete
@override
bool get isInstalled => true;
@override
Version? version;
}
class FakeAndroidSdkWithDir extends Fake implements AndroidSdk {
@ -1755,3 +1872,16 @@ class FakeAndroidSdkWithDir extends Fake implements AndroidSdk {
@override
Directory get directory => _directory;
}
class FakeFlutterManifest extends Fake implements FlutterManifest {
FakeFlutterManifest({
this.disabledSwiftPackageManager = false,
this.isModule = false,
});
@override
bool disabledSwiftPackageManager;
@override
bool isModule;
}

View File

@ -223,6 +223,179 @@ void main() {
);
});
});
group('prepare', () {
test('exits with useful error message when build mode not set', () {
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
..createSync(recursive: true);
final File pipe = fileSystem.file('/tmp/pipe')
..createSync(recursive: true);
const String buildMode = 'Debug';
final TestContext context = TestContext(
<String>['prepare'],
<String, String>{
'ACTION': 'build',
'BUILT_PRODUCTS_DIR': buildDir.path,
'FLUTTER_ROOT': flutterRoot.path,
'INFOPLIST_PATH': 'Info.plist',
},
commands: <FakeCommand>[
FakeCommand(
command: <String>[
'${flutterRoot.path}/bin/flutter',
'assemble',
'--no-version-check',
'--output=${buildDir.path}/',
'-dTargetPlatform=ios',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dIosArchs=',
'-dSdkRoot=',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=build',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'debug_unpack_ios',
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
);
expect(
() => context.run(),
throwsException,
);
expect(
context.stderr,
contains('ERROR: Unknown FLUTTER_BUILD_MODE: null.\n'),
);
});
test('calls flutter assemble', () {
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
..createSync(recursive: true);
final File pipe = fileSystem.file('/tmp/pipe')
..createSync(recursive: true);
const String buildMode = 'Debug';
final TestContext context = TestContext(
<String>['prepare'],
<String, String>{
'BUILT_PRODUCTS_DIR': buildDir.path,
'CONFIGURATION': buildMode,
'FLUTTER_ROOT': flutterRoot.path,
'INFOPLIST_PATH': 'Info.plist',
},
commands: <FakeCommand>[
FakeCommand(
command: <String>[
'${flutterRoot.path}/bin/flutter',
'assemble',
'--no-version-check',
'--output=${buildDir.path}/',
'-dTargetPlatform=ios',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dIosArchs=',
'-dSdkRoot=',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'debug_unpack_ios',
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
)..run();
expect(context.stderr, isEmpty);
});
test('forwards all env variables to flutter assemble', () {
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
..createSync(recursive: true);
const String archs = 'arm64';
const String buildMode = 'Release';
const String dartObfuscation = 'false';
const String dartDefines = 'flutter.inspector.structuredErrors%3Dtrue';
const String expandedCodeSignIdentity = 'F1326572E0B71C3C8442805230CB4B33B708A2E2';
const String extraFrontEndOptions = '--some-option';
const String extraGenSnapshotOptions = '--obfuscate';
const String frontendServerStarterPath = '/path/to/frontend_server_starter.dart';
const String sdkRoot = '/path/to/sdk';
const String splitDebugInfo = '/path/to/split/debug/info';
const String trackWidgetCreation = 'true';
const String treeShake = 'true';
final TestContext context = TestContext(
<String>['prepare'],
<String, String>{
'ACTION': 'install',
'ARCHS': archs,
'BUILT_PRODUCTS_DIR': buildDir.path,
'CODE_SIGNING_REQUIRED': 'YES',
'CONFIGURATION': buildMode,
'DART_DEFINES': dartDefines,
'DART_OBFUSCATION': dartObfuscation,
'EXPANDED_CODE_SIGN_IDENTITY': expandedCodeSignIdentity,
'EXTRA_FRONT_END_OPTIONS': extraFrontEndOptions,
'EXTRA_GEN_SNAPSHOT_OPTIONS': extraGenSnapshotOptions,
'FLUTTER_ROOT': flutterRoot.path,
'FRONTEND_SERVER_STARTER_PATH': frontendServerStarterPath,
'INFOPLIST_PATH': 'Info.plist',
'SDKROOT': sdkRoot,
'FLAVOR': 'strawberry',
'SPLIT_DEBUG_INFO': splitDebugInfo,
'TRACK_WIDGET_CREATION': trackWidgetCreation,
'TREE_SHAKE_ICONS': treeShake,
},
commands: <FakeCommand>[
FakeCommand(
command: <String>[
'${flutterRoot.path}/bin/flutter',
'assemble',
'--no-version-check',
'--output=${buildDir.path}/',
'-dTargetPlatform=ios',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dFlavor=strawberry',
'-dIosArchs=$archs',
'-dSdkRoot=$sdkRoot',
'-dSplitDebugInfo=$splitDebugInfo',
'-dTreeShakeIcons=$treeShake',
'-dTrackWidgetCreation=$trackWidgetCreation',
'-dDartObfuscation=$dartObfuscation',
'-dAction=install',
'-dFrontendServerStarterPath=$frontendServerStarterPath',
'--ExtraGenSnapshotOptions=$extraGenSnapshotOptions',
'--DartDefines=$dartDefines',
'--ExtraFrontEndOptions=$extraFrontEndOptions',
'-dCodesignIdentity=$expandedCodeSignIdentity',
'release_unpack_ios',
],
),
],
fileSystem: fileSystem,
)..run();
expect(context.stderr, isEmpty);
});
});
}
class TestContext extends Context {

View File

@ -0,0 +1,213 @@
// 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/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:test/fake.dart';
import '../src/common.dart';
import '../src/context.dart';
void main() {
group('IosProject', () {
testWithoutContext('managedDirectory', () {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(
FakeFlutterProject(fileSystem: fs),
);
expect(project.managedDirectory.path, 'app_name/ios/Flutter');
});
testWithoutContext('module managedDirectory', () {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(
FakeFlutterProject(fileSystem: fs, isModule: true),
);
expect(project.managedDirectory.path, 'app_name/.ios/Flutter');
});
testWithoutContext('ephemeralDirectory', () {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(
FakeFlutterProject(fileSystem: fs),
);
expect(project.ephemeralDirectory.path, 'app_name/ios/Flutter/ephemeral');
});
testWithoutContext('module ephemeralDirectory', () {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(
FakeFlutterProject(fileSystem: fs, isModule: true),
);
expect(project.ephemeralDirectory.path, 'app_name/.ios/Flutter/ephemeral');
});
testWithoutContext('flutterPluginSwiftPackageDirectory', () {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(
FakeFlutterProject(fileSystem: fs),
);
expect(
project.flutterPluginSwiftPackageDirectory.path,
'app_name/ios/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage',
);
});
testWithoutContext('module flutterPluginSwiftPackageDirectory', () {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(
FakeFlutterProject(fileSystem: fs, isModule: true),
);
expect(
project.flutterPluginSwiftPackageDirectory.path,
'app_name/.ios/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage',
);
});
testWithoutContext('xcodeConfigFor', () {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(
FakeFlutterProject(fileSystem: fs),
);
expect(
project.xcodeConfigFor('Debug').path,
'app_name/ios/Flutter/Debug.xcconfig',
);
});
group('projectInfo', () {
testUsingContext('is null if XcodeProjectInterpreter is null', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(
FakeFlutterProject(fileSystem: fs),
);
project.xcodeProject.createSync(recursive: true);
expect(await project.projectInfo(), isNull);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => null,
});
testUsingContext('is null if XcodeProjectInterpreter is not installed', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(
FakeFlutterProject(fileSystem: fs),
);
project.xcodeProject.createSync(recursive: true);
expect(await project.projectInfo(), isNull);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(
isInstalled: false,
),
});
testUsingContext('is null if xcodeproj does not exist', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(
FakeFlutterProject(fileSystem: fs),
);
expect(await project.projectInfo(), isNull);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(),
});
testUsingContext('returns XcodeProjectInfo', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(
FakeFlutterProject(fileSystem: fs),
);
project.xcodeProject.createSync(recursive: true);
expect(await project.projectInfo(), isNotNull);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(),
});
});
});
group('MacOSProject', () {
testWithoutContext('managedDirectory', () {
final MemoryFileSystem fs = MemoryFileSystem.test();
final MacOSProject project = MacOSProject.fromFlutter(
FakeFlutterProject(fileSystem: fs),
);
expect(project.managedDirectory.path, 'app_name/macos/Flutter');
});
testWithoutContext('module managedDirectory', () {
final MemoryFileSystem fs = MemoryFileSystem.test();
final MacOSProject project = MacOSProject.fromFlutter(
FakeFlutterProject(fileSystem: fs),
);
expect(project.managedDirectory.path, 'app_name/macos/Flutter');
});
testWithoutContext('ephemeralDirectory', () {
final MemoryFileSystem fs = MemoryFileSystem.test();
final MacOSProject project = MacOSProject.fromFlutter(
FakeFlutterProject(fileSystem: fs),
);
expect(project.ephemeralDirectory.path, 'app_name/macos/Flutter/ephemeral');
});
testWithoutContext('flutterPluginSwiftPackageDirectory', () {
final MemoryFileSystem fs = MemoryFileSystem.test();
final MacOSProject project = MacOSProject.fromFlutter(
FakeFlutterProject(fileSystem: fs),
);
expect(
project.flutterPluginSwiftPackageDirectory.path,
'app_name/macos/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage',
);
});
testWithoutContext('xcodeConfigFor', () {
final MemoryFileSystem fs = MemoryFileSystem.test();
final MacOSProject project = MacOSProject.fromFlutter(
FakeFlutterProject(fileSystem: fs),
);
expect(
project.xcodeConfigFor('Debug').path,
'app_name/macos/Flutter/Flutter-Debug.xcconfig',
);
});
});
}
class FakeFlutterProject extends Fake implements FlutterProject {
FakeFlutterProject({
required this.fileSystem,
this.isModule = false,
});
MemoryFileSystem fileSystem;
@override
late final Directory directory = fileSystem.directory('app_name');
@override
bool isModule = false;
}
class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
FakeXcodeProjectInterpreter({
this.isInstalled = true,
});
@override
final bool isInstalled;
@override
Future<XcodeProjectInfo?> getInfo(String projectPath, {String? projectFilename}) async {
return XcodeProjectInfo(
<String>[],
<String>[],
<String>['Runner'],
BufferLogger.test(),
);
}
}

View File

@ -213,4 +213,80 @@ void main() {
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
testWithoutContext('PlistParser.plistJsonContent can parse pbxproj file', () async {
final String xcodeProjectFile = fileSystem.path.join(
getFlutterRoot(),
'dev',
'integration_tests',
'flutter_gallery',
'ios',
'Runner.xcodeproj',
'project.pbxproj'
);
final BufferLogger logger = BufferLogger(
terminal: Terminal.test(),
outputPreferences: OutputPreferences(),
);
final PlistParser parser = PlistParser(
fileSystem: fileSystem,
processManager: processManager,
logger: logger,
);
final String? projectFileAsJson = parser.plistJsonContent(xcodeProjectFile);
expect(projectFileAsJson, isNotNull);
expect(projectFileAsJson, contains('"PRODUCT_NAME":"Flutter Gallery"'));
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
testWithoutContext('PlistParser.plistJsonContent can parse pbxproj file with unicode and emojis', () async {
String xcodeProjectFile = fileSystem.path.join(
getFlutterRoot(),
'dev',
'integration_tests',
'flutter_gallery',
'ios',
'Runner.xcodeproj',
'project.pbxproj'
);
final BufferLogger logger = BufferLogger(
terminal: Terminal.test(),
outputPreferences: OutputPreferences(),
);
final PlistParser parser = PlistParser(
fileSystem: fileSystem,
processManager: processManager,
logger: logger,
);
xcodeProjectFile = xcodeProjectFile.replaceAll('AppDelegate.m', 'AppDélegate.m');
xcodeProjectFile = xcodeProjectFile.replaceAll('AppDelegate.h', 'App👍Delegate.h');
final String? projectFileAsJson = parser.plistJsonContent(xcodeProjectFile);
expect(projectFileAsJson, isNotNull);
expect(projectFileAsJson, contains('"PRODUCT_NAME":"Flutter Gallery"'));
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
testWithoutContext('PlistParser.plistJsonContent returns null when errors', () async {
final BufferLogger logger = BufferLogger(
terminal: Terminal.test(),
outputPreferences: OutputPreferences(),
);
final PlistParser parser = PlistParser(
fileSystem: fileSystem,
processManager: processManager,
logger: logger,
);
final String? projectFileAsJson = parser.plistJsonContent('bad/path');
expect(projectFileAsJson, isNull);
expect(logger.errorText, isNotEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
}

View File

@ -0,0 +1,770 @@
// 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/error_handling_io.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import '../src/common.dart';
import 'test_utils.dart';
void main() {
final String flutterBin = fileSystem.path.join(
getFlutterRoot(),
'bin',
'flutter',
);
final List<String> platforms = <String>['ios', 'macos'];
for (final String platformName in platforms) {
final List<String> iosLanguages = <String>[
if (platformName == 'ios') 'objc',
'swift',
];
final _Plugin integrationTestPlugin = _integrationTestPlugin(platformName);
for (final String iosLanguage in iosLanguages) {
test('Swift Package Manager not used when feature is disabled for $platformName with $iosLanguage', () async {
final Directory workingDirectory = fileSystem.systemTempDirectory
.createTempSync('swift_package_manager_disabled.');
final String workingDirectoryPath = workingDirectory.path;
try {
await _disableSwiftPackageManager(flutterBin, workingDirectoryPath);
// Create and build an app using the CocoaPods version of
// integration_test.
final String appDirectoryPath = await _createApp(
flutterBin,
workingDirectoryPath,
iosLanguage: iosLanguage,
platform: platformName,
options: <String>['--platforms=$platformName'],
);
_addDependency(
appDirectoryPath: appDirectoryPath,
plugin: integrationTestPlugin,
);
await _buildApp(
flutterBin,
appDirectoryPath,
options: <String>[platformName, '--debug', '-v'],
expectedLines: _expectedLines(
platform: platformName,
appDirectoryPath: appDirectoryPath,
cococapodsPlugin: integrationTestPlugin,
),
unexpectedLines: _unexpectedLines(
platform: platformName,
appDirectoryPath: appDirectoryPath,
cococapodsPlugin: integrationTestPlugin,
),
);
expect(
fileSystem
.directory(appDirectoryPath)
.childDirectory(platformName)
.childFile('Podfile')
.existsSync(),
isTrue,
);
expect(
fileSystem
.directory(appDirectoryPath)
.childDirectory(platformName)
.childDirectory('Flutter')
.childDirectory('ephemeral')
.childDirectory('Packages')
.childDirectory('FlutterGeneratedPluginSwiftPackage')
.existsSync(),
isFalse,
);
} finally {
ErrorHandlingFileSystem.deleteIfExists(
workingDirectory,
recursive: true,
);
}
}, skip: !platform.isMacOS); // [intended] Swift Package Manager only works on macos.
test('Swift Package Manager integration for $platformName with $iosLanguage', () async {
final Directory workingDirectory = fileSystem.systemTempDirectory
.createTempSync('swift_package_manager_enabled.');
final String workingDirectoryPath = workingDirectory.path;
try {
// Create and build an app using the Swift Package Manager version of
// integration_test.
await _enableSwiftPackageManager(flutterBin, workingDirectoryPath);
final String appDirectoryPath = await _createApp(
flutterBin,
workingDirectoryPath,
iosLanguage: iosLanguage,
platform: platformName,
usesSwiftPackageManager: true,
options: <String>['--platforms=$platformName'],
);
_addDependency(appDirectoryPath: appDirectoryPath, plugin: integrationTestPlugin);
await _buildApp(
flutterBin,
appDirectoryPath,
options: <String>[platformName, '--debug', '-v'],
expectedLines: _expectedLines(
platform: platformName,
appDirectoryPath: appDirectoryPath,
swiftPackageMangerEnabled: true,
swiftPackagePlugin: integrationTestPlugin,
),
unexpectedLines: _unexpectedLines(
platform: platformName,
appDirectoryPath: appDirectoryPath,
swiftPackageMangerEnabled: true,
swiftPackagePlugin: integrationTestPlugin,
),
);
expect(
fileSystem
.directory(appDirectoryPath)
.childDirectory(platformName)
.childFile('Podfile')
.existsSync(),
isFalse,
);
expect(
fileSystem
.directory(appDirectoryPath)
.childDirectory(platformName)
.childDirectory('Flutter')
.childDirectory('ephemeral')
.childDirectory('Packages')
.childDirectory('FlutterGeneratedPluginSwiftPackage')
.existsSync(),
isTrue,
);
// Build an app using both a CocoaPods and Swift Package Manager plugin.
await _cleanApp(flutterBin, appDirectoryPath);
final _Plugin createdCocoaPodsPlugin = await _createPlugin(
flutterBin,
workingDirectoryPath,
platform: platformName,
iosLanguage: iosLanguage,
);
_addDependency(
appDirectoryPath: appDirectoryPath,
plugin: createdCocoaPodsPlugin,
);
await _buildApp(
flutterBin,
appDirectoryPath,
options: <String>[platformName, '--debug', '-v'],
expectedLines: _expectedLines(
platform: platformName,
appDirectoryPath: appDirectoryPath,
cococapodsPlugin: createdCocoaPodsPlugin,
swiftPackageMangerEnabled: true,
swiftPackagePlugin: integrationTestPlugin,
),
unexpectedLines: _unexpectedLines(
platform: platformName,
appDirectoryPath: appDirectoryPath,
cococapodsPlugin: createdCocoaPodsPlugin,
swiftPackageMangerEnabled: true,
swiftPackagePlugin: integrationTestPlugin,
),
);
expect(
fileSystem
.directory(appDirectoryPath)
.childDirectory(platformName)
.childFile('Podfile')
.existsSync(),
isTrue,
);
expect(
fileSystem
.directory(appDirectoryPath)
.childDirectory(platformName)
.childDirectory('Flutter')
.childDirectory('ephemeral')
.childDirectory('Packages')
.childDirectory('FlutterGeneratedPluginSwiftPackage')
.existsSync(),
isTrue,
);
// Build app again but with Swift Package Manager disabled by config.
// App will now use CocoaPods version of integration_test plugin.
await _disableSwiftPackageManager(flutterBin, workingDirectoryPath);
await _cleanApp(flutterBin, appDirectoryPath);
await _buildApp(
flutterBin,
appDirectoryPath,
options: <String>[platformName, '--debug', '-v'],
expectedLines: _expectedLines(
platform: platformName,
appDirectoryPath: appDirectoryPath,
cococapodsPlugin: integrationTestPlugin,
),
unexpectedLines: _unexpectedLines(
platform: platformName,
appDirectoryPath: appDirectoryPath,
cococapodsPlugin: integrationTestPlugin,
),
);
// Build app again but with Swift Package Manager disabled by pubspec.
// App will still use CocoaPods version of integration_test plugin.
await _enableSwiftPackageManager(flutterBin, workingDirectoryPath);
await _cleanApp(flutterBin, appDirectoryPath);
_disableSwiftPackageManagerByPubspec(appDirectoryPath: appDirectoryPath);
await _buildApp(
flutterBin,
appDirectoryPath,
options: <String>[platformName, '--debug', '-v'],
expectedLines: _expectedLines(
platform: platformName,
appDirectoryPath: appDirectoryPath,
cococapodsPlugin: integrationTestPlugin,
),
unexpectedLines: _unexpectedLines(
platform: platformName,
appDirectoryPath: appDirectoryPath,
cococapodsPlugin: integrationTestPlugin,
),
);
} finally {
await _disableSwiftPackageManager(flutterBin, workingDirectoryPath);
ErrorHandlingFileSystem.deleteIfExists(
workingDirectory,
recursive: true,
);
}
}, skip: !platform.isMacOS); // [intended] Swift Package Manager only works on macos.
}
test('Build $platformName-framework with non-module app uses CocoaPods', () async {
final Directory workingDirectory = fileSystem.systemTempDirectory
.createTempSync('swift_package_manager_build_framework.');
final String workingDirectoryPath = workingDirectory.path;
try {
// Create and build an app using the Swift Package Manager version of
// integration_test.
await _enableSwiftPackageManager(flutterBin, workingDirectoryPath);
final String appDirectoryPath = await _createApp(
flutterBin,
workingDirectoryPath,
iosLanguage: 'swift',
platform: platformName,
usesSwiftPackageManager: true,
options: <String>['--platforms=$platformName'],
);
_addDependency(appDirectoryPath: appDirectoryPath, plugin: integrationTestPlugin);
await _buildApp(
flutterBin,
appDirectoryPath,
options: <String>[platformName, '--config-only', '-v'],
expectedLines: <String>[
'Adding Swift Package Manager integration...'
]
);
expect(
fileSystem
.directory(appDirectoryPath)
.childDirectory(platformName)
.childFile('Podfile')
.existsSync(),
isFalse,
);
expect(
fileSystem
.directory(appDirectoryPath)
.childDirectory(platformName)
.childDirectory('Flutter')
.childDirectory('ephemeral')
.childDirectory('Packages')
.childDirectory('FlutterGeneratedPluginSwiftPackage')
.existsSync(),
isTrue,
);
// Create and build framework using the CocoaPods version of
// integration_test even though Swift Package Manager is enabled.
await _buildApp(
flutterBin,
appDirectoryPath,
options: <String>[
'$platformName-framework',
'--no-debug',
'--no-profile',
'-v',
],
expectedLines: <String>[
'Swift Package Manager does not yet support this command. CocoaPods will be used instead.'
]
);
expect(
fileSystem
.directory(appDirectoryPath)
.childDirectory('build')
.childDirectory(platformName)
.childDirectory('framework')
.childDirectory('Release')
.childDirectory('${integrationTestPlugin.pluginName}.xcframework')
.existsSync(),
isTrue,
);
} finally {
await _disableSwiftPackageManager(flutterBin, workingDirectoryPath);
ErrorHandlingFileSystem.deleteIfExists(
workingDirectory,
recursive: true,
);
}
}, skip: !platform.isMacOS); // [intended] Swift Package Manager only works on macos.
}
test('Build ios-framework with module app uses CocoaPods', () async {
final Directory workingDirectory = fileSystem.systemTempDirectory
.createTempSync('swift_package_manager_build_framework_module.');
final String workingDirectoryPath = workingDirectory.path;
try {
// Create and build module and framework using the CocoaPods version of
// integration_test even though Swift Package Manager is enabled.
await _enableSwiftPackageManager(flutterBin, workingDirectoryPath);
final String appDirectoryPath = await _createApp(
flutterBin,
workingDirectoryPath,
iosLanguage: 'swift',
platform: 'ios',
usesSwiftPackageManager: true,
options: <String>['--template=module'],
);
final _Plugin integrationTestPlugin = _integrationTestPlugin('ios');
_addDependency(appDirectoryPath: appDirectoryPath, plugin: integrationTestPlugin);
await _buildApp(
flutterBin,
appDirectoryPath,
options: <String>['ios', '--config-only', '-v'],
unexpectedLines: <String>[
'Adding Swift Package Manager integration...'
]
);
expect(
fileSystem
.directory(appDirectoryPath)
.childDirectory('.ios')
.childFile('Podfile')
.existsSync(),
isTrue,
);
expect(
fileSystem
.directory(appDirectoryPath)
.childDirectory('.ios')
.childDirectory('Flutter')
.childDirectory('ephemeral')
.childDirectory('Packages')
.childDirectory('FlutterGeneratedPluginSwiftPackage')
.existsSync(),
isFalse,
);
await _buildApp(
flutterBin,
appDirectoryPath,
options: <String>[
'ios-framework',
'--no-debug',
'--no-profile',
'-v',
],
unexpectedLines: <String>[
'Adding Swift Package Manager integration...',
'Swift Package Manager does not yet support this command. CocoaPods will be used instead.'
]
);
expect(
fileSystem
.directory(appDirectoryPath)
.childDirectory('build')
.childDirectory('ios')
.childDirectory('framework')
.childDirectory('Release')
.childDirectory('${integrationTestPlugin.pluginName}.xcframework')
.existsSync(),
isTrue,
);
} finally {
await _disableSwiftPackageManager(flutterBin, workingDirectoryPath);
ErrorHandlingFileSystem.deleteIfExists(
workingDirectory,
recursive: true,
);
}
}, skip: !platform.isMacOS); // [intended] Swift Package Manager only works on macos.
}
Future<void> _enableSwiftPackageManager(
String flutterBin,
String workingDirectory,
) async {
final ProcessResult result = await processManager.run(
<String>[
flutterBin,
...getLocalEngineArguments(),
'config',
'--enable-swift-package-manager',
'-v',
],
workingDirectory: workingDirectory,
);
expect(
result.exitCode,
0,
reason: 'Failed to enable Swift Package Manager: \n'
'stdout: \n${result.stdout}\n'
'stderr: \n${result.stderr}\n',
verbose: true,
);
}
Future<void> _disableSwiftPackageManager(
String flutterBin,
String workingDirectory,
) async {
final ProcessResult result = await processManager.run(
<String>[
flutterBin,
...getLocalEngineArguments(),
'config',
'--no-enable-swift-package-manager',
'-v',
],
workingDirectory: workingDirectory,
);
expect(
result.exitCode,
0,
reason: 'Failed to disable Swift Package Manager: \n'
'stdout: \n${result.stdout}\n'
'stderr: \n${result.stderr}\n',
verbose: true,
);
}
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,
);
}
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 && unexpectedLines.contains(trimmedLine)) {
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',
);
}
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',
);
}
Future<_Plugin> _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 _Plugin(
pluginName: pluginName,
pluginPath: pluginDirectory.path,
platform: platform,
);
}
void _addDependency({
required _Plugin 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',
),
);
}
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',
),
);
}
_Plugin _integrationTestPlugin(String platform) {
final String flutterRoot = getFlutterRoot();
return _Plugin(
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'),
);
}
List<Pattern> _expectedLines({
required String platform,
required String appDirectoryPath,
_Plugin? cococapodsPlugin,
_Plugin? swiftPackagePlugin,
bool swiftPackageMangerEnabled = 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 (cococapodsPlugin != null) {
expectedLines.addAll(<String>[
'Running pod install...',
'-> Installing $frameworkName (1.0.0)',
'-> Installing ${cococapodsPlugin.pluginName} (0.0.1)',
"Target 'Pods-Runner' in project 'Pods'",
"➜ Explicit dependency on target '$frameworkName' in project 'Pods'",
"➜ Explicit dependency on target '${cococapodsPlugin.pluginName}' in project 'Pods'",
]);
}
return expectedLines;
}
List<String> _unexpectedLines({
required String platform,
required String appDirectoryPath,
_Plugin? cococapodsPlugin,
_Plugin? swiftPackagePlugin,
bool swiftPackageMangerEnabled = false,
}) {
final String frameworkName = platform == 'ios' ? 'Flutter' : 'FlutterMacOS';
final List<String> unexpectedLines = <String>[];
if (cococapodsPlugin == null) {
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}'",
]);
}
}
return unexpectedLines;
}
class _Plugin {
_Plugin({
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);
}

View File

@ -315,6 +315,11 @@ class FakePlistParser implements PlistParser {
@override
String? plistXmlContent(String plistFilePath) => throw UnimplementedError();
@override
String? plistJsonContent(String filePath, {bool sorted = false}) {
throw UnimplementedError();
}
@override
Map<String, Object> parseFile(String plistFilePath) {
return _underlyingValues;
@ -472,6 +477,7 @@ class TestFeatureFlags implements FeatureFlags {
this.isCliAnimationEnabled = true,
this.isNativeAssetsEnabled = false,
this.isPreviewDeviceEnabled = false,
this.isSwiftPackageManagerEnabled = false,
});
@override
@ -507,6 +513,9 @@ class TestFeatureFlags implements FeatureFlags {
@override
final bool isPreviewDeviceEnabled;
@override
final bool isSwiftPackageManagerEnabled;
@override
bool isEnabled(Feature feature) {
return switch (feature) {

View File

@ -15,10 +15,10 @@ LICENSE
}
s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' }
s.source = { :http => 'https://github.com/flutter/flutter/tree/main/packages/integration_test/integration_test_macos' }
s.source_files = 'Classes/**/*'
s.source_files = 'integration_test_macos/Sources/integration_test_macos/**/*'
s.dependency 'FlutterMacOS'
s.platform = :osx, '10.11'
s.platform = :osx, '10.14'
s.swift_version = '5.0'
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
end

View File

@ -0,0 +1,22 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "integration_test_macos",
platforms: [
.macOS("10.14"),
],
products: [
.library(name: "integration-test-macos", targets: ["integration_test_macos"]),
],
targets: [
.target(
name: "integration_test_macos",
resources: [
.process("Resources"),
]
),
]
)

View File

@ -15,8 +15,8 @@ LICENSE
}
s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' }
s.source = { :http => 'https://github.com/flutter/flutter/tree/main/packages/integration_test' }
s.source_files = 'Classes/**/*'
s.public_header_files = 'Classes/**/*.h'
s.source_files = 'integration_test/Sources/integration_test/**/*.{h,m}'
s.public_header_files = 'integration_test/Sources/integration_test/**/*.h'
s.dependency 'Flutter'
s.ios.framework = 'UIKit'

View File

@ -0,0 +1,25 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "integration_test",
platforms: [
.iOS("12.0"),
],
products: [
.library(name: "integration-test", targets: ["integration_test"]),
],
targets: [
.target(
name: "integration_test",
resources: [
.process("Resources"),
],
cSettings: [
.headerSearchPath("include/integration_test"),
]
),
]
)