diff --git a/packages/flutter_tools/lib/src/macos/cocoapods.dart b/packages/flutter_tools/lib/src/macos/cocoapods.dart index d7161112b41..8975934c4dd 100644 --- a/packages/flutter_tools/lib/src/macos/cocoapods.dart +++ b/packages/flutter_tools/lib/src/macos/cocoapods.dart @@ -355,7 +355,7 @@ class CocoaPods { if (result.exitCode != 0) { invalidatePodInstallOutput(xcodeProject); - _diagnosePodInstallFailure(result); + _diagnosePodInstallFailure(result, xcodeProject); throwToolExit('Error running pod install'); } else if (xcodeProject.podfileLock.existsSync()) { // Even if the Podfile.lock didn't change, update its modified date to now @@ -367,7 +367,7 @@ class CocoaPods { } } - void _diagnosePodInstallFailure(ProcessResult result) { + void _diagnosePodInstallFailure(ProcessResult result, XcodeBasedProject xcodeProject) { final Object? stdout = result.stdout; final Object? stderr = result.stderr; if (stdout is! String || stderr is! String) { @@ -397,9 +397,132 @@ class CocoaPods { ' sudo gem uninstall ffi && sudo gem install ffi -- --enable-libffi-alloc\n', emphasis: true, ); + } else if (stdout.contains('required a higher minimum deployment target')) { + final ({String failingPod, String sourcePlugin, String podPluginSubdir})? + podInfo = _parseMinDeploymentFailureInfo(stdout); + if (podInfo != null) { + final String sourcePlugin = podInfo.sourcePlugin; + // If the plugin's podfile has set its own minimum version correctly + // based on the requirements of its dependencies the failing pod should + // be the plugin itself, but if not they may be different (e.g., if + // a plugin says its minimum iOS version is 11, but depends on a pod + // with a minimum version of 12, then building for 11 will report that + // pod as failing.) + if (podInfo.failingPod == podInfo.sourcePlugin) { + final Directory symlinksDir; + final String podPlatformString; + final String platformName; + final String docsLink; + if (xcodeProject is IosProject) { + symlinksDir = xcodeProject.symlinks; + podPlatformString = 'ios'; + platformName = 'iOS'; + docsLink = 'https://docs.flutter.dev/deployment/ios'; + } else if (xcodeProject is MacOSProject) { + symlinksDir = xcodeProject.ephemeralDirectory.childDirectory('.symlinks'); + podPlatformString = 'osx'; + platformName = 'macOS'; + docsLink = 'https://docs.flutter.dev/deployment/macos'; + } else { + return; + } + final File podspec = symlinksDir + .childDirectory('plugins') + .childDirectory(sourcePlugin) + .childDirectory(podInfo.podPluginSubdir) + .childFile('$sourcePlugin.podspec'); + final String? minDeploymentVersion = _findPodspecMinDeploymentVersion( + podspec, + podPlatformString + ); + if (minDeploymentVersion != null) { + _logger.printError( + 'Error: The plugin "$sourcePlugin" requires a higher minimum ' + '$platformName deployment version than your application is targeting.\n' + "To build, increase your application's deployment target to at " + 'least $minDeploymentVersion as described at $docsLink', + emphasis: true, + ); + } else { + // If for some reason the min version can't be parsed out, provide + // a less specific error message that still describes the problem, + // but also requests filing a Flutter issue so the parsing in + // _findPodspecMinDeploymentVersion can be improved. + _logger.printError( + 'Error: The plugin "$sourcePlugin" requires a higher minimum ' + '$platformName deployment version than your application is targeting.\n' + "To build, increase your application's deployment target as " + 'described at $docsLink\n\n' + 'The minimum required version for "$sourcePlugin" could not be ' + 'determined. Please file an issue at ' + 'https://github.com/flutter/flutter/issues about this error message.', + emphasis: true, + ); + } + } else { + // In theory this could find the failing pod's spec and parse out its + // minimum deployment version, but finding that spec would add a lot + // of complexity to handle a case that plugin authors should not + // create, so this just provides the actionable step of following up + // with the plugin developer. + _logger.printError( + 'Error: The pod "${podInfo.failingPod}" required by the plugin ' + '"$sourcePlugin" requires a higher minimum iOS deployment version ' + "than the plugin's reported minimum version.\n" + 'To build, remove the plugin "$sourcePlugin", or contact the plugin\'s ' + 'developers for assistance.', + emphasis: true, + ); + } + } } } + ({String failingPod, String sourcePlugin, String podPluginSubdir})? + _parseMinDeploymentFailureInfo(String podInstallOutput) { + final RegExp sourceLine = RegExp(r'\(from `.*\.symlinks/plugins/([^/]+)/([^/]+)`\)'); + final RegExp dependencyLine = RegExp(r'Specs satisfying the `([^ ]+).*` dependency were found, ' + 'but they required a higher minimum deployment target'); + final RegExpMatch? sourceMatch = sourceLine.firstMatch(podInstallOutput); + final RegExpMatch? dependencyMatch = dependencyLine.firstMatch(podInstallOutput); + if (sourceMatch == null || dependencyMatch == null) { + return null; + } + return ( + failingPod: dependencyMatch.group(1)!, + sourcePlugin: sourceMatch.group(1)!, + podPluginSubdir: sourceMatch.group(2)! + ); + } + + String? _findPodspecMinDeploymentVersion(File podspec, String platformString) { + if (!podspec.existsSync()) { + return null; + } + // There are two ways the deployment target can be specified; see + // https://guides.cocoapods.org/syntax/podspec.html#group_platform + final RegExp platformPattern = RegExp( + // Example: spec.platform = :osx, '10.8' + // where "spec" is an arbitrary variable name. + r'^\s*[a-zA-Z_]+\.platform\s*=\s*' + ':$platformString' + r'''\s*,\s*["']([^"']+)["']''', + multiLine: true + ); + final RegExp deploymentTargetPlatform = RegExp( + // Example: spec.osx.deployment_target = '10.8' + // where "spec" is an arbitrary variable name. + r'^\s*[a-zA-Z_]+\.' + '$platformString\\.deployment_target' + r'''\s*=\s*["']([^"']+)["']''', + multiLine: true + ); + final String podspecContents = podspec.readAsStringSync(); + final RegExpMatch? match = platformPattern.firstMatch(podspecContents) ?? + deploymentTargetPlatform.firstMatch(podspecContents); + return match?.group(1); + } + bool _isFfiX86Error(String error) { return error.contains('ffi_c.bundle') || error.contains('/ffi/'); } diff --git a/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart b/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart index c9bab1db809..f780a2702d2 100644 --- a/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart @@ -502,6 +502,448 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by ); }); + testUsingContext('throws if plugin requires higher minimum iOS version using "platform"', () async { + final FlutterProject projectUnderTest = setupProjectUnderTest(); + pretendPodIsInstalled(); + pretendPodVersionIs('100.0.0'); + fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile')) + ..createSync() + ..writeAsStringSync('Existing Podfile'); + const String fakePluginName = 'some_plugin'; + final File podspec = projectUnderTest.ios.symlinks + .childDirectory('plugins') + .childDirectory(fakePluginName) + .childDirectory('ios') + .childFile('$fakePluginName.podspec'); + podspec.createSync(recursive: true); + podspec.writeAsStringSync(''' +Pod::Spec.new do |s| + s.name = '$fakePluginName' + s.version = '0.0.1' + s.summary = 'A plugin' + s.source_files = 'Classes/**/*.{h,m}' + s.dependency 'Flutter' + s.static_framework = true + s.platform = :ios, '15.0' +end'''); + + fakeProcessManager.addCommand( + FakeCommand( + command: const ['pod', 'install', '--verbose'], + workingDirectory: 'project/ios', + environment: const { + 'COCOAPODS_DISABLE_STATS': 'true', + 'LANG': 'en_US.UTF-8', + }, + exitCode: 1, + stdout: _fakeHigherMinimumIOSVersionPodInstallOutput(fakePluginName), + ), + ); + + await expectLater(cocoaPodsUnderTest.processPods( + xcodeProject: projectUnderTest.ios, + buildMode: BuildMode.debug, + ), throwsToolExit()); + expect( + logger.errorText, + contains( + 'The plugin "$fakePluginName" requires a higher minimum iOS ' + 'deployment version than your application is targeting.' + ), + ); + // The error should contain specific instructions for fixing the build + // based on parsing the plugin's podspec. + expect( + logger.errorText, + contains( + "To build, increase your application's deployment target to at least " + '15.0 as described at https://docs.flutter.dev/deployment/ios' + ), + ); + }); + + testUsingContext('throws if plugin requires higher minimum iOS version using "deployment_target"', () async { + final FlutterProject projectUnderTest = setupProjectUnderTest(); + pretendPodIsInstalled(); + pretendPodVersionIs('100.0.0'); + fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile')) + ..createSync() + ..writeAsStringSync('Existing Podfile'); + const String fakePluginName = 'some_plugin'; + final File podspec = projectUnderTest.ios.symlinks + .childDirectory('plugins') + .childDirectory(fakePluginName) + .childDirectory('ios') + .childFile('$fakePluginName.podspec'); + podspec.createSync(recursive: true); + podspec.writeAsStringSync(''' +Pod::Spec.new do |s| + s.name = '$fakePluginName' + s.version = '0.0.1' + s.summary = 'A plugin' + s.source_files = 'Classes/**/*.{h,m}' + s.dependency 'Flutter' + s.static_framework = true + s.ios.deployment_target = '15.0' +end'''); + + fakeProcessManager.addCommand( + FakeCommand( + command: const ['pod', 'install', '--verbose'], + workingDirectory: 'project/ios', + environment: const { + 'COCOAPODS_DISABLE_STATS': 'true', + 'LANG': 'en_US.UTF-8', + }, + exitCode: 1, + stdout: _fakeHigherMinimumIOSVersionPodInstallOutput(fakePluginName), + ), + ); + + await expectLater(cocoaPodsUnderTest.processPods( + xcodeProject: projectUnderTest.ios, + buildMode: BuildMode.debug, + ), throwsToolExit()); + expect( + logger.errorText, + contains( + 'The plugin "$fakePluginName" requires a higher minimum iOS ' + 'deployment version than your application is targeting.' + ), + ); + // The error should contain specific instructions for fixing the build + // based on parsing the plugin's podspec. + expect( + logger.errorText, + contains( + "To build, increase your application's deployment target to at least " + '15.0 as described at https://docs.flutter.dev/deployment/ios' + ), + ); + }); + + testUsingContext('throws if plugin requires higher minimum iOS version with darwin layout', () async { + final FlutterProject projectUnderTest = setupProjectUnderTest(); + pretendPodIsInstalled(); + pretendPodVersionIs('100.0.0'); + fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile')) + ..createSync() + ..writeAsStringSync('Existing Podfile'); + const String fakePluginName = 'some_plugin'; + final File podspec = projectUnderTest.ios.symlinks + .childDirectory('plugins') + .childDirectory(fakePluginName) + .childDirectory('darwin') + .childFile('$fakePluginName.podspec'); + podspec.createSync(recursive: true); + podspec.writeAsStringSync(''' +Pod::Spec.new do |s| + s.name = '$fakePluginName' + s.version = '0.0.1' + s.summary = 'A plugin' + s.source_files = 'Classes/**/*.{h,m}' + s.dependency 'Flutter' + s.static_framework = true + s.osx.deployment_target = '10.15' + s.ios.deployment_target = '15.0' +end'''); + + fakeProcessManager.addCommand( + FakeCommand( + command: const ['pod', 'install', '--verbose'], + workingDirectory: 'project/ios', + environment: const { + 'COCOAPODS_DISABLE_STATS': 'true', + 'LANG': 'en_US.UTF-8', + }, + exitCode: 1, + stdout: _fakeHigherMinimumIOSVersionPodInstallOutput(fakePluginName, subdir: 'darwin'), + ), + ); + + await expectLater(cocoaPodsUnderTest.processPods( + xcodeProject: projectUnderTest.ios, + buildMode: BuildMode.debug, + ), throwsToolExit()); + expect( + logger.errorText, + contains( + 'The plugin "$fakePluginName" requires a higher minimum iOS ' + 'deployment version than your application is targeting.' + ), + ); + // The error should contain specific instructions for fixing the build + // based on parsing the plugin's podspec. + expect( + logger.errorText, + contains( + "To build, increase your application's deployment target to at least " + '15.0 as described at https://docs.flutter.dev/deployment/ios' + ), + ); + }); + + testUsingContext('throws if plugin requires unknown higher minimum iOS version', () async { + final FlutterProject projectUnderTest = setupProjectUnderTest(); + pretendPodIsInstalled(); + pretendPodVersionIs('100.0.0'); + fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile')) + ..createSync() + ..writeAsStringSync('Existing Podfile'); + const String fakePluginName = 'some_plugin'; + final File podspec = projectUnderTest.ios.symlinks + .childDirectory('plugins') + .childDirectory(fakePluginName) + .childDirectory('ios') + .childFile('$fakePluginName.podspec'); + podspec.createSync(recursive: true); + // It's very unlikely that someone would actually ever do anything like + // this, but arbitrary code is possible, so test that if it's not what + // the error handler parsing expects, a fallback is used. + podspec.writeAsStringSync(''' +Pod::Spec.new do |s| + s.name = '$fakePluginName' + s.version = '0.0.1' + s.summary = 'A plugin' + s.source_files = 'Classes/**/*.{h,m}' + s.dependency 'Flutter' + s.static_framework = true + version_var = '15.0' + s.platform = :ios, version_var +end'''); + + fakeProcessManager.addCommand( + FakeCommand( + command: const ['pod', 'install', '--verbose'], + workingDirectory: 'project/ios', + environment: const { + 'COCOAPODS_DISABLE_STATS': 'true', + 'LANG': 'en_US.UTF-8', + }, + exitCode: 1, + stdout: _fakeHigherMinimumIOSVersionPodInstallOutput(fakePluginName), + ), + ); + + await expectLater(cocoaPodsUnderTest.processPods( + xcodeProject: projectUnderTest.ios, + buildMode: BuildMode.debug, + ), throwsToolExit()); + expect( + logger.errorText, + contains( + 'The plugin "$fakePluginName" requires a higher minimum iOS ' + 'deployment version than your application is targeting.' + ), + ); + // The error should contain non-specific instructions for fixing the build + // and note that the minimum version could not be determined. + expect( + logger.errorText, + contains( + "To build, increase your application's deployment target as " + 'described at https://docs.flutter.dev/deployment/ios', + ), + ); + expect( + logger.errorText, + contains( + 'The minimum required version for "$fakePluginName" could not be ' + 'determined', + ), + ); + }); + + testUsingContext('throws if plugin has a dependency that requires a higher minimum iOS version', () async { + final FlutterProject projectUnderTest = setupProjectUnderTest(); + pretendPodIsInstalled(); + pretendPodVersionIs('100.0.0'); + fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile')) + ..createSync() + ..writeAsStringSync('Existing Podfile'); + + fakeProcessManager.addCommand( + const FakeCommand( + command: ['pod', 'install', '--verbose'], + workingDirectory: 'project/ios', + environment: { + 'COCOAPODS_DISABLE_STATS': 'true', + 'LANG': 'en_US.UTF-8', + }, + exitCode: 1, + // This is the (very slightly abridged) output from updating the + // minimum version of the GoogleMaps dependency in + // google_maps_flutter_ios without updating the minimum iOS version to + // match, as an example of a misconfigured plugin. + stdout: ''' +Analyzing dependencies + +Inspecting targets to integrate + Using `ARCHS` setting to build architectures of target `Pods-Runner`: (``) + Using `ARCHS` setting to build architectures of target `Pods-RunnerTests`: (``) + +Fetching external sources +-> Fetching podspec for `Flutter` from `Flutter` +-> Fetching podspec for `google_maps_flutter_ios` from `.symlinks/plugins/google_maps_flutter_ios/ios` + +Resolving dependencies of `Podfile` + CDN: trunk Relative path: CocoaPods-version.yml exists! Returning local because checking is only performed in repo update + CDN: trunk Relative path: Specs/a/d/d/GoogleMaps/8.0.0/GoogleMaps.podspec.json exists! Returning local because checking is only performed in repo update +[!] CocoaPods could not find compatible versions for pod "GoogleMaps": + In Podfile: + google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`) was resolved to 0.0.1, which depends on + GoogleMaps (~> 8.0) + +Specs satisfying the `GoogleMaps (~> 8.0)` dependency were found, but they required a higher minimum deployment target.''', + ), + ); + + await expectLater(cocoaPodsUnderTest.processPods( + xcodeProject: projectUnderTest.ios, + buildMode: BuildMode.debug, + ), throwsToolExit()); + expect( + logger.errorText, + contains( + 'The pod "GoogleMaps" required by the plugin "google_maps_flutter_ios" ' + "requires a higher minimum iOS deployment version than the plugin's " + 'reported minimum version.' + ), + ); + // The error should tell the user to contact the plugin author, as this + // case is hard for us to give exact advice on, and should only be + // possible if there's a mistake in the plugin's podspec. + expect( + logger.errorText, + contains( + 'To build, remove the plugin "google_maps_flutter_ios", or contact ' + "the plugin's developers for assistance.", + ), + ); + }); + + testUsingContext('throws if plugin requires higher minimum macOS version using "platform"', () async { + final FlutterProject projectUnderTest = setupProjectUnderTest(); + pretendPodIsInstalled(); + pretendPodVersionIs('100.0.0'); + fileSystem.file(fileSystem.path.join('project', 'macos', 'Podfile')) + ..createSync() + ..writeAsStringSync('Existing Podfile'); + const String fakePluginName = 'some_plugin'; + final File podspec = projectUnderTest.macos.ephemeralDirectory + .childDirectory('.symlinks') + .childDirectory('plugins') + .childDirectory(fakePluginName) + .childDirectory('macos') + .childFile('$fakePluginName.podspec'); + podspec.createSync(recursive: true); + podspec.writeAsStringSync(''' +Pod::Spec.new do |spec| + spec.name = '$fakePluginName' + spec.version = '0.0.1' + spec.summary = 'A plugin' + spec.source_files = 'Classes/**/*.swift' + spec.dependency 'FlutterMacOS' + spec.static_framework = true + spec.platform = :osx, "12.7" +end'''); + + fakeProcessManager.addCommand( + FakeCommand( + command: const ['pod', 'install', '--verbose'], + workingDirectory: 'project/macos', + environment: const { + 'COCOAPODS_DISABLE_STATS': 'true', + 'LANG': 'en_US.UTF-8', + }, + exitCode: 1, + stdout: _fakeHigherMinimumMacOSVersionPodInstallOutput(fakePluginName), + ), + ); + + await expectLater(cocoaPodsUnderTest.processPods( + xcodeProject: projectUnderTest.macos, + buildMode: BuildMode.debug, + ), throwsToolExit()); + expect( + logger.errorText, + contains( + 'The plugin "$fakePluginName" requires a higher minimum macOS ' + 'deployment version than your application is targeting.' + ), + ); + // The error should contain specific instructions for fixing the build + // based on parsing the plugin's podspec. + expect( + logger.errorText, + contains( + "To build, increase your application's deployment target to at least " + '12.7 as described at https://docs.flutter.dev/deployment/macos' + ), + ); + }); + + testUsingContext('throws if plugin requires higher minimum macOS version using "deployment_target"', () async { + final FlutterProject projectUnderTest = setupProjectUnderTest(); + pretendPodIsInstalled(); + pretendPodVersionIs('100.0.0'); + fileSystem.file(fileSystem.path.join('project', 'macos', 'Podfile')) + ..createSync() + ..writeAsStringSync('Existing Podfile'); + const String fakePluginName = 'some_plugin'; + final File podspec = projectUnderTest.macos.ephemeralDirectory + .childDirectory('.symlinks') + .childDirectory('plugins') + .childDirectory(fakePluginName) + .childDirectory('macos') + .childFile('$fakePluginName.podspec'); + podspec.createSync(recursive: true); + podspec.writeAsStringSync(''' +Pod::Spec.new do |spec| + spec.name = '$fakePluginName' + spec.version = '0.0.1' + spec.summary = 'A plugin' + spec.source_files = 'Classes/**/*.{h,m}' + spec.dependency 'Flutter' + spec.static_framework = true + spec.osx.deployment_target = '12.7' +end'''); + + fakeProcessManager.addCommand( + FakeCommand( + command: const ['pod', 'install', '--verbose'], + workingDirectory: 'project/macos', + environment: const { + 'COCOAPODS_DISABLE_STATS': 'true', + 'LANG': 'en_US.UTF-8', + }, + exitCode: 1, + stdout: _fakeHigherMinimumMacOSVersionPodInstallOutput(fakePluginName), + ), + ); + + await expectLater(cocoaPodsUnderTest.processPods( + xcodeProject: projectUnderTest.macos, + buildMode: BuildMode.debug, + ), throwsToolExit()); + expect( + logger.errorText, + contains( + 'The plugin "$fakePluginName" requires a higher minimum macOS ' + 'deployment version than your application is targeting.' + ), + ); + // The error should contain specific instructions for fixing the build + // based on parsing the plugin's podspec. + expect( + logger.errorText, + contains( + "To build, increase your application's deployment target to at least " + '12.7 as described at https://docs.flutter.dev/deployment/macos' + ), + ); + }); + final Map possibleErrors = { 'symbol not found': 'LoadError - dlsym(0x7fbbeb6837d0, Init_ffi_c): symbol not found - /Library/Ruby/Gems/2.6.0/gems/ffi-1.13.1/lib/ffi_c.bundle', 'incompatible architecture': "LoadError - (mach-o file, but is an incompatible architecture (have 'arm64', need 'x86_64')), '/usr/lib/ffi_c.bundle' (no such file) - /Library/Ruby/Gems/2.6.0/gems/ffi-1.15.4/lib/ffi_c.bundle", @@ -889,6 +1331,54 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by }); } +String _fakeHigherMinimumIOSVersionPodInstallOutput(String fakePluginName, {String subdir = 'ios'}) { + return ''' +Preparing + +Analyzing dependencies + +Inspecting targets to integrate + Using `ARCHS` setting to build architectures of target `Pods-Runner`: (``) + Using `ARCHS` setting to build architectures of target `Pods-RunnerTests`: (``) + +Fetching external sources +-> Fetching podspec for `Flutter` from `Flutter` +-> Fetching podspec for `$fakePluginName` from `.symlinks/plugins/$fakePluginName/$subdir` +-> Fetching podspec for `another_plugin` from `.symlinks/plugins/another_plugin/ios` + +Resolving dependencies of `Podfile` + CDN: trunk Relative path: CocoaPods-version.yml exists! Returning local because checking is only performed in repo update +[!] CocoaPods could not find compatible versions for pod "$fakePluginName": + In Podfile: + $fakePluginName (from `.symlinks/plugins/$fakePluginName/$subdir`) + +Specs satisfying the `$fakePluginName (from `.symlinks/plugins/$fakePluginName/subdir`)` dependency were found, but they required a higher minimum deployment target.'''; +} + +String _fakeHigherMinimumMacOSVersionPodInstallOutput(String fakePluginName, {String subdir = 'macos'}) { + return ''' +Preparing + +Analyzing dependencies + +Inspecting targets to integrate + Using `ARCHS` setting to build architectures of target `Pods-Runner`: (``) + Using `ARCHS` setting to build architectures of target `Pods-RunnerTests`: (``) + +Fetching external sources +-> Fetching podspec for `FlutterMacOS` from `Flutter/ephemeral` +-> Fetching podspec for `$fakePluginName` from `Flutter/ephemeral/.symlinks/plugins/$fakePluginName/$subdir` +-> Fetching podspec for `another_plugin` from `Flutter/ephemeral/.symlinks/plugins/another_plugin/macos` + +Resolving dependencies of `Podfile` + CDN: trunk Relative path: CocoaPods-version.yml exists! Returning local because checking is only performed in repo update +[!] CocoaPods could not find compatible versions for pod "$fakePluginName": + In Podfile: + $fakePluginName (from `Flutter/ephemeral/.symlinks/plugins/$fakePluginName/$subdir`) + +Specs satisfying the `$fakePluginName (from `Flutter/ephemeral/.symlinks/plugins/$fakePluginName/$subdir`)` dependency were found, but they required a higher minimum deployment target.'''; +} + class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter { FakeXcodeProjectInterpreter({this.isInstalled = true, this.buildSettings = const {}});