Skip to content
Draft
Next Next commit
Parse common SwiftPM errors
  • Loading branch information
vashworth committed Apr 17, 2026
commit fbf28313cbf0de19642eef73cd77f4b24f4d291d
6 changes: 6 additions & 0 deletions packages/flutter_tools/lib/src/ios/mac.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,9 @@ Future<bool> _handleIssues(
final XcodeBasedProject xcodeProject = platform.xcodeProject(project);
final String? swiftPackageManagerMinPlatformMismatchMessage =
_swiftPackageManagerMinPlatformMismatchMessageFromStdout(result.stdout);
final String? swiftPackageManagerError =
SwiftPackageManager.parseError(result.stdout) ??
SwiftPackageManager.parseError(result.stderr);

if (requiresProvisioningProfile) {
logger.printError(noProvisioningProfileInstruction, emphasis: true);
Expand Down Expand Up @@ -1159,6 +1162,9 @@ Future<bool> _handleIssues(
} else if (swiftPackageManagerMinPlatformMismatchMessage != null) {
logger.printError(swiftPackageManagerMinPlatformMismatchMessage, emphasis: true);
issueDetected = true;
} else if (swiftPackageManagerError != null) {
logger.printError(swiftPackageManagerError, emphasis: true);
issueDetected = true;
} else if (duplicateModules.isNotEmpty) {
final bool usesCocoapods = xcodeProject.podfile.existsSync();
final bool usesSwiftPackageManager = xcodeProject.usesSwiftPackageManager;
Expand Down
7 changes: 7 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,7 @@ import '../base/utils.dart';
import '../base/version.dart';
import '../build_info.dart';
import '../convert.dart';
import '../macos/swift_package_manager.dart';
import '../reporting/reporting.dart';

final _settingExpr = RegExp(r'(\w+)\s*=\s*(.*)$');
Expand Down Expand Up @@ -460,6 +461,12 @@ class XcodeProjectInterpreter {
await _swiftPackageFetchStderrSubscription?.cancel();
});
if (exitCode != 0) {
final stderrString = stderrBuffer.toString();
final String? swiftPackageManagerError = SwiftPackageManager.parseError(stderrString);

if (swiftPackageManagerError != null) {
throwToolExit(swiftPackageManagerError);
}
throwToolExit('Xcode failed to resolve Swift Package Manager dependencies:\n$stderrBuffer');
}
} finally {
Expand Down
31 changes: 31 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,35 @@ class SwiftPackageManager {
manifestContents.replaceFirst(oldSupportedPlatform, newSupportedPlatform),
);
}

static final List<_SwiftPMErrorMatcher> _errorMatchers = <_SwiftPMErrorMatcher>[
_SwiftPMErrorMatcher(
pattern: RegExp(r"target '([^']+)' in package '[^']+' is outside the package root"),
message: (RegExpMatch match) =>
'Flutter plugin "${match.group(1)}" is not formatted correctly.\n'
'Its target path is outside the package root, which is not supported by Swift Package Manager.\n'
'Please contact the plugin author.',
),
];

/// Parses a Swift Package Manager error message and returns a guided error
/// message if the error matches a known pattern.
static String? parseError(String? message) {
if (message == null || message.isEmpty) {
return null;
}
for (final _SwiftPMErrorMatcher matcher in _errorMatchers) {
final RegExpMatch? match = matcher.pattern.firstMatch(message);
if (match != null) {
return matcher.message(match);
}
}
return null;
}
}

class _SwiftPMErrorMatcher {
const _SwiftPMErrorMatcher({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 @@ -236,6 +236,13 @@ class SwiftPackageManagerIntegrationMigration extends ProjectMigrator {
);
} on Exception catch (e) {
restoreFromBackup(schemeInfo);
final errorString = e.toString();
final String? swiftPackageManagerError = SwiftPackageManager.parseError(errorString);

if (swiftPackageManagerError != null) {
throwToolExit(swiftPackageManagerError);
}

if (optionalOnly) {
// This part of the migration is optional. We'll log this for debugging sake but don't
// really expect the user to see it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,26 @@ let package = Package(
});
});
}

