Skip to content
Draft
4 changes: 3 additions & 1 deletion packages/flutter_tools/lib/src/commands/clean.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class CleanCommand extends FlutterCommand {
try {
final XcodeProjectInterpreter xcodeProjectInterpreter = globals.xcodeProjectInterpreter!;
final XcodeProjectInfo projectInfo = (await xcodeProjectInterpreter.getInfo(
xcodeWorkspace.parent.path,
xcodeProject,
buildDirectory: globals.fs.directory(xcodeProject.darwinPlatform.buildDirectory()),
))!;
if (argResults?.wasParsed('scheme') ?? false) {
Expand All @@ -116,6 +116,7 @@ class CleanCommand extends FlutterCommand {
throwToolExit('Scheme "$scheme" not found in ${projectInfo.schemes}');
}
await xcodeProjectInterpreter.cleanWorkspace(
xcodeProject,
xcodeWorkspace.path,
scheme,
verbose: _verbose,
Expand All @@ -124,6 +125,7 @@ class CleanCommand extends FlutterCommand {
} else {
for (final String scheme in projectInfo.schemes) {
await xcodeProjectInterpreter.cleanWorkspace(
xcodeProject,
xcodeWorkspace.path,
scheme,
verbose: _verbose,
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter_tools/lib/src/ios/mac.dart
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ Future<XcodeBuildResult> buildXcodeProject({

final List<String> xcodebuildCommandArgs = await globals.xcode!
.fetchDependenciesAndGenerateXcodebuildArgs(
app.project.hostAppRoot.path,
app.project,
globals.fs.directory(buildDirectoryPath),
skipPackageUpdatesAndValidation: false,
);
Expand Down
145 changes: 30 additions & 115 deletions packages/flutter_tools/lib/src/ios/xcodeproj.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import '../base/terminal.dart';
import '../base/utils.dart';
import '../base/version.dart';
import '../build_info.dart';
import '../convert.dart';
import '../project.dart';
import '../reporting/reporting.dart';

final _settingExpr = RegExp(r'(\w+)\s*=\s*(.*)$');
Expand Down Expand Up @@ -186,14 +186,18 @@ class XcodeProjectInterpreter {
/// Using this method when running `xcodebuild` commands ensures that `xcrun` is used properly
/// and that the Swift package cache is properly configured.
Future<List<String>> fetchDependenciesAndGenerateXcodebuildArgs(
String projectPath,
XcodeBasedProject xcodeProject,
Directory buildDirectory, {
bool skipPackageUpdatesAndValidation = true,
}) async {
// All `xcodebuild` project commands will download and resolve Swift packages.
// We should always prefetch Swift packages before running any `xcodebuild` project command
// to control the output.
await prefetchSwiftPackages(projectPath, buildDirectory: buildDirectory, quiet: false);
await prefetchSwiftPackagesForProject(
xcodeProject,
buildDirectory: buildDirectory,
quiet: false,
);

return _xcodebuildProjectCommandArguments(
buildDirectory,
Expand Down Expand Up @@ -232,7 +236,7 @@ class XcodeProjectInterpreter {
/// If [XcodeProjectBuildContext.scheme] is `null`, `xcodebuild` will
/// return build settings for the first discovered target (by default this is Runner).
Future<Map<String, String>> getBuildSettings(
String projectPath, {
XcodeBasedProject xcodeProject, {
required XcodeProjectBuildContext buildContext,
Duration timeout = const Duration(minutes: 1),
}) async {
Expand All @@ -247,9 +251,10 @@ class XcodeProjectInterpreter {
XcodeSdk.WatchOS || XcodeSdk.WatchSimulator => getIosBuildDirectory(),
};
final List<String> xcodebuildCommandArgs = await fetchDependenciesAndGenerateXcodebuildArgs(
projectPath,
xcodeProject,
_fileSystem.directory(buildDir),
);
final String projectPath = xcodeProject.hostAppRoot.path;
final showBuildSettingsCommand = <String>[
...xcodebuildCommandArgs,
'-project',
Expand Down Expand Up @@ -360,14 +365,15 @@ class XcodeProjectInterpreter {
}

Future<void> cleanWorkspace(
XcodeBasedProject xcodeProject,
String workspacePath,
String scheme, {
required Directory buildDirectory,
bool verbose = false,
}) async {
final String projectPath = _fileSystem.currentDirectory.path;
final List<String> xcodebuildCommandArgs = await fetchDependenciesAndGenerateXcodebuildArgs(
projectPath,
xcodeProject,
buildDirectory,
);
await _processUtils.run(<String>[
Expand All @@ -382,120 +388,29 @@ class XcodeProjectInterpreter {
], workingDirectory: projectPath);
}

/// The process used to fetch Swift packages.
Process? _swiftPackageFetchProcess;
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 logic was moved to XcodeBasedProject so that the process could be per project (ios and macos).


/// The stdout subscription for the Swift package fetch process.
StreamSubscription<String>? _swiftPackageFetchStdoutSubscription;

/// The stderr subscription for the Swift package fetch process.
StreamSubscription<String>? _swiftPackageFetchStderrSubscription;

/// Prefetches Swift packages for the given Xcode project.
///
/// If a process is already running from a previous Flutter command, kill it before starting
/// the command. If the process is already running from the same Flutter command, wait for it to
/// complete if [waitForCompletion] is true.
///
/// If [quiet] is false, it will print a spinner while the command is running and print logs of
/// what Swift packages are being fetched.
Future<void> prefetchSwiftPackages(
String projectPath, {
/// Prefetches Swift packages for the given [xcodeProject] if the project has migrated to SwiftPM.
Future<void> prefetchSwiftPackagesForProject(
XcodeBasedProject xcodeProject, {
required Directory buildDirectory,
bool quiet = true,
bool waitForCompletion = true,
}) async {
Status? status;
try {
final command = <String>[
..._xcodebuildProjectCommandArguments(
buildDirectory,
// skipPackageUpdatesAndValidation should be false so that when subsequent xcodebuild
// commands run, packages should already be resolved, downloaded, updated, and validated.
skipPackageUpdatesAndValidation: false,
),
'-resolvePackageDependencies',
];
if (_swiftPackageFetchProcess == null) {
// Remove the `xcrun` prefixes from the command before comparing because the process name
// will resolve to the actual xcodebuild path, such as this:
// /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild
final int xcodebuildIndex = command.indexOf('xcodebuild');
if (xcodebuildIndex == -1) {
// This should never happen. The _xcodebuildProjectCommandArguments always includes
// xcodebuild.
throw StateError('Command "${command.join(' ')}" is expected to contain `xcodebuild`.');
}
final String commandToMatch = command.sublist(xcodebuildIndex).join(' ');

// 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
// one to error. The new process will pick up where the old one left off.
final RunResult result = await _processUtils.run([
'pgrep',
'-n', // Select only the newest
'-f', // Match against full argument lists
'-l', // Print the process name and process ID
commandToMatch, // command must be a string rather than a list so it matches on all of it
]);
if (result.exitCode == 0) {
final String processOutput = result.stdout.trim();
// Process output is formatted like this:
// 89012 /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild -clonedSourcePackagesDirPath...
final int? pid = int.tryParse(processOutput.split(' ').firstOrNull ?? '');
if (pid != null && processOutput.endsWith(commandToMatch)) {
_logger.printTrace(
'Swift Package Manager dependencies are already being fetched by PID $pid',
);
await _processUtils.run(['kill', '$pid']);
}
}
}

final Process process =
_swiftPackageFetchProcess ??
await _processUtils.start(command, workingDirectory: projectPath);
_swiftPackageFetchProcess ??= process;
if (!waitForCompletion) {
return;
}
if (!quiet) {
var printFetchWarnings = false;
_swiftPackageFetchStdoutSubscription ??= process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((String line) {
if (line.startsWith('Fetching')) {
status?.cancel();
if (!printFetchWarnings) {
_logger.printStatus(
'Xcode is fetching Swift Package Manager dependencies. This may take several minutes...',
);
printFetchWarnings = true;
}
status = _logger.startProgress(' $line...');
}
});
}
final stderrBuffer = StringBuffer();
_swiftPackageFetchStderrSubscription ??= process.stderr
.transform<String>(const Utf8Decoder(reportErrors: false))
.listen(stderrBuffer.write);

final int exitCode = await process.exitCode.whenComplete(() async {
await _swiftPackageFetchStdoutSubscription?.cancel();
await _swiftPackageFetchStderrSubscription?.cancel();
});
if (exitCode != 0) {
throwToolExit('Xcode failed to resolve Swift Package Manager dependencies:\n$stderrBuffer');
}
} finally {
status?.cancel();
}
await xcodeProject.prefetchSwiftPackages(
xcodebuildProjectCommandArguments: _xcodebuildProjectCommandArguments(
buildDirectory,
// skipPackageUpdatesAndValidation should be false so that when subsequent xcodebuild
// commands run, packages should already be resolved, downloaded, updated, and validated.
skipPackageUpdatesAndValidation: false,
),
processUtils: _processUtils,
logger: _logger,
quiet: quiet,
waitForCompletion: waitForCompletion,
);
}

Future<XcodeProjectInfo?> getInfo(
String projectPath, {
XcodeBasedProject xcodeProject, {
String? projectFilename,
required Directory buildDirectory,
}) async {
Expand All @@ -507,7 +422,7 @@ class XcodeProjectInterpreter {
const corruptedProjectExitCode = 74;
bool allowedFailures(int c) => c == missingProjectExitCode || c == corruptedProjectExitCode;
final List<String> xcodebuildCommandArgs = await fetchDependenciesAndGenerateXcodebuildArgs(
projectPath,
xcodeProject,
buildDirectory,
);
final RunResult result = await _processUtils.run(
Expand All @@ -518,7 +433,7 @@ class XcodeProjectInterpreter {
],
throwOnError: true,
allowedFailures: allowedFailures,
workingDirectory: projectPath,
workingDirectory: xcodeProject.hostAppRoot.path,
);
if (allowedFailures(result.exitCode)) {
// User configuration error, tool exit instead of crashing.
Expand Down
4 changes: 2 additions & 2 deletions packages/flutter_tools/lib/src/macos/build_macos.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Future<void> buildMacOS({
// regardless of the project name so long as there is exactly one project.
final String? xcodeProjectName = xcodeProject.existsSync() ? xcodeProject.basename : null;
final XcodeProjectInfo? projectInfo = await globals.xcodeProjectInterpreter?.getInfo(
xcodeProject.parent.path,
flutterProject.macos,
projectFilename: xcodeProjectName,
buildDirectory: flutterBuildDir,
);
Expand Down Expand Up @@ -217,7 +217,7 @@ Future<void> buildMacOS({
try {
final List<String> xcodebuildCommandArgs = await globals.xcode!
.fetchDependenciesAndGenerateXcodebuildArgs(
flutterProject.macos.hostAppRoot.path,
flutterProject.macos,
globals.fs.directory(buildDirectoryPath),
skipPackageUpdatesAndValidation: false,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,16 @@ class DarwinDependencyManagement {
if (xcodeProject.usesSwiftPackageManager) {
await _swiftPackageManager.generatePluginsSwiftPackage(_plugins, platform, xcodeProject);

// Start the SwiftPM dependency resolution in the background.
await _xcodeProjectInterpreter?.prefetchSwiftPackages(
xcodeProject.hostAppRoot.path,
waitForCompletion: false,
buildDirectory: _fileSystem.directory(
platform.buildDirectory(config: _config, fileSystem: _fileSystem),
),
);
if (_hostPlatform.isMacOS && xcodeProject.flutterPluginSwiftPackageInProjectSettings) {
// Start the SwiftPM dependency resolution in the background.
await _xcodeProjectInterpreter?.prefetchSwiftPackagesForProject(
xcodeProject,
buildDirectory: _fileSystem.directory(
platform.buildDirectory(config: _config, fileSystem: _fileSystem),
),
waitForCompletion: false,
);
}
} 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.
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;
}
5 changes: 3 additions & 2 deletions packages/flutter_tools/lib/src/macos/xcode.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import '../base/version.dart';
import '../build_info.dart';
import '../cache.dart';
import '../ios/xcodeproj.dart';
import '../xcode_project.dart';

Version get xcodeRequiredVersion => Version(15, null, null);

Expand Down Expand Up @@ -232,11 +233,11 @@ class Xcode {
List<String> xcrunCommand() => _xcodeProjectInterpreter.xcrunCommand();

Future<List<String>> fetchDependenciesAndGenerateXcodebuildArgs(
String projectPath,
XcodeBasedProject xcodeProject,
Directory buildDirectory, {
bool skipPackageUpdatesAndValidation = true,
}) async => _xcodeProjectInterpreter.fetchDependenciesAndGenerateXcodebuildArgs(
projectPath,
xcodeProject,
buildDirectory,
skipPackageUpdatesAndValidation: skipPackageUpdatesAndValidation,
);
Expand Down
Loading
Loading