Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 18 additions & 107 deletions packages/flutter_tools/lib/src/ios/xcodeproj.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import '../base/terminal.dart';
import '../base/utils.dart';
import '../base/version.dart';
import '../build_info.dart';
import '../convert.dart';
import '../xcode_project.dart';

final _settingExpr = RegExp(r'(\w+)\s*=\s*(.*)$');
Expand Down Expand Up @@ -193,7 +192,11 @@ class XcodeProjectInterpreter {
// 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(xcodeProject, buildDirectory: buildDirectory, quiet: false);
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.

just to clarify - the original code prefetches for both macOS & iOS?

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.

The original code prefetched for whichever was first (iOS) and then wouldn't prefetch for the next (macOS) because the process would have already been set.

await prefetchSwiftPackagesForProject(
xcodeProject,
buildDirectory: buildDirectory,
quiet: false,
);

return _xcodebuildProjectCommandArguments(
buildDirectory,
Expand Down Expand Up @@ -386,117 +389,25 @@ class XcodeProjectInterpreter {
], workingDirectory: projectPath);
}

/// The process used to fetch Swift packages.
Process? _swiftPackageFetchProcess;

/// 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(
Future<void> prefetchSwiftPackagesForProject(
XcodeBasedProject xcodeProject, {
required Directory buildDirectory,
bool quiet = true,
bool waitForCompletion = true,
}) async {
final String projectPath = xcodeProject.hostAppRoot.path;
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.
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.

is this comment accurate? what if subsequent xcodebuild command runs before packages are done fetching?

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 is accurate. Subsequent xcodebuild commands should not be run before packages are done fetching. The only case where that could happen is if waitForCompletion is false, which is not set when using fetchDependenciesAndGenerateXcodebuildArgs.

skipPackageUpdatesAndValidation: false,
),
processUtils: _processUtils,
logger: _logger,
quiet: quiet,
waitForCompletion: waitForCompletion,
);
}

Future<XcodeProjectInfo?> getInfo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class DarwinDependencyManagement {
await _swiftPackageManager.generatePluginsSwiftPackage(_plugins, platform, xcodeProject);

// Start the SwiftPM dependency resolution in the background.
await _xcodeProjectInterpreter?.prefetchSwiftPackages(
await _xcodeProjectInterpreter?.prefetchSwiftPackagesForProject(
xcodeProject,
waitForCompletion: false,
buildDirectory: _fileSystem.directory(
Expand Down
110 changes: 110 additions & 0 deletions packages/flutter_tools/lib/src/xcode_project.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
/// @docImport 'ios/mac.dart';
library;

import 'dart:async';

import 'package:yaml/yaml.dart' as yaml;

import 'base/common.dart';
import 'base/error_handling_io.dart';
import 'base/file_system.dart';
import 'base/io.dart';
import 'base/logger.dart';
import 'base/process.dart';
import 'base/template.dart';
import 'base/utils.dart';
import 'base/version.dart';
Comment thread
vashworth marked this conversation as resolved.
Expand Down Expand Up @@ -384,6 +389,111 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform {
}
return flavor;
}

/// The process used to fetch Swift packages.
Process? _swiftPackageFetchProcess;

/// 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 Xcode project if the project has migrated to SwiftPM.
///
/// 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({
required List<String> xcodebuildProjectCommandArguments,
required ProcessUtils processUtils,
required Logger logger,
bool quiet = true,
bool waitForCompletion = true,
}) async {
Status? status;
try {
final command = <String>[...xcodebuildProjectCommandArguments, '-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: hostAppRoot.path);
_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();
}
}
}

/// Represents the iOS sub-project of a Flutter project.
Expand Down
Loading
Loading