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 'dart:convert';
6
7import 'package:flutter_tools/src/base/file_system.dart';
8import 'package:flutter_tools/src/base/io.dart';
9
10import '../src/common.dart';
11import 'test_utils.dart';
12
13class SwiftPackageManagerUtils {
14 static Future<void> enableSwiftPackageManager(String flutterBin, String workingDirectory) async {
15 final ProcessResult result = await processManager.run(<String>[
16 flutterBin,
17 ...getLocalEngineArguments(),
18 'config',
19 '--enable-swift-package-manager',
20 '-v',
21 ], workingDirectory: workingDirectory);
22 expect(
23 result.exitCode,
24 0,
25 reason:
26 'Failed to enable Swift Package Manager: \n'
27 'stdout: \n${result.stdout}\n'
28 'stderr: \n${result.stderr}\n',
29 );
30 }
31
32 static Future<void> disableSwiftPackageManager(String flutterBin, String workingDirectory) async {
33 final ProcessResult result = await processManager.run(<String>[
34 flutterBin,
35 ...getLocalEngineArguments(),
36 'config',
37 '--no-enable-swift-package-manager',
38 '-v',
39 ], workingDirectory: workingDirectory);
40 expect(
41 result.exitCode,
42 0,
43 reason:
44 'Failed to disable Swift Package Manager: \n'
45 'stdout: \n${result.stdout}\n'
46 'stderr: \n${result.stderr}\n',
47 );
48 }
49
50 static Future<String> createApp(
51 String flutterBin,
52 String workingDirectory, {
53 required String platform,
54 required List<String> options,
55 bool usesSwiftPackageManager = false,
56 }) async {
57 final appTemplateType = usesSwiftPackageManager ? 'spm' : 'default';
58
59 final appName = '${platform}_${appTemplateType}_app';
60 final ProcessResult result = await processManager.run(<String>[
61 flutterBin,
62 ...getLocalEngineArguments(),
63 'create',
64 '--org',
65 'io.flutter.devicelab',
66 ...options,
67 appName,
68 ], workingDirectory: workingDirectory);
69
70 expect(
71 result.exitCode,
72 0,
73 reason:
74 'Failed to create app: \n'
75 'stdout: \n${result.stdout}\n'
76 'stderr: \n${result.stderr}\n',
77 );
78
79 return fileSystem.path.join(workingDirectory, appName);
80 }
81
82 static Future<void> buildApp(
83 String flutterBin,
84 String workingDirectory, {
85 required List<String> options,
86 List<Pattern>? expectedLines,
87 List<String>? unexpectedLines,
88 }) async {
89 final List<Pattern> remainingExpectedLines = expectedLines ?? <Pattern>[];
90 final unexpectedLinesFound = <String>[];
91 final command = <String>[flutterBin, ...getLocalEngineArguments(), 'build', ...options];
92
93 final ProcessResult result = await processManager.run(
94 command,
95 workingDirectory: workingDirectory,
96 );
97
98 final List<String> stdout = LineSplitter.split(result.stdout.toString()).toList();
99 final List<String> stderr = LineSplitter.split(result.stderr.toString()).toList();
100 final List<String> output = stdout + stderr;
101 for (final line in output) {
102 // Remove "[ +3 ms] " prefix
103 String trimmedLine = line.trim();
104 if (trimmedLine.startsWith('[')) {
105 final int prefixEndIndex = trimmedLine.indexOf(']');
106 if (prefixEndIndex > 0) {
107 trimmedLine = trimmedLine.substring(prefixEndIndex + 1, trimmedLine.length).trim();
108 }
109 }
110 remainingExpectedLines.remove(trimmedLine);
111 remainingExpectedLines.removeWhere(
112 (Pattern expectedLine) => trimmedLine.contains(expectedLine),
113 );
114 if (unexpectedLines != null) {
115 if (unexpectedLines
116 .where((String unexpectedLine) => trimmedLine.contains(unexpectedLine))
117 .firstOrNull !=
118 null) {
119 unexpectedLinesFound.add(trimmedLine);
120 }
121 }
122 }
123 expect(
124 result.exitCode,
125 0,
126 reason:
127 'Failed to build app for "${command.join(' ')}":\n'
128 'stdout: \n${result.stdout}\n'
129 'stderr: \n${result.stderr}\n',
130 );
131 expect(
132 remainingExpectedLines,
133 isEmpty,
134 reason:
135 'Did not find expected lines for "${command.join(' ')}":\n'
136 'stdout: \n${result.stdout}\n'
137 'stderr: \n${result.stderr}\n',
138 );
139 expect(
140 unexpectedLinesFound,
141 isEmpty,
142 reason:
143 'Found unexpected lines for "${command.join(' ')}":\n'
144 'stdout: \n${result.stdout}\n'
145 'stderr: \n${result.stderr}\n',
146 );
147 }
148
149 static Future<void> cleanApp(String flutterBin, String workingDirectory) async {
150 final ProcessResult result = await processManager.run(<String>[
151 flutterBin,
152 ...getLocalEngineArguments(),
153 'clean',
154 ], workingDirectory: workingDirectory);
155 expect(
156 result.exitCode,
157 0,
158 reason:
159 'Failed to clean app: \n'
160 'stdout: \n${result.stdout}\n'
161 'stderr: \n${result.stderr}\n',
162 );
163 }
164
165 static Future<SwiftPackageManagerPlugin> createPlugin(
166 String flutterBin,
167 String workingDirectory, {
168 required String platform,
169 required String iosLanguage,
170 bool usesSwiftPackageManager = false,
171 }) async {
172 final dependencyManager = usesSwiftPackageManager ? 'spm' : 'cocoapods';
173
174 // Create plugin
175 final pluginName = '${platform}_${iosLanguage}_${dependencyManager}_plugin';
176 final ProcessResult result = await processManager.run(<String>[
177 flutterBin,
178 ...getLocalEngineArguments(),
179 'create',
180 '--org',
181 'io.flutter.devicelab',
182 '--template=plugin',
183 '--platforms=$platform',
184 '-i',
185 iosLanguage,
186 pluginName,
187 ], workingDirectory: workingDirectory);
188
189 expect(
190 result.exitCode,
191 0,
192 reason:
193 'Failed to create plugin: \n'
194 'stdout: \n${result.stdout}\n'
195 'stderr: \n${result.stderr}\n',
196 );
197
198 final Directory pluginDirectory = fileSystem.directory(
199 fileSystem.path.join(workingDirectory, pluginName),
200 );
201
202 return SwiftPackageManagerPlugin(
203 pluginName: pluginName,
204 pluginPath: pluginDirectory.path,
205 platform: platform,
206 className:
207 '${_capitalize(platform)}${_capitalize(iosLanguage)}${_capitalize(dependencyManager)}Plugin',
208 );
209 }
210
211 static String _capitalize(String str) {
212 return str[0].toUpperCase() + str.substring(1);
213 }
214
215 static void addDependency({
216 required SwiftPackageManagerPlugin plugin,
217 required String appDirectoryPath,
218 }) {
219 final File pubspec = fileSystem.file(fileSystem.path.join(appDirectoryPath, 'pubspec.yaml'));
220 final String pubspecContent = pubspec.readAsStringSync();
221 pubspec.writeAsStringSync(
222 pubspecContent.replaceFirst(
223 '\ndependencies:\n',
224 '\ndependencies:\n ${plugin.pluginName}:\n path: ${plugin.pluginPath}\n',
225 ),
226 );
227 }
228
229 static void removeDependency({
230 required SwiftPackageManagerPlugin plugin,
231 required String appDirectoryPath,
232 }) {
233 final File pubspec = fileSystem.file(fileSystem.path.join(appDirectoryPath, 'pubspec.yaml'));
234 final String pubspecContent = pubspec.readAsStringSync();
235 final String updatedPubspecContent = pubspecContent.replaceFirst(
236 '\n ${plugin.pluginName}:\n path: ${plugin.pluginPath}\n',
237 '\n',
238 );
239
240 expect(updatedPubspecContent, isNot(pubspecContent));
241
242 pubspec.writeAsStringSync(updatedPubspecContent);
243 }
244
245 static void disableSwiftPackageManagerByPubspec({required String appDirectoryPath}) {
246 final File pubspec = fileSystem.file(fileSystem.path.join(appDirectoryPath, 'pubspec.yaml'));
247 final String pubspecContent = pubspec.readAsStringSync();
248 pubspec.writeAsStringSync(
249 pubspecContent.replaceFirst(
250 '\n# The following section is specific to Flutter packages.\nflutter:\n',
251 '\n# The following section is specific to Flutter packages.\nflutter:\n config: \n enable-swift-package-manager: false\n',
252 ),
253 );
254 }
255
256 static SwiftPackageManagerPlugin integrationTestPlugin(String platform) {
257 final String flutterRoot = getFlutterRoot();
258 return SwiftPackageManagerPlugin(
259 platform: platform,
260 pluginName: (platform == 'ios') ? 'integration_test' : 'integration_test_macos',
261 pluginPath: (platform == 'ios')
262 ? fileSystem.path.join(flutterRoot, 'packages', 'integration_test')
263 : fileSystem.path.join(
264 flutterRoot,
265 'packages',
266 'integration_test',
267 'integration_test_macos',
268 ),
269 className: 'IntegrationTestPlugin',
270 );
271 }
272
273 static List<Pattern> expectedLines({
274 required String platform,
275 required String appDirectoryPath,
276 SwiftPackageManagerPlugin? cocoaPodsPlugin,
277 SwiftPackageManagerPlugin? swiftPackagePlugin,
278 bool swiftPackageMangerEnabled = false,
279 bool migrated = false,
280 }) {
281 final frameworkName = platform == 'ios' ? 'Flutter' : 'FlutterMacOS';
282 final String appPlatformDirectoryPath = fileSystem.path.join(appDirectoryPath, platform);
283
284 final expectedLines = <Pattern>[];
285 if (swiftPackageMangerEnabled) {
286 expectedLines.addAll(<String>[
287 'FlutterGeneratedPluginSwiftPackage: $appPlatformDirectoryPath/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage',
288 "➜ Explicit dependency on target 'FlutterGeneratedPluginSwiftPackage' in project 'FlutterGeneratedPluginSwiftPackage'",
289 ]);
290 }
291 if (swiftPackagePlugin != null) {
292 // If using a Swift Package plugin, but Swift Package Manager is not enabled, it falls back to being used as a CocoaPods plugin.
293 if (swiftPackageMangerEnabled) {
294 expectedLines.addAll(<Pattern>[
295 RegExp(
296 '${swiftPackagePlugin.pluginName}: [/private]*$appPlatformDirectoryPath/Flutter/ephemeral/Packages/.packages/${swiftPackagePlugin.pluginName} @ local',
297 ),
298 "➜ Explicit dependency on target '${swiftPackagePlugin.pluginName}' in project '${swiftPackagePlugin.pluginName}'",
299 ]);
300 } else {
301 expectedLines.addAll(<String>[
302 '-> Installing ${swiftPackagePlugin.pluginName} (0.0.1)',
303 "➜ Explicit dependency on target '${swiftPackagePlugin.pluginName}' in project 'Pods'",
304 ]);
305 }
306 }
307 if (cocoaPodsPlugin != null) {
308 expectedLines.addAll(<String>[
309 'Running pod install...',
310 '-> Installing $frameworkName (1.0.0)',
311 '-> Installing ${cocoaPodsPlugin.pluginName} (0.0.1)',
312 "Target 'Pods-Runner' in project 'Pods'",
313 "➜ Explicit dependency on target '$frameworkName' in project 'Pods'",
314 "➜ Explicit dependency on target '${cocoaPodsPlugin.pluginName}' in project 'Pods'",
315 ]);
316 }
317 if (migrated) {
318 expectedLines.addAll(<String>[
319 'Adding Swift Package Manager integration...',
320 'Running pod install...',
321 "Target 'Pods-Runner' in project 'Pods'",
322 ]);
323 }
324 return expectedLines;
325 }
326
327 static List<String> unexpectedLines({
328 required String platform,
329 required String appDirectoryPath,
330 SwiftPackageManagerPlugin? cocoaPodsPlugin,
331 SwiftPackageManagerPlugin? swiftPackagePlugin,
332 bool swiftPackageMangerEnabled = false,
333 bool migrated = false,
334 }) {
335 final frameworkName = platform == 'ios' ? 'Flutter' : 'FlutterMacOS';
336 final String appPlatformDirectoryPath = fileSystem.path.join(appDirectoryPath, platform);
337
338 final unexpectedLines = <String>[];
339 if (cocoaPodsPlugin == null && !migrated) {
340 unexpectedLines.addAll(<String>[
341 'Running pod install...',
342 '-> Installing $frameworkName (1.0.0)',
343 "Target 'Pods-Runner' in project 'Pods'",
344 ]);
345 }
346 if (swiftPackagePlugin != null) {
347 if (swiftPackageMangerEnabled) {
348 unexpectedLines.addAll(<String>[
349 '-> Installing ${swiftPackagePlugin.pluginName} (0.0.1)',
350 "➜ Explicit dependency on target '${swiftPackagePlugin.pluginName}' in project 'Pods'",
351 ]);
352 } else {
353 unexpectedLines.addAll(<String>[
354 '${swiftPackagePlugin.pluginName}: $appPlatformDirectoryPath/Flutter/ephemeral/Packages/.packages/${swiftPackagePlugin.pluginName} @ local',
355 "➜ Explicit dependency on target '${swiftPackagePlugin.pluginName}' in project '${swiftPackagePlugin.pluginName}'",
356 ]);
357 }
358 }
359 if (!migrated) {
360 unexpectedLines.addAll(<String>['Adding Swift Package Manager integration...']);
361 }
362 return unexpectedLines;
363 }
364}
365
366class SwiftPackageManagerPlugin {
367 SwiftPackageManagerPlugin({
368 required this.pluginName,
369 required this.pluginPath,
370 required this.platform,
371 required this.className,
372 });
373
374 final String pluginName;
375 final String pluginPath;
376 final String platform;
377 final String className;
378 String get exampleAppPath => fileSystem.path.join(pluginPath, 'example');
379 String get exampleAppPlatformPath => fileSystem.path.join(exampleAppPath, platform);
380 String get swiftPackagePlatformPath => fileSystem.path.join(pluginPath, platform, pluginName);
381}
382