1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:file/file.dart';
6import 'package:file/memory.dart';
7import 'package:flutter_tools/src/base/logger.dart';
8import 'package:flutter_tools/src/base/platform.dart';
9import 'package:flutter_tools/src/base/version.dart';
10import 'package:flutter_tools/src/build_info.dart';
11import 'package:flutter_tools/src/cache.dart';
12import 'package:flutter_tools/src/features.dart';
13import 'package:flutter_tools/src/flutter_plugins.dart';
14import 'package:flutter_tools/src/ios/xcodeproj.dart';
15import 'package:flutter_tools/src/macos/cocoapods.dart';
16import 'package:flutter_tools/src/project.dart';
17import 'package:flutter_tools/src/reporting/reporting.dart';
18import 'package:test/fake.dart';
19import 'package:unified_analytics/unified_analytics.dart';
20
21import '../../src/common.dart';
22import '../../src/context.dart';
23import '../../src/fake_process_manager.dart';
24import '../../src/fakes.dart';
25
26enum _StdioStream {
27 stdout,
28 stderr,
29}
30
31void main() {
32 late MemoryFileSystem fileSystem;
33 late FakeProcessManager fakeProcessManager;
34 late CocoaPods cocoaPodsUnderTest;
35 late BufferLogger logger;
36 late TestUsage usage;
37 late FakeAnalytics fakeAnalytics;
38
39 void pretendPodVersionFails() {
40 fakeProcessManager.addCommand(
41 const FakeCommand(
42 command: <String>['pod', '--version'],
43 exitCode: 1,
44 ),
45 );
46 }
47
48 void pretendPodVersionIs(String versionText) {
49 fakeProcessManager.addCommand(
50 FakeCommand(
51 command: const <String>['pod', '--version'],
52 stdout: versionText,
53 ),
54 );
55 }
56
57 void podsIsInHomeDir() {
58 fileSystem.directory(fileSystem.path.join(
59 '.cocoapods',
60 'repos',
61 'master',
62 )).createSync(recursive: true);
63 }
64
65 FlutterProject setupProjectUnderTest() {
66 // This needs to be run within testWithoutContext and not setUp since FlutterProject uses context.
67 final FlutterProject projectUnderTest = FlutterProject.fromDirectory(fileSystem.directory('project'));
68 projectUnderTest.ios.xcodeProject.createSync(recursive: true);
69 projectUnderTest.macos.xcodeProject.createSync(recursive: true);
70 return projectUnderTest;
71 }
72
73 setUp(() async {
74 Cache.flutterRoot = 'flutter';
75 fileSystem = MemoryFileSystem.test();
76 fakeProcessManager = FakeProcessManager.empty();
77 logger = BufferLogger.test();
78 usage = TestUsage();
79 fakeAnalytics = getInitializedFakeAnalyticsInstance(
80 fs: fileSystem,
81 fakeFlutterVersion: FakeFlutterVersion(),
82 );
83 cocoaPodsUnderTest = CocoaPods(
84 fileSystem: fileSystem,
85 processManager: fakeProcessManager,
86 logger: logger,
87 platform: FakePlatform(operatingSystem: 'macos'),
88 xcodeProjectInterpreter: FakeXcodeProjectInterpreter(),
89 usage: usage,
90 analytics: fakeAnalytics,
91 );
92 fileSystem.file(fileSystem.path.join(
93 Cache.flutterRoot!, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-ios-objc',
94 ))
95 ..createSync(recursive: true)
96 ..writeAsStringSync('Objective-C iOS podfile template');
97 fileSystem.file(fileSystem.path.join(
98 Cache.flutterRoot!, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-ios-swift',
99 ))
100 ..createSync(recursive: true)
101 ..writeAsStringSync('Swift iOS podfile template');
102 fileSystem.file(fileSystem.path.join(
103 Cache.flutterRoot!, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-macos',
104 ))
105 ..createSync(recursive: true)
106 ..writeAsStringSync('macOS podfile template');
107 });
108
109 void pretendPodIsNotInstalled() {
110 fakeProcessManager.addCommand(
111 const FakeCommand(
112 command: <String>['which', 'pod'],
113 exitCode: 1,
114 ),
115 );
116 }
117
118 void pretendPodIsBroken() {
119 fakeProcessManager.addCommands(<FakeCommand>[
120 // it is present
121 const FakeCommand(
122 command: <String>['which', 'pod'],
123 ),
124 // but is not working
125 const FakeCommand(
126 command: <String>['pod', '--version'],
127 exitCode: 1,
128 ),
129 ]);
130 }
131
132 void pretendPodIsInstalled() {
133 fakeProcessManager.addCommands(<FakeCommand>[
134 const FakeCommand(
135 command: <String>['which', 'pod'],
136 ),
137 ]);
138 }
139
140 group('Evaluate installation', () {
141 testWithoutContext('detects not installed, if pod exec does not exist', () async {
142 pretendPodIsNotInstalled();
143 expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.notInstalled);
144 });
145
146 testWithoutContext('detects not installed, if pod is installed but version fails', () async {
147 pretendPodIsInstalled();
148 pretendPodVersionFails();
149 expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.brokenInstall);
150 });
151
152 testWithoutContext('detects installed', () async {
153 pretendPodIsInstalled();
154 pretendPodVersionIs('0.0.1');
155 expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, isNot(CocoaPodsStatus.notInstalled));
156 });
157
158 testWithoutContext('detects unknown version', () async {
159 pretendPodIsInstalled();
160 pretendPodVersionIs('Plugin loaded.\n1.5.3');
161 expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.unknownVersion);
162 });
163
164 testWithoutContext('detects below minimum version', () async {
165 pretendPodIsInstalled();
166 pretendPodVersionIs('1.9.0');
167 expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.belowMinimumVersion);
168 });
169
170 testWithoutContext('detects below recommended version', () async {
171 pretendPodIsInstalled();
172 pretendPodVersionIs('1.12.5');
173 expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.belowRecommendedVersion);
174 });
175
176 testWithoutContext('detects at recommended version', () async {
177 pretendPodIsInstalled();
178 pretendPodVersionIs('1.13.0');
179 expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended);
180 });
181
182 testWithoutContext('detects above recommended version', () async {
183 pretendPodIsInstalled();
184 pretendPodVersionIs('1.13.1');
185 expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended);
186 });
187 });
188
189 group('Setup Podfile', () {
190 testUsingContext('creates objective-c Podfile when not present', () async {
191 final FlutterProject projectUnderTest = setupProjectUnderTest();
192 await cocoaPodsUnderTest.setupPodfile(projectUnderTest.ios);
193
194 expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Objective-C iOS podfile template');
195 });
196
197 testUsingContext('creates swift Podfile if swift', () async {
198 final FlutterProject projectUnderTest = setupProjectUnderTest();
199 final FakeXcodeProjectInterpreter fakeXcodeProjectInterpreter = FakeXcodeProjectInterpreter(buildSettings: <String, String>{
200 'SWIFT_VERSION': '5.0',
201 });
202 final CocoaPods cocoaPodsUnderTest = CocoaPods(
203 fileSystem: fileSystem,
204 processManager: fakeProcessManager,
205 logger: logger,
206 platform: FakePlatform(operatingSystem: 'macos'),
207 xcodeProjectInterpreter: fakeXcodeProjectInterpreter,
208 usage: usage,
209 analytics: fakeAnalytics,
210 );
211
212 final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
213 await cocoaPodsUnderTest.setupPodfile(project.ios);
214
215 expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Swift iOS podfile template');
216 });
217
218 testUsingContext('creates macOS Podfile when not present', () async {
219 final FlutterProject projectUnderTest = setupProjectUnderTest();
220 projectUnderTest.macos.xcodeProject.createSync(recursive: true);
221 await cocoaPodsUnderTest.setupPodfile(projectUnderTest.macos);
222
223 expect(projectUnderTest.macos.podfile.readAsStringSync(), 'macOS podfile template');
224 });
225
226 testUsingContext('does not recreate Podfile when already present', () async {
227 final FlutterProject projectUnderTest = setupProjectUnderTest();
228 projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile');
229
230 final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
231 await cocoaPodsUnderTest.setupPodfile(project.ios);
232
233 expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Existing Podfile');
234 });
235
236 testUsingContext('does not create Podfile when we cannot interpret Xcode projects', () async {
237 final FlutterProject projectUnderTest = setupProjectUnderTest();
238 final CocoaPods cocoaPodsUnderTest = CocoaPods(
239 fileSystem: fileSystem,
240 processManager: fakeProcessManager,
241 logger: logger,
242 platform: FakePlatform(operatingSystem: 'macos'),
243 xcodeProjectInterpreter: FakeXcodeProjectInterpreter(isInstalled: false),
244 usage: usage,
245 analytics: fakeAnalytics,
246 );
247
248 final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
249 await cocoaPodsUnderTest.setupPodfile(project.ios);
250
251 expect(projectUnderTest.ios.podfile.existsSync(), false);
252 });
253
254 testUsingContext('includes Pod config in xcconfig files, if not present', () async {
255 final FlutterProject projectUnderTest = setupProjectUnderTest();
256 projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile');
257 projectUnderTest.ios.xcodeConfigFor('Debug')
258 ..createSync(recursive: true)
259 ..writeAsStringSync('Existing debug config');
260 projectUnderTest.ios.xcodeConfigFor('Release')
261 ..createSync(recursive: true)
262 ..writeAsStringSync('Existing release config');
263
264 final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
265 await cocoaPodsUnderTest.setupPodfile(project.ios);
266
267 final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync();
268 expect(debugContents, contains(
269 '#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"\n'));
270 expect(debugContents, contains('Existing debug config'));
271 final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync();
272 expect(releaseContents, contains(
273 '#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"\n'));
274 expect(releaseContents, contains('Existing release config'));
275 });
276
277 testUsingContext('does not include Pod config in xcconfig files, if legacy non-option include present', () async {
278 final FlutterProject projectUnderTest = setupProjectUnderTest();
279 projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile');
280
281 const String legacyDebugInclude = '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig';
282 projectUnderTest.ios.xcodeConfigFor('Debug')
283 ..createSync(recursive: true)
284 ..writeAsStringSync(legacyDebugInclude);
285 const String legacyReleaseInclude = '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig';
286 projectUnderTest.ios.xcodeConfigFor('Release')
287 ..createSync(recursive: true)
288 ..writeAsStringSync(legacyReleaseInclude);
289
290 final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
291 await cocoaPodsUnderTest.setupPodfile(project.ios);
292
293 final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync();
294 // Redundant contains check, but this documents what we're testing--that the optional
295 // #include? doesn't get written in addition to the previous style #include.
296 expect(debugContents, isNot(contains('#include?')));
297 expect(debugContents, equals(legacyDebugInclude));
298 final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync();
299 expect(releaseContents, isNot(contains('#include?')));
300 expect(releaseContents, equals(legacyReleaseInclude));
301 });
302
303 testUsingContext('does not include Pod config in xcconfig files, if flavor include present', () async {
304 final FlutterProject projectUnderTest = setupProjectUnderTest();
305 projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile');
306
307 const String flavorDebugInclude = '#include? "Pods/Target Support Files/Pods-Free App/Pods-Free App.debug free.xcconfig"';
308 projectUnderTest.ios.xcodeConfigFor('Debug')
309 ..createSync(recursive: true)
310 ..writeAsStringSync(flavorDebugInclude);
311 const String flavorReleaseInclude = '#include? "Pods/Target Support Files/Pods-Free App/Pods-Free App.release free.xcconfig"';
312 projectUnderTest.ios.xcodeConfigFor('Release')
313 ..createSync(recursive: true)
314 ..writeAsStringSync(flavorReleaseInclude);
315
316 final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
317 await cocoaPodsUnderTest.setupPodfile(project.ios);
318
319 final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync();
320 // Redundant contains check, but this documents what we're testing--that the optional
321 // #include? doesn't get written in addition to the previous style #include.
322 expect(debugContents, isNot(contains('Pods-Runner/Pods-Runner.debug')));
323 expect(debugContents, equals(flavorDebugInclude));
324 final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync();
325 expect(releaseContents, isNot(contains('Pods-Runner/Pods-Runner.release')));
326 expect(releaseContents, equals(flavorReleaseInclude));
327 });
328 });
329
330 group('Update xcconfig', () {
331 testUsingContext('includes Pod config in xcconfig files, if the user manually added Pod dependencies without using Flutter plugins', () async {
332 final FlutterProject projectUnderTest = setupProjectUnderTest();
333 final File packageConfigFile = fileSystem.file(
334 fileSystem.path.join('project', '.dart_tool', 'package_config.json'),
335 );
336 packageConfigFile.createSync(recursive: true);
337 packageConfigFile.writeAsStringSync('{"configVersion":2,"packages":[]}');
338 projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Custom Podfile');
339 projectUnderTest.ios.podfileLock..createSync()..writeAsStringSync('Podfile.lock from user executed `pod install`');
340 projectUnderTest.ios.xcodeConfigFor('Debug')
341 ..createSync(recursive: true)
342 ..writeAsStringSync('Existing debug config');
343 projectUnderTest.ios.xcodeConfigFor('Release')
344 ..createSync(recursive: true)
345 ..writeAsStringSync('Existing release config');
346
347 final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
348 await injectPlugins(project, iosPlatform: true);
349
350 final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync();
351 expect(debugContents, contains(
352 '#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"\n'));
353 expect(debugContents, contains('Existing debug config'));
354 final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync();
355 expect(releaseContents, contains(
356 '#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"\n'));
357 expect(releaseContents, contains('Existing release config'));
358 }, overrides: <Type, Generator>{
359 FileSystem: () => fileSystem,
360 ProcessManager: () => FakeProcessManager.any(),
361 });
362 });
363
364 group('Process pods', () {
365 setUp(() {
366 podsIsInHomeDir();
367 });
368
369 testUsingContext('throwsToolExit if CocoaPods is not installed', () async {
370 final FlutterProject projectUnderTest = setupProjectUnderTest();
371 pretendPodIsNotInstalled();
372 projectUnderTest.ios.podfile.createSync();
373 await expectLater(cocoaPodsUnderTest.processPods(
374 xcodeProject: projectUnderTest.ios,
375 buildMode: BuildMode.debug,
376 ), throwsToolExit(message: 'CocoaPods not installed or not in valid state'));
377 expect(fakeProcessManager, hasNoRemainingExpectations);
378 expect(fakeProcessManager, hasNoRemainingExpectations);
379 });
380
381 testUsingContext('throwsToolExit if CocoaPods install is broken', () async {
382 final FlutterProject projectUnderTest = setupProjectUnderTest();
383 pretendPodIsBroken();
384 projectUnderTest.ios.podfile.createSync();
385 await expectLater(cocoaPodsUnderTest.processPods(
386 xcodeProject: projectUnderTest.ios,
387 buildMode: BuildMode.debug,
388 ), throwsToolExit(message: 'CocoaPods not installed or not in valid state'));
389 expect(fakeProcessManager, hasNoRemainingExpectations);
390 expect(fakeProcessManager, hasNoRemainingExpectations);
391 });
392
393 testUsingContext('exits if Podfile creates the Flutter engine symlink', () async {
394 final FlutterProject projectUnderTest = setupProjectUnderTest();
395 fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
396 ..createSync()
397 ..writeAsStringSync('Existing Podfile');
398
399 final Directory symlinks = projectUnderTest.ios.symlinks
400 ..createSync(recursive: true);
401 symlinks.childLink('flutter').createSync('cache');
402
403 await expectLater(cocoaPodsUnderTest.processPods(
404 xcodeProject: projectUnderTest.ios,
405 buildMode: BuildMode.debug,
406 ), throwsToolExit(message: 'Podfile is out of date'));
407 expect(fakeProcessManager, hasNoRemainingExpectations);
408 });
409
410 testUsingContext('exits if iOS Podfile parses .flutter-plugins', () async {
411 final FlutterProject projectUnderTest = setupProjectUnderTest();
412 fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
413 ..createSync()
414 ..writeAsStringSync("plugin_pods = parse_KV_file('../.flutter-plugins')");
415
416 await expectLater(cocoaPodsUnderTest.processPods(
417 xcodeProject: projectUnderTest.ios,
418 buildMode: BuildMode.debug,
419 ), throwsToolExit(message: 'Podfile is out of date'));
420 expect(fakeProcessManager, hasNoRemainingExpectations);
421 });
422
423 testUsingContext('prints warning if macOS Podfile parses .flutter-plugins', () async {
424 final FlutterProject projectUnderTest = setupProjectUnderTest();
425 pretendPodIsInstalled();
426 pretendPodVersionIs('100.0.0');
427 fakeProcessManager.addCommands(const <FakeCommand>[
428 FakeCommand(
429 command: <String>['pod', 'install', '--verbose'],
430 ),
431 FakeCommand(
432 command: <String>['touch', 'project/macos/Podfile.lock'],
433 ),
434 ]);
435
436 projectUnderTest.macos.podfile
437 ..createSync()
438 ..writeAsStringSync("plugin_pods = parse_KV_file('../.flutter-plugins')");
439 projectUnderTest.macos.podfileLock
440 ..createSync()
441 ..writeAsStringSync('Existing lock file.');
442
443 await cocoaPodsUnderTest.processPods(
444 xcodeProject: projectUnderTest.macos,
445 buildMode: BuildMode.debug,
446 );
447
448 expect(logger.warningText, contains('Warning: Podfile is out of date'));
449 expect(logger.warningText, contains('rm macos/Podfile'));
450 expect(fakeProcessManager, hasNoRemainingExpectations);
451 });
452
453 testUsingContext('throws, if Podfile is missing.', () async {
454 final FlutterProject projectUnderTest = setupProjectUnderTest();
455 await expectLater(cocoaPodsUnderTest.processPods(
456 xcodeProject: projectUnderTest.ios,
457 buildMode: BuildMode.debug,
458 ), throwsToolExit(message: 'Podfile missing'));
459 expect(fakeProcessManager, hasNoRemainingExpectations);
460 });
461
462 testUsingContext("doesn't throw, if using Swift Package Manager and Podfile is missing.", () async {
463 final FlutterProject projectUnderTest = setupProjectUnderTest();
464 final bool didInstall = await cocoaPodsUnderTest.processPods(
465 xcodeProject: projectUnderTest.ios,
466 buildMode: BuildMode.debug,
467 );
468 expect(didInstall, isFalse);
469 expect(fakeProcessManager, hasNoRemainingExpectations);
470 }, overrides: <Type, Generator>{
471 FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true),
472 XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)),
473 });
474
475 testUsingContext('throws, if specs repo is outdated.', () async {
476 final FlutterProject projectUnderTest = setupProjectUnderTest();
477 pretendPodIsInstalled();
478 pretendPodVersionIs('100.0.0');
479 fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
480 ..createSync()
481 ..writeAsStringSync('Existing Podfile');
482
483 fakeProcessManager.addCommand(
484 const FakeCommand(
485 command: <String>['pod', 'install', '--verbose'],
486 workingDirectory: 'project/ios',
487 environment: <String, String>{
488 'COCOAPODS_DISABLE_STATS': 'true',
489 'LANG': 'en_US.UTF-8',
490 },
491 exitCode: 1,
492 // This output is the output that a real CocoaPods install would generate.
493 stdout: '''
494[!] Unable to satisfy the following requirements:
495
496- `Firebase/Auth` required by `Podfile`
497- `Firebase/Auth (= 4.0.0)` required by `Podfile.lock`
498
499None of your spec sources contain a spec satisfying the dependencies: `Firebase/Auth, Firebase/Auth (= 4.0.0)`.
500
501You have either:
502 * out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`.
503 * mistyped the name or version.
504 * not added the source repo that hosts the Podspec to your Podfile.
505
506Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by default.''',
507 ),
508 );
509
510 await expectLater(cocoaPodsUnderTest.processPods(
511 xcodeProject: projectUnderTest.ios,
512 buildMode: BuildMode.debug,
513 ), throwsToolExit());
514 expect(
515 logger.errorText,
516 contains("CocoaPods's specs repository is too out-of-date to satisfy dependencies"),
517 );
518 });
519
520 testUsingContext('throws if plugin requires higher minimum iOS version using "platform"', () async {
521 final FlutterProject projectUnderTest = setupProjectUnderTest();
522 pretendPodIsInstalled();
523 pretendPodVersionIs('100.0.0');
524 fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
525 ..createSync()
526 ..writeAsStringSync('Existing Podfile');
527 const String fakePluginName = 'some_plugin';
528 final File podspec = projectUnderTest.ios.symlinks
529 .childDirectory('plugins')
530 .childDirectory(fakePluginName)
531 .childDirectory('ios')
532 .childFile('$fakePluginName.podspec');
533 podspec.createSync(recursive: true);
534 podspec.writeAsStringSync('''
535Pod::Spec.new do |s|
536 s.name = '$fakePluginName'
537 s.version = '0.0.1'
538 s.summary = 'A plugin'
539 s.source_files = 'Classes/**/*.{h,m}'
540 s.dependency 'Flutter'
541 s.static_framework = true
542 s.platform = :ios, '15.0'
543end''');
544
545 fakeProcessManager.addCommand(
546 FakeCommand(
547 command: const ['pod', 'install', '--verbose'],
548 workingDirectory: 'project/ios',
549 environment: const {
550 'COCOAPODS_DISABLE_STATS': 'true',
551 'LANG': 'en_US.UTF-8',
552 },
553 exitCode: 1,
554 stdout: _fakeHigherMinimumIOSVersionPodInstallOutput(fakePluginName),
555 ),
556 );
557
558 await expectLater(cocoaPodsUnderTest.processPods(
559 xcodeProject: projectUnderTest.ios,
560 buildMode: BuildMode.debug,
561 ), throwsToolExit());
562 expect(
563 logger.errorText,
564 contains(
565 'The plugin "$fakePluginName" requires a higher minimum iOS '
566 'deployment version than your application is targeting.'
567 ),
568 );
569 // The error should contain specific instructions for fixing the build
570 // based on parsing the plugin's podspec.
571 expect(
572 logger.errorText,
573 contains(
574 "To build, increase your application's deployment target to at least "
575 '15.0 as described at https://flutter.dev/to/ios-deploy'
576 ),
577 );
578 });
579
580 testUsingContext('throws if plugin requires higher minimum iOS version using "deployment_target"', () async {
581 final FlutterProject projectUnderTest = setupProjectUnderTest();
582 pretendPodIsInstalled();
583 pretendPodVersionIs('100.0.0');
584 fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
585 ..createSync()
586 ..writeAsStringSync('Existing Podfile');
587 const String fakePluginName = 'some_plugin';
588 final File podspec = projectUnderTest.ios.symlinks
589 .childDirectory('plugins')
590 .childDirectory(fakePluginName)
591 .childDirectory('ios')
592 .childFile('$fakePluginName.podspec');
593 podspec.createSync(recursive: true);
594 podspec.writeAsStringSync('''
595Pod::Spec.new do |s|
596 s.name = '$fakePluginName'
597 s.version = '0.0.1'
598 s.summary = 'A plugin'
599 s.source_files = 'Classes/**/*.{h,m}'
600 s.dependency 'Flutter'
601 s.static_framework = true
602 s.ios.deployment_target = '15.0'
603end''');
604
605 fakeProcessManager.addCommand(
606 FakeCommand(
607 command: const ['pod', 'install', '--verbose'],
608 workingDirectory: 'project/ios',
609 environment: const {
610 'COCOAPODS_DISABLE_STATS': 'true',
611 'LANG': 'en_US.UTF-8',
612 },
613 exitCode: 1,
614 stdout: _fakeHigherMinimumIOSVersionPodInstallOutput(fakePluginName),
615 ),
616 );
617
618 await expectLater(cocoaPodsUnderTest.processPods(
619 xcodeProject: projectUnderTest.ios,
620 buildMode: BuildMode.debug,
621 ), throwsToolExit());
622 expect(
623 logger.errorText,
624 contains(
625 'The plugin "$fakePluginName" requires a higher minimum iOS '
626 'deployment version than your application is targeting.'
627 ),
628 );
629 // The error should contain specific instructions for fixing the build
630 // based on parsing the plugin's podspec.
631 expect(
632 logger.errorText,
633 contains(
634 "To build, increase your application's deployment target to at least "
635 '15.0 as described at https://flutter.dev/to/ios-deploy'
636 ),
637 );
638 });
639
640 testUsingContext('throws if plugin requires higher minimum iOS version with darwin layout', () async {
641 final FlutterProject projectUnderTest = setupProjectUnderTest();
642 pretendPodIsInstalled();
643 pretendPodVersionIs('100.0.0');
644 fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
645 ..createSync()
646 ..writeAsStringSync('Existing Podfile');
647 const String fakePluginName = 'some_plugin';
648 final File podspec = projectUnderTest.ios.symlinks
649 .childDirectory('plugins')
650 .childDirectory(fakePluginName)
651 .childDirectory('darwin')
652 .childFile('$fakePluginName.podspec');
653 podspec.createSync(recursive: true);
654 podspec.writeAsStringSync('''
655Pod::Spec.new do |s|
656 s.name = '$fakePluginName'
657 s.version = '0.0.1'
658 s.summary = 'A plugin'
659 s.source_files = 'Classes/**/*.{h,m}'
660 s.dependency 'Flutter'
661 s.static_framework = true
662 s.osx.deployment_target = '10.15'
663 s.ios.deployment_target = '15.0'
664end''');
665
666 fakeProcessManager.addCommand(
667 FakeCommand(
668 command: const ['pod', 'install', '--verbose'],
669 workingDirectory: 'project/ios',
670 environment: const {
671 'COCOAPODS_DISABLE_STATS': 'true',
672 'LANG': 'en_US.UTF-8',
673 },
674 exitCode: 1,
675 stdout: _fakeHigherMinimumIOSVersionPodInstallOutput(fakePluginName, subdir: 'darwin'),
676 ),
677 );
678
679 await expectLater(cocoaPodsUnderTest.processPods(
680 xcodeProject: projectUnderTest.ios,
681 buildMode: BuildMode.debug,
682 ), throwsToolExit());
683 expect(
684 logger.errorText,
685 contains(
686 'The plugin "$fakePluginName" requires a higher minimum iOS '
687 'deployment version than your application is targeting.'
688 ),
689 );
690 // The error should contain specific instructions for fixing the build
691 // based on parsing the plugin's podspec.
692 expect(
693 logger.errorText,
694 contains(
695 "To build, increase your application's deployment target to at least "
696 '15.0 as described at https://flutter.dev/to/ios-deploy'
697 ),
698 );
699 });
700
701 testUsingContext('throws if plugin requires unknown higher minimum iOS version', () async {
702 final FlutterProject projectUnderTest = setupProjectUnderTest();
703 pretendPodIsInstalled();
704 pretendPodVersionIs('100.0.0');
705 fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
706 ..createSync()
707 ..writeAsStringSync('Existing Podfile');
708 const String fakePluginName = 'some_plugin';
709 final File podspec = projectUnderTest.ios.symlinks
710 .childDirectory('plugins')
711 .childDirectory(fakePluginName)
712 .childDirectory('ios')
713 .childFile('$fakePluginName.podspec');
714 podspec.createSync(recursive: true);
715 // It's very unlikely that someone would actually ever do anything like
716 // this, but arbitrary code is possible, so test that if it's not what
717 // the error handler parsing expects, a fallback is used.
718 podspec.writeAsStringSync('''
719Pod::Spec.new do |s|
720 s.name = '$fakePluginName'
721 s.version = '0.0.1'
722 s.summary = 'A plugin'
723 s.source_files = 'Classes/**/*.{h,m}'
724 s.dependency 'Flutter'
725 s.static_framework = true
726 version_var = '15.0'
727 s.platform = :ios, version_var
728end''');
729
730 fakeProcessManager.addCommand(
731 FakeCommand(
732 command: const ['pod', 'install', '--verbose'],
733 workingDirectory: 'project/ios',
734 environment: const {
735 'COCOAPODS_DISABLE_STATS': 'true',
736 'LANG': 'en_US.UTF-8',
737 },
738 exitCode: 1,
739 stdout: _fakeHigherMinimumIOSVersionPodInstallOutput(fakePluginName),
740 ),
741 );
742
743 await expectLater(cocoaPodsUnderTest.processPods(
744 xcodeProject: projectUnderTest.ios,
745 buildMode: BuildMode.debug,
746 ), throwsToolExit());
747 expect(
748 logger.errorText,
749 contains(
750 'The plugin "$fakePluginName" requires a higher minimum iOS '
751 'deployment version than your application is targeting.'
752 ),
753 );
754 // The error should contain non-specific instructions for fixing the build
755 // and note that the minimum version could not be determined.
756 expect(
757 logger.errorText,
758 contains(
759 "To build, increase your application's deployment target as "
760 'described at https://flutter.dev/to/ios-deploy',
761 ),
762 );
763 expect(
764 logger.errorText,
765 contains(
766 'The minimum required version for "$fakePluginName" could not be '
767 'determined',
768 ),
769 );
770 });
771
772 testUsingContext('throws if plugin has a dependency that requires a higher minimum iOS version', () async {
773 final FlutterProject projectUnderTest = setupProjectUnderTest();
774 pretendPodIsInstalled();
775 pretendPodVersionIs('100.0.0');
776 fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
777 ..createSync()
778 ..writeAsStringSync('Existing Podfile');
779
780 fakeProcessManager.addCommand(
781 const FakeCommand(
782 command: ['pod', 'install', '--verbose'],
783 workingDirectory: 'project/ios',
784 environment: {
785 'COCOAPODS_DISABLE_STATS': 'true',
786 'LANG': 'en_US.UTF-8',
787 },
788 exitCode: 1,
789 // This is the (very slightly abridged) output from updating the
790 // minimum version of the GoogleMaps dependency in
791 // google_maps_flutter_ios without updating the minimum iOS version to
792 // match, as an example of a misconfigured plugin.
793 stdout: '''
794Analyzing dependencies
795
796Inspecting targets to integrate
797 Using `ARCHS` setting to build architectures of target `Pods-Runner`: (``)
798 Using `ARCHS` setting to build architectures of target `Pods-RunnerTests`: (``)
799
800Fetching external sources
801-> Fetching podspec for `Flutter` from `Flutter`
802-> Fetching podspec for `google_maps_flutter_ios` from `.symlinks/plugins/google_maps_flutter_ios/ios`
803
804Resolving dependencies of `Podfile`
805 CDN: trunk Relative path: CocoaPods-version.yml exists! Returning local because checking is only performed in repo update
806 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
807[!] CocoaPods could not find compatible versions for pod "GoogleMaps":
808 In Podfile:
809 google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`) was resolved to 0.0.1, which depends on
810 GoogleMaps (~> 8.0)
811
812Specs satisfying the `GoogleMaps (~> 8.0)` dependency were found, but they required a higher minimum deployment target.''',
813 ),
814 );
815
816 await expectLater(cocoaPodsUnderTest.processPods(
817 xcodeProject: projectUnderTest.ios,
818 buildMode: BuildMode.debug,
819 ), throwsToolExit());
820 expect(
821 logger.errorText,
822 contains(
823 'The pod "GoogleMaps" required by the plugin "google_maps_flutter_ios" '
824 "requires a higher minimum iOS deployment version than the plugin's "
825 'reported minimum version.'
826 ),
827 );
828 // The error should tell the user to contact the plugin author, as this
829 // case is hard for us to give exact advice on, and should only be
830 // possible if there's a mistake in the plugin's podspec.
831 expect(
832 logger.errorText,
833 contains(
834 'To build, remove the plugin "google_maps_flutter_ios", or contact '
835 "the plugin's developers for assistance.",
836 ),
837 );
838 });
839
840 testUsingContext('throws if plugin has a dependency that requires a higher minimum macOS version', () async {
841 final FlutterProject projectUnderTest = setupProjectUnderTest();
842 pretendPodIsInstalled();
843 pretendPodVersionIs('100.0.0');
844 fileSystem.file(fileSystem.path.join('project', 'macos', 'Podfile'))
845 ..createSync()
846 ..writeAsStringSync('Existing Podfile');
847
848 fakeProcessManager.addCommand(
849 const FakeCommand(
850 command: ['pod', 'install', '--verbose'],
851 workingDirectory: 'project/macos',
852 environment: {
853 'COCOAPODS_DISABLE_STATS': 'true',
854 'LANG': 'en_US.UTF-8',
855 },
856 exitCode: 1,
857 // This is the (very slightly abridged) output from updating the
858 // minimum version of the GoogleMaps dependency in
859 // google_maps_flutter_ios without updating the minimum iOS version to
860 // match, as an example of a misconfigured plugin, but with the paths
861 // modified to simulate a macOS plugin.
862 stdout: '''
863Analyzing dependencies
864
865Inspecting targets to integrate
866 Using `ARCHS` setting to build architectures of target `Pods-Runner`: (``)
867 Using `ARCHS` setting to build architectures of target `Pods-RunnerTests`: (``)
868
869Fetching external sources
870-> Fetching podspec for `Flutter` from `Flutter`
871-> Fetching podspec for `google_maps_flutter_ios` from `.symlinks/plugins/google_maps_flutter_ios/macos`
872
873Resolving dependencies of `Podfile`
874 CDN: trunk Relative path: CocoaPods-version.yml exists! Returning local because checking is only performed in repo update
875 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
876[!] CocoaPods could not find compatible versions for pod "GoogleMaps":
877 In Podfile:
878 google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/macos`) was resolved to 0.0.1, which depends on
879 GoogleMaps (~> 8.0)
880
881Specs satisfying the `GoogleMaps (~> 8.0)` dependency were found, but they required a higher minimum deployment target.''',
882 ),
883 );
884
885 await expectLater(cocoaPodsUnderTest.processPods(
886 xcodeProject: projectUnderTest.macos,
887 buildMode: BuildMode.debug,
888 ), throwsToolExit());
889 expect(
890 logger.errorText,
891 contains(
892 'The pod "GoogleMaps" required by the plugin "google_maps_flutter_ios" '
893 "requires a higher minimum macOS deployment version than the plugin's "
894 'reported minimum version.'
895 ),
896 );
897 // The error should tell the user to contact the plugin author, as this
898 // case is hard for us to give exact advice on, and should only be
899 // possible if there's a mistake in the plugin's podspec.
900 expect(
901 logger.errorText,
902 contains(
903 'To build, remove the plugin "google_maps_flutter_ios", or contact '
904 "the plugin's developers for assistance.",
905 ),
906 );
907 });
908
909 testUsingContext('throws if plugin requires higher minimum macOS version using "platform"', () async {
910 final FlutterProject projectUnderTest = setupProjectUnderTest();
911 pretendPodIsInstalled();
912 pretendPodVersionIs('100.0.0');
913 fileSystem.file(fileSystem.path.join('project', 'macos', 'Podfile'))
914 ..createSync()
915 ..writeAsStringSync('Existing Podfile');
916 const String fakePluginName = 'some_plugin';
917 final File podspec = projectUnderTest.macos.ephemeralDirectory
918 .childDirectory('.symlinks')
919 .childDirectory('plugins')
920 .childDirectory(fakePluginName)
921 .childDirectory('macos')
922 .childFile('$fakePluginName.podspec');
923 podspec.createSync(recursive: true);
924 podspec.writeAsStringSync('''
925Pod::Spec.new do |spec|
926 spec.name = '$fakePluginName'
927 spec.version = '0.0.1'
928 spec.summary = 'A plugin'
929 spec.source_files = 'Classes/**/*.swift'
930 spec.dependency 'FlutterMacOS'
931 spec.static_framework = true
932 spec.platform = :osx, "12.7"
933end''');
934
935 fakeProcessManager.addCommand(
936 FakeCommand(
937 command: const ['pod', 'install', '--verbose'],
938 workingDirectory: 'project/macos',
939 environment: const {
940 'COCOAPODS_DISABLE_STATS': 'true',
941 'LANG': 'en_US.UTF-8',
942 },
943 exitCode: 1,
944 stdout: _fakeHigherMinimumMacOSVersionPodInstallOutput(fakePluginName),
945 ),
946 );
947
948 await expectLater(cocoaPodsUnderTest.processPods(
949 xcodeProject: projectUnderTest.macos,
950 buildMode: BuildMode.debug,
951 ), throwsToolExit());
952 expect(
953 logger.errorText,
954 contains(
955 'The plugin "$fakePluginName" requires a higher minimum macOS '
956 'deployment version than your application is targeting.'
957 ),
958 );
959 // The error should contain specific instructions for fixing the build
960 // based on parsing the plugin's podspec.
961 expect(
962 logger.errorText,
963 contains(
964 "To build, increase your application's deployment target to at least "
965 '12.7 as described at https://flutter.dev/to/macos-deploy'
966 ),
967 );
968 });
969
970 testUsingContext('throws if plugin requires higher minimum macOS version using "deployment_target"', () async {
971 final FlutterProject projectUnderTest = setupProjectUnderTest();
972 pretendPodIsInstalled();
973 pretendPodVersionIs('100.0.0');
974 fileSystem.file(fileSystem.path.join('project', 'macos', 'Podfile'))
975 ..createSync()
976 ..writeAsStringSync('Existing Podfile');
977 const String fakePluginName = 'some_plugin';
978 final File podspec = projectUnderTest.macos.ephemeralDirectory
979 .childDirectory('.symlinks')
980 .childDirectory('plugins')
981 .childDirectory(fakePluginName)
982 .childDirectory('macos')
983 .childFile('$fakePluginName.podspec');
984 podspec.createSync(recursive: true);
985 podspec.writeAsStringSync('''
986Pod::Spec.new do |spec|
987 spec.name = '$fakePluginName'
988 spec.version = '0.0.1'
989 spec.summary = 'A plugin'
990 spec.source_files = 'Classes/**/*.{h,m}'
991 spec.dependency 'Flutter'
992 spec.static_framework = true
993 spec.osx.deployment_target = '12.7'
994end''');
995
996 fakeProcessManager.addCommand(
997 FakeCommand(
998 command: const ['pod', 'install', '--verbose'],
999 workingDirectory: 'project/macos',
1000 environment: const {
1001 'COCOAPODS_DISABLE_STATS': 'true',
1002 'LANG': 'en_US.UTF-8',
1003 },
1004 exitCode: 1,
1005 stdout: _fakeHigherMinimumMacOSVersionPodInstallOutput(fakePluginName),
1006 ),
1007 );
1008
1009 await expectLater(cocoaPodsUnderTest.processPods(
1010 xcodeProject: projectUnderTest.macos,
1011 buildMode: BuildMode.debug,
1012 ), throwsToolExit());
1013 expect(
1014 logger.errorText,
1015 contains(
1016 'The plugin "$fakePluginName" requires a higher minimum macOS '
1017 'deployment version than your application is targeting.'
1018 ),
1019 );
1020 // The error should contain specific instructions for fixing the build
1021 // based on parsing the plugin's podspec.
1022 expect(
1023 logger.errorText,
1024 contains(
1025 "To build, increase your application's deployment target to at least "
1026 '12.7 as described at https://flutter.dev/to/macos-deploy'
1027 ),
1028 );
1029 });
1030
1031 final Map possibleErrors = {
1032 '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',
1033 '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",
1034 'bus error': '/Library/Ruby/Gems/2.6.0/gems/ffi-1.15.5/lib/ffi/library.rb:275: [BUG] Bus Error at 0x000000010072c000',
1035 };
1036 possibleErrors.forEach((String errorName, String cocoaPodsError) {
1037 void testToolExitsWithCocoapodsMessage(_StdioStream outputStream) {
1038 final String streamName = outputStream == _StdioStream.stdout ? 'stdout' : 'stderr';
1039 testUsingContext('ffi $errorName failure to $streamName on ARM macOS prompts gem install', () async {
1040 final FlutterProject projectUnderTest = setupProjectUnderTest();
1041 pretendPodIsInstalled();
1042 pretendPodVersionIs('100.0.0');
1043 fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
1044 ..createSync()
1045 ..writeAsStringSync('Existing Podfile');
1046
1047 fakeProcessManager.addCommands([
1048 FakeCommand(
1049 command: const ['pod', 'install', '--verbose'],
1050 workingDirectory: 'project/ios',
1051 environment: const {
1052 'COCOAPODS_DISABLE_STATS': 'true',
1053 'LANG': 'en_US.UTF-8',
1054 },
1055 exitCode: 1,
1056 stdout: outputStream == _StdioStream.stdout ? cocoaPodsError : '',
1057 stderr: outputStream == _StdioStream.stderr ? cocoaPodsError : '',
1058 ),
1059 const FakeCommand(
1060 command: ['which', 'sysctl'],
1061 ),
1062 const FakeCommand(
1063 command: ['sysctl', 'hw.optional.arm64'],
1064 stdout: 'hw.optional.arm64: 1',
1065 ),
1066 ]);
1067
1068 await expectToolExitLater(
1069 cocoaPodsUnderTest.processPods(
1070 xcodeProject: projectUnderTest.ios,
1071 buildMode: BuildMode.debug,
1072 ),
1073 equals('Error running pod install'),
1074 );
1075 expect(
1076 logger.errorText,
1077 contains('set up CocoaPods for ARM macOS'),
1078 );
1079 expect(
1080 logger.errorText,
1081 contains('enable-libffi-alloc'),
1082 );
1083 expect(usage.events, contains(const TestUsageEvent('pod-install-failure', 'arm-ffi')));
1084 expect(fakeAnalytics.sentEvents, contains(Event.appleUsageEvent(workflow: 'pod-install-failure', parameter: 'arm-ffi')));
1085 });
1086 }
1087 testToolExitsWithCocoapodsMessage(_StdioStream.stdout);
1088 testToolExitsWithCocoapodsMessage(_StdioStream.stderr);
1089 });
1090
1091 testUsingContext('ffi failure on x86 macOS does not prompt gem install', () async {
1092 final FlutterProject projectUnderTest = setupProjectUnderTest();
1093 pretendPodIsInstalled();
1094 pretendPodVersionIs('100.0.0');
1095 fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
1096 ..createSync()
1097 ..writeAsStringSync('Existing Podfile');
1098
1099 fakeProcessManager.addCommands([
1100 const FakeCommand(
1101 command: ['pod', 'install', '--verbose'],
1102 workingDirectory: 'project/ios',
1103 environment: {
1104 'COCOAPODS_DISABLE_STATS': 'true',
1105 'LANG': 'en_US.UTF-8',
1106 },
1107 exitCode: 1,
1108 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',
1109 ),
1110 const FakeCommand(
1111 command: ['which', 'sysctl'],
1112 ),
1113 const FakeCommand(
1114 command: ['sysctl', 'hw.optional.arm64'],
1115 exitCode: 1,
1116 ),
1117 ]);
1118
1119 // Capture Usage.test() events.
1120 final StringBuffer buffer =
1121 await capturedConsolePrint(() => expectToolExitLater(
1122 cocoaPodsUnderTest.processPods(
1123 xcodeProject: projectUnderTest.ios,
1124 buildMode: BuildMode.debug,
1125 ),
1126 equals('Error running pod install'),
1127 ));
1128 expect(
1129 logger.errorText,
1130 isNot(contains('ARM macOS')),
1131 );
1132 expect(buffer.isEmpty, true);
1133 });
1134
1135 testUsingContext('run pod install, if Podfile.lock is missing', () async {
1136 final FlutterProject projectUnderTest = setupProjectUnderTest();
1137 pretendPodIsInstalled();
1138 pretendPodVersionIs('100.0.0');
1139 projectUnderTest.ios.podfile
1140 ..createSync()
1141 ..writeAsStringSync('Existing Podfile');
1142 projectUnderTest.ios.podManifestLock
1143 ..createSync(recursive: true)
1144 ..writeAsStringSync('Existing lock file.');
1145
1146 fakeProcessManager.addCommands(const [
1147 FakeCommand(
1148 command: ['pod', 'install', '--verbose'],
1149 workingDirectory: 'project/ios',
1150 environment: {'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
1151 ),
1152 ]);
1153 final bool didInstall = await cocoaPodsUnderTest.processPods(
1154 xcodeProject: projectUnderTest.ios,
1155 buildMode: BuildMode.debug,
1156 dependenciesChanged: false,
1157 );
1158 expect(didInstall, isTrue);
1159 expect(fakeProcessManager, hasNoRemainingExpectations);
1160 });
1161
1162 testUsingContext('runs iOS pod install, if Manifest.lock is missing', () async {
1163 final FlutterProject projectUnderTest = setupProjectUnderTest();
1164 pretendPodIsInstalled();
1165 pretendPodVersionIs('100.0.0');
1166 projectUnderTest.ios.podfile
1167 ..createSync()
1168 ..writeAsStringSync('Existing Podfile');
1169 projectUnderTest.ios.podfileLock
1170 ..createSync()
1171 ..writeAsStringSync('Existing lock file.');
1172 fakeProcessManager.addCommands(const [
1173 FakeCommand(
1174 command: ['pod', 'install', '--verbose'],
1175 workingDirectory: 'project/ios',
1176 environment: {'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
1177 ),
1178 FakeCommand(
1179 command: ['touch', 'project/ios/Podfile.lock'],
1180 ),
1181 ]);
1182 final bool didInstall = await cocoaPodsUnderTest.processPods(
1183 xcodeProject: projectUnderTest.ios,
1184 buildMode: BuildMode.debug,
1185 dependenciesChanged: false,
1186 );
1187 expect(didInstall, isTrue);
1188 expect(fakeProcessManager, hasNoRemainingExpectations);
1189 });
1190
1191 testUsingContext('runs macOS pod install, if Manifest.lock is missing', () async {
1192 final FlutterProject projectUnderTest = setupProjectUnderTest();
1193 pretendPodIsInstalled();
1194 pretendPodVersionIs('100.0.0');
1195 projectUnderTest.macos.podfile
1196 ..createSync()
1197 ..writeAsStringSync('Existing Podfile');
1198 projectUnderTest.macos.podfileLock
1199 ..createSync()
1200 ..writeAsStringSync('Existing lock file.');
1201 fakeProcessManager.addCommands(const [
1202 FakeCommand(
1203 command: ['pod', 'install', '--verbose'],
1204 workingDirectory: 'project/macos',
1205 environment: {'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
1206 ),
1207 FakeCommand(
1208 command: ['touch', 'project/macos/Podfile.lock'],
1209 ),
1210 ]);
1211 final bool didInstall = await cocoaPodsUnderTest.processPods(
1212 xcodeProject: projectUnderTest.macos,
1213 buildMode: BuildMode.debug,
1214 dependenciesChanged: false,
1215 );
1216 expect(didInstall, isTrue);
1217 expect(fakeProcessManager, hasNoRemainingExpectations);
1218 });
1219
1220 testUsingContext('runs pod install, if Manifest.lock different from Podspec.lock', () async {
1221 final FlutterProject projectUnderTest = setupProjectUnderTest();
1222 pretendPodIsInstalled();
1223 pretendPodVersionIs('100.0.0');
1224 projectUnderTest.ios.podfile
1225 ..createSync()
1226 ..writeAsStringSync('Existing Podfile');
1227 projectUnderTest.ios.podfileLock
1228 ..createSync()
1229 ..writeAsStringSync('Existing lock file.');
1230 projectUnderTest.ios.podManifestLock
1231 ..createSync(recursive: true)
1232 ..writeAsStringSync('Different lock file.');
1233 fakeProcessManager.addCommands(const [
1234 FakeCommand(
1235 command: ['pod', 'install', '--verbose'],
1236 workingDirectory: 'project/ios',
1237 environment: {'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
1238 ),
1239 FakeCommand(
1240 command: ['touch', 'project/ios/Podfile.lock'],
1241 ),
1242 ]);
1243 final bool didInstall = await cocoaPodsUnderTest.processPods(
1244 xcodeProject: projectUnderTest.ios,
1245 buildMode: BuildMode.debug,
1246 dependenciesChanged: false,
1247 );
1248 expect(didInstall, isTrue);
1249 expect(fakeProcessManager, hasNoRemainingExpectations);
1250 });
1251
1252 testUsingContext('runs pod install, if flutter framework changed', () async {
1253 final FlutterProject projectUnderTest = setupProjectUnderTest();
1254 pretendPodIsInstalled();
1255 pretendPodVersionIs('100.0.0');
1256 projectUnderTest.ios.podfile
1257 ..createSync()
1258 ..writeAsStringSync('Existing Podfile');
1259 projectUnderTest.ios.podfileLock
1260 ..createSync()
1261 ..writeAsStringSync('Existing lock file.');
1262 projectUnderTest.ios.podManifestLock
1263 ..createSync(recursive: true)
1264 ..writeAsStringSync('Existing lock file.');
1265 fakeProcessManager.addCommands(const [
1266 FakeCommand(
1267 command: ['pod', 'install', '--verbose'],
1268 workingDirectory: 'project/ios',
1269 environment: {'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
1270 ),
1271 FakeCommand(
1272 command: ['touch', 'project/ios/Podfile.lock'],
1273 ),
1274 ]);
1275 final bool didInstall = await cocoaPodsUnderTest.processPods(
1276 xcodeProject: projectUnderTest.ios,
1277 buildMode: BuildMode.debug,
1278 );
1279 expect(didInstall, isTrue);
1280 expect(fakeProcessManager, hasNoRemainingExpectations);
1281 expect(logger.traceText, contains('CocoaPods Pods-Runner-frameworks.sh script not found'));
1282 });
1283
1284 testUsingContext('runs CocoaPods Pod runner script migrator', () async {
1285 final FlutterProject projectUnderTest = setupProjectUnderTest();
1286 pretendPodIsInstalled();
1287 pretendPodVersionIs('100.0.0');
1288 projectUnderTest.ios.podfile
1289 ..createSync()
1290 ..writeAsStringSync('Existing Podfile');
1291 projectUnderTest.ios.podfileLock
1292 ..createSync()
1293 ..writeAsStringSync('Existing lock file.');
1294 projectUnderTest.ios.podManifestLock
1295 ..createSync(recursive: true)
1296 ..writeAsStringSync('Existing lock file.');
1297 projectUnderTest.ios.podRunnerFrameworksScript
1298 ..createSync(recursive: true)
1299 ..writeAsStringSync(r'source="$(readlink "${source}")"');
1300
1301 fakeProcessManager.addCommands(const [
1302 FakeCommand(
1303 command: ['pod', 'install', '--verbose'],
1304 workingDirectory: 'project/ios',
1305 environment: {'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
1306 ),
1307 FakeCommand(
1308 command: ['touch', 'project/ios/Podfile.lock'],
1309 ),
1310 ]);
1311
1312 final CocoaPods cocoaPodsUnderTestXcode143 = CocoaPods(
1313 fileSystem: fileSystem,
1314 processManager: fakeProcessManager,
1315 logger: logger,
1316 platform: FakePlatform(operatingSystem: 'macos'),
1317 xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: fakeProcessManager, version: Version(14, 3, 0)),
1318 usage: usage,
1319 analytics: fakeAnalytics,
1320 );
1321
1322 final bool didInstall = await cocoaPodsUnderTestXcode143.processPods(
1323 xcodeProject: projectUnderTest.ios,
1324 buildMode: BuildMode.debug,
1325 );
1326 expect(didInstall, isTrue);
1327 expect(fakeProcessManager, hasNoRemainingExpectations);
1328 // Now has readlink -f flag.
1329 expect(projectUnderTest.ios.podRunnerFrameworksScript.readAsStringSync(), contains(r'source="$(readlink -f "${source}")"'));
1330 expect(logger.statusText, contains('Upgrading Pods-Runner-frameworks.sh'));
1331 });
1332
1333 testUsingContext('runs pod install, if Podfile.lock is older than Podfile', () async {
1334 final FlutterProject projectUnderTest = setupProjectUnderTest();
1335 pretendPodIsInstalled();
1336 pretendPodVersionIs('100.0.0');
1337 projectUnderTest.ios.podfile
1338 ..createSync()
1339 ..writeAsStringSync('Existing Podfile');
1340 projectUnderTest.ios.podfileLock
1341 ..createSync()
1342 ..writeAsStringSync('Existing lock file.');
1343 projectUnderTest.ios.podManifestLock
1344 ..createSync(recursive: true)
1345 ..writeAsStringSync('Existing lock file.');
1346 await Future.delayed(const Duration(milliseconds: 10));
1347 projectUnderTest.ios.podfile
1348 .writeAsStringSync('Updated Podfile');
1349 fakeProcessManager.addCommands(const [
1350 FakeCommand(
1351 command: ['pod', 'install', '--verbose'],
1352 workingDirectory: 'project/ios',
1353 environment: {'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
1354 ),
1355 FakeCommand(
1356 command: ['touch', 'project/ios/Podfile.lock'],
1357 ),
1358 ]);
1359 await cocoaPodsUnderTest.processPods(
1360 xcodeProject: projectUnderTest.ios,
1361 buildMode: BuildMode.debug,
1362 dependenciesChanged: false,
1363 );
1364 expect(fakeProcessManager, hasNoRemainingExpectations);
1365 });
1366
1367 testUsingContext('skips pod install, if nothing changed', () async {
1368 final FlutterProject projectUnderTest = setupProjectUnderTest();
1369 projectUnderTest.ios.podfile
1370 ..createSync()
1371 ..writeAsStringSync('Existing Podfile');
1372 projectUnderTest.ios.podfileLock
1373 ..createSync()
1374 ..writeAsStringSync('Existing lock file.');
1375 projectUnderTest.ios.podManifestLock
1376 ..createSync(recursive: true)
1377 ..writeAsStringSync('Existing lock file.');
1378 final bool didInstall = await cocoaPodsUnderTest.processPods(
1379 xcodeProject: projectUnderTest.ios,
1380 buildMode: BuildMode.debug,
1381 dependenciesChanged: false,
1382 );
1383 expect(didInstall, isFalse);
1384 expect(fakeProcessManager, hasNoRemainingExpectations);
1385 });
1386
1387 testUsingContext('a failed pod install deletes Pods/Manifest.lock', () async {
1388 final FlutterProject projectUnderTest = setupProjectUnderTest();
1389 pretendPodIsInstalled();
1390 pretendPodVersionIs('100.0.0');
1391 projectUnderTest.ios.podfile
1392 ..createSync()
1393 ..writeAsStringSync('Existing Podfile');
1394 projectUnderTest.ios.podfileLock
1395 ..createSync()
1396 ..writeAsStringSync('Existing lock file.');
1397 projectUnderTest.ios.podManifestLock
1398 ..createSync(recursive: true)
1399 ..writeAsStringSync('Existing lock file.');
1400 fakeProcessManager.addCommand(
1401 const FakeCommand(
1402 command: ['pod', 'install', '--verbose'],
1403 workingDirectory: 'project/ios',
1404 environment: {'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
1405 exitCode: 1,
1406 ),
1407 );
1408
1409 await expectLater(cocoaPodsUnderTest.processPods(
1410 xcodeProject: projectUnderTest.ios,
1411 buildMode: BuildMode.debug,
1412 ), throwsToolExit(message: 'Error running pod install'));
1413 expect(projectUnderTest.ios.podManifestLock.existsSync(), isFalse);
1414 });
1415 });
1416}
1417
1418String _fakeHigherMinimumIOSVersionPodInstallOutput(String fakePluginName, {String subdir = 'ios'}) {
1419 return '''
1420Preparing
1421
1422Analyzing dependencies
1423
1424Inspecting targets to integrate
1425 Using `ARCHS` setting to build architectures of target `Pods-Runner`: (``)
1426 Using `ARCHS` setting to build architectures of target `Pods-RunnerTests`: (``)
1427
1428Fetching external sources
1429-> Fetching podspec for `Flutter` from `Flutter`
1430-> Fetching podspec for `$fakePluginName` from `.symlinks/plugins/$fakePluginName/$subdir`
1431-> Fetching podspec for `another_plugin` from `.symlinks/plugins/another_plugin/ios`
1432
1433Resolving dependencies of `Podfile`
1434 CDN: trunk Relative path: CocoaPods-version.yml exists! Returning local because checking is only performed in repo update
1435[!] CocoaPods could not find compatible versions for pod "$fakePluginName":
1436 In Podfile:
1437 $fakePluginName (from `.symlinks/plugins/$fakePluginName/$subdir`)
1438
1439Specs satisfying the `$fakePluginName (from `.symlinks/plugins/$fakePluginName/subdir`)` dependency were found, but they required a higher minimum deployment target.''';
1440}
1441
1442String _fakeHigherMinimumMacOSVersionPodInstallOutput(String fakePluginName, {String subdir = 'macos'}) {
1443 return '''
1444Preparing
1445
1446Analyzing dependencies
1447
1448Inspecting targets to integrate
1449 Using `ARCHS` setting to build architectures of target `Pods-Runner`: (``)
1450 Using `ARCHS` setting to build architectures of target `Pods-RunnerTests`: (``)
1451
1452Fetching external sources
1453-> Fetching podspec for `FlutterMacOS` from `Flutter/ephemeral`
1454-> Fetching podspec for `$fakePluginName` from `Flutter/ephemeral/.symlinks/plugins/$fakePluginName/$subdir`
1455-> Fetching podspec for `another_plugin` from `Flutter/ephemeral/.symlinks/plugins/another_plugin/macos`
1456
1457Resolving dependencies of `Podfile`
1458 CDN: trunk Relative path: CocoaPods-version.yml exists! Returning local because checking is only performed in repo update
1459[!] CocoaPods could not find compatible versions for pod "$fakePluginName":
1460 In Podfile:
1461 $fakePluginName (from `Flutter/ephemeral/.symlinks/plugins/$fakePluginName/$subdir`)
1462
1463Specs satisfying the `$fakePluginName (from `Flutter/ephemeral/.symlinks/plugins/$fakePluginName/$subdir`)` dependency were found, but they required a higher minimum deployment target.''';
1464}
1465
1466class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
1467 FakeXcodeProjectInterpreter({
1468 this.isInstalled = true,
1469 this.buildSettings = const {},
1470 this.version,
1471 });
1472
1473 @override
1474 final bool isInstalled;
1475
1476 @override
1477 Future> getBuildSettings(
1478 String projectPath, {
1479 XcodeProjectBuildContext? buildContext,
1480 Duration timeout = const Duration(minutes: 1),
1481 }) async => buildSettings;
1482
1483 final Map buildSettings;
1484
1485 @override
1486 Version? version;
1487}
1488

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com