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 | |
5 | import 'package:file/file.dart' ; |
6 | import 'package:file/memory.dart' ; |
7 | import 'package:flutter_tools/src/base/logger.dart'; |
8 | import 'package:flutter_tools/src/base/platform.dart'; |
9 | import 'package:flutter_tools/src/base/version.dart'; |
10 | import 'package:flutter_tools/src/build_info.dart'; |
11 | import 'package:flutter_tools/src/cache.dart'; |
12 | import 'package:flutter_tools/src/features.dart'; |
13 | import 'package:flutter_tools/src/flutter_plugins.dart'; |
14 | import 'package:flutter_tools/src/ios/xcodeproj.dart'; |
15 | import 'package:flutter_tools/src/macos/cocoapods.dart'; |
16 | import 'package:flutter_tools/src/project.dart'; |
17 | import 'package:flutter_tools/src/reporting/reporting.dart'; |
18 | import 'package:test/fake.dart' ; |
19 | import 'package:unified_analytics/unified_analytics.dart' ; |
20 | |
21 | import '../../src/common.dart'; |
22 | import '../../src/context.dart'; |
23 | import '../../src/fake_process_manager.dart'; |
24 | import '../../src/fakes.dart'; |
25 | |
26 | enum _StdioStream { |
27 | stdout, |
28 | stderr, |
29 | } |
30 | |
31 | void 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 | |
499 | None of your spec sources contain a spec satisfying the dependencies: `Firebase/Auth, Firebase/Auth (= 4.0.0)`. |
500 | |
501 | You 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 | |
506 | Note: 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(''' |
535 | Pod::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' |
543 | end'''); |
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(''' |
595 | Pod::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' |
603 | end'''); |
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(''' |
655 | Pod::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' |
664 | end'''); |
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(''' |
719 | Pod::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 |
728 | end'''); |
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: ''' |
794 | Analyzing dependencies |
795 | |
796 | Inspecting 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 | |
800 | Fetching 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 | |
804 | Resolving 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 | |
812 | Specs 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: ''' |
863 | Analyzing dependencies |
864 | |
865 | Inspecting 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 | |
869 | Fetching 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 | |
873 | Resolving 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 | |
881 | Specs 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(''' |
925 | Pod::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" |
933 | end'''); |
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(''' |
986 | Pod::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' |
994 | end'''); |
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 | |
1418 | String _fakeHigherMinimumIOSVersionPodInstallOutput(String fakePluginName, {String subdir = 'ios'}) { |
1419 | return ''' |
1420 | Preparing |
1421 | |
1422 | Analyzing dependencies |
1423 | |
1424 | Inspecting 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 | |
1428 | Fetching 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 | |
1433 | Resolving 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 | |
1439 | Specs satisfying the `$fakePluginName (from `.symlinks/plugins/$fakePluginName/subdir`)` dependency were found, but they required a higher minimum deployment target.'''; |
1440 | } |
1441 | |
1442 | String _fakeHigherMinimumMacOSVersionPodInstallOutput(String fakePluginName, {String subdir = 'macos'}) { |
1443 | return ''' |
1444 | Preparing |
1445 | |
1446 | Analyzing dependencies |
1447 | |
1448 | Inspecting 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 | |
1452 | Fetching 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 | |
1457 | Resolving 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 | |
1463 | Specs satisfying the `$fakePluginName (from `Flutter/ephemeral/.symlinks/plugins/$fakePluginName/$subdir`)` dependency were found, but they required a higher minimum deployment target.'''; |
1464 | } |
1465 | |
1466 | class 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 |
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 | |