| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:flutter_devicelab/common.dart'; |
| import 'package:flutter_devicelab/framework/framework.dart'; |
| import 'package:flutter_devicelab/framework/ios.dart'; |
| import 'package:flutter_devicelab/framework/task_result.dart'; |
| import 'package:flutter_devicelab/framework/utils.dart'; |
| import 'package:path/path.dart' as path; |
| |
| Future<void> main() async { |
| await task(() async { |
| section('Copy test Flutter App with watchOS Companion'); |
| |
| String? watchDeviceID; |
| String? phoneDeviceID; |
| final Directory tempDir = Directory.systemTemp |
| .createTempSync('flutter_ios_app_with_extensions_test.'); |
| final Directory projectDir = |
| Directory(path.join(tempDir.path, 'app_with_extensions')); |
| try { |
| mkdir(projectDir); |
| recursiveCopy( |
| Directory(path.join(flutterDirectory.path, 'dev', 'integration_tests', |
| 'ios_app_with_extensions')), |
| projectDir, |
| ); |
| |
| section('Create release build'); |
| |
| await inDirectory(projectDir, () async { |
| await flutter( |
| 'build', |
| options: <String>['ios', '--no-codesign', '--release', '--verbose'], |
| ); |
| }); |
| |
| final String appBundle = Directory(path.join( |
| projectDir.path, |
| 'build', |
| 'ios', |
| 'iphoneos', |
| 'Runner.app', |
| )).path; |
| |
| final String appFrameworkPath = path.join( |
| appBundle, |
| 'Frameworks', |
| 'App.framework', |
| 'App', |
| ); |
| final String flutterFrameworkPath = path.join( |
| appBundle, |
| 'Frameworks', |
| 'Flutter.framework', |
| 'Flutter', |
| ); |
| |
| checkDirectoryExists(appBundle); |
| await _checkFlutterFrameworkArchs(appFrameworkPath); |
| await _checkFlutterFrameworkArchs(flutterFrameworkPath); |
| |
| // Check the watch extension framework added in the Podfile |
| // is in place with the expected watch archs. |
| final String watchExtensionFrameworkPath = path.join( |
| appBundle, |
| 'Watch', |
| 'watch.app', |
| 'PlugIns', |
| 'watch Extension.appex', |
| 'Frameworks', |
| 'EFQRCode.framework', |
| 'EFQRCode', |
| ); |
| unawaited(_checkWatchExtensionFrameworkArchs(watchExtensionFrameworkPath)); |
| |
| section('Clean build'); |
| |
| await inDirectory(projectDir, () async { |
| await flutter('clean'); |
| }); |
| |
| section('Create debug build'); |
| |
| await inDirectory(projectDir, () async { |
| await flutter( |
| 'build', |
| options: <String>['ios', '--debug', '--no-codesign', '--verbose'], |
| ); |
| }); |
| |
| checkDirectoryExists(appBundle); |
| await _checkFlutterFrameworkArchs(appFrameworkPath); |
| await _checkFlutterFrameworkArchs(flutterFrameworkPath); |
| unawaited(_checkWatchExtensionFrameworkArchs(watchExtensionFrameworkPath)); |
| |
| section('Clean build'); |
| |
| await inDirectory(projectDir, () async { |
| await flutter('clean'); |
| }); |
| |
| section('Run app on simulator device'); |
| |
| // Xcode 11.4 simctl create makes the runtime argument optional, and defaults to latest. |
| // TODO(jmagman): Remove runtime parsing when devicelab upgrades to Xcode 11.4 https://github.com/flutter/flutter/issues/54889 |
| final String availableRuntimes = await eval( |
| 'xcrun', |
| <String>[ |
| 'simctl', |
| 'list', |
| 'runtimes', |
| ], |
| canFail: false, |
| workingDirectory: flutterDirectory.path, |
| ); |
| |
| // Example simctl list: |
| // == Runtimes == |
| // iOS 10.3 (10.3.1 - 14E8301) - com.apple.CoreSimulator.SimRuntime.iOS-10-3 |
| // iOS 13.4 (13.4 - 17E255) - com.apple.CoreSimulator.SimRuntime.iOS-13-4 |
| // tvOS 13.4 (13.4 - 17L255) - com.apple.CoreSimulator.SimRuntime.tvOS-13-4 |
| // watchOS 6.2 (6.2 - 17T256) - com.apple.CoreSimulator.SimRuntime.watchOS-6-2 |
| String? iOSSimRuntime; |
| String? watchSimRuntime; |
| |
| final RegExp iOSRuntimePattern = RegExp(r'iOS .*\) - (.*)'); |
| final RegExp watchOSRuntimePattern = RegExp(r'watchOS .*\) - (.*)'); |
| |
| for (final String runtime in LineSplitter.split(availableRuntimes)) { |
| // These seem to be in order, so allow matching multiple lines so it grabs |
| // the last (hopefully latest) one. |
| final RegExpMatch? iOSRuntimeMatch = iOSRuntimePattern.firstMatch(runtime); |
| if (iOSRuntimeMatch != null) { |
| iOSSimRuntime = iOSRuntimeMatch.group(1)!.trim(); |
| continue; |
| } |
| final RegExpMatch? watchOSRuntimeMatch = watchOSRuntimePattern.firstMatch(runtime); |
| if (watchOSRuntimeMatch != null) { |
| watchSimRuntime = watchOSRuntimeMatch.group(1)!.trim(); |
| } |
| } |
| if (iOSSimRuntime == null || watchSimRuntime == null) { |
| String message; |
| if (iOSSimRuntime != null) { |
| message = 'Found "$iOSSimRuntime", but no watchOS simulator runtime found.'; |
| } else if (watchSimRuntime != null) { |
| message = 'Found "$watchSimRuntime", but no iOS simulator runtime found.'; |
| } else { |
| message = 'watchOS and iOS simulator runtimes not found.'; |
| } |
| return TaskResult.failure('$message Available runtimes:\n$availableRuntimes'); |
| } |
| |
| // Create iOS simulator. |
| phoneDeviceID = await eval( |
| 'xcrun', |
| <String>[ |
| 'simctl', |
| 'create', |
| 'TestFlutteriPhoneWithWatch', |
| 'com.apple.CoreSimulator.SimDeviceType.iPhone-11', |
| iOSSimRuntime, |
| ], |
| canFail: false, |
| workingDirectory: flutterDirectory.path, |
| ); |
| |
| // Create watchOS simulator. |
| watchDeviceID = await eval( |
| 'xcrun', |
| <String>[ |
| 'simctl', |
| 'create', |
| 'TestFlutterWatch', |
| 'com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-5-44mm', |
| watchSimRuntime, |
| ], |
| canFail: false, |
| workingDirectory: flutterDirectory.path, |
| ); |
| |
| // Pair watch with phone. |
| await eval( |
| 'xcrun', |
| <String>['simctl', 'pair', watchDeviceID, phoneDeviceID], |
| canFail: false, |
| workingDirectory: flutterDirectory.path, |
| ); |
| |
| // Boot simulator devices. |
| await eval( |
| 'xcrun', |
| <String>['simctl', 'bootstatus', phoneDeviceID, '-b'], |
| canFail: false, |
| workingDirectory: flutterDirectory.path, |
| ); |
| await eval( |
| 'xcrun', |
| <String>['simctl', 'bootstatus', watchDeviceID, '-b'], |
| canFail: false, |
| workingDirectory: flutterDirectory.path, |
| ); |
| |
| // Start app on simulated device. |
| final Process process = await startProcess( |
| path.join(flutterDirectory.path, 'bin', 'flutter'), |
| <String>['run', '-d', phoneDeviceID], |
| workingDirectory: projectDir.path); |
| |
| process.stdout |
| .transform<String>(utf8.decoder) |
| .transform<String>(const LineSplitter()) |
| .listen((String line) { |
| print('stdout: $line'); |
| // Wait for app startup to complete and quit immediately afterwards. |
| if (line.startsWith('An Observatory debugger')) { |
| process.stdin.write('q'); |
| } |
| }); |
| process.stderr |
| .transform<String>(utf8.decoder) |
| .transform<String>(const LineSplitter()) |
| .listen((String line) { |
| print('stderr: $line'); |
| }); |
| |
| final int exitCode = await process.exitCode; |
| |
| if (exitCode != 0) { |
| return TaskResult.failure( |
| 'Failed to start flutter iOS app with WatchOS companion on simulated device.'); |
| } |
| |
| final String simulatorAppBundle = Directory(path.join( |
| projectDir.path, |
| 'build', |
| 'ios', |
| 'iphonesimulator', |
| 'Runner.app', |
| )).path; |
| |
| checkDirectoryExists(simulatorAppBundle); |
| checkFileExists(path.join( |
| simulatorAppBundle, |
| 'Frameworks', |
| 'App.framework', |
| 'App', |
| )); |
| checkFileExists(path.join( |
| simulatorAppBundle, |
| 'Frameworks', |
| 'Flutter.framework', |
| 'Flutter', |
| )); |
| |
| return TaskResult.success(null); |
| } catch (e) { |
| return TaskResult.failure(e.toString()); |
| } finally { |
| rmTree(tempDir); |
| // Delete simulator devices |
| if (watchDeviceID != null && watchDeviceID != '') { |
| await eval( |
| 'xcrun', |
| <String>['simctl', 'shutdown', watchDeviceID], |
| canFail: true, |
| workingDirectory: flutterDirectory.path, |
| ); |
| await eval( |
| 'xcrun', |
| <String>['simctl', 'delete', watchDeviceID], |
| canFail: true, |
| workingDirectory: flutterDirectory.path, |
| ); |
| } |
| if (phoneDeviceID != null && phoneDeviceID != '') { |
| await eval( |
| 'xcrun', |
| <String>['simctl', 'shutdown', phoneDeviceID], |
| canFail: true, |
| workingDirectory: flutterDirectory.path, |
| ); |
| await eval( |
| 'xcrun', |
| <String>['simctl', 'delete', phoneDeviceID], |
| canFail: true, |
| workingDirectory: flutterDirectory.path, |
| ); |
| } |
| } |
| }); |
| } |
| |
| Future<void> _checkFlutterFrameworkArchs(String frameworkPath) async { |
| checkFileExists(frameworkPath); |
| |
| final String archs = await fileType(frameworkPath); |
| if (!archs.contains('arm64')) { |
| throw TaskResult.failure('$frameworkPath arm64 architecture missing'); |
| } |
| |
| if (archs.contains('x86_64')) { |
| throw TaskResult.failure('$frameworkPath x86_64 architecture unexpectedly present'); |
| } |
| } |
| |
| Future<void> _checkWatchExtensionFrameworkArchs(String frameworkPath) async { |
| checkFileExists(frameworkPath); |
| final String archs = await fileType(frameworkPath); |
| if (!archs.contains('armv7k')) { |
| throw TaskResult.failure('$frameworkPath armv7k architecture missing'); |
| } |
| |
| if (!archs.contains('arm64_32')) { |
| throw TaskResult.failure('$frameworkPath arm64_32 architecture missing'); |
| } |
| } |