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