• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2018 The Chromium 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:async';
6import 'dart:io';
7
8import 'package:flutter_devicelab/framework/framework.dart';
9import 'package:flutter_devicelab/framework/ios.dart';
10import 'package:flutter_devicelab/framework/utils.dart';
11import 'package:path/path.dart' as path;
12
13/// Tests that the Flutter module project template works and supports
14/// adding Flutter to an existing iOS app.
15Future<void> main() async {
16  await task(() async {
17    section('Create Flutter module project');
18
19    final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.');
20    final Directory projectDir = Directory(path.join(tempDir.path, 'hello'));
21    try {
22      await inDirectory(tempDir, () async {
23        await flutter(
24          'create',
25          options: <String>[
26            '--org',
27            'io.flutter.devicelab',
28            '--template=module',
29            'hello',
30          ],
31        );
32      });
33      await prepareProvisioningCertificates(projectDir.path);
34
35      section('Build ephemeral host app in release mode without CocoaPods');
36
37      await inDirectory(projectDir, () async {
38        await flutter(
39          'build',
40          options: <String>['ios', '--no-codesign'],
41        );
42      });
43
44      final Directory ephemeralReleaseHostApp = Directory(path.join(
45        projectDir.path,
46        'build',
47        'ios',
48        'iphoneos',
49        'Runner.app',
50      ));
51
52      if (!exists(ephemeralReleaseHostApp)) {
53        return TaskResult.failure('Failed to build ephemeral host .app');
54      }
55
56      if (!await _isAppAotBuild(ephemeralReleaseHostApp)) {
57        return TaskResult.failure(
58          'Ephemeral host app ${ephemeralReleaseHostApp.path} was not a release build as expected'
59        );
60      }
61
62      if (await _hasDebugSymbols(ephemeralReleaseHostApp)) {
63        return TaskResult.failure(
64          "Ephemeral host app ${ephemeralReleaseHostApp.path}'s App.framework's "
65          "debug symbols weren't stripped in release mode"
66        );
67      }
68
69      section('Clean build');
70
71      await inDirectory(projectDir, () async {
72        await flutter('clean');
73      });
74
75      section('Build ephemeral host app in profile mode without CocoaPods');
76
77      await inDirectory(projectDir, () async {
78        await flutter(
79          'build',
80          options: <String>['ios', '--no-codesign', '--profile'],
81        );
82      });
83
84      final Directory ephemeralProfileHostApp = Directory(path.join(
85        projectDir.path,
86        'build',
87        'ios',
88        'iphoneos',
89        'Runner.app',
90      ));
91
92      if (!exists(ephemeralProfileHostApp)) {
93        return TaskResult.failure('Failed to build ephemeral host .app');
94      }
95
96      if (!await _isAppAotBuild(ephemeralProfileHostApp)) {
97        return TaskResult.failure(
98          'Ephemeral host app ${ephemeralProfileHostApp.path} was not a profile build as expected'
99        );
100      }
101
102      if (!await _hasDebugSymbols(ephemeralProfileHostApp)) {
103        return TaskResult.failure(
104          "Ephemeral host app ${ephemeralProfileHostApp.path}'s App.framework does not contain debug symbols"
105        );
106      }
107
108      section('Clean build');
109
110      await inDirectory(projectDir, () async {
111        await flutter('clean');
112      });
113
114      section('Build ephemeral host app in debug mode for simulator without CocoaPods');
115
116      await inDirectory(projectDir, () async {
117        await flutter(
118          'build',
119          options: <String>['ios', '--no-codesign', '--simulator', '--debug'],
120        );
121      });
122
123      final Directory ephemeralDebugHostApp = Directory(path.join(
124        projectDir.path,
125        'build',
126        'ios',
127        'iphonesimulator',
128        'Runner.app',
129      ));
130
131      if (!exists(ephemeralDebugHostApp)) {
132        return TaskResult.failure('Failed to build ephemeral host .app');
133      }
134
135      if (!exists(File(path.join(
136        ephemeralDebugHostApp.path,
137        'Frameworks',
138        'App.framework',
139        'flutter_assets',
140        'isolate_snapshot_data',
141      )))) {
142        return TaskResult.failure(
143          'Ephemeral host app ${ephemeralDebugHostApp.path} was not a debug build as expected'
144        );
145      }
146
147      section('Clean build');
148
149      await inDirectory(projectDir, () async {
150        await flutter('clean');
151      });
152
153      section('Add plugins');
154
155      final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
156      String content = await pubspec.readAsString();
157      content = content.replaceFirst(
158        '\ndependencies:\n',
159        '\ndependencies:\n  device_info:\n  package_info:\n',
160      );
161      await pubspec.writeAsString(content, flush: true);
162      await inDirectory(projectDir, () async {
163        await flutter(
164          'packages',
165          options: <String>['get'],
166        );
167      });
168
169      section('Build ephemeral host app with CocoaPods');
170
171      await inDirectory(projectDir, () async {
172        await flutter(
173          'build',
174          options: <String>['ios', '--no-codesign'],
175        );
176      });
177
178      final bool ephemeralHostAppWithCocoaPodsBuilt = exists(Directory(path.join(
179        projectDir.path,
180        'build',
181        'ios',
182        'iphoneos',
183        'Runner.app',
184      )));
185
186      if (!ephemeralHostAppWithCocoaPodsBuilt) {
187        return TaskResult.failure('Failed to build ephemeral host .app with CocoaPods');
188      }
189
190      final File podfileLockFile = File(path.join(projectDir.path, '.ios', 'Podfile.lock'));
191      final String podfileLockOutput = podfileLockFile.readAsStringSync();
192      if (!podfileLockOutput.contains(':path: Flutter/engine')
193        || !podfileLockOutput.contains(':path: Flutter/FlutterPluginRegistrant')
194        || !podfileLockOutput.contains(':path: Flutter/.symlinks/device_info/ios')
195        || !podfileLockOutput.contains(':path: Flutter/.symlinks/package_info/ios')) {
196        return TaskResult.failure('Building ephemeral host app Podfile.lock does not contain expected pods');
197      }
198
199      section('Clean build');
200
201      await inDirectory(projectDir, () async {
202        await flutter('clean');
203      });
204
205      section('Make iOS host app editable');
206
207      await inDirectory(projectDir, () async {
208        await flutter(
209          'make-host-app-editable',
210          options: <String>['ios'],
211        );
212      });
213
214      section('Build editable host app');
215
216      await inDirectory(projectDir, () async {
217        await flutter(
218          'build',
219          options: <String>['ios', '--no-codesign'],
220        );
221      });
222
223      final bool editableHostAppBuilt = exists(Directory(path.join(
224        projectDir.path,
225        'build',
226        'ios',
227        'iphoneos',
228        'Runner.app',
229      )));
230
231      if (!editableHostAppBuilt) {
232        return TaskResult.failure('Failed to build editable host .app');
233      }
234
235      section('Add to existing iOS app');
236
237      final Directory hostApp = Directory(path.join(tempDir.path, 'hello_host_app'));
238      mkdir(hostApp);
239      recursiveCopy(
240        Directory(path.join(flutterDirectory.path, 'dev', 'integration_tests', 'ios_host_app')),
241        hostApp,
242      );
243
244      final File analyticsOutputFile = File(path.join(tempDir.path, 'analytics.log'));
245
246      await inDirectory(hostApp, () async {
247        await exec('pod', <String>['install']);
248        await exec(
249          'xcodebuild',
250          <String>[
251            '-workspace',
252            'Host.xcworkspace',
253            '-scheme',
254            'Host',
255            '-configuration',
256            'Debug',
257            'CODE_SIGNING_ALLOWED=NO',
258            'CODE_SIGNING_REQUIRED=NO',
259            'CODE_SIGN_IDENTITY=-',
260            'EXPANDED_CODE_SIGN_IDENTITY=-',
261            'CONFIGURATION_BUILD_DIR=${tempDir.path}',
262            'COMPILER_INDEX_STORE_ENABLE=NO',
263          ],
264          environment: <String, String> {
265            'FLUTTER_ANALYTICS_LOG_FILE': analyticsOutputFile.path,
266          }
267        );
268      });
269
270      final bool existingAppBuilt = exists(File(path.join(
271        tempDir.path,
272        'Host.app',
273        'Host',
274      )));
275      if (!existingAppBuilt) {
276        return TaskResult.failure('Failed to build existing app .app');
277      }
278
279      final String analyticsOutput = analyticsOutputFile.readAsStringSync();
280      if (!analyticsOutput.contains('cd24: ios')
281          || !analyticsOutput.contains('cd25: true')
282          || !analyticsOutput.contains('viewName: build/bundle')) {
283        return TaskResult.failure(
284          'Building outer app produced the following analytics: "$analyticsOutput"'
285          'but not the expected strings: "cd24: ios", "cd25: true", "viewName: build/bundle"'
286        );
287      }
288
289      section('Fail building existing iOS app if flutter script fails');
290      int xcodebuildExitCode = 0;
291      await inDirectory(hostApp, () async {
292        xcodebuildExitCode = await exec(
293          'xcodebuild',
294          <String>[
295            '-workspace',
296            'Host.xcworkspace',
297            '-scheme',
298            'Host',
299            '-configuration',
300            'Debug',
301            'ARCHS=i386', // i386 is not supported in Debug mode.
302            'CODE_SIGNING_ALLOWED=NO',
303            'CODE_SIGNING_REQUIRED=NO',
304            'CODE_SIGN_IDENTITY=-',
305            'EXPANDED_CODE_SIGN_IDENTITY=-',
306            'CONFIGURATION_BUILD_DIR=${tempDir.path}',
307            'COMPILER_INDEX_STORE_ENABLE=NO',
308          ],
309          canFail: true
310        );
311      });
312
313      if (xcodebuildExitCode != 65) { // 65 returned on PhaseScriptExecution failure.
314        return TaskResult.failure('Host app build succeeded though flutter script failed');
315      }
316
317      return TaskResult.success(null);
318    } catch (e) {
319      return TaskResult.failure(e.toString());
320    } finally {
321      rmTree(tempDir);
322    }
323  });
324}
325
326Future<bool> _isAppAotBuild(Directory app) async {
327  final String binary = path.join(
328    app.path,
329    'Frameworks',
330    'App.framework',
331    'App'
332  );
333
334  final String symbolTable = await eval(
335    'nm',
336    <String> [
337      '-gU',
338      binary,
339    ],
340  );
341
342  return symbolTable.contains('kDartIsolateSnapshotInstructions');
343}
344
345Future<bool> _hasDebugSymbols(Directory app) async {
346  final String binary = path.join(
347    app.path,
348    'Frameworks',
349    'App.framework',
350    'App'
351  );
352
353  final String symbolTable = await eval(
354    'dsymutil',
355    <String> [
356      '--dump-debug-map',
357      binary,
358    ],
359    // The output is huge.
360    printStdout: false,
361  );
362
363  // Search for some random Flutter framework Dart function which should always
364  // be in App.framework.
365  return symbolTable.contains('BuildOwner_reassemble');
366}
367