Skip to content
Draft
36 changes: 36 additions & 0 deletions packages/flutter_tools/lib/src/ios/xcodeproj.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import '../base/utils.dart';
import '../base/version.dart';
import '../build_info.dart';
import '../convert.dart';
import '../flutter_plugins.dart';
import '../macos/swift_package_manager.dart';
import '../plugins.dart';
import '../project.dart';
import '../reporting/reporting.dart';

final _settingExpr = RegExp(r'(\w+)\s*=\s*(.*)$');
Expand Down Expand Up @@ -391,6 +395,17 @@ class XcodeProjectInterpreter {
/// The stderr subscription for the Swift package fetch process.
StreamSubscription<String>? _swiftPackageFetchStderrSubscription;

/// Resets the Swift package fetch process by killing it if it's running and clearing the
/// process and subscriptions.
Future<void> _resetSwiftPackageFetchProcess() async {
_swiftPackageFetchProcess?.kill();
_swiftPackageFetchProcess = null;
await _swiftPackageFetchStdoutSubscription?.cancel();
await _swiftPackageFetchStderrSubscription?.cancel();
_swiftPackageFetchStdoutSubscription = null;
_swiftPackageFetchStderrSubscription = null;
}

/// Prefetches Swift packages for the given Xcode project.
///
/// If a process is already running from a previous Flutter command, kill it before starting
Expand All @@ -399,11 +414,14 @@ class XcodeProjectInterpreter {
///
/// If [quiet] is false, it will print a spinner while the command is running and print logs of
/// what Swift packages are being fetched.
///
/// If [force] is true, it will run the process even if it's already been run before.
Future<void> prefetchSwiftPackages(
String projectPath, {
required Directory buildDirectory,
bool quiet = true,
bool waitForCompletion = true,
bool force = false,
}) async {
Status? status;
try {
Expand All @@ -416,6 +434,9 @@ class XcodeProjectInterpreter {
),
'-resolvePackageDependencies',
];
if (force) {
await _resetSwiftPackageFetchProcess();
}
if (_swiftPackageFetchProcess == null) {
// Check if process is already running from a previous Flutter command. If it is, kill it
// so we don't have the process running twice. When this process is run twice, it'll cause
Expand Down Expand Up @@ -472,6 +493,21 @@ class XcodeProjectInterpreter {
await _swiftPackageFetchStderrSubscription?.cancel();
});
if (exitCode != 0) {
final stderrString = stderrBuffer.toString();

final FlutterProject project = FlutterProject.fromDirectory(
_fileSystem.directory(projectPath).parent,
);
Comment thread
vashworth marked this conversation as resolved.
Outdated
final List<Plugin> plugins = await findPlugins(project);
final String? swiftPackageManagerError = SwiftPackageManager.parsePluginError(
Comment thread
vashworth marked this conversation as resolved.
Outdated
stderrString,
pluginNames: plugins.map((p) => p.name).toList(),
);

if (swiftPackageManagerError != null) {
_logger.printError(stderrString);
throwToolExit(swiftPackageManagerError);
}
throwToolExit('Xcode failed to resolve Swift Package Manager dependencies:\n$stderrBuffer');
}
} finally {
Expand Down
47 changes: 47 additions & 0 deletions packages/flutter_tools/lib/src/macos/swift_package_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -403,4 +403,51 @@ class SwiftPackageManager {
manifestContents.replaceFirst(oldSupportedPlatform, newSupportedPlatform),
);
}

static final List<_SwiftPMPluginErrorMatcher> _errorMatchers = <_SwiftPMPluginErrorMatcher>[
_SwiftPMPluginErrorMatcher(
// Example: target 'plugin_a' in package 'plugin_a' is outside the package root
pattern: RegExp(r"target '([^']+)' in package '[^']+' is outside the package root"),
message: (RegExpMatch match) =>
'Flutter plugin "${match.group(1)}" has an incorrectly configured Package.swift file.\n'
'Please contact the plugin maintainers for assistance.',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we also provide the details in error mesage? (same for the other matchers below)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put the onus or printing the actual error on whatever call this method. For example in prefetchSwiftPackages, it prints the complete error before throwing with the guided message:

         if (swiftPackageManagerError != null) {
          _logger.printError(stderrString);
          throwToolExit(swiftPackageManagerError);
        }

),
_SwiftPMPluginErrorMatcher(
// Example: /path/to/plugin_a/Package.swift:25:17: error: expected ',' separator
pattern: RegExp(r'([^\/]+)\/Package\.swift:\d+:\d+: error:'),
Comment thread
vashworth marked this conversation as resolved.
message: (RegExpMatch match) =>
'Flutter plugin "${match.group(1)}" has an incorrectly configured Package.swift file.\n'
'Please contact the plugin maintainers for assistance.',
),
_SwiftPMPluginErrorMatcher(
// Example: unknown package 'some-package' in dependencies of target 'plugin_a'
pattern: RegExp(r"unknown package '[^']+' in dependencies of target '([^']+)'"),
message: (RegExpMatch match) =>
'Flutter plugin "${match.group(1)}" has an incorrectly configured Package.swift file.\n'
'Please contact the plugin maintainers for assistance.',
),
];

/// Parses a Swift Package Manager error message and returns a guided error
/// message if the error matches a known pattern.
static String? parsePluginError(String? message, {required List<String> pluginNames}) {
if (message == null || message.isEmpty) {
return null;
}
for (final _SwiftPMPluginErrorMatcher matcher in _errorMatchers) {
for (final RegExpMatch match in matcher.pattern.allMatches(message)) {
final String? packageName = match.group(1);
if (packageName != null && pluginNames.contains(packageName)) {
Comment thread
vashworth marked this conversation as resolved.
return matcher.message(match);
}
}
}
return null;
}
}

class _SwiftPMPluginErrorMatcher {
const _SwiftPMPluginErrorMatcher({required this.pattern, required this.message});
final RegExp pattern;
final String Function(RegExpMatch match) message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -227,13 +227,35 @@ class SwiftPackageManagerIntegrationMigration extends ProjectMigrator {
}
}

try {
// When migrating for the first time, this will be the first time packages are downloaded
// and may take a while.
await _xcodeProjectInterpreter.prefetchSwiftPackages(
_xcodeProject.hostAppRoot.path,
buildDirectory: _fileSystem.directory(
_platform.buildDirectory(config: _config, fileSystem: _fileSystem),
),
quiet: false,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note I was able to remove the force here because now prefetchSwiftPackagesForProject only runs if the project has migrated to SwiftPM already so it doesn't get run before here.

force: true,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why need to reset & restart the process when calling this from migration.dart (but not other places?)

Copy link
Copy Markdown
Contributor Author

@vashworth vashworth May 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This migrator is what adds SwiftPM integration, so when the prefetchSwiftPackages runs before the migration is run (triggered by xcodebuild -list), it doesn't have any Swift packages so nothing is downloaded and _swiftPackageFetchProcess will be set and not run again unless reset. After the migration, the project may now have Swift package dependencies. Without this call here, the getInfo below may silently hang for a long time while it's downloading dependencies.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. Can you add a comment about this corner case?

This also feels like a bit of hack/workaround since we shouldn't start pre-fetch for un-migrated projects in the first place (feel free to leave a TODO tho).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: forceRestart

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also feels like a bit of hack/workaround since we shouldn't start pre-fetch for un-migrated projects in the first place (feel free to leave a TODO tho).

I decided to go ahead and fix. I was trying to keep this small but then I realized there was also a flaw in that prefetchSwiftPackages would only be called for one platform, it couldn't be called for both iOS and macOS because XcodeProjectInterpreter is used for both. So I moved prefetchSwiftPackages to XcodeBasedProject so it's per project (IosProject, MacOSProject).

);
} on Exception catch (e) {
throw _PrefetchSwiftPackageException(e.toString());
}

// Get the project info to make sure it compiles with xcodebuild
await _xcodeProjectInterpreter.getInfo(
_xcodeProject.hostAppRoot.path,
buildDirectory: _fileSystem.directory(
_platform.buildDirectory(config: _config, fileSystem: _fileSystem),
),
);
} on _PrefetchSwiftPackageException catch (e) {
restoreFromBackup(schemeInfo);
throwToolExit(
'An error occurred when adding Swift Package Manager integration:\n'
' ${e.message}\n\n'
'$kDisableSwiftPMInstructions'
);
} on Exception catch (e) {
restoreFromBackup(schemeInfo);
if (optionalOnly) {
Expand All @@ -248,8 +270,7 @@ class SwiftPackageManagerIntegrationMigration extends ProjectMigrator {
throwToolExit(
'An error occurred when adding Swift Package Manager integration:\n'
' $e\n\n'
'Swift Package Manager is currently an experimental feature, please file a bug at\n'
' https://github.com/flutter/flutter/issues/new?template=01_activation.yml \n'
'Please file a bug at https://github.com/flutter/flutter/issues/new?template=01_activation.yml \n'
'Consider including a copy of the following files in your bug report:\n'
' ${_platform.name}/Runner.xcodeproj/project.pbxproj\n'
' ${_platform.name}/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme '
Expand Down Expand Up @@ -1430,3 +1451,12 @@ class ParsedProject {
final String identifier;
final List<String>? packageReferences;
}

class _PrefetchSwiftPackageException implements Exception {
_PrefetchSwiftPackageException(this.message);

final String message;

@override
String toString() => message;
}
Loading
Loading