group('parseError', () {
testWithoutContext('returns null for empty or null message', () {
expect(SwiftPackageManager.parseError(null), isNull);
expect(SwiftPackageManager.parseError(''), isNull);
});

testWithoutContext('returns null for unknown error', () {
expect(SwiftPackageManager.parseError('some random error'), isNull);
});

testWithoutContext('returns guided message for outside package root error', () {
const message = 'xcodebuild: error: Could not resolve package dependencies:\n'
" target 'plugin_1' in package 'plugin_1' is outside the package root";
const expected = 'Flutter plugin "plugin_1" is not formatted correctly.\n'
'Its target path is outside the package root, which is not supported by Swift Package Manager.\n'
'Please contact the plugin author.';
expect(SwiftPackageManager.parseError(message), expected);
});
});
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import 'dart:convert';
import 'dart:io';

import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/error_handling_io.dart';
Expand Down Expand Up @@ -416,6 +417,99 @@ void main() {
},
skip: !platform.isMacOS, // [intended] Swift Package Manager only works on macos.
);

test(
'SwiftPM plugin with target path outside package root fails for $platformName',
() async {
final Directory workingDirectory = fileSystem.systemTempDirectory.createTempSync(
'swift_package_manager_relative_path.',
);
final String workingDirectoryPath = workingDirectory.path;

addTearDown(() async {
await SwiftPackageManagerUtils.disableSwiftPackageManager(
flutterBin,
workingDirectoryPath,
);
ErrorHandlingFileSystem.deleteIfExists(workingDirectory, recursive: true);
});

await SwiftPackageManagerUtils.enableSwiftPackageManager(flutterBin, workingDirectoryPath);

// Create an app
final String appDirectoryPath = await SwiftPackageManagerUtils.createApp(
flutterBin,
workingDirectoryPath,
platform: platformName,
usesSwiftPackageManager: true,
options: <String>['--platforms=$platformName'],
);

// Create a SwiftPM plugin
final SwiftPackageManagerPlugin createdSwiftPMPlugin =
await SwiftPackageManagerUtils.createPlugin(
flutterBin,
workingDirectoryPath,
platform: platformName,
usesSwiftPackageManager: true,
);

// Modify its Package.swift to use a relative path outside the package root
final File swiftPMPluginPackageManifest = fileSystem
.directory(createdSwiftPMPlugin.pluginPath)
.childDirectory(platformName)
.childDirectory(createdSwiftPMPlugin.pluginName)
.childFile('Package.swift');

final String manifestContents = swiftPMPluginPackageManifest.readAsStringSync();

// We want to add path: "../Classes" to the target.
// The path must come after dependencies or we get a compilation error.
const targetToReplace =
'dependencies: [\n .product(name: "FlutterFramework", package: "FlutterFramework")\n ]';
expect(
manifestContents,
contains(targetToReplace),
reason: 'Target dependencies not found in manifest',
);

final String updatedManifestContents = manifestContents.replaceFirst(
targetToReplace,
'dependencies: [\n .product(name: "FlutterFramework", package: "FlutterFramework")\n ],\n path: "../Classes"',
);

swiftPMPluginPackageManifest.writeAsStringSync(updatedManifestContents);

// Add the plugin as a dependency to the app
SwiftPackageManagerUtils.addDependency(
appDirectoryPath: appDirectoryPath,
plugin: createdSwiftPMPlugin,
);

// Build the app and expect it to fail
final command = <String>[
flutterBin,
...getLocalEngineArguments(),
'build',
platformName,
'--debug',
'-v',
];
final ProcessResult result = await processManager.run(
command,
workingDirectory: appDirectoryPath,
);

expect(result.exitCode, isNot(0));
expect(
result.stdout.toString() + result.stderr.toString(),
contains(
'Flutter plugin "${createdSwiftPMPlugin.pluginName}" is not formatted correctly.',
),
);
},
skip: !platform.isMacOS, // [intended] Swift Package Manager only works on macos.
);
}

test('Build ios-framework with module app uses CocoaPods', () async {
Expand Down
Loading