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