// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:file/file.dart'; import 'package:file/memory.dart'; 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/cache.dart'; import 'package:flutter_tools/src/flutter_plugins.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/macos/cocoapods.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'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fake_process_manager.dart'; import '../../src/fakes.dart'; enum _StdioStream { stdout, stderr, } void main() { late FileSystem fileSystem; late FakeProcessManager fakeProcessManager; late CocoaPods cocoaPodsUnderTest; late BufferLogger logger; late TestUsage usage; late FakeAnalytics fakeAnalytics; void pretendPodVersionFails() { fakeProcessManager.addCommand( const FakeCommand( command: <String>['pod', '--version'], exitCode: 1, ), ); } void pretendPodVersionIs(String versionText) { fakeProcessManager.addCommand( FakeCommand( command: const <String>['pod', '--version'], stdout: versionText, ), ); } void podsIsInHomeDir() { fileSystem.directory(fileSystem.path.join( '.cocoapods', 'repos', 'master', )).createSync(recursive: true); } FlutterProject setupProjectUnderTest() { // This needs to be run within testWithoutContext and not setUp since FlutterProject uses context. final FlutterProject projectUnderTest = FlutterProject.fromDirectory(fileSystem.directory('project')); projectUnderTest.ios.xcodeProject.createSync(recursive: true); projectUnderTest.macos.xcodeProject.createSync(recursive: true); return projectUnderTest; } setUp(() async { Cache.flutterRoot = 'flutter'; fileSystem = MemoryFileSystem.test(); fakeProcessManager = FakeProcessManager.empty(); logger = BufferLogger.test(); usage = TestUsage(); fakeAnalytics = getInitializedFakeAnalyticsInstance( fs: fileSystem, fakeFlutterVersion: FakeFlutterVersion(), ); cocoaPodsUnderTest = CocoaPods( fileSystem: fileSystem, processManager: fakeProcessManager, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), usage: usage, analytics: fakeAnalytics, ); fileSystem.file(fileSystem.path.join( Cache.flutterRoot!, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-ios-objc', )) ..createSync(recursive: true) ..writeAsStringSync('Objective-C iOS podfile template'); fileSystem.file(fileSystem.path.join( Cache.flutterRoot!, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-ios-swift', )) ..createSync(recursive: true) ..writeAsStringSync('Swift iOS podfile template'); fileSystem.file(fileSystem.path.join( Cache.flutterRoot!, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-macos', )) ..createSync(recursive: true) ..writeAsStringSync('macOS podfile template'); }); void pretendPodIsNotInstalled() { fakeProcessManager.addCommand( const FakeCommand( command: <String>['which', 'pod'], exitCode: 1, ), ); } void pretendPodIsBroken() { fakeProcessManager.addCommands(<FakeCommand>[ // it is present const FakeCommand( command: <String>['which', 'pod'], ), // but is not working const FakeCommand( command: <String>['pod', '--version'], exitCode: 1, ), ]); } void pretendPodIsInstalled() { fakeProcessManager.addCommands(<FakeCommand>[ const FakeCommand( command: <String>['which', 'pod'], ), ]); } group('Evaluate installation', () { testWithoutContext('detects not installed, if pod exec does not exist', () async { pretendPodIsNotInstalled(); expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.notInstalled); }); testWithoutContext('detects not installed, if pod is installed but version fails', () async { pretendPodIsInstalled(); pretendPodVersionFails(); expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.brokenInstall); }); testWithoutContext('detects installed', () async { pretendPodIsInstalled(); pretendPodVersionIs('0.0.1'); expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, isNot(CocoaPodsStatus.notInstalled)); }); testWithoutContext('detects unknown version', () async { pretendPodIsInstalled(); pretendPodVersionIs('Plugin loaded.\n1.5.3'); expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.unknownVersion); }); testWithoutContext('detects below minimum version', () async { pretendPodIsInstalled(); pretendPodVersionIs('1.9.0'); expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.belowMinimumVersion); }); testWithoutContext('detects below recommended version', () async { pretendPodIsInstalled(); pretendPodVersionIs('1.12.5'); expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.belowRecommendedVersion); }); testWithoutContext('detects at recommended version', () async { pretendPodIsInstalled(); pretendPodVersionIs('1.13.0'); expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended); }); testWithoutContext('detects above recommended version', () async { pretendPodIsInstalled(); pretendPodVersionIs('1.13.1'); expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended); }); }); group('Setup Podfile', () { testUsingContext('creates objective-c Podfile when not present', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); await cocoaPodsUnderTest.setupPodfile(projectUnderTest.ios); expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Objective-C iOS podfile template'); }); testUsingContext('creates swift Podfile if swift', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); final FakeXcodeProjectInterpreter fakeXcodeProjectInterpreter = FakeXcodeProjectInterpreter(buildSettings: <String, String>{ 'SWIFT_VERSION': '5.0', }); final CocoaPods cocoaPodsUnderTest = CocoaPods( fileSystem: fileSystem, processManager: fakeProcessManager, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), xcodeProjectInterpreter: fakeXcodeProjectInterpreter, usage: usage, analytics: fakeAnalytics, ); final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project')); await cocoaPodsUnderTest.setupPodfile(project.ios); expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Swift iOS podfile template'); }); testUsingContext('creates macOS Podfile when not present', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); projectUnderTest.macos.xcodeProject.createSync(recursive: true); await cocoaPodsUnderTest.setupPodfile(projectUnderTest.macos); expect(projectUnderTest.macos.podfile.readAsStringSync(), 'macOS podfile template'); }); testUsingContext('does not recreate Podfile when already present', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile'); final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project')); await cocoaPodsUnderTest.setupPodfile(project.ios); expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Existing Podfile'); }); testUsingContext('does not create Podfile when we cannot interpret Xcode projects', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); final CocoaPods cocoaPodsUnderTest = CocoaPods( fileSystem: fileSystem, processManager: fakeProcessManager, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), xcodeProjectInterpreter: FakeXcodeProjectInterpreter(isInstalled: false), usage: usage, analytics: fakeAnalytics, ); final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project')); await cocoaPodsUnderTest.setupPodfile(project.ios); expect(projectUnderTest.ios.podfile.existsSync(), false); }); testUsingContext('includes Pod config in xcconfig files, if not present', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile'); projectUnderTest.ios.xcodeConfigFor('Debug') ..createSync(recursive: true) ..writeAsStringSync('Existing debug config'); projectUnderTest.ios.xcodeConfigFor('Release') ..createSync(recursive: true) ..writeAsStringSync('Existing release config'); final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project')); await cocoaPodsUnderTest.setupPodfile(project.ios); final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync(); expect(debugContents, contains( '#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"\n')); expect(debugContents, contains('Existing debug config')); final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync(); expect(releaseContents, contains( '#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"\n')); expect(releaseContents, contains('Existing release config')); }); testUsingContext('does not include Pod config in xcconfig files, if legacy non-option include present', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile'); const String legacyDebugInclude = '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig'; projectUnderTest.ios.xcodeConfigFor('Debug') ..createSync(recursive: true) ..writeAsStringSync(legacyDebugInclude); const String legacyReleaseInclude = '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig'; projectUnderTest.ios.xcodeConfigFor('Release') ..createSync(recursive: true) ..writeAsStringSync(legacyReleaseInclude); final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project')); await cocoaPodsUnderTest.setupPodfile(project.ios); final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync(); // Redundant contains check, but this documents what we're testing--that the optional // #include? doesn't get written in addition to the previous style #include. expect(debugContents, isNot(contains('#include?'))); expect(debugContents, equals(legacyDebugInclude)); final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync(); expect(releaseContents, isNot(contains('#include?'))); expect(releaseContents, equals(legacyReleaseInclude)); }); testUsingContext('does not include Pod config in xcconfig files, if flavor include present', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile'); const String flavorDebugInclude = '#include? "Pods/Target Support Files/Pods-Free App/Pods-Free App.debug free.xcconfig"'; projectUnderTest.ios.xcodeConfigFor('Debug') ..createSync(recursive: true) ..writeAsStringSync(flavorDebugInclude); const String flavorReleaseInclude = '#include? "Pods/Target Support Files/Pods-Free App/Pods-Free App.release free.xcconfig"'; projectUnderTest.ios.xcodeConfigFor('Release') ..createSync(recursive: true) ..writeAsStringSync(flavorReleaseInclude); final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project')); await cocoaPodsUnderTest.setupPodfile(project.ios); final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync(); // Redundant contains check, but this documents what we're testing--that the optional // #include? doesn't get written in addition to the previous style #include. expect(debugContents, isNot(contains('Pods-Runner/Pods-Runner.debug'))); expect(debugContents, equals(flavorDebugInclude)); final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync(); expect(releaseContents, isNot(contains('Pods-Runner/Pods-Runner.release'))); expect(releaseContents, equals(flavorReleaseInclude)); }); }); group('Update xcconfig', () { testUsingContext('includes Pod config in xcconfig files, if the user manually added Pod dependencies without using Flutter plugins', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); fileSystem.file(fileSystem.path.join('project', 'foo', '.packages')) ..createSync(recursive: true) ..writeAsStringSync('\n'); projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Custom Podfile'); projectUnderTest.ios.podfileLock..createSync()..writeAsStringSync('Podfile.lock from user executed `pod install`'); projectUnderTest.packagesFile..createSync()..writeAsStringSync(''); projectUnderTest.ios.xcodeConfigFor('Debug') ..createSync(recursive: true) ..writeAsStringSync('Existing debug config'); projectUnderTest.ios.xcodeConfigFor('Release') ..createSync(recursive: true) ..writeAsStringSync('Existing release config'); final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project')); await injectPlugins(project, iosPlatform: true); final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync(); expect(debugContents, contains( '#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"\n')); expect(debugContents, contains('Existing debug config')); final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync(); expect(releaseContents, contains( '#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"\n')); expect(releaseContents, contains('Existing release config')); }, overrides: <Type, Generator>{ FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.any(), }); }); group('Process pods', () { setUp(() { podsIsInHomeDir(); }); testUsingContext('throwsToolExit if CocoaPods is not installed', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsNotInstalled(); projectUnderTest.ios.podfile.createSync(); await expectLater(cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, ), throwsToolExit(message: 'CocoaPods not installed or not in valid state')); expect(fakeProcessManager, hasNoRemainingExpectations); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('throwsToolExit if CocoaPods install is broken', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsBroken(); projectUnderTest.ios.podfile.createSync(); await expectLater(cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, ), throwsToolExit(message: 'CocoaPods not installed or not in valid state')); expect(fakeProcessManager, hasNoRemainingExpectations); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('exits if Podfile creates the Flutter engine symlink', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile')) ..createSync() ..writeAsStringSync('Existing Podfile'); final Directory symlinks = projectUnderTest.ios.symlinks ..createSync(recursive: true); symlinks.childLink('flutter').createSync('cache'); await expectLater(cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, ), throwsToolExit(message: 'Podfile is out of date')); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('exits if iOS Podfile parses .flutter-plugins', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile')) ..createSync() ..writeAsStringSync("plugin_pods = parse_KV_file('../.flutter-plugins')"); await expectLater(cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, ), throwsToolExit(message: 'Podfile is out of date')); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('prints warning if macOS Podfile parses .flutter-plugins', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); fakeProcessManager.addCommands(const <FakeCommand>[ FakeCommand( command: <String>['pod', 'install', '--verbose'], ), FakeCommand( command: <String>['touch', 'project/macos/Podfile.lock'], ), ]); projectUnderTest.macos.podfile ..createSync() ..writeAsStringSync("plugin_pods = parse_KV_file('../.flutter-plugins')"); projectUnderTest.macos.podfileLock ..createSync() ..writeAsStringSync('Existing lock file.'); await cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.macos, buildMode: BuildMode.debug, ); expect(logger.warningText, contains('Warning: Podfile is out of date')); expect(logger.warningText, contains('rm macos/Podfile')); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('throws, if Podfile is missing.', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); await expectLater(cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, ), throwsToolExit(message: 'Podfile missing')); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('throws, if specs repo is outdated.', () 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: <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: <String, String>{ 'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8', }, exitCode: 1, // This output is the output that a real CocoaPods install would generate. stdout: ''' [!] Unable to satisfy the following requirements: - `Firebase/Auth` required by `Podfile` - `Firebase/Auth (= 4.0.0)` required by `Podfile.lock` None of your spec sources contain a spec satisfying the dependencies: `Firebase/Auth, Firebase/Auth (= 4.0.0)`. You have either: * out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`. * mistyped the name or version. * not added the source repo that hosts the Podspec to your Podfile. Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by default.''', ), ); await expectLater(cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, ), throwsToolExit()); expect( logger.errorText, contains("CocoaPods's specs repository is too out-of-date to satisfy dependencies"), ); }); 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 <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: const <String, String>{ '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 <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: const <String, String>{ '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 <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: const <String, String>{ '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 <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: const <String, String>{ '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: <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: <String, String>{ '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 <String>['pod', 'install', '--verbose'], workingDirectory: 'project/macos', environment: const <String, String>{ '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 <String>['pod', 'install', '--verbose'], workingDirectory: 'project/macos', environment: const <String, String>{ '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<String, String> possibleErrors = <String, String>{ '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", 'bus error': '/Library/Ruby/Gems/2.6.0/gems/ffi-1.15.5/lib/ffi/library.rb:275: [BUG] Bus Error at 0x000000010072c000', }; possibleErrors.forEach((String errorName, String cocoaPodsError) { void testToolExitsWithCocoapodsMessage(_StdioStream outputStream) { final String streamName = outputStream == _StdioStream.stdout ? 'stdout' : 'stderr'; testUsingContext('ffi $errorName failure to $streamName on ARM macOS prompts gem install', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile')) ..createSync() ..writeAsStringSync('Existing Podfile'); fakeProcessManager.addCommands(<FakeCommand>[ FakeCommand( command: const <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: const <String, String>{ 'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8', }, exitCode: 1, stdout: outputStream == _StdioStream.stdout ? cocoaPodsError : '', stderr: outputStream == _StdioStream.stderr ? cocoaPodsError : '', ), const FakeCommand( command: <String>['which', 'sysctl'], ), const FakeCommand( command: <String>['sysctl', 'hw.optional.arm64'], stdout: 'hw.optional.arm64: 1', ), ]); await expectToolExitLater( cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, ), equals('Error running pod install'), ); expect( logger.errorText, contains('set up CocoaPods for ARM macOS'), ); expect( logger.errorText, contains('enable-libffi-alloc'), ); expect(usage.events, contains(const TestUsageEvent('pod-install-failure', 'arm-ffi'))); expect(fakeAnalytics.sentEvents, contains(Event.appleUsageEvent(workflow: 'pod-install-failure', parameter: 'arm-ffi'))); }); } testToolExitsWithCocoapodsMessage(_StdioStream.stdout); testToolExitsWithCocoapodsMessage(_StdioStream.stderr); }); testUsingContext('ffi failure on x86 macOS does not prompt gem install', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile')) ..createSync() ..writeAsStringSync('Existing Podfile'); fakeProcessManager.addCommands(<FakeCommand>[ const FakeCommand( command: <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: <String, String>{ 'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8', }, exitCode: 1, stderr: 'LoadError - dlsym(0x7fbbeb6837d0, Init_ffi_c): symbol not found - /Library/Ruby/Gems/2.6.0/gems/ffi-1.13.1/lib/ffi_c.bundle', ), const FakeCommand( command: <String>['which', 'sysctl'], ), const FakeCommand( command: <String>['sysctl', 'hw.optional.arm64'], exitCode: 1, ), ]); // Capture Usage.test() events. final StringBuffer buffer = await capturedConsolePrint(() => expectToolExitLater( cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, ), equals('Error running pod install'), )); expect( logger.errorText, isNot(contains('ARM macOS')), ); expect(buffer.isEmpty, true); }); testUsingContext('run pod install, if Podfile.lock is missing', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); projectUnderTest.ios.podfile ..createSync() ..writeAsStringSync('Existing Podfile'); projectUnderTest.ios.podManifestLock ..createSync(recursive: true) ..writeAsStringSync('Existing lock file.'); fakeProcessManager.addCommands(const <FakeCommand>[ FakeCommand( command: <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'}, ), ]); final bool didInstall = await cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, dependenciesChanged: false, ); expect(didInstall, isTrue); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('runs iOS pod install, if Manifest.lock is missing', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); projectUnderTest.ios.podfile ..createSync() ..writeAsStringSync('Existing Podfile'); projectUnderTest.ios.podfileLock ..createSync() ..writeAsStringSync('Existing lock file.'); fakeProcessManager.addCommands(const <FakeCommand>[ FakeCommand( command: <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'}, ), FakeCommand( command: <String>['touch', 'project/ios/Podfile.lock'], ), ]); final bool didInstall = await cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, dependenciesChanged: false, ); expect(didInstall, isTrue); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('runs macOS pod install, if Manifest.lock is missing', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); projectUnderTest.macos.podfile ..createSync() ..writeAsStringSync('Existing Podfile'); projectUnderTest.macos.podfileLock ..createSync() ..writeAsStringSync('Existing lock file.'); fakeProcessManager.addCommands(const <FakeCommand>[ FakeCommand( command: <String>['pod', 'install', '--verbose'], workingDirectory: 'project/macos', environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'}, ), FakeCommand( command: <String>['touch', 'project/macos/Podfile.lock'], ), ]); final bool didInstall = await cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.macos, buildMode: BuildMode.debug, dependenciesChanged: false, ); expect(didInstall, isTrue); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('runs pod install, if Manifest.lock different from Podspec.lock', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); projectUnderTest.ios.podfile ..createSync() ..writeAsStringSync('Existing Podfile'); projectUnderTest.ios.podfileLock ..createSync() ..writeAsStringSync('Existing lock file.'); projectUnderTest.ios.podManifestLock ..createSync(recursive: true) ..writeAsStringSync('Different lock file.'); fakeProcessManager.addCommands(const <FakeCommand>[ FakeCommand( command: <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'}, ), FakeCommand( command: <String>['touch', 'project/ios/Podfile.lock'], ), ]); final bool didInstall = await cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, dependenciesChanged: false, ); expect(didInstall, isTrue); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('runs pod install, if flutter framework changed', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); projectUnderTest.ios.podfile ..createSync() ..writeAsStringSync('Existing Podfile'); projectUnderTest.ios.podfileLock ..createSync() ..writeAsStringSync('Existing lock file.'); projectUnderTest.ios.podManifestLock ..createSync(recursive: true) ..writeAsStringSync('Existing lock file.'); fakeProcessManager.addCommands(const <FakeCommand>[ FakeCommand( command: <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'}, ), FakeCommand( command: <String>['touch', 'project/ios/Podfile.lock'], ), ]); final bool didInstall = await cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, ); expect(didInstall, isTrue); expect(fakeProcessManager, hasNoRemainingExpectations); expect(logger.traceText, contains('CocoaPods Pods-Runner-frameworks.sh script not found')); }); testUsingContext('runs CocoaPods Pod runner script migrator', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); projectUnderTest.ios.podfile ..createSync() ..writeAsStringSync('Existing Podfile'); projectUnderTest.ios.podfileLock ..createSync() ..writeAsStringSync('Existing lock file.'); projectUnderTest.ios.podManifestLock ..createSync(recursive: true) ..writeAsStringSync('Existing lock file.'); projectUnderTest.ios.podRunnerFrameworksScript ..createSync(recursive: true) ..writeAsStringSync(r'source="$(readlink "${source}")"'); fakeProcessManager.addCommands(const <FakeCommand>[ FakeCommand( command: <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'}, ), FakeCommand( command: <String>['touch', 'project/ios/Podfile.lock'], ), ]); final CocoaPods cocoaPodsUnderTestXcode143 = CocoaPods( fileSystem: fileSystem, processManager: fakeProcessManager, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: fakeProcessManager, version: Version(14, 3, 0)), usage: usage, analytics: fakeAnalytics, ); final bool didInstall = await cocoaPodsUnderTestXcode143.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, ); expect(didInstall, isTrue); expect(fakeProcessManager, hasNoRemainingExpectations); // Now has readlink -f flag. expect(projectUnderTest.ios.podRunnerFrameworksScript.readAsStringSync(), contains(r'source="$(readlink -f "${source}")"')); expect(logger.statusText, contains('Upgrading Pods-Runner-frameworks.sh')); }); testUsingContext('runs pod install, if Podfile.lock is older than Podfile', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); projectUnderTest.ios.podfile ..createSync() ..writeAsStringSync('Existing Podfile'); projectUnderTest.ios.podfileLock ..createSync() ..writeAsStringSync('Existing lock file.'); projectUnderTest.ios.podManifestLock ..createSync(recursive: true) ..writeAsStringSync('Existing lock file.'); await Future<void>.delayed(const Duration(milliseconds: 10)); projectUnderTest.ios.podfile .writeAsStringSync('Updated Podfile'); fakeProcessManager.addCommands(const <FakeCommand>[ FakeCommand( command: <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'}, ), FakeCommand( command: <String>['touch', 'project/ios/Podfile.lock'], ), ]); await cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, dependenciesChanged: false, ); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('skips pod install, if nothing changed', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); projectUnderTest.ios.podfile ..createSync() ..writeAsStringSync('Existing Podfile'); projectUnderTest.ios.podfileLock ..createSync() ..writeAsStringSync('Existing lock file.'); projectUnderTest.ios.podManifestLock ..createSync(recursive: true) ..writeAsStringSync('Existing lock file.'); final bool didInstall = await cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, dependenciesChanged: false, ); expect(didInstall, isFalse); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('a failed pod install deletes Pods/Manifest.lock', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); projectUnderTest.ios.podfile ..createSync() ..writeAsStringSync('Existing Podfile'); projectUnderTest.ios.podfileLock ..createSync() ..writeAsStringSync('Existing lock file.'); projectUnderTest.ios.podManifestLock ..createSync(recursive: true) ..writeAsStringSync('Existing lock file.'); fakeProcessManager.addCommand( const FakeCommand( command: <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'}, exitCode: 1, ), ); await expectLater(cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, ), throwsToolExit(message: 'Error running pod install')); expect(projectUnderTest.ios.podManifestLock.existsSync(), isFalse); }); }); } 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 <String, String>{}}); @override final bool isInstalled; @override Future<Map<String, String>> getBuildSettings( String projectPath, { XcodeProjectBuildContext? buildContext, Duration timeout = const Duration(minutes: 1), }) async => buildSettings; final Map<String, String> buildSettings; }