diff --git a/packages/flutter_tools/lib/src/commands/clean.dart b/packages/flutter_tools/lib/src/commands/clean.dart index c350ce18e397b..e382891db70ee 100644 --- a/packages/flutter_tools/lib/src/commands/clean.dart +++ b/packages/flutter_tools/lib/src/commands/clean.dart @@ -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) { @@ -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, @@ -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, diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 89bfe785fc4f3..4d9ba4dc877d3 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -307,7 +307,7 @@ Future buildXcodeProject({ final List xcodebuildCommandArgs = await globals.xcode! .fetchDependenciesAndGenerateXcodebuildArgs( - app.project.hostAppRoot.path, + app.project, globals.fs.directory(buildDirectoryPath), skipPackageUpdatesAndValidation: false, ); diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart index 732a858d483ef..7924953ca832e 100644 --- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart +++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart @@ -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*(.*)$'); @@ -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> 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, @@ -234,7 +238,7 @@ class XcodeProjectInterpreter { /// If [XcodeProjectBuildContext.scheme] is `null`, `xcodebuild` will /// return build settings for the first discovered target (by default this is Runner). Future> getBuildSettings( - String projectPath, { + XcodeBasedProject xcodeProject, { required XcodeProjectBuildContext buildContext, Duration timeout = const Duration(minutes: 1), }) async { @@ -249,9 +253,10 @@ class XcodeProjectInterpreter { XcodeSdk.WatchOS || XcodeSdk.WatchSimulator => getIosBuildDirectory(), }; final List xcodebuildCommandArgs = await fetchDependenciesAndGenerateXcodebuildArgs( - projectPath, + xcodeProject, _fileSystem.directory(buildDir), ); + final String projectPath = xcodeProject.xcodeProject.path; final showBuildSettingsCommand = [ ...xcodebuildCommandArgs, '-project', @@ -362,6 +367,7 @@ class XcodeProjectInterpreter { } Future cleanWorkspace( + XcodeBasedProject xcodeProject, String workspacePath, String scheme, { required Directory buildDirectory, @@ -369,7 +375,7 @@ class XcodeProjectInterpreter { }) async { final String projectPath = _fileSystem.currentDirectory.path; final List xcodebuildCommandArgs = await fetchDependenciesAndGenerateXcodebuildArgs( - projectPath, + xcodeProject, buildDirectory, ); await _processUtils.run([ @@ -384,120 +390,29 @@ class XcodeProjectInterpreter { ], workingDirectory: projectPath); } - /// The process used to fetch Swift packages. - Process? _swiftPackageFetchProcess; - - /// The stdout subscription for the Swift package fetch process. - StreamSubscription? _swiftPackageFetchStdoutSubscription; - - /// The stderr subscription for the Swift package fetch process. - StreamSubscription? _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 prefetchSwiftPackages( - String projectPath, { + /// Prefetches Swift packages for the given [xcodeProject] if the project has migrated to SwiftPM. + Future prefetchSwiftPackagesForProject( + XcodeBasedProject xcodeProject, { required Directory buildDirectory, bool quiet = true, bool waitForCompletion = true, }) async { - Status? status; - try { - final command = [ - ..._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(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 getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { @@ -509,7 +424,7 @@ class XcodeProjectInterpreter { const corruptedProjectExitCode = 74; bool allowedFailures(int c) => c == missingProjectExitCode || c == corruptedProjectExitCode; final List xcodebuildCommandArgs = await fetchDependenciesAndGenerateXcodebuildArgs( - projectPath, + xcodeProject, buildDirectory, ); final RunResult result = await _processUtils.run( @@ -520,7 +435,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. diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart index ece1adae40230..057df94cef995 100644 --- a/packages/flutter_tools/lib/src/macos/build_macos.dart +++ b/packages/flutter_tools/lib/src/macos/build_macos.dart @@ -125,7 +125,7 @@ Future 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, ); @@ -217,7 +217,7 @@ Future buildMacOS({ try { final List xcodebuildCommandArgs = await globals.xcode! .fetchDependenciesAndGenerateXcodebuildArgs( - flutterProject.macos.hostAppRoot.path, + flutterProject.macos, globals.fs.directory(buildDirectoryPath), skipPackageUpdatesAndValidation: false, ); diff --git a/packages/flutter_tools/lib/src/macos/darwin_dependency_management.dart b/packages/flutter_tools/lib/src/macos/darwin_dependency_management.dart index d44babc451ead..425097b6ee64b 100644 --- a/packages/flutter_tools/lib/src/macos/darwin_dependency_management.dart +++ b/packages/flutter_tools/lib/src/macos/darwin_dependency_management.dart @@ -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. diff --git a/packages/flutter_tools/lib/src/macos/swift_package_manager.dart b/packages/flutter_tools/lib/src/macos/swift_package_manager.dart index f35576e07dcbd..b25de8c10a9bc 100644 --- a/packages/flutter_tools/lib/src/macos/swift_package_manager.dart +++ b/packages/flutter_tools/lib/src/macos/swift_package_manager.dart @@ -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.', + ), + _SwiftPMPluginErrorMatcher( + // Example: /path/to/plugin_a/Package.swift:25:17: error: expected ',' separator + pattern: RegExp(r'([^\/]+)\/Package\.swift:\d+:\d+: error:'), + 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 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)) { + 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; } diff --git a/packages/flutter_tools/lib/src/macos/xcode.dart b/packages/flutter_tools/lib/src/macos/xcode.dart index 493b7beca701c..5485c2ef1c22e 100644 --- a/packages/flutter_tools/lib/src/macos/xcode.dart +++ b/packages/flutter_tools/lib/src/macos/xcode.dart @@ -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); @@ -232,11 +233,11 @@ class Xcode { List xcrunCommand() => _xcodeProjectInterpreter.xcrunCommand(); Future> fetchDependenciesAndGenerateXcodebuildArgs( - String projectPath, + XcodeBasedProject xcodeProject, Directory buildDirectory, { bool skipPackageUpdatesAndValidation = true, }) async => _xcodeProjectInterpreter.fetchDependenciesAndGenerateXcodebuildArgs( - projectPath, + xcodeProject, buildDirectory, skipPackageUpdatesAndValidation: skipPackageUpdatesAndValidation, ); diff --git a/packages/flutter_tools/lib/src/migrations/swift_package_manager_integration_migration.dart b/packages/flutter_tools/lib/src/migrations/swift_package_manager_integration_migration.dart index 33db4ae6a9c78..d78dbfce304f3 100644 --- a/packages/flutter_tools/lib/src/migrations/swift_package_manager_integration_migration.dart +++ b/packages/flutter_tools/lib/src/migrations/swift_package_manager_integration_migration.dart @@ -192,8 +192,8 @@ class SwiftPackageManagerIntegrationMigration extends ProjectMigrator { if (isSchemeMigrated && isPbxprojMigrated && isOptionalFilesMigrated) { return; } - - migrationStatus = logger.startProgress('Adding Swift Package Manager integration...'); + logger.printStatus('Adding Swift Package Manager integration...'); + migrationStatus = logger.startSpinner(); if (isSchemeMigrated) { logger.printTrace('${schemeInfo.schemeFile.basename} already migrated. Skipping...'); @@ -227,13 +227,36 @@ 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. + migrationStatus.stop(); + await _xcodeProjectInterpreter.prefetchSwiftPackagesForProject( + _xcodeProject, + buildDirectory: _fileSystem.directory( + _platform.buildDirectory(config: _config, fileSystem: _fileSystem), + ), + quiet: false, + ); + migrationStatus = logger.startSpinner(); + } 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, + _xcodeProject, 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) { @@ -248,8 +271,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 ' @@ -1430,3 +1452,12 @@ class ParsedProject { final String identifier; final List? packageReferences; } + +class _PrefetchSwiftPackageException implements Exception { + _PrefetchSwiftPackageException(this.message); + + final String message; + + @override + String toString() => message; +} diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart index bdfe5ff3a3ce3..17b1b6757aa39 100644 --- a/packages/flutter_tools/lib/src/xcode_project.dart +++ b/packages/flutter_tools/lib/src/xcode_project.dart @@ -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'; @@ -240,7 +245,7 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform { return null; } return _projectInfo ??= await xcodeProjectInterpreter.getInfo( - hostAppRoot.path, + this, buildDirectory: globals.fs.directory(darwinPlatform.buildDirectory()), ); } @@ -339,7 +344,7 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform { } final Map buildSettings = await xcodeProjectInterpreter.getBuildSettings( - xcodeProject.path, + this, buildContext: buildContext, ); if (buildSettings.isNotEmpty) { @@ -384,6 +389,127 @@ 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? _swiftPackageFetchStdoutSubscription; + + /// The stderr subscription for the Swift package fetch process. + StreamSubscription? _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 prefetchSwiftPackages({ + required List xcodebuildProjectCommandArguments, + required ProcessUtils processUtils, + required Logger logger, + bool quiet = true, + bool waitForCompletion = true, + }) async { + // If the project has not migrated to SwiftPM, we don't need to prefetch Swift packages. + if (!usesSwiftPackageManager || !flutterPluginSwiftPackageInProjectSettings) { + return; + } + Status? status; + try { + final command = [...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(const Utf8Decoder(reportErrors: false)) + .listen(stderrBuffer.write); + + final int exitCode = await process.exitCode.whenComplete(() async { + await _swiftPackageFetchStdoutSubscription?.cancel(); + await _swiftPackageFetchStderrSubscription?.cancel(); + }); + if (exitCode != 0) { + final stderrString = stderrBuffer.toString(); + + final List plugins = await getPlugins(); + final String? pluginError = SwiftPackageManager.parsePluginError( + stderrString, + pluginNames: plugins.map((p) => p.name).toList(), + ); + + if (pluginError != null) { + logger.printError(stderrString); + throwToolExit(pluginError); + } + throwToolExit('Xcode failed to resolve Swift Package Manager dependencies:\n$stderrBuffer'); + } + } finally { + status?.cancel(); + } + } } /// Represents the iOS sub-project of a Flutter project. diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart index 425e6cc098b84..12f7aa477e6b3 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart @@ -42,7 +42,7 @@ class FakeXcodeProjectInterpreterWithBuildSettings extends FakeXcodeProjectInter @override Future> getBuildSettings( - String projectPath, { + XcodeBasedProject xcodeProject, { XcodeProjectBuildContext? buildContext, Duration timeout = const Duration(minutes: 1), }) async { diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart index e452477b68322..207dd069f8899 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart @@ -18,6 +18,7 @@ import 'package:flutter_tools/src/commands/build_ios.dart'; import 'package:flutter_tools/src/dart/pub.dart'; import 'package:flutter_tools/src/ios/plist_parser.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; +import 'package:flutter_tools/src/project.dart'; import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart'; @@ -40,7 +41,7 @@ class FakeXcodeProjectInterpreterWithBuildSettings extends FakeXcodeProjectInter Map get overrides => _overrides; @override Future> getBuildSettings( - String projectPath, { + XcodeBasedProject xcodeProject, { XcodeProjectBuildContext? buildContext, Duration timeout = const Duration(minutes: 1), }) async { diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart index 0e81686c96250..c0b8cd8496a69 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart @@ -35,7 +35,7 @@ import '../../src/throwing_pub.dart'; class FakeXcodeProjectInterpreterWithProfile extends FakeXcodeProjectInterpreter { @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { @@ -52,7 +52,7 @@ class FakeXcodeProjectInterpreterWithBuildSettings extends FakeXcodeProjectInter @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { @@ -63,7 +63,7 @@ class FakeXcodeProjectInterpreterWithBuildSettings extends FakeXcodeProjectInter @override Future> getBuildSettings( - String projectPath, { + XcodeBasedProject xcodeProject, { XcodeProjectBuildContext? buildContext, Duration timeout = const Duration(minutes: 1), }) async { @@ -825,42 +825,38 @@ STDERR STUFF }, ); - testUsingContext( - 'Refuses to build for macOS when feature is disabled', - () { - final CommandRunner runner = createTestCommandRunner( - BuildCommand( - androidSdk: FakeAndroidSdk(), - buildSystem: TestBuildSystem.all(BuildResult(success: true)), - fileSystem: fileSystem, - logger: logger, - osUtils: FakeOperatingSystemUtils(), - config: FakeConfig(), - platform: FakePlatform(), - fileSystemUtils: FakeFileSystemUtils(), - terminal: FakeTerminal(), - plistParser: FakePlistParser(), - processUtils: FakeProcessUtils(), - processManager: FakeProcessManager.any(), - templateRenderer: FakeTemplateRenderer(), - xcode: FakeXcode(), - artifacts: FakeArtifacts(), - cache: FakeCache(), - flutterVersion: FakeFlutterVersion(), - ), - ); + testUsingContext('Refuses to build for macOS when feature is disabled', () { + final CommandRunner runner = createTestCommandRunner( + BuildCommand( + androidSdk: FakeAndroidSdk(), + buildSystem: TestBuildSystem.all(BuildResult(success: true)), + fileSystem: fileSystem, + logger: logger, + osUtils: FakeOperatingSystemUtils(), + config: FakeConfig(), + platform: FakePlatform(), + fileSystemUtils: FakeFileSystemUtils(), + terminal: FakeTerminal(), + plistParser: FakePlistParser(), + processUtils: FakeProcessUtils(), + processManager: FakeProcessManager.any(), + templateRenderer: FakeTemplateRenderer(), + xcode: FakeXcode(), + artifacts: FakeArtifacts(), + cache: FakeCache(), + flutterVersion: FakeFlutterVersion(), + ), + ); - final bool supported = BuildMacosCommand( - logger: BufferLogger.test(), - verboseHelp: false, - ).supported; - expect( - () => runner.run(['build', 'macos', '--no-pub']), - supported ? throwsToolExit() : throwsA(isA()), - ); - }, - overrides: {FeatureFlags: () => TestFeatureFlags()}, - ); + final bool supported = BuildMacosCommand( + logger: BufferLogger.test(), + verboseHelp: false, + ).supported; + expect( + () => runner.run(['build', 'macos', '--no-pub']), + supported ? throwsToolExit() : throwsA(isA()), + ); + }, overrides: {FeatureFlags: () => TestFeatureFlags()}); testUsingContext( 'hidden when not enabled on macOS host', diff --git a/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart index 8cdb34b902a17..1733e9efdbff8 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart @@ -354,39 +354,35 @@ void main() { }, ); - testUsingContext( - '$CleanCommand handles missing delete permissions', - () async { - final handler = FileExceptionHandler(); - - // Ensures we handle ErrorHandlingFileSystem appropriately in prod. - // See https://github.com/flutter/flutter/issues/108978. - final FileSystem fileSystem = ErrorHandlingFileSystem( - delegate: MemoryFileSystem.test(opHandle: handler.opHandle), - platform: windowsPlatform, - ); - final File throwingFile = fileSystem.file('bad')..createSync(); - handler.addError( - throwingFile, - FileSystemOp.delete, - const FileSystemException('OS error: Access Denied'), - ); - - xcodeProjectInterpreter.isInstalled = false; - - final command = CleanCommand(); - command.deleteFile(throwingFile); - - expect( - testLogger.errorText, - contains( - 'Failed to remove bad. A program may still be using a file in the directory or the directory itself', - ), - ); - expect(throwingFile, exists); - }, - overrides: {Platform: () => windowsPlatform, Xcode: () => xcode}, - ); + testUsingContext('$CleanCommand handles missing delete permissions', () async { + final handler = FileExceptionHandler(); + + // Ensures we handle ErrorHandlingFileSystem appropriately in prod. + // See https://github.com/flutter/flutter/issues/108978. + final FileSystem fileSystem = ErrorHandlingFileSystem( + delegate: MemoryFileSystem.test(opHandle: handler.opHandle), + platform: windowsPlatform, + ); + final File throwingFile = fileSystem.file('bad')..createSync(); + handler.addError( + throwingFile, + FileSystemOp.delete, + const FileSystemException('OS error: Access Denied'), + ); + + xcodeProjectInterpreter.isInstalled = false; + + final command = CleanCommand(); + command.deleteFile(throwingFile); + + expect( + testLogger.errorText, + contains( + 'Failed to remove bad. A program may still be using a file in the directory or the directory itself', + ), + ); + expect(throwingFile, exists); + }, overrides: {Platform: () => windowsPlatform, Xcode: () => xcode}); }); }); } @@ -434,7 +430,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { @@ -448,6 +444,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future cleanWorkspace( + XcodeBasedProject xcodeProject, String workspacePath, String scheme, { required Directory buildDirectory, diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart index ffedf58498f3c..19f43b4aa8cf8 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart @@ -16,6 +16,7 @@ import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/features.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'; @@ -72,110 +73,98 @@ void main() { iosEnvironment.buildDir.createSync(recursive: true); }); - testUsingContext( - 'KernelSnapshot throws error if missing build mode', - () async { - androidEnvironment.defines.remove(kBuildMode); - expect( - const KernelSnapshot().build(androidEnvironment), - throwsA(isA()), - ); - }, - overrides: {FeatureFlags: () => TestFeatureFlags()}, - ); + testUsingContext('KernelSnapshot throws error if missing build mode', () async { + androidEnvironment.defines.remove(kBuildMode); + expect( + const KernelSnapshot().build(androidEnvironment), + throwsA(isA()), + ); + }, overrides: {FeatureFlags: () => TestFeatureFlags()}); - testUsingContext( - 'KernelSnapshot handles null result from kernel compilation', - () async { - fileSystem.file('.dart_tool/package_config.json') - ..createSync(recursive: true) - ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); - final String build = androidEnvironment.buildDir.path; - final String flutterPatchedSdkPath = artifacts.getArtifactPath( - Artifact.flutterPatchedSdkPath, - platform: TargetPlatform.android_arm, - mode: BuildMode.profile, - ); - processManager.addCommands([ - FakeCommand( - command: [ - artifacts.getArtifactPath(Artifact.engineDartAotRuntime), - artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk), - '--sdk-root', - '$flutterPatchedSdkPath/', - '--target=flutter', - '--no-print-incremental-dependencies', - ...buildModeOptions(BuildMode.profile, []), - '--track-widget-creation', - '--aot', - '--tfa', - '--target-os', - 'android', - '--packages', - '/.dart_tool/package_config.json', - '--output-dill', - '$build/app.dill', - '--depfile', - '$build/kernel_snapshot_program.d', - '--verbosity=error', - 'file:///lib/main.dart', - ], - exitCode: 1, - ), - ]); + testUsingContext('KernelSnapshot handles null result from kernel compilation', () async { + fileSystem.file('.dart_tool/package_config.json') + ..createSync(recursive: true) + ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + final String build = androidEnvironment.buildDir.path; + final String flutterPatchedSdkPath = artifacts.getArtifactPath( + Artifact.flutterPatchedSdkPath, + platform: TargetPlatform.android_arm, + mode: BuildMode.profile, + ); + processManager.addCommands([ + FakeCommand( + command: [ + artifacts.getArtifactPath(Artifact.engineDartAotRuntime), + artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk), + '--sdk-root', + '$flutterPatchedSdkPath/', + '--target=flutter', + '--no-print-incremental-dependencies', + ...buildModeOptions(BuildMode.profile, []), + '--track-widget-creation', + '--aot', + '--tfa', + '--target-os', + 'android', + '--packages', + '/.dart_tool/package_config.json', + '--output-dill', + '$build/app.dill', + '--depfile', + '$build/kernel_snapshot_program.d', + '--verbosity=error', + 'file:///lib/main.dart', + ], + exitCode: 1, + ), + ]); - await expectLater(() => const KernelSnapshot().build(androidEnvironment), throwsException); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {FeatureFlags: () => TestFeatureFlags()}, - ); + await expectLater(() => const KernelSnapshot().build(androidEnvironment), throwsException); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {FeatureFlags: () => TestFeatureFlags()}); - testUsingContext( - 'KernelSnapshot does use track widget creation on profile builds', - () async { - fileSystem.file('.dart_tool/package_config.json') - ..createSync(recursive: true) - ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); - final String build = androidEnvironment.buildDir.path; - final String flutterPatchedSdkPath = artifacts.getArtifactPath( - Artifact.flutterPatchedSdkPath, - platform: TargetPlatform.android_arm, - mode: BuildMode.profile, - ); - processManager.addCommands([ - FakeCommand( - command: [ - artifacts.getArtifactPath(Artifact.engineDartAotRuntime), - artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk), - '--sdk-root', - '$flutterPatchedSdkPath/', - '--target=flutter', - '--no-print-incremental-dependencies', - ...buildModeOptions(BuildMode.profile, []), - '--track-widget-creation', - '--aot', - '--tfa', - '--target-os', - 'android', - '--packages', - '/.dart_tool/package_config.json', - '--output-dill', - '$build/app.dill', - '--depfile', - '$build/kernel_snapshot_program.d', - '--verbosity=error', - 'file:///lib/main.dart', - ], - stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n', - ), - ]); + testUsingContext('KernelSnapshot does use track widget creation on profile builds', () async { + fileSystem.file('.dart_tool/package_config.json') + ..createSync(recursive: true) + ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + final String build = androidEnvironment.buildDir.path; + final String flutterPatchedSdkPath = artifacts.getArtifactPath( + Artifact.flutterPatchedSdkPath, + platform: TargetPlatform.android_arm, + mode: BuildMode.profile, + ); + processManager.addCommands([ + FakeCommand( + command: [ + artifacts.getArtifactPath(Artifact.engineDartAotRuntime), + artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk), + '--sdk-root', + '$flutterPatchedSdkPath/', + '--target=flutter', + '--no-print-incremental-dependencies', + ...buildModeOptions(BuildMode.profile, []), + '--track-widget-creation', + '--aot', + '--tfa', + '--target-os', + 'android', + '--packages', + '/.dart_tool/package_config.json', + '--output-dill', + '$build/app.dill', + '--depfile', + '$build/kernel_snapshot_program.d', + '--verbosity=error', + 'file:///lib/main.dart', + ], + stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n', + ), + ]); - await const KernelSnapshot().build(androidEnvironment); + await const KernelSnapshot().build(androidEnvironment); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {FeatureFlags: () => TestFeatureFlags()}, - ); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {FeatureFlags: () => TestFeatureFlags()}); testUsingContext( 'KernelSnapshot correctly handles an empty string in ExtraFrontEndOptions', @@ -224,157 +213,145 @@ void main() { overrides: {FeatureFlags: () => TestFeatureFlags()}, ); - testUsingContext( - 'KernelSnapshot correctly forwards FrontendServerStarterPath', - () async { - fileSystem.file('.dart_tool/package_config.json') - ..createSync(recursive: true) - ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); - final String build = androidEnvironment.buildDir.path; - final String flutterPatchedSdkPath = artifacts.getArtifactPath( - Artifact.flutterPatchedSdkPath, - platform: TargetPlatform.android_arm, - mode: BuildMode.profile, - ); - processManager.addCommands([ - FakeCommand( - command: [ - artifacts.getArtifactPath(Artifact.engineDartBinary), - 'path/to/frontend_server_starter.dart', - '--sdk-root', - '$flutterPatchedSdkPath/', - '--target=flutter', - '--no-print-incremental-dependencies', - ...buildModeOptions(BuildMode.profile, []), - '--track-widget-creation', - '--aot', - '--tfa', - '--target-os', - 'android', - '--packages', - '/.dart_tool/package_config.json', - '--output-dill', - '$build/app.dill', - '--depfile', - '$build/kernel_snapshot_program.d', - '--verbosity=error', - 'file:///lib/main.dart', - ], - stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n', - ), - ]); + testUsingContext('KernelSnapshot correctly forwards FrontendServerStarterPath', () async { + fileSystem.file('.dart_tool/package_config.json') + ..createSync(recursive: true) + ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + final String build = androidEnvironment.buildDir.path; + final String flutterPatchedSdkPath = artifacts.getArtifactPath( + Artifact.flutterPatchedSdkPath, + platform: TargetPlatform.android_arm, + mode: BuildMode.profile, + ); + processManager.addCommands([ + FakeCommand( + command: [ + artifacts.getArtifactPath(Artifact.engineDartBinary), + 'path/to/frontend_server_starter.dart', + '--sdk-root', + '$flutterPatchedSdkPath/', + '--target=flutter', + '--no-print-incremental-dependencies', + ...buildModeOptions(BuildMode.profile, []), + '--track-widget-creation', + '--aot', + '--tfa', + '--target-os', + 'android', + '--packages', + '/.dart_tool/package_config.json', + '--output-dill', + '$build/app.dill', + '--depfile', + '$build/kernel_snapshot_program.d', + '--verbosity=error', + 'file:///lib/main.dart', + ], + stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n', + ), + ]); - await const KernelSnapshot().build( - androidEnvironment - ..defines[kFrontendServerStarterPath] = 'path/to/frontend_server_starter.dart', - ); + await const KernelSnapshot().build( + androidEnvironment + ..defines[kFrontendServerStarterPath] = 'path/to/frontend_server_starter.dart', + ); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {FeatureFlags: () => TestFeatureFlags()}, - ); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {FeatureFlags: () => TestFeatureFlags()}); - testUsingContext( - 'KernelSnapshot correctly forwards ExtraFrontEndOptions', - () async { - fileSystem.file('.dart_tool/package_config.json') - ..createSync(recursive: true) - ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); - final String build = androidEnvironment.buildDir.path; - final String flutterPatchedSdkPath = artifacts.getArtifactPath( - Artifact.flutterPatchedSdkPath, - platform: TargetPlatform.android_arm, - mode: BuildMode.profile, - ); - processManager.addCommands([ - FakeCommand( - command: [ - artifacts.getArtifactPath(Artifact.engineDartAotRuntime), - artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk), - '--sdk-root', - '$flutterPatchedSdkPath/', - '--target=flutter', - '--no-print-incremental-dependencies', - ...buildModeOptions(BuildMode.profile, []), - '--track-widget-creation', - '--aot', - '--tfa', - '--target-os', - 'android', - '--packages', - '/.dart_tool/package_config.json', - '--output-dill', - '$build/app.dill', - '--depfile', - '$build/kernel_snapshot_program.d', - '--verbosity=error', - 'foo', - 'bar', - 'file:///lib/main.dart', - ], - stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n', - ), - ]); + testUsingContext('KernelSnapshot correctly forwards ExtraFrontEndOptions', () async { + fileSystem.file('.dart_tool/package_config.json') + ..createSync(recursive: true) + ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + final String build = androidEnvironment.buildDir.path; + final String flutterPatchedSdkPath = artifacts.getArtifactPath( + Artifact.flutterPatchedSdkPath, + platform: TargetPlatform.android_arm, + mode: BuildMode.profile, + ); + processManager.addCommands([ + FakeCommand( + command: [ + artifacts.getArtifactPath(Artifact.engineDartAotRuntime), + artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk), + '--sdk-root', + '$flutterPatchedSdkPath/', + '--target=flutter', + '--no-print-incremental-dependencies', + ...buildModeOptions(BuildMode.profile, []), + '--track-widget-creation', + '--aot', + '--tfa', + '--target-os', + 'android', + '--packages', + '/.dart_tool/package_config.json', + '--output-dill', + '$build/app.dill', + '--depfile', + '$build/kernel_snapshot_program.d', + '--verbosity=error', + 'foo', + 'bar', + 'file:///lib/main.dart', + ], + stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n', + ), + ]); - await const KernelSnapshot().build( - androidEnvironment..defines[kExtraFrontEndOptions] = 'foo,bar', - ); + await const KernelSnapshot().build( + androidEnvironment..defines[kExtraFrontEndOptions] = 'foo,bar', + ); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {FeatureFlags: () => TestFeatureFlags()}, - ); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {FeatureFlags: () => TestFeatureFlags()}); - testUsingContext( - 'KernelSnapshot can disable track-widget-creation on debug builds', - () async { - fileSystem.file('.dart_tool/package_config.json') - ..createSync(recursive: true) - ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + testUsingContext('KernelSnapshot can disable track-widget-creation on debug builds', () async { + fileSystem.file('.dart_tool/package_config.json') + ..createSync(recursive: true) + ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); - final String build = androidEnvironment.buildDir.path; - final String flutterPatchedSdkPath = artifacts.getArtifactPath( - Artifact.flutterPatchedSdkPath, - platform: TargetPlatform.android_arm, - mode: BuildMode.debug, - ); - processManager.addCommands([ - FakeCommand( - command: [ - artifacts.getArtifactPath(Artifact.engineDartAotRuntime), - artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk), - '--sdk-root', - '$flutterPatchedSdkPath/', - '--target=flutter', - '--no-print-incremental-dependencies', - ...buildModeOptions(BuildMode.debug, []), - '--no-link-platform', - '--packages', - '/.dart_tool/package_config.json', - '--output-dill', - '$build/app.dill', - '--depfile', - '$build/kernel_snapshot_program.d', - '--incremental', - '--initialize-from-dill', - '$build/app.dill', - '--verbosity=error', - 'file:///lib/main.dart', - ], - stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n', - ), - ]); + final String build = androidEnvironment.buildDir.path; + final String flutterPatchedSdkPath = artifacts.getArtifactPath( + Artifact.flutterPatchedSdkPath, + platform: TargetPlatform.android_arm, + mode: BuildMode.debug, + ); + processManager.addCommands([ + FakeCommand( + command: [ + artifacts.getArtifactPath(Artifact.engineDartAotRuntime), + artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk), + '--sdk-root', + '$flutterPatchedSdkPath/', + '--target=flutter', + '--no-print-incremental-dependencies', + ...buildModeOptions(BuildMode.debug, []), + '--no-link-platform', + '--packages', + '/.dart_tool/package_config.json', + '--output-dill', + '$build/app.dill', + '--depfile', + '$build/kernel_snapshot_program.d', + '--incremental', + '--initialize-from-dill', + '$build/app.dill', + '--verbosity=error', + 'file:///lib/main.dart', + ], + stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n', + ), + ]); - await const KernelSnapshot().build( - androidEnvironment - ..defines[kBuildMode] = BuildMode.debug.cliName - ..defines[kTrackWidgetCreation] = 'false', - ); + await const KernelSnapshot().build( + androidEnvironment + ..defines[kBuildMode] = BuildMode.debug.cliName + ..defines[kTrackWidgetCreation] = 'false', + ); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {FeatureFlags: () => TestFeatureFlags()}, - ); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {FeatureFlags: () => TestFeatureFlags()}); testUsingContext( 'KernelSnapshot forces platform linking on debug for darwin target platforms', @@ -639,64 +616,60 @@ void main() { }, ); - testUsingContext( - 'KernelSnapshot does use track widget creation on debug builds', - () async { - fileSystem.file('.dart_tool/package_config.json') - ..createSync(recursive: true) - ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); - final testEnvironment = Environment.test( - fileSystem.currentDirectory, - defines: { - kBuildMode: BuildMode.debug.cliName, - kTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm), - }, - processManager: processManager, - artifacts: artifacts, - fileSystem: fileSystem, - logger: logger, - ); - final String build = testEnvironment.buildDir.path; - final String flutterPatchedSdkPath = artifacts.getArtifactPath( - Artifact.flutterPatchedSdkPath, - platform: TargetPlatform.android_arm, - mode: BuildMode.debug, - ); - processManager.addCommands([ - FakeCommand( - command: [ - artifacts.getArtifactPath(Artifact.engineDartAotRuntime), - artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk), - '--sdk-root', - '$flutterPatchedSdkPath/', - '--target=flutter', - '--no-print-incremental-dependencies', - ...buildModeOptions(BuildMode.debug, []), - '--track-widget-creation', - '--no-link-platform', - '--packages', - '/.dart_tool/package_config.json', - '--output-dill', - '$build/app.dill', - '--depfile', - '$build/kernel_snapshot_program.d', - '--incremental', - '--initialize-from-dill', - '$build/app.dill', - '--verbosity=error', - 'file:///lib/main.dart', - ], - stdout: - 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey /build/653e11a8e6908714056a57cd6b4f602a/app.dill 0\n', - ), - ]); + testUsingContext('KernelSnapshot does use track widget creation on debug builds', () async { + fileSystem.file('.dart_tool/package_config.json') + ..createSync(recursive: true) + ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + final testEnvironment = Environment.test( + fileSystem.currentDirectory, + defines: { + kBuildMode: BuildMode.debug.cliName, + kTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm), + }, + processManager: processManager, + artifacts: artifacts, + fileSystem: fileSystem, + logger: logger, + ); + final String build = testEnvironment.buildDir.path; + final String flutterPatchedSdkPath = artifacts.getArtifactPath( + Artifact.flutterPatchedSdkPath, + platform: TargetPlatform.android_arm, + mode: BuildMode.debug, + ); + processManager.addCommands([ + FakeCommand( + command: [ + artifacts.getArtifactPath(Artifact.engineDartAotRuntime), + artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk), + '--sdk-root', + '$flutterPatchedSdkPath/', + '--target=flutter', + '--no-print-incremental-dependencies', + ...buildModeOptions(BuildMode.debug, []), + '--track-widget-creation', + '--no-link-platform', + '--packages', + '/.dart_tool/package_config.json', + '--output-dill', + '$build/app.dill', + '--depfile', + '$build/kernel_snapshot_program.d', + '--incremental', + '--initialize-from-dill', + '$build/app.dill', + '--verbosity=error', + 'file:///lib/main.dart', + ], + stdout: + 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey /build/653e11a8e6908714056a57cd6b4f602a/app.dill 0\n', + ), + ]); - await const KernelSnapshot().build(testEnvironment); + await const KernelSnapshot().build(testEnvironment); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {FeatureFlags: () => TestFeatureFlags()}, - ); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {FeatureFlags: () => TestFeatureFlags()}); testUsingContext('AotElfProfile Produces correct output directory', () async { final String build = androidEnvironment.buildDir.path; @@ -926,7 +899,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { required Directory buildDirectory, String? projectFilename, }) async { diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart index 9554f4967c015..743e6ffb37a84 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart @@ -17,6 +17,7 @@ import 'package:flutter_tools/src/build_system/build_system.dart'; import 'package:flutter_tools/src/build_system/targets/ios.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/ios/xcodeproj.dart'; +import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart'; @@ -1622,7 +1623,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { required Directory buildDirectory, String? projectFilename, }) async { diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart index a751d65b9c88c..15e6dbe62cba7 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart @@ -13,6 +13,7 @@ import 'package:flutter_tools/src/build_system/build_system.dart'; import 'package:flutter_tools/src/build_system/targets/macos.dart'; import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; +import 'package:flutter_tools/src/project.dart'; import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart'; @@ -954,7 +955,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { required Directory buildDirectory, String? projectFilename, }) async { diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart index 938e3ac224d9c..f5e6005c6ebf0 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart @@ -1537,7 +1537,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future> fetchDependenciesAndGenerateXcodebuildArgs( - String projectPath, + XcodeBasedProject xcodeProject, Directory buildDirectory, { bool skipPackageUpdatesAndValidation = true, }) async { @@ -1546,14 +1546,14 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async => projectInfo; @override Future> getBuildSettings( - String projectPath, { + XcodeBasedProject xcodeProject, { required XcodeProjectBuildContext buildContext, Duration timeout = const Duration(minutes: 1), }) async => buildSettings; diff --git a/packages/flutter_tools/test/general.shard/ios/mac_test.dart b/packages/flutter_tools/test/general.shard/ios/mac_test.dart index dcde308320ed4..a74981e3a22e9 100644 --- a/packages/flutter_tools/test/general.shard/ios/mac_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/mac_test.dart @@ -1321,7 +1321,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { @@ -1335,7 +1335,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future> fetchDependenciesAndGenerateXcodebuildArgs( - String projectPath, + XcodeBasedProject xcodeProject, Directory buildDirectory, { bool skipPackageUpdatesAndValidation = true, }) async { diff --git a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart index b2abfbf87efd6..7ea917b01afb0 100644 --- a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart @@ -13,10 +13,13 @@ import 'package:flutter_tools/src/base/logger.dart'; 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/flutter_manifest.dart'; import 'package:flutter_tools/src/ios/xcode_build_settings.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/macos/xcode.dart'; +import 'package:flutter_tools/src/plugins.dart'; import 'package:flutter_tools/src/project.dart'; +import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; @@ -292,7 +295,7 @@ void main() { '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-project', - '/', + '/Runner.xcodeproj', '-scheme', 'Free', '-destination', @@ -306,7 +309,7 @@ void main() { expect( await xcodeProjectInterpreter.getBuildSettings( - '', + FakeXcodeBasedProject(hostAppRoot: fileSystem.directory('')), buildContext: const XcodeProjectBuildContext(deviceId: '123', scheme: 'Free'), ), const {}, @@ -339,7 +342,7 @@ void main() { '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-project', - '/', + '/Runner.xcodeproj', '-sdk', 'iphonesimulator', '-destination', @@ -353,7 +356,7 @@ void main() { expect( await xcodeProjectInterpreter.getBuildSettings( - '', + FakeXcodeBasedProject(hostAppRoot: fileSystem.directory('')), buildContext: const XcodeProjectBuildContext(sdk: XcodeSdk.IPhoneSimulator), ), const {}, @@ -386,7 +389,7 @@ void main() { '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-project', - '/', + '/Runner.xcodeproj', '-destination', 'generic/platform=iOS', '-showBuildSettings', @@ -398,7 +401,7 @@ void main() { expect( await xcodeProjectInterpreter.getBuildSettings( - '', + FakeXcodeBasedProject(hostAppRoot: fileSystem.directory('')), buildContext: const XcodeProjectBuildContext(), ), const {}, @@ -433,7 +436,7 @@ void main() { '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-project', - fileSystem.path.separator, + '/Runner.xcodeproj', '-scheme', 'Free', '-destination', @@ -447,7 +450,7 @@ void main() { ]); expect( await xcodeProjectInterpreter.getBuildSettings( - '', + FakeXcodeBasedProject(hostAppRoot: fileSystem.directory('')), buildContext: const XcodeProjectBuildContext(scheme: 'Free'), ), const {}, @@ -480,7 +483,7 @@ void main() { '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-project', - '/', + '/Runner.xcodeproj', '-destination', 'generic/platform=watchOS', '-showBuildSettings', @@ -492,7 +495,7 @@ void main() { expect( await xcodeProjectInterpreter.getBuildSettings( - '', + FakeXcodeBasedProject(hostAppRoot: fileSystem.directory('')), buildContext: const XcodeProjectBuildContext(sdk: XcodeSdk.WatchOS), ), const {}, @@ -525,7 +528,7 @@ void main() { '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-project', - '/', + '/Runner.xcodeproj', '-destination', 'generic/platform=watchOS Simulator', '-showBuildSettings', @@ -537,7 +540,7 @@ void main() { expect( await xcodeProjectInterpreter.getBuildSettings( - '', + FakeXcodeBasedProject(hostAppRoot: fileSystem.directory('')), buildContext: const XcodeProjectBuildContext(sdk: XcodeSdk.WatchSimulator), ), const {}, @@ -586,7 +589,7 @@ void main() { '-skipPackagePluginValidation', '-skipPackageSignatureValidation', '-project', - '/', + '/Runner.xcodeproj', '-destination', 'generic/platform=macOS', '-showBuildSettings', @@ -598,7 +601,7 @@ void main() { expect( await xcodeProjectInterpreter.getBuildSettings( - '', + FakeXcodeBasedProject(hostAppRoot: fileSystem.directory('')), buildContext: const XcodeProjectBuildContext(sdk: XcodeSdk.MacOSX), ), const {}, @@ -645,6 +648,7 @@ void main() { ]); await xcodeProjectInterpreter.cleanWorkspace( + FakeXcodeBasedProject(hostAppRoot: fileSystem.directory('')), 'workspace_path', 'Free', buildDirectory: buildDirectory, @@ -685,7 +689,10 @@ void main() { ); expect( - await xcodeProjectInterpreter.getInfo(workingDirectory, buildDirectory: buildDirectory), + await xcodeProjectInterpreter.getInfo( + FakeXcodeBasedProject(hostAppRoot: fileSystem.directory(workingDirectory)), + buildDirectory: buildDirectory, + ), isNotNull, ); expect(fakeProcessManager, hasNoRemainingExpectations); @@ -729,7 +736,10 @@ void main() { ); await expectLater( - () => xcodeProjectInterpreter.getInfo(workingDirectory, buildDirectory: buildDirectory), + () => xcodeProjectInterpreter.getInfo( + FakeXcodeBasedProject(hostAppRoot: fileSystem.directory(workingDirectory)), + buildDirectory: buildDirectory, + ), throwsToolExit(message: stderr), ); expect(fakeProcessManager, hasNoRemainingExpectations); @@ -773,7 +783,10 @@ void main() { ); await expectLater( - () => xcodeProjectInterpreter.getInfo(workingDirectory, buildDirectory: buildDirectory), + () => xcodeProjectInterpreter.getInfo( + FakeXcodeBasedProject(hostAppRoot: fileSystem.directory(workingDirectory)), + buildDirectory: buildDirectory, + ), throwsToolExit(message: stderr), ); expect(fakeProcessManager, hasNoRemainingExpectations); @@ -2484,450 +2497,56 @@ flutter: }, ); }); +} - testWithoutContext( - 'prefetchSwiftPackages kills previously running process before starting new one', - () async { - final fs = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); - final platform = FakePlatform(operatingSystem: 'macos'); - const projectPath = 'path/to/project'; - final Directory buildDirectory = fileSystem.directory('$projectPath/build/ios'); - final fakeProcessManager = FakeProcessManager.empty(); - fakeProcessManager.addCommands([ - kWhichSysctlCommand, - kx64CheckCommand, - FakeCommand( - command: [ - 'pgrep', - '-n', - '-f', - '-l', - 'xcodebuild -clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages -resolvePackageDependencies', - ], - stdout: - '12345 /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild ' - '-clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages ' - '-resolvePackageDependencies', - ), - const FakeCommand(command: ['kill', '12345']), - FakeCommand( - command: [ - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/${buildDirectory.path}/SourcePackages', - '-resolvePackageDependencies', - ], - stdout: ''' -Resolve Package Graph - -Fetching from https://github.com/apple/swift-algorithms.git (cached) - -Fetching from https://github.com/apple/swift-numerics.git (cached) - -Creating working copy of package ‘swift-numerics’ - -Creating working copy of package ‘swift-algorithms’ - -Checking out 1.2.2 of package ‘swift-algorithms’ - -Checking out 1.1.1 of package ‘swift-numerics’ - - -Resolved source packages: - swift-numerics: https://github.com/apple/swift-numerics.git @ 1.1.1 - swift-algorithms: https://github.com/apple/swift-algorithms.git @ 1.2.2 -''', - ), - ]); - - final xcodeProjectInterpreter = XcodeProjectInterpreter( - logger: testLogger, - fileSystem: fs, - platform: platform, - processManager: fakeProcessManager, - analytics: const NoOpAnalytics(), - ); - await xcodeProjectInterpreter.prefetchSwiftPackages( - projectPath, - buildDirectory: buildDirectory, - quiet: false, - ); - expect(fakeProcessManager, hasNoRemainingExpectations); - expect( - testLogger.statusText, - contains(''' -Xcode is fetching Swift Package Manager dependencies. This may take several minutes... - Fetching from https://github.com/apple/swift-algorithms.git (cached)... - Fetching from https://github.com/apple/swift-numerics.git (cached)... -'''), - ); - }, - ); - - testWithoutContext('prefetchSwiftPackages does not kill process if pid is invalid', () async { - final fs = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); - final platform = FakePlatform(operatingSystem: 'macos'); - const projectPath = 'path/to/project'; - final Directory buildDirectory = fs.directory('$projectPath/build/ios'); - final fakeProcessManager = FakeProcessManager.empty(); - fakeProcessManager.addCommands([ - kWhichSysctlCommand, - kx64CheckCommand, - FakeCommand( - command: [ - 'pgrep', - '-n', - '-f', - '-l', - 'xcodebuild -clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages -resolvePackageDependencies', - ], - stdout: - 'abc /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild ' - '-clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages ' - '-resolvePackageDependencies', - ), - FakeCommand( - command: [ - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/${buildDirectory.path}/SourcePackages', - '-resolvePackageDependencies', - ], - ), - ]); - - final xcodeProjectInterpreter = XcodeProjectInterpreter( - logger: testLogger, - fileSystem: fs, - platform: platform, - processManager: fakeProcessManager, - analytics: const NoOpAnalytics(), - ); - await xcodeProjectInterpreter.prefetchSwiftPackages( - projectPath, - buildDirectory: buildDirectory, - quiet: false, - ); - expect(fakeProcessManager, hasNoRemainingExpectations); - }); - - testWithoutContext( - 'prefetchSwiftPackages does not kill process if command does not match', - () async { - final fs = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); - final platform = FakePlatform(operatingSystem: 'macos'); - const projectPath = 'path/to/project'; - final Directory buildDirectory = fs.directory('$projectPath/build/ios'); - final fakeProcessManager = FakeProcessManager.empty(); - fakeProcessManager.addCommands([ - kWhichSysctlCommand, - kx64CheckCommand, - FakeCommand( - command: [ - 'pgrep', - '-n', - '-f', - '-l', - 'xcodebuild -clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages -resolvePackageDependencies', - ], - stdout: '12345 xcodebuild something else', - ), - FakeCommand( - command: [ - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/${buildDirectory.path}/SourcePackages', - '-resolvePackageDependencies', - ], - ), - ]); - - final xcodeProjectInterpreter = XcodeProjectInterpreter( - logger: testLogger, - fileSystem: fs, - platform: platform, - processManager: fakeProcessManager, - analytics: const NoOpAnalytics(), - ); - await xcodeProjectInterpreter.prefetchSwiftPackages( - projectPath, - buildDirectory: buildDirectory, - quiet: false, - ); - expect(fakeProcessManager, hasNoRemainingExpectations); - }, - ); - - testWithoutContext('prefetchSwiftPackages skips running if already completed', () async { - final fs = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); - final platform = FakePlatform(operatingSystem: 'macos'); - const projectPath = 'path/to/project'; - final Directory buildDirectory = fs.directory('$projectPath/build/ios'); - final fakeProcessManager = FakeProcessManager.empty(); - - fakeProcessManager.addCommands([ - kWhichSysctlCommand, - kx64CheckCommand, - FakeCommand( - command: [ - 'pgrep', - '-n', - '-f', - '-l', - 'xcodebuild -clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages -resolvePackageDependencies', - ], - exitCode: 1, - ), - FakeCommand( - command: [ - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/${buildDirectory.path}/SourcePackages', - '-resolvePackageDependencies', - ], - ), - ]); - - final xcodeProjectInterpreter = XcodeProjectInterpreter( - logger: testLogger, - fileSystem: fs, - platform: platform, - processManager: fakeProcessManager, - analytics: const NoOpAnalytics(), - ); - - await xcodeProjectInterpreter.prefetchSwiftPackages( - projectPath, - buildDirectory: buildDirectory, - quiet: false, - ); - expect(fakeProcessManager, hasNoRemainingExpectations); - - await xcodeProjectInterpreter.prefetchSwiftPackages( - projectPath, - buildDirectory: buildDirectory, - quiet: false, - ); - - expect(fakeProcessManager, hasNoRemainingExpectations); - }); - - testWithoutContext('prefetchSwiftPackages does not print when quiet is true', () async { - final fs = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); - final platform = FakePlatform(operatingSystem: 'macos'); - const projectPath = 'path/to/project'; - final Directory buildDirectory = fileSystem.directory('$projectPath/build/ios'); - final fakeProcessManager = FakeProcessManager.empty(); - fakeProcessManager.addCommands([ - kWhichSysctlCommand, - kx64CheckCommand, - FakeCommand( - command: [ - 'pgrep', - '-n', - '-f', - '-l', - 'xcodebuild -clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages -resolvePackageDependencies', - ], - stdout: - '12345 xcodebuild -clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages -resolvePackageDependencies', - ), - const FakeCommand(command: ['kill', '12345']), - FakeCommand( - command: [ - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/${buildDirectory.path}/SourcePackages', - '-resolvePackageDependencies', - ], - stdout: ''' -Resolve Package Graph - -Fetching from https://github.com/apple/swift-algorithms.git (cached) - -Fetching from https://github.com/apple/swift-numerics.git (cached) - -Creating working copy of package ‘swift-numerics’ - -Creating working copy of package ‘swift-algorithms’ - -Checking out 1.2.2 of package ‘swift-algorithms’ - -Checking out 1.1.1 of package ‘swift-numerics’ - - -Resolved source packages: - swift-numerics: https://github.com/apple/swift-numerics.git @ 1.1.1 - swift-algorithms: https://github.com/apple/swift-algorithms.git @ 1.2.2 -''', - ), - ]); - - final xcodeProjectInterpreter = XcodeProjectInterpreter( - logger: testLogger, - fileSystem: fs, - platform: platform, - processManager: fakeProcessManager, - analytics: const NoOpAnalytics(), - ); - await xcodeProjectInterpreter.prefetchSwiftPackages( - projectPath, - buildDirectory: buildDirectory, - ); - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(testLogger.statusText, isEmpty); - }); - - testWithoutContext( - 'prefetchSwiftPackages does not wait for command to complete when waitForCompletion is false', - () async { - final fs = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); - final platform = FakePlatform(operatingSystem: 'macos'); - const projectPath = 'path/to/project'; - final Directory buildDirectory = fileSystem.directory('$projectPath/build/ios'); - final fakeProcessManager = FakeProcessManager.empty(); - final commandCompleter = Completer(); - fakeProcessManager.addCommands([ - kWhichSysctlCommand, - kx64CheckCommand, - FakeCommand( - command: [ - 'pgrep', - '-n', - '-f', - '-l', - 'xcodebuild -clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages -resolvePackageDependencies', - ], - stdout: - '12345 /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild ' - '-clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages ' - '-resolvePackageDependencies', - ), - const FakeCommand(command: ['kill', '12345']), - FakeCommand( - command: [ - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/${buildDirectory.path}/SourcePackages', - '-resolvePackageDependencies', - ], - stdout: ''' -Resolve Package Graph +class FakeFlutterManifest extends Fake implements FlutterManifest { + @override + final dependencies = {}; -Fetching from https://github.com/apple/swift-algorithms.git (cached) + @override + String get appName => 'my_app'; +} -Fetching from https://github.com/apple/swift-numerics.git (cached) +class FakeFlutterProject extends Fake implements FlutterProject { + FakeFlutterProject({required this.directory}); -Creating working copy of package ‘swift-numerics’ + @override + final Directory directory; -Creating working copy of package ‘swift-algorithms’ + @override + File get packageConfig => directory.childDirectory('.dart_tool').childFile('package_config.json'); -Checking out 1.2.2 of package ‘swift-algorithms’ + @override + File get flutterPluginsDependenciesFile => directory.childFile('.flutter-plugins-dependencies'); -Checking out 1.1.1 of package ‘swift-numerics’ + @override + FlutterManifest get manifest => FakeFlutterManifest(); +} +class FakeXcodeBasedProject extends IosProject { + FakeXcodeBasedProject({ + required this.hostAppRoot, + this.usesSwiftPackageManager = true, + this.flutterPluginSwiftPackageInProjectSettings = true, + this.plugins, + }) : super.fromFlutter(FakeFlutterProject(directory: hostAppRoot.parent)); -Resolved source packages: - swift-numerics: https://github.com/apple/swift-numerics.git @ 1.1.1 - swift-algorithms: https://github.com/apple/swift-algorithms.git @ 1.2.2 -''', - completer: commandCompleter, - ), - ]); + @override + final Directory hostAppRoot; - final xcodeProjectInterpreter = XcodeProjectInterpreter( - logger: testLogger, - fileSystem: fs, - platform: platform, - processManager: fakeProcessManager, - analytics: const NoOpAnalytics(), - ); - await xcodeProjectInterpreter.prefetchSwiftPackages( - projectPath, - buildDirectory: buildDirectory, - waitForCompletion: false, - ); - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(testLogger.statusText, isEmpty); - expect(commandCompleter.isCompleted, isFalse); - commandCompleter.complete(); - }, - ); + @override + final bool usesSwiftPackageManager; - testWithoutContext('prefetchSwiftPackages throws exception when resolving fails', () async { - final fs = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); - final platform = FakePlatform(operatingSystem: 'macos'); - const projectPath = 'path/to/project'; - final Directory buildDirectory = fileSystem.directory('$projectPath/build/ios'); - final fakeProcessManager = FakeProcessManager.empty(); - fakeProcessManager.addCommands([ - kWhichSysctlCommand, - kx64CheckCommand, - FakeCommand( - command: [ - 'pgrep', - '-n', - '-f', - '-l', - 'xcodebuild -clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages -resolvePackageDependencies', - ], - ), - FakeCommand( - command: [ - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/${buildDirectory.path}/SourcePackages', - '-resolvePackageDependencies', - ], - exitCode: 1, - stderr: 'error: some error', - ), - ]); + @override + final bool flutterPluginSwiftPackageInProjectSettings; - final xcodeProjectInterpreter = XcodeProjectInterpreter( - logger: testLogger, - fileSystem: fs, - platform: platform, - processManager: fakeProcessManager, - analytics: const NoOpAnalytics(), - ); - await expectLater( - xcodeProjectInterpreter.prefetchSwiftPackages( - projectPath, - buildDirectory: buildDirectory, - quiet: false, - ), - throwsToolExit(), - ); - }); + final List? plugins; - testWithoutContext('swiftPackageCachePath returns the correct cache path', () { - final fs = MemoryFileSystem.test(); - final Directory buildDirectory = fs.directory('/build/ios'); - final xcodeProjectInterpreter = XcodeProjectInterpreter( - logger: logger, - fileSystem: fs, - platform: platform, - processManager: fakeProcessManager, - analytics: const NoOpAnalytics(), - ); - expect( - xcodeProjectInterpreter.swiftPackageCachePath(buildDirectory), - '/build/ios/SourcePackages', - ); - }); + @override + Future> getPlugins() async { + if (plugins != null) { + return plugins!; + } + return super.getPlugins(); + } } diff --git a/packages/flutter_tools/test/general.shard/macos/darwin_dependency_management_test.dart b/packages/flutter_tools/test/general.shard/macos/darwin_dependency_management_test.dart index 5cd365864c9ae..55677bd243ad8 100644 --- a/packages/flutter_tools/test/general.shard/macos/darwin_dependency_management_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/darwin_dependency_management_test.dart @@ -1554,8 +1554,8 @@ class FakePluginPlatform extends Fake implements PluginPlatform {} class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter { @override - Future prefetchSwiftPackages( - String projectPath, { + Future prefetchSwiftPackagesForProject( + XcodeBasedProject xcodeProject, { required Directory buildDirectory, bool quiet = true, bool waitForCompletion = true, diff --git a/packages/flutter_tools/test/general.shard/macos/swift_package_manager_test.dart b/packages/flutter_tools/test/general.shard/macos/swift_package_manager_test.dart index da5825584ea08..2a792f983978a 100644 --- a/packages/flutter_tools/test/general.shard/macos/swift_package_manager_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/swift_package_manager_test.dart @@ -562,6 +562,63 @@ let package = Package( }); }); } + + group('parseError', () { + testWithoutContext('returns null for empty or null message', () { + expect(SwiftPackageManager.parsePluginError(null, pluginNames: []), isNull); + expect(SwiftPackageManager.parsePluginError('', pluginNames: []), isNull); + }); + + testWithoutContext('returns null for unknown error', () { + expect( + SwiftPackageManager.parsePluginError('some random error', pluginNames: []), + 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" has an incorrectly configured Package.swift file.\n' + 'Please contact the plugin maintainers for assistance.'; + expect( + SwiftPackageManager.parsePluginError(message, pluginNames: ['plugin_1']), + expected, + ); + }); + + testWithoutContext('returns guided message for Package.swift error', () { + const message = 'plugin_1/Package.swift:1:1: error: some error'; + const expected = + 'Flutter plugin "plugin_1" has an incorrectly configured Package.swift file.\n' + 'Please contact the plugin maintainers for assistance.'; + expect( + SwiftPackageManager.parsePluginError(message, pluginNames: ['plugin_1']), + expected, + ); + }); + + testWithoutContext('returns guided message for unknown package in dependencies error', () { + const message = "unknown package 'some_package' in dependencies of target 'plugin_1'"; + const expected = + 'Flutter plugin "plugin_1" has an incorrectly configured Package.swift file.\n' + 'Please contact the plugin maintainers for assistance.'; + expect( + SwiftPackageManager.parsePluginError(message, pluginNames: ['plugin_1']), + expected, + ); + }); + + testWithoutContext('returns null if error matches but plugin is not in pluginNames', () { + const message = "unknown package 'some_package' in dependencies of target 'plugin_1'"; + expect( + SwiftPackageManager.parsePluginError(message, pluginNames: ['other_plugin']), + isNull, + ); + }); + }); }); } diff --git a/packages/flutter_tools/test/general.shard/migrations/swift_package_manager_integration_migration_test.dart b/packages/flutter_tools/test/general.shard/migrations/swift_package_manager_integration_migration_test.dart index df6e5d4aee199..2b7f99f1ebdb3 100644 --- a/packages/flutter_tools/test/general.shard/migrations/swift_package_manager_integration_migration_test.dart +++ b/packages/flutter_tools/test/general.shard/migrations/swift_package_manager_integration_migration_test.dart @@ -540,11 +540,12 @@ void main() { project.xcodeProjectInfoFile.writeAsStringSync( _projectSettings(_allSectionsMigrated(platform)), ); + final fakeXcodeProjectInterpreter = FakeXcodeProjectInterpreter(); final projectMigration = SwiftPackageManagerIntegrationMigration( project, platform, BuildInfo.debug, - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), + xcodeProjectInterpreter: fakeXcodeProjectInterpreter, logger: testLogger, fileSystem: memoryFileSystem, plistParser: plistParser, @@ -556,6 +557,7 @@ void main() { project.xcodeProjectSchemeFile().readAsStringSync(), _validBuildActions(platform, hasFrameworkScript: true, hasBuildEntries: false), ); + expect(fakeXcodeProjectInterpreter.prefetchSwiftPackagesCalled, isTrue); }); testWithoutContext('successfully updates scheme with preexisting PreActions', () async { @@ -3498,6 +3500,47 @@ void main() { ); }); + testWithoutContext('throw if prefetch Swift packages fails', () async { + final memoryFileSystem = MemoryFileSystem(); + final testLogger = BufferLogger.test(); + const FlutterDarwinPlatform platform = FlutterDarwinPlatform.ios; + final project = FakeXcodeProject( + platform: platform.name, + fileSystem: memoryFileSystem, + logger: testLogger, + ); + _createProjectFiles(project, platform); + project.xcodeProjectInfoFile.writeAsStringSync( + _projectSettings(_allSectionsUnmigrated(platform)), + ); + + final plistParser = FakePlistParser.multiple([ + _plutilOutput(_allSectionsUnmigratedAsJson(platform)), + _plutilOutput(_allSectionsMigratedAsJson(platform)), + ]); + + final projectMigration = SwiftPackageManagerIntegrationMigration( + project, + platform, + BuildInfo.debug, + xcodeProjectInterpreter: FakeXcodeProjectInterpreter(throwErrorOnPrefetch: true), + logger: testLogger, + fileSystem: memoryFileSystem, + plistParser: plistParser, + config: FakeConfig(), + ); + await expectLater( + () => projectMigration.migrate(), + throwsA( + isA().having( + (Exception e) => e.toString(), + 'toString()', + contains('Exception: Failed to prefetch Swift packages'), + ), + ), + ); + }); + testWithoutContext('restore project settings from backup on failure', () async { final memoryFileSystem = MemoryFileSystem(); final testLogger = BufferLogger.test(); @@ -4507,7 +4550,10 @@ const migratedSwiftPackageProductDependencySectionAsJson = ''' }'''; class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter { - FakeXcodeProjectInterpreter({this.throwErrorOnGetInfo = false}); + FakeXcodeProjectInterpreter({ + this.throwErrorOnGetInfo = false, + this.throwErrorOnPrefetch = false, + }); @override bool isInstalled = false; @@ -4516,10 +4562,25 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete List xcrunCommand() => ['xcrun']; final bool throwErrorOnGetInfo; + final bool throwErrorOnPrefetch; + bool prefetchSwiftPackagesCalled = false; + + @override + Future prefetchSwiftPackagesForProject( + XcodeBasedProject xcodeProject, { + required Directory buildDirectory, + bool quiet = false, + bool waitForCompletion = false, + }) async { + prefetchSwiftPackagesCalled = true; + if (throwErrorOnPrefetch) { + throw Exception('Failed to prefetch Swift packages'); + } + } @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { diff --git a/packages/flutter_tools/test/general.shard/project_test.dart b/packages/flutter_tools/test/general.shard/project_test.dart index e3aef32f6d59c..93f620bc640a2 100644 --- a/packages/flutter_tools/test/general.shard/project_test.dart +++ b/packages/flutter_tools/test/general.shard/project_test.dart @@ -2639,7 +2639,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future> getBuildSettings( - String projectPath, { + XcodeBasedProject xcodeProject, { XcodeProjectBuildContext? buildContext, Duration timeout = const Duration(minutes: 1), }) async { @@ -2651,7 +2651,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { diff --git a/packages/flutter_tools/test/general.shard/xcode_project_test.dart b/packages/flutter_tools/test/general.shard/xcode_project_test.dart index 9cb3ca6904e80..23cb5cdf309d5 100644 --- a/packages/flutter_tools/test/general.shard/xcode_project_test.dart +++ b/packages/flutter_tools/test/general.shard/xcode_project_test.dart @@ -2,11 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:file/memory.dart'; import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/process.dart' show ProcessUtils; import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_system/build_system.dart'; @@ -14,11 +17,13 @@ import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/flutter_manifest.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; +import 'package:flutter_tools/src/plugins.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:test/fake.dart'; import '../src/common.dart'; import '../src/context.dart'; +import '../src/fake_process_manager.dart'; import '../src/fakes.dart'; void main() { @@ -111,16 +116,12 @@ void main() { }); group('projectInfo', () { - testUsingContext( - 'is null if XcodeProjectInterpreter is null', - () async { - final fs = MemoryFileSystem.test(); - final project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); - project.xcodeProject.createSync(recursive: true); - expect(await project.projectInfo(), isNull); - }, - overrides: {XcodeProjectInterpreter: () => null}, - ); + testUsingContext('is null if XcodeProjectInterpreter is null', () async { + final fs = MemoryFileSystem.test(); + final project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + project.xcodeProject.createSync(recursive: true); + expect(await project.projectInfo(), isNull); + }, overrides: {XcodeProjectInterpreter: () => null}); testUsingContext( 'is null if XcodeProjectInterpreter is not installed', @@ -157,28 +158,20 @@ void main() { ); }); - testUsingContext( - 'schemeForBuildInfo succeeds', - () async { - final fs = MemoryFileSystem.test(); - final project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); - project.xcodeProject.createSync(recursive: true); - const BuildInfo buildInfo = BuildInfo.debug; - expect(await project.schemeForBuildInfo(buildInfo), 'Runner'); - }, - overrides: {XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter()}, - ); + testUsingContext('schemeForBuildInfo succeeds', () async { + final fs = MemoryFileSystem.test(); + final project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + project.xcodeProject.createSync(recursive: true); + const BuildInfo buildInfo = BuildInfo.debug; + expect(await project.schemeForBuildInfo(buildInfo), 'Runner'); + }, overrides: {XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter()}); - testUsingContext( - 'schemeForBuildInfo returns null if unable to find project', - () async { - final fs = MemoryFileSystem.test(); - final project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); - const BuildInfo buildInfo = BuildInfo.debug; - expect(await project.schemeForBuildInfo(buildInfo), isNull); - }, - overrides: {XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter()}, - ); + testUsingContext('schemeForBuildInfo returns null if unable to find project', () async { + final fs = MemoryFileSystem.test(); + final project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + const BuildInfo buildInfo = BuildInfo.debug; + expect(await project.schemeForBuildInfo(buildInfo), isNull); + }, overrides: {XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter()}); testUsingContext( 'schemeForBuildInfo succeeds with flavor', @@ -399,53 +392,42 @@ void main() { group('ensureReadyForPlatformSpecificTooling', () { group('lldb files are generated', () { - testUsingContext( - 'when they are missing', - () async { - final fs = MemoryFileSystem.test(); - final Directory projectDirectory = fs.directory('path'); - projectDirectory.childDirectory('ios').createSync(recursive: true); - final FlutterManifest manifest = FakeFlutterManifest(); - final flutterProject = FlutterProject(projectDirectory, manifest, manifest); - final project = IosProject.fromFlutter(flutterProject); - expect(project.lldbInitFile, isNot(exists)); - expect(project.lldbHelperPythonFile, isNot(exists)); - - await project.ensureReadyForPlatformSpecificTooling(); - - expect(project.lldbInitFile, exists); - expect(project.lldbHelperPythonFile, exists); - }, - overrides: {Cache: () => FakeCache(olderThanToolsStamp: true)}, - ); + testUsingContext('when they are missing', () async { + final fs = MemoryFileSystem.test(); + final Directory projectDirectory = fs.directory('path'); + projectDirectory.childDirectory('ios').createSync(recursive: true); + final FlutterManifest manifest = FakeFlutterManifest(); + final flutterProject = FlutterProject(projectDirectory, manifest, manifest); + final project = IosProject.fromFlutter(flutterProject); + expect(project.lldbInitFile, isNot(exists)); + expect(project.lldbHelperPythonFile, isNot(exists)); - testUsingContext( - 'when they are older than tool', - () async { - final fs = MemoryFileSystem.test(); - final Directory projectDirectory = fs.directory('path'); - projectDirectory.childDirectory('ios').createSync(recursive: true); - final FlutterManifest manifest = FakeFlutterManifest(); - final flutterProject = FlutterProject(projectDirectory, manifest, manifest); - final project = IosProject.fromFlutter(flutterProject); - project.lldbInitFile.createSync(recursive: true); - project.lldbInitFile.writeAsStringSync('old'); - project.lldbHelperPythonFile.createSync(recursive: true); - project.lldbHelperPythonFile.writeAsStringSync('old'); - - await project.ensureReadyForPlatformSpecificTooling(); - - expect( - project.lldbInitFile.readAsStringSync(), - contains('Generated file, do not edit.'), - ); - expect( - project.lldbHelperPythonFile.readAsStringSync(), - contains('Generated file, do not edit.'), - ); - }, - overrides: {Cache: () => FakeCache(olderThanToolsStamp: true)}, - ); + await project.ensureReadyForPlatformSpecificTooling(); + + expect(project.lldbInitFile, exists); + expect(project.lldbHelperPythonFile, exists); + }, overrides: {Cache: () => FakeCache(olderThanToolsStamp: true)}); + + testUsingContext('when they are older than tool', () async { + final fs = MemoryFileSystem.test(); + final Directory projectDirectory = fs.directory('path'); + projectDirectory.childDirectory('ios').createSync(recursive: true); + final FlutterManifest manifest = FakeFlutterManifest(); + final flutterProject = FlutterProject(projectDirectory, manifest, manifest); + final project = IosProject.fromFlutter(flutterProject); + project.lldbInitFile.createSync(recursive: true); + project.lldbInitFile.writeAsStringSync('old'); + project.lldbHelperPythonFile.createSync(recursive: true); + project.lldbHelperPythonFile.writeAsStringSync('old'); + + await project.ensureReadyForPlatformSpecificTooling(); + + expect(project.lldbInitFile.readAsStringSync(), contains('Generated file, do not edit.')); + expect( + project.lldbHelperPythonFile.readAsStringSync(), + contains('Generated file, do not edit.'), + ); + }, overrides: {Cache: () => FakeCache(olderThanToolsStamp: true)}); }); }); }); @@ -584,6 +566,599 @@ void main() { ); }); }); + + testWithoutContext( + 'prefetchSwiftPackages kills previously running process before starting new one', + () async { + final fs = MemoryFileSystem.test(); + final testLogger = BufferLogger.test(); + const projectPath = 'path/to/project'; + final Directory buildDirectory = fs.directory('$projectPath/build/ios'); + final fakeProcessManager = FakeProcessManager.list([ + FakeCommand( + command: [ + 'pgrep', + '-n', + '-f', + '-l', + 'xcodebuild -clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages -resolvePackageDependencies', + ], + stdout: + '12345 /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild ' + '-clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages ' + '-resolvePackageDependencies', + ), + const FakeCommand(command: ['kill', '12345']), + FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + '-resolvePackageDependencies', + ], + stdout: ''' +Resolve Package Graph + +Fetching from https://github.com/apple/swift-algorithms.git (cached) + +Fetching from https://github.com/apple/swift-numerics.git (cached) + +Creating working copy of package ‘swift-numerics’ + +Creating working copy of package ‘swift-algorithms’ + +Checking out 1.2.2 of package ‘swift-algorithms’ + +Checking out 1.1.1 of package ‘swift-numerics’ + +Resolved source packages: + swift-numerics: https://github.com/apple/swift-numerics.git @ 1.1.1 + swift-algorithms: https://github.com/apple/swift-algorithms.git @ 1.2.2 +''', + ), + ]); + final processUtils = ProcessUtils(logger: testLogger, processManager: fakeProcessManager); + + final iosProject = FakeIosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + await iosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + quiet: false, + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + expect( + testLogger.statusText, + contains(''' +Xcode is fetching Swift Package Manager dependencies. This may take several minutes... + Fetching from https://github.com/apple/swift-algorithms.git (cached)... + Fetching from https://github.com/apple/swift-numerics.git (cached)... +'''), + ); + + testLogger.clear(); + }, + ); + + testWithoutContext('prefetchSwiftPackages does not kill process if pid is invalid', () async { + final fs = MemoryFileSystem.test(); + final testLogger = BufferLogger.test(); + const projectPath = 'path/to/project'; + final Directory buildDirectory = fs.directory('$projectPath/build/ios'); + final fakeProcessManager = FakeProcessManager.empty(); + fakeProcessManager.addCommands([ + FakeCommand( + command: [ + 'pgrep', + '-n', + '-f', + '-l', + 'xcodebuild -clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages -resolvePackageDependencies', + ], + stdout: + 'abc /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild ' + '-clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages ' + '-resolvePackageDependencies', + ), + FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + '-resolvePackageDependencies', + ], + ), + ]); + final processUtils = ProcessUtils(logger: testLogger, processManager: fakeProcessManager); + + final iosProject = FakeIosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + await iosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + quiet: false, + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + }); + + testWithoutContext( + 'prefetchSwiftPackages does not kill process if command does not match', + () async { + final fs = MemoryFileSystem.test(); + final testLogger = BufferLogger.test(); + const projectPath = 'path/to/project'; + final Directory buildDirectory = fs.directory('$projectPath/build/ios'); + final fakeProcessManager = FakeProcessManager.empty(); + fakeProcessManager.addCommands([ + FakeCommand( + command: [ + 'pgrep', + '-n', + '-f', + '-l', + 'xcodebuild -clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages -resolvePackageDependencies', + ], + stdout: '12345 xcodebuild something else', + ), + FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + '-resolvePackageDependencies', + ], + ), + ]); + final processUtils = ProcessUtils(logger: testLogger, processManager: fakeProcessManager); + + final iosProject = FakeIosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + await iosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + quiet: false, + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + }, + ); + + testWithoutContext('prefetchSwiftPackages skips running if already completed', () async { + final fs = MemoryFileSystem.test(); + final testLogger = BufferLogger.test(); + const projectPath = 'path/to/project'; + final Directory buildDirectory = fs.directory('$projectPath/build/ios'); + final fakeProcessManager = FakeProcessManager.empty(); + fakeProcessManager.addCommands([ + FakeCommand( + command: [ + 'pgrep', + '-n', + '-f', + '-l', + 'xcodebuild -clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages -resolvePackageDependencies', + ], + exitCode: 1, + ), + FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + '-resolvePackageDependencies', + ], + ), + ]); + final processUtils = ProcessUtils(logger: testLogger, processManager: fakeProcessManager); + + final iosProject = FakeIosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + await iosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + quiet: false, + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + + await iosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + quiet: false, + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + }); + + testWithoutContext('prefetchSwiftPackages does not print when quiet is true', () async { + final fs = MemoryFileSystem.test(); + final testLogger = BufferLogger.test(); + const projectPath = 'path/to/project'; + final Directory buildDirectory = fs.directory('$projectPath/build/ios'); + final fakeProcessManager = FakeProcessManager.empty(); + fakeProcessManager.addCommands([ + FakeCommand( + command: [ + 'pgrep', + '-n', + '-f', + '-l', + 'xcodebuild -clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages -resolvePackageDependencies', + ], + stdout: + '12345 xcodebuild -clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages -resolvePackageDependencies', + ), + const FakeCommand(command: ['kill', '12345']), + FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + '-resolvePackageDependencies', + ], + stdout: ''' +Resolve Package Graph + +Fetching from https://github.com/apple/swift-algorithms.git (cached) + +Fetching from https://github.com/apple/swift-numerics.git (cached) + +Creating working copy of package ‘swift-numerics’ + +Creating working copy of package ‘swift-algorithms’ + +Checking out 1.2.2 of package ‘swift-algorithms’ + +Checking out 1.1.1 of package ‘swift-numerics’ + +Resolved source packages: + swift-numerics: https://github.com/apple/swift-numerics.git @ 1.1.1 + swift-algorithms: https://github.com/apple/swift-algorithms.git @ 1.2.2 + ''', + ), + ]); + final processUtils = ProcessUtils(logger: testLogger, processManager: fakeProcessManager); + + final iosProject = FakeIosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + await iosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + ); + + expect(fakeProcessManager, hasNoRemainingExpectations); + expect(testLogger.statusText, isEmpty); + }); + + testWithoutContext( + 'prefetchSwiftPackages does not wait for command to complete when waitForCompletion is false', + () async { + final fs = MemoryFileSystem.test(); + final testLogger = BufferLogger.test(); + const projectPath = 'path/to/project'; + final Directory buildDirectory = fs.directory('$projectPath/build/ios'); + final fakeProcessManager = FakeProcessManager.empty(); + final commandCompleter = Completer(); + fakeProcessManager.addCommands([ + FakeCommand( + command: [ + 'pgrep', + '-n', + '-f', + '-l', + 'xcodebuild -clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages -resolvePackageDependencies', + ], + stdout: + '12345 /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild ' + '-clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages ' + '-resolvePackageDependencies', + ), + const FakeCommand(command: ['kill', '12345']), + FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + '-resolvePackageDependencies', + ], + stdout: ''' +Resolve Package Graph + +Fetching from https://github.com/apple/swift-algorithms.git (cached) + +Fetching from https://github.com/apple/swift-numerics.git (cached) + +Creating working copy of package ‘swift-numerics’ + +Creating working copy of package ‘swift-algorithms’ + +Checking out 1.2.2 of package ‘swift-algorithms’ + +Checking out 1.1.1 of package ‘swift-numerics’ + +Resolved source packages: + swift-numerics: https://github.com/apple/swift-numerics.git @ 1.1.1 + swift-algorithms: https://github.com/apple/swift-algorithms.git @ 1.2.2 + ''', + completer: commandCompleter, + ), + ]); + final processUtils = ProcessUtils(logger: testLogger, processManager: fakeProcessManager); + + final iosProject = FakeIosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + await iosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + waitForCompletion: false, + ); + + expect(fakeProcessManager, hasNoRemainingExpectations); + expect(testLogger.statusText, isEmpty); + expect(commandCompleter.isCompleted, isFalse); + commandCompleter.complete(); + }, + ); + + testWithoutContext('prefetchSwiftPackages throws exception when resolving fails', () async { + final fs = MemoryFileSystem.test(); + final testLogger = BufferLogger.test(); + const projectPath = 'path/to/project'; + final Directory buildDirectory = fs.directory('$projectPath/build/ios'); + final fakeProcessManager = FakeProcessManager.empty(); + fakeProcessManager.addCommands([ + FakeCommand( + command: [ + 'pgrep', + '-n', + '-f', + '-l', + 'xcodebuild -clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages -resolvePackageDependencies', + ], + ), + FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + '-resolvePackageDependencies', + ], + exitCode: 1, + stderr: 'error: some error', + ), + ]); + final processUtils = ProcessUtils(logger: testLogger, processManager: fakeProcessManager); + + final iosProject = FakeIosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + await expectLater( + iosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + ), + throwsToolExit(), + ); + }); + + testWithoutContext('throws guided message when it matches a _SwiftPMPluginErrorMatcher', () async { + final fs = MemoryFileSystem.test(); + final testLogger = BufferLogger.test(); + const projectPath = 'path/to/project'; + final Directory buildDirectory = fs.directory('$projectPath/build/ios'); + final fakeProcessManager = FakeProcessManager.empty(); + fakeProcessManager.addCommands([ + FakeCommand( + command: [ + 'pgrep', + '-n', + '-f', + '-l', + 'xcodebuild -clonedSourcePackagesDirPath /${buildDirectory.path}/SourcePackages -resolvePackageDependencies', + ], + ), + FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + '-resolvePackageDependencies', + ], + exitCode: 1, + stderr: "target 'plugin_a' in package 'plugin_a' is outside the package root", + ), + ]); + final processUtils = ProcessUtils(logger: testLogger, processManager: fakeProcessManager); + + final iosProject = FakeIosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + iosProject.plugins.add(FakePlugin('plugin_a')); + await expectLater( + iosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + ), + throwsToolExit( + message: 'Flutter plugin "plugin_a" has an incorrectly configured Package.swift file.', + ), + ); + }); + + testWithoutContext('prefetchSwiftPackages skips if SwiftPM not enabled', () async { + final fs = MemoryFileSystem.test(); + final testLogger = BufferLogger.test(); + const projectPath = 'path/to/project'; + final Directory buildDirectory = fs.directory('$projectPath/build/ios'); + final fakeProcessManager = FakeProcessManager.empty(); + final processUtils = ProcessUtils(logger: testLogger, processManager: fakeProcessManager); + + final iosProject = FakeIosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + iosProject.usesSwiftPackageManager = false; + await iosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + quiet: false, + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + }); + + testWithoutContext('prefetchSwiftPackages skips if not migrated to SwiftPM yet', () async { + final fs = MemoryFileSystem.test(); + final testLogger = BufferLogger.test(); + const projectPath = 'path/to/project'; + final Directory buildDirectory = fs.directory('$projectPath/build/ios'); + final fakeProcessManager = FakeProcessManager.empty(); + final processUtils = ProcessUtils(logger: testLogger, processManager: fakeProcessManager); + + final iosProject = FakeIosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + iosProject.flutterPluginSwiftPackageInProjectSettings = false; + await iosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + quiet: false, + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + }); + + testWithoutContext('prefetchSwiftPackages can run for both platforms', () async { + final fs = MemoryFileSystem.test(); + final testLogger = BufferLogger.test(); + const projectPath = 'path/to/project'; + final Directory iosBuildDirectory = fs.directory('$projectPath/build/ios'); + final Directory macosBuildDirectory = fs.directory('$projectPath/build/macos'); + + final fakeProcessManager = FakeProcessManager.empty(); + fakeProcessManager.addCommands([ + FakeCommand( + command: [ + 'pgrep', + '-n', + '-f', + '-l', + 'xcodebuild -clonedSourcePackagesDirPath /${iosBuildDirectory.path}/SourcePackages -resolvePackageDependencies', + ], + exitCode: 1, + ), + FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${iosBuildDirectory.path}/SourcePackages', + '-resolvePackageDependencies', + ], + ), + FakeCommand( + command: [ + 'pgrep', + '-n', + '-f', + '-l', + 'xcodebuild -clonedSourcePackagesDirPath /${macosBuildDirectory.path}/SourcePackages -resolvePackageDependencies', + ], + exitCode: 1, + ), + FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${macosBuildDirectory.path}/SourcePackages', + '-resolvePackageDependencies', + ], + ), + ]); + final processUtils = ProcessUtils(logger: testLogger, processManager: fakeProcessManager); + + final iosProject = FakeIosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + await iosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${iosBuildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + quiet: false, + ); + + final macosProject = FakeMacOSProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + await macosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${macosBuildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + quiet: false, + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + }); } class FakeFlutterProject extends Fake implements FlutterProject { @@ -618,7 +1193,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { @@ -665,3 +1240,44 @@ class FakeCache extends Fake implements Cache { return olderThanToolsStamp; } } + +class FakeIosProject extends IosProject { + FakeIosProject.fromFlutter(super.parent) : super.fromFlutter(); + + final List plugins = []; + + @override + bool usesSwiftPackageManager = true; + + @override + bool flutterPluginSwiftPackageInProjectSettings = true; + + @override + Future> getPlugins() async { + return plugins; + } +} + +class FakeMacOSProject extends MacOSProject { + FakeMacOSProject.fromFlutter(super.parent) : super.fromFlutter(); + + final List plugins = []; + + @override + bool usesSwiftPackageManager = true; + + @override + bool flutterPluginSwiftPackageInProjectSettings = true; + + @override + Future> getPlugins() async { + return plugins; + } +} + +class FakePlugin extends Fake implements Plugin { + FakePlugin(this.name); + + @override + final String name; +} diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart index 6c3371ce489bd..1d735482a83ea 100644 --- a/packages/flutter_tools/test/src/context.dart +++ b/packages/flutter_tools/test/src/context.dart @@ -367,7 +367,7 @@ class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter { @override Future> getBuildSettings( - String projectPath, { + XcodeBasedProject xcodeProject, { XcodeProjectBuildContext? buildContext, Duration timeout = const Duration(minutes: 1), }) async { @@ -384,6 +384,7 @@ class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter { @override Future cleanWorkspace( + XcodeBasedProject xcodeProject, String workspacePath, String scheme, { required Directory buildDirectory, @@ -392,7 +393,7 @@ class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter { @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { @@ -405,8 +406,8 @@ class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter { List xcrunCommand() => ['xcrun']; @override - Future prefetchSwiftPackages( - String projectPath, { + Future prefetchSwiftPackagesForProject( + XcodeBasedProject xcodeProject, { required Directory buildDirectory, bool quiet = true, bool waitForCompletion = true, @@ -414,7 +415,7 @@ class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter { @override Future> fetchDependenciesAndGenerateXcodebuildArgs( - String projectPath, + XcodeBasedProject xcodeProject, Directory buildDirectory, { bool skipPackageUpdatesAndValidation = true, }) async {