mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
refactor platform specific code out of device.dart
remove device type specific checks
This commit is contained in:
parent
0f505fbf20
commit
5bce2fbdec
7
packages/flutter_tools/lib/src/android/android.dart
Normal file
7
packages/flutter_tools/lib/src/android/android.dart
Normal file
@ -0,0 +1,7 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
const int minApiLevel = 16;
|
||||
const String minVersionName = 'Jelly Bean';
|
||||
const String minVersionText = '4.1.x';
|
||||
531
packages/flutter_tools/lib/src/android/device_android.dart
Normal file
531
packages/flutter_tools/lib/src/android/device_android.dart
Normal file
@ -0,0 +1,531 @@
|
||||
// Copyright 2016 The Chromium 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:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../base/logging.dart';
|
||||
import '../base/process.dart';
|
||||
import '../build_configuration.dart';
|
||||
import '../device.dart';
|
||||
import '../flx.dart' as flx;
|
||||
import '../toolchain.dart';
|
||||
import 'android.dart';
|
||||
|
||||
class AndroidDevice extends Device {
|
||||
static const String _defaultAdbPath = 'adb';
|
||||
static const int _observatoryPort = 8181;
|
||||
|
||||
static final String defaultDeviceID = 'default_android_device';
|
||||
|
||||
String productID;
|
||||
String modelID;
|
||||
String deviceCodeName;
|
||||
|
||||
bool _connected;
|
||||
String _adbPath;
|
||||
String get adbPath => _adbPath;
|
||||
bool _hasAdb = false;
|
||||
bool _hasValidAndroid = false;
|
||||
|
||||
factory AndroidDevice({
|
||||
String id: null,
|
||||
String productID: null,
|
||||
String modelID: null,
|
||||
String deviceCodeName: null,
|
||||
bool connected
|
||||
}) {
|
||||
AndroidDevice device = Device.unique(id ?? defaultDeviceID, (String id) => new AndroidDevice.fromId(id));
|
||||
device.productID = productID;
|
||||
device.modelID = modelID;
|
||||
device.deviceCodeName = deviceCodeName;
|
||||
if (connected != null)
|
||||
device._connected = connected;
|
||||
return device;
|
||||
}
|
||||
|
||||
/// This constructor is intended as protected access; prefer [AndroidDevice].
|
||||
AndroidDevice.fromId(id) : super.fromId(id) {
|
||||
_adbPath = getAdbPath();
|
||||
_hasAdb = _checkForAdb();
|
||||
|
||||
// Checking for [minApiName] only needs to be done if we are starting an
|
||||
// app, but it has an important side effect, which is to discard any
|
||||
// progress messages if the adb server is restarted.
|
||||
_hasValidAndroid = _checkForSupportedAndroidVersion();
|
||||
|
||||
if (!_hasAdb || !_hasValidAndroid) {
|
||||
logging.warning('Unable to run on Android.');
|
||||
}
|
||||
}
|
||||
|
||||
/// mockAndroid argument is only to facilitate testing with mocks, so that
|
||||
/// we don't have to rely on the test setup having adb available to it.
|
||||
static List<AndroidDevice> getAttachedDevices([AndroidDevice mockAndroid]) {
|
||||
List<AndroidDevice> devices = [];
|
||||
String adbPath = (mockAndroid != null) ? mockAndroid.adbPath : getAdbPath();
|
||||
|
||||
try {
|
||||
runCheckedSync([adbPath, 'version']);
|
||||
} catch (e) {
|
||||
logging.severe('Unable to find adb. Is "adb" in your path?');
|
||||
return devices;
|
||||
}
|
||||
|
||||
List<String> output = runSync([adbPath, 'devices', '-l']).trim().split('\n');
|
||||
|
||||
// 015d172c98400a03 device usb:340787200X product:nakasi model:Nexus_7 device:grouper
|
||||
RegExp deviceRegex1 = new RegExp(
|
||||
r'^(\S+)\s+device\s+.*product:(\S+)\s+model:(\S+)\s+device:(\S+)$');
|
||||
|
||||
// 0149947A0D01500C device usb:340787200X
|
||||
RegExp deviceRegex2 = new RegExp(r'^(\S+)\s+device\s+\S+$');
|
||||
RegExp unauthorizedRegex = new RegExp(r'^(\S+)\s+unauthorized\s+\S+$');
|
||||
RegExp offlineRegex = new RegExp(r'^(\S+)\s+offline\s+\S+$');
|
||||
|
||||
// Skip first line, which is always 'List of devices attached'.
|
||||
for (String line in output.skip(1)) {
|
||||
// Skip lines like:
|
||||
// * daemon not running. starting it now on port 5037 *
|
||||
// * daemon started successfully *
|
||||
if (line.startsWith('* daemon '))
|
||||
continue;
|
||||
|
||||
if (line.startsWith('List of devices'))
|
||||
continue;
|
||||
|
||||
if (deviceRegex1.hasMatch(line)) {
|
||||
Match match = deviceRegex1.firstMatch(line);
|
||||
String deviceID = match[1];
|
||||
String productID = match[2];
|
||||
String modelID = match[3];
|
||||
String deviceCodeName = match[4];
|
||||
|
||||
devices.add(new AndroidDevice(
|
||||
id: deviceID,
|
||||
productID: productID,
|
||||
modelID: modelID,
|
||||
deviceCodeName: deviceCodeName
|
||||
));
|
||||
} else if (deviceRegex2.hasMatch(line)) {
|
||||
Match match = deviceRegex2.firstMatch(line);
|
||||
String deviceID = match[1];
|
||||
devices.add(new AndroidDevice(id: deviceID));
|
||||
} else if (unauthorizedRegex.hasMatch(line)) {
|
||||
Match match = unauthorizedRegex.firstMatch(line);
|
||||
String deviceID = match[1];
|
||||
logging.warning(
|
||||
'Device $deviceID is not authorized.\n'
|
||||
'You might need to check your device for an authorization dialog.'
|
||||
);
|
||||
} else if (offlineRegex.hasMatch(line)) {
|
||||
Match match = offlineRegex.firstMatch(line);
|
||||
String deviceID = match[1];
|
||||
logging.warning('Device $deviceID is offline.');
|
||||
} else {
|
||||
logging.warning(
|
||||
'Unexpected failure parsing device information from adb output:\n'
|
||||
'$line\n'
|
||||
'Please report a bug at https://github.com/flutter/flutter/issues/new');
|
||||
}
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
static String getAndroidSdkPath() {
|
||||
if (Platform.environment.containsKey('ANDROID_HOME')) {
|
||||
String androidHomeDir = Platform.environment['ANDROID_HOME'];
|
||||
if (FileSystemEntity.isDirectorySync(
|
||||
path.join(androidHomeDir, 'platform-tools'))) {
|
||||
return androidHomeDir;
|
||||
} else if (FileSystemEntity.isDirectorySync(
|
||||
path.join(androidHomeDir, 'sdk', 'platform-tools'))) {
|
||||
return path.join(androidHomeDir, 'sdk');
|
||||
} else {
|
||||
logging.warning('Android SDK not found at $androidHomeDir');
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
logging.warning('Android SDK not found. The ANDROID_HOME variable must be set.');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static String getAdbPath() {
|
||||
if (Platform.environment.containsKey('ANDROID_HOME')) {
|
||||
String androidHomeDir = Platform.environment['ANDROID_HOME'];
|
||||
String adbPath1 = path.join(androidHomeDir, 'sdk', 'platform-tools', 'adb');
|
||||
String adbPath2 = path.join(androidHomeDir, 'platform-tools', 'adb');
|
||||
if (FileSystemEntity.isFileSync(adbPath1)) {
|
||||
return adbPath1;
|
||||
} else if (FileSystemEntity.isFileSync(adbPath2)) {
|
||||
return adbPath2;
|
||||
} else {
|
||||
logging.info('"adb" not found at\n "$adbPath1" or\n "$adbPath2"\n' +
|
||||
'using default path "$_defaultAdbPath"');
|
||||
return _defaultAdbPath;
|
||||
}
|
||||
} else {
|
||||
return _defaultAdbPath;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> adbCommandForDevice(List<String> args) {
|
||||
List<String> result = <String>[adbPath];
|
||||
if (id != defaultDeviceID) {
|
||||
result.addAll(['-s', id]);
|
||||
}
|
||||
result.addAll(args);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool _isValidAdbVersion(String adbVersion) {
|
||||
// Sample output: 'Android Debug Bridge version 1.0.31'
|
||||
Match versionFields =
|
||||
new RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
|
||||
if (versionFields != null) {
|
||||
int majorVersion = int.parse(versionFields[1]);
|
||||
int minorVersion = int.parse(versionFields[2]);
|
||||
int patchVersion = int.parse(versionFields[3]);
|
||||
if (majorVersion > 1) {
|
||||
return true;
|
||||
}
|
||||
if (majorVersion == 1 && minorVersion > 0) {
|
||||
return true;
|
||||
}
|
||||
if (majorVersion == 1 && minorVersion == 0 && patchVersion >= 32) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
logging.warning(
|
||||
'Unrecognized adb version string $adbVersion. Skipping version check.');
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _checkForAdb() {
|
||||
try {
|
||||
String adbVersion = runCheckedSync([adbPath, 'version']);
|
||||
if (_isValidAdbVersion(adbVersion)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String locatedAdbPath = runCheckedSync(['which', 'adb']);
|
||||
logging.severe('"$locatedAdbPath" is too old. '
|
||||
'Please install version 1.0.32 or later.\n'
|
||||
'Try setting ANDROID_HOME to the path to your Android SDK install. '
|
||||
'Android builds are unavailable.');
|
||||
} catch (e, stack) {
|
||||
logging.severe('"adb" not found in \$PATH. '
|
||||
'Please install the Android SDK or set ANDROID_HOME '
|
||||
'to the path of your Android SDK install.');
|
||||
logging.info(e);
|
||||
logging.info(stack);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _checkForSupportedAndroidVersion() {
|
||||
try {
|
||||
// If the server is automatically restarted, then we get irrelevant
|
||||
// output lines like this, which we want to ignore:
|
||||
// adb server is out of date. killing..
|
||||
// * daemon started successfully *
|
||||
runCheckedSync(adbCommandForDevice(['start-server']));
|
||||
|
||||
String ready = runSync(adbCommandForDevice(['shell', 'echo', 'ready']));
|
||||
if (ready.trim() != 'ready') {
|
||||
logging.info('Android device not found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sample output: '22'
|
||||
String sdkVersion =
|
||||
runCheckedSync(adbCommandForDevice(['shell', 'getprop', 'ro.build.version.sdk']))
|
||||
.trimRight();
|
||||
|
||||
int sdkVersionParsed =
|
||||
int.parse(sdkVersion, onError: (String source) => null);
|
||||
if (sdkVersionParsed == null) {
|
||||
logging.severe('Unexpected response from getprop: "$sdkVersion"');
|
||||
return false;
|
||||
}
|
||||
if (sdkVersionParsed < minApiLevel) {
|
||||
logging.severe(
|
||||
'The Android version ($sdkVersion) on the target device is too old. Please '
|
||||
'use a $minVersionName (version $minApiLevel / $minVersionText) device or later.');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
logging.severe('Unexpected failure from adb: ', e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String _getDeviceSha1Path(ApplicationPackage app) {
|
||||
return '/data/local/tmp/sky.${app.id}.sha1';
|
||||
}
|
||||
|
||||
String _getDeviceApkSha1(ApplicationPackage app) {
|
||||
return runCheckedSync(adbCommandForDevice(['shell', 'cat', _getDeviceSha1Path(app)]));
|
||||
}
|
||||
|
||||
String _getSourceSha1(ApplicationPackage app) {
|
||||
var sha1 = new SHA1();
|
||||
var file = new File(app.localPath);
|
||||
sha1.add(file.readAsBytesSync());
|
||||
return CryptoUtils.bytesToHex(sha1.close());
|
||||
}
|
||||
|
||||
String get name => modelID;
|
||||
|
||||
@override
|
||||
bool isAppInstalled(ApplicationPackage app) {
|
||||
if (!isConnected()) {
|
||||
return false;
|
||||
}
|
||||
if (runCheckedSync(adbCommandForDevice(['shell', 'pm', 'path', app.id])) == '') {
|
||||
logging.info(
|
||||
'TODO(iansf): move this log to the caller. ${app.name} is not on the device. Installing now...');
|
||||
return false;
|
||||
}
|
||||
if (_getDeviceApkSha1(app) != _getSourceSha1(app)) {
|
||||
logging.info(
|
||||
'TODO(iansf): move this log to the caller. ${app.name} is out of date. Installing now...');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
bool installApp(ApplicationPackage app) {
|
||||
if (!isConnected()) {
|
||||
logging.info('Android device not connected. Not installing.');
|
||||
return false;
|
||||
}
|
||||
if (!FileSystemEntity.isFileSync(app.localPath)) {
|
||||
logging.severe('"${app.localPath}" does not exist.');
|
||||
return false;
|
||||
}
|
||||
|
||||
print('Installing ${app.name} on device.');
|
||||
runCheckedSync(adbCommandForDevice(['install', '-r', app.localPath]));
|
||||
runCheckedSync(adbCommandForDevice(['shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app)]));
|
||||
return true;
|
||||
}
|
||||
|
||||
void _forwardObservatoryPort() {
|
||||
// Set up port forwarding for observatory.
|
||||
String portString = 'tcp:$_observatoryPort';
|
||||
try {
|
||||
runCheckedSync(adbCommandForDevice(['forward', portString, portString]));
|
||||
} catch (e) {
|
||||
logging.warning('Unable to forward observatory port ($_observatoryPort):\n$e');
|
||||
}
|
||||
}
|
||||
|
||||
bool startBundle(AndroidApk apk, String bundlePath, {
|
||||
bool poke: false,
|
||||
bool checked: true,
|
||||
bool traceStartup: false,
|
||||
String route,
|
||||
bool clearLogs: false
|
||||
}) {
|
||||
logging.fine('$this startBundle');
|
||||
|
||||
if (!FileSystemEntity.isFileSync(bundlePath)) {
|
||||
logging.severe('Cannot find $bundlePath');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!poke)
|
||||
_forwardObservatoryPort();
|
||||
|
||||
if (clearLogs)
|
||||
this.clearLogs();
|
||||
|
||||
String deviceTmpPath = '/data/local/tmp/dev.flx';
|
||||
runCheckedSync(adbCommandForDevice(['push', bundlePath, deviceTmpPath]));
|
||||
List<String> cmd = adbCommandForDevice([
|
||||
'shell', 'am', 'start',
|
||||
'-a', 'android.intent.action.RUN',
|
||||
'-d', deviceTmpPath,
|
||||
]);
|
||||
if (checked)
|
||||
cmd.addAll(['--ez', 'enable-checked-mode', 'true']);
|
||||
if (traceStartup)
|
||||
cmd.addAll(['--ez', 'trace-startup', 'true']);
|
||||
if (route != null)
|
||||
cmd.addAll(['--es', 'route', route]);
|
||||
cmd.add(apk.launchActivity);
|
||||
runCheckedSync(cmd);
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> startApp(
|
||||
ApplicationPackage package,
|
||||
Toolchain toolchain, {
|
||||
String mainPath,
|
||||
String route,
|
||||
bool checked: true,
|
||||
Map<String, dynamic> platformArgs
|
||||
}) {
|
||||
return flx.buildInTempDir(
|
||||
toolchain,
|
||||
mainPath: mainPath
|
||||
).then((flx.DirectoryResult buildResult) {
|
||||
logging.fine('Starting bundle for $this.');
|
||||
|
||||
try {
|
||||
if (startBundle(
|
||||
package,
|
||||
buildResult.localBundlePath,
|
||||
poke: platformArgs['poke'],
|
||||
checked: checked,
|
||||
traceStartup: platformArgs['trace-startup'],
|
||||
route: route,
|
||||
clearLogs: platformArgs['clear-logs']
|
||||
)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} finally {
|
||||
buildResult.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> stopApp(ApplicationPackage app) async {
|
||||
final AndroidApk apk = app;
|
||||
runSync(adbCommandForDevice(['shell', 'am', 'force-stop', apk.id]));
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
TargetPlatform get platform => TargetPlatform.android;
|
||||
|
||||
void clearLogs() {
|
||||
runSync(adbCommandForDevice(['logcat', '-c']));
|
||||
}
|
||||
|
||||
Future<int> logs({bool clear: false}) async {
|
||||
if (!isConnected()) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (clear) {
|
||||
clearLogs();
|
||||
}
|
||||
|
||||
return await runCommandAndStreamOutput(adbCommandForDevice([
|
||||
'logcat',
|
||||
'-v',
|
||||
'tag', // Only log the tag and the message
|
||||
'-s',
|
||||
'flutter:V',
|
||||
'ActivityManager:W',
|
||||
'System.err:W',
|
||||
'*:F',
|
||||
]), prefix: 'android: ');
|
||||
}
|
||||
|
||||
void startTracing(AndroidApk apk) {
|
||||
runCheckedSync(adbCommandForDevice([
|
||||
'shell',
|
||||
'am',
|
||||
'broadcast',
|
||||
'-a',
|
||||
'${apk.id}.TRACING_START'
|
||||
]));
|
||||
}
|
||||
|
||||
static String _threeDigits(int n) {
|
||||
if (n >= 100) return "$n";
|
||||
if (n >= 10) return "0$n";
|
||||
return "00$n";
|
||||
}
|
||||
|
||||
static String _twoDigits(int n) {
|
||||
if (n >= 10) return "$n";
|
||||
return "0$n";
|
||||
}
|
||||
|
||||
static String _logcatDateFormat(DateTime dt) {
|
||||
// Doing this manually, instead of using package:intl for simplicity.
|
||||
// adb logcat -T wants "%m-%d %H:%M:%S.%3q"
|
||||
String m = _twoDigits(dt.month);
|
||||
String d = _twoDigits(dt.day);
|
||||
String H = _twoDigits(dt.hour);
|
||||
String M = _twoDigits(dt.minute);
|
||||
String S = _twoDigits(dt.second);
|
||||
String q = _threeDigits(dt.millisecond);
|
||||
return "$m-$d $H:$M:$S.$q";
|
||||
}
|
||||
|
||||
// TODO(eseidel): This is fragile, there must be a better way!
|
||||
DateTime timeOnDevice() {
|
||||
// Careful: Android's date command is super-lame, any arguments are taken as
|
||||
// attempts to set the timezone and will screw your device.
|
||||
String output = runCheckedSync(adbCommandForDevice(['shell', 'date'])).trim();
|
||||
// format: Fri Dec 18 13:22:07 PST 2015
|
||||
// intl doesn't handle timezones: https://github.com/dart-lang/intl/issues/93
|
||||
// So we use the local date command to parse dates for us.
|
||||
String seconds = runSync(['date', '--date', output, '+%s']);
|
||||
// Although '%s' is supposed to be UTC, date appears to be ignoring the
|
||||
// timezone in the passed string, so using isUTC: false here.
|
||||
return new DateTime.fromMillisecondsSinceEpoch(int.parse(seconds) * 1000, isUtc: false);
|
||||
}
|
||||
|
||||
String stopTracing(AndroidApk apk, { String outPath: null }) {
|
||||
// Workaround for logcat -c not always working:
|
||||
// http://stackoverflow.com/questions/25645012/logcat-on-android-l-not-clearing-after-unplugging-and-reconnecting
|
||||
String beforeStop = _logcatDateFormat(timeOnDevice());
|
||||
runCheckedSync(adbCommandForDevice([
|
||||
'shell',
|
||||
'am',
|
||||
'broadcast',
|
||||
'-a',
|
||||
'${apk.id}.TRACING_STOP'
|
||||
]));
|
||||
|
||||
RegExp traceRegExp = new RegExp(r'Saving trace to (\S+)', multiLine: true);
|
||||
RegExp completeRegExp = new RegExp(r'Trace complete', multiLine: true);
|
||||
|
||||
String tracePath = null;
|
||||
bool isComplete = false;
|
||||
while (!isComplete) {
|
||||
String logs = runCheckedSync(adbCommandForDevice(['logcat', '-d', '-T', beforeStop]));
|
||||
Match fileMatch = traceRegExp.firstMatch(logs);
|
||||
if (fileMatch != null && fileMatch[1] != null) {
|
||||
tracePath = fileMatch[1];
|
||||
}
|
||||
isComplete = completeRegExp.hasMatch(logs);
|
||||
}
|
||||
|
||||
if (tracePath != null) {
|
||||
String localPath = (outPath != null) ? outPath : path.basename(tracePath);
|
||||
runCheckedSync(adbCommandForDevice(['root']));
|
||||
runSync(adbCommandForDevice(['shell', 'run-as', apk.id, 'chmod', '777', tracePath]));
|
||||
runCheckedSync(adbCommandForDevice(['pull', tracePath, localPath]));
|
||||
runSync(adbCommandForDevice(['shell', 'rm', tracePath]));
|
||||
return localPath;
|
||||
}
|
||||
logging.warning('No trace file detected. '
|
||||
'Did you remember to start the trace before stopping it?');
|
||||
return null;
|
||||
}
|
||||
|
||||
bool isConnected() => _connected != null ? _connected : _hasValidAndroid;
|
||||
|
||||
void setConnected(bool value) {
|
||||
_connected = value;
|
||||
}
|
||||
}
|
||||
@ -9,12 +9,12 @@ import 'dart:io';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
import '../android/device_android.dart';
|
||||
import '../artifacts.dart';
|
||||
import '../base/file_system.dart';
|
||||
import '../base/logging.dart';
|
||||
import '../base/process.dart';
|
||||
import '../build_configuration.dart';
|
||||
import '../device.dart';
|
||||
import '../flx.dart' as flx;
|
||||
import '../runner/flutter_command.dart';
|
||||
import 'start.dart';
|
||||
@ -392,16 +392,13 @@ class ApkCommand extends FlutterCommand {
|
||||
String mainPath = findMainDartFile(argResults['target']);
|
||||
|
||||
// Build the FLX.
|
||||
int result;
|
||||
await flx.buildInTempDir(
|
||||
toolchain,
|
||||
mainPath: mainPath,
|
||||
onBundleAvailable: (String localBundlePath) {
|
||||
result = _buildApk(components, localBundlePath);
|
||||
}
|
||||
);
|
||||
flx.DirectoryResult buildResult = await flx.buildInTempDir(toolchain, mainPath: mainPath);
|
||||
|
||||
return result;
|
||||
try {
|
||||
return _buildApk(components, buildResult.localBundlePath);
|
||||
} finally {
|
||||
buildResult.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import '../android/adb.dart';
|
||||
import '../android/device_android.dart';
|
||||
import '../base/logging.dart';
|
||||
import '../device.dart';
|
||||
import '../runner/flutter_command.dart';
|
||||
|
||||
@ -9,10 +9,10 @@ import 'package:args/command_runner.dart';
|
||||
import 'package:mustache4dart/mustache4dart.dart' as mustache;
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../android/android.dart' as android;
|
||||
import '../artifacts.dart';
|
||||
import '../base/logging.dart';
|
||||
import '../base/process.dart';
|
||||
import '../device.dart';
|
||||
|
||||
class InitCommand extends Command {
|
||||
final String name = 'init';
|
||||
@ -247,7 +247,7 @@ final String _apkManifest = '''
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.{{projectName}}">
|
||||
|
||||
<uses-sdk android:minSdkVersion="${AndroidDevice.minApiLevel}" android:targetSdkVersion="21" />
|
||||
<uses-sdk android:minSdkVersion="${android.minApiLevel}" android:targetSdkVersion="21" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<application android:name="org.domokit.sky.shell.SkyApplication" android:label="{{projectName}}">
|
||||
|
||||
@ -4,7 +4,8 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import '../device.dart';
|
||||
import '../android/device_android.dart';
|
||||
import '../ios/device_ios.dart';
|
||||
import '../runner/flutter_command.dart';
|
||||
|
||||
class ListCommand extends FlutterCommand {
|
||||
@ -29,6 +30,8 @@ class ListCommand extends FlutterCommand {
|
||||
if (details)
|
||||
print('Android Devices:');
|
||||
|
||||
// TODO(devoncarew): We should have a more generic mechanism for device discovery.
|
||||
// DeviceDiscoveryService? DeviceDiscoveryParticipant?
|
||||
for (AndroidDevice device in AndroidDevice.getAttachedDevices(devices.android)) {
|
||||
if (details) {
|
||||
print('${device.id}\t'
|
||||
|
||||
@ -9,9 +9,7 @@ import 'package:path/path.dart' as path;
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../base/logging.dart';
|
||||
import '../build_configuration.dart';
|
||||
import '../device.dart';
|
||||
import '../flx.dart' as flx;
|
||||
import '../runner/flutter_command.dart';
|
||||
import '../toolchain.dart';
|
||||
import 'install.dart';
|
||||
@ -138,31 +136,28 @@ Future<int> startApp(
|
||||
|
||||
logging.fine('Running build command for $device.');
|
||||
|
||||
if (device.platform == TargetPlatform.android) {
|
||||
await flx.buildInTempDir(
|
||||
toolchain,
|
||||
mainPath: mainPath,
|
||||
onBundleAvailable: (String localBundlePath) {
|
||||
logging.fine('Starting bundle for $device.');
|
||||
final AndroidDevice androidDevice = device; // https://github.com/flutter/flutter/issues/1035
|
||||
if (androidDevice.startBundle(package, localBundlePath,
|
||||
poke: poke,
|
||||
checked: checked,
|
||||
traceStartup: traceStartup,
|
||||
route: route,
|
||||
clearLogs: clearLogs
|
||||
)) {
|
||||
startedSomething = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
Map<String, dynamic> platformArgs = <String, dynamic>{};
|
||||
|
||||
if (poke != null)
|
||||
platformArgs['poke'] = poke;
|
||||
if (traceStartup != null)
|
||||
platformArgs['trace-startup'] = traceStartup;
|
||||
if (clearLogs != null)
|
||||
platformArgs['clear-logs'] = clearLogs;
|
||||
|
||||
bool result = await device.startApp(
|
||||
package,
|
||||
toolchain,
|
||||
mainPath: mainPath,
|
||||
route: route,
|
||||
checked: checked,
|
||||
platformArgs: platformArgs
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
logging.severe('Could not start \'${package.name}\' on \'${device.id}\'');
|
||||
} else {
|
||||
bool result = await device.startApp(package);
|
||||
if (!result) {
|
||||
logging.severe('Could not start \'${package.name}\' on \'${device.id}\'');
|
||||
} else {
|
||||
startedSomething = true;
|
||||
}
|
||||
startedSomething = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import '../android/device_android.dart';
|
||||
import '../application_package.dart';
|
||||
import '../base/logging.dart';
|
||||
import '../device.dart';
|
||||
import '../runner/flutter_command.dart';
|
||||
|
||||
class TraceCommand extends FlutterCommand {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -114,28 +114,38 @@ ArchiveFile _createSnapshotFile(String snapshotPath) {
|
||||
return new ArchiveFile(_kSnapshotKey, content.length, content);
|
||||
}
|
||||
|
||||
Future<int> buildInTempDir(
|
||||
/// Build the flx in a temp dir and return `localBundlePath` on success.
|
||||
Future<DirectoryResult> buildInTempDir(
|
||||
Toolchain toolchain, {
|
||||
String mainPath: defaultMainPath,
|
||||
void onBundleAvailable(String bundlePath)
|
||||
String mainPath: defaultMainPath
|
||||
}) async {
|
||||
int result;
|
||||
Directory tempDir = await Directory.systemTemp.createTemp('flutter_tools');
|
||||
try {
|
||||
String localBundlePath = path.join(tempDir.path, 'app.flx');
|
||||
String localSnapshotPath = path.join(tempDir.path, 'snapshot_blob.bin');
|
||||
result = await build(
|
||||
toolchain,
|
||||
snapshotPath: localSnapshotPath,
|
||||
outputPath: localBundlePath,
|
||||
mainPath: mainPath
|
||||
);
|
||||
if (result == 0)
|
||||
onBundleAvailable(localBundlePath);
|
||||
} finally {
|
||||
tempDir.deleteSync(recursive: true);
|
||||
String localBundlePath = path.join(tempDir.path, 'app.flx');
|
||||
String localSnapshotPath = path.join(tempDir.path, 'snapshot_blob.bin');
|
||||
result = await build(
|
||||
toolchain,
|
||||
snapshotPath: localSnapshotPath,
|
||||
outputPath: localBundlePath,
|
||||
mainPath: mainPath
|
||||
);
|
||||
if (result == 0)
|
||||
return new DirectoryResult(tempDir, localBundlePath);
|
||||
else
|
||||
throw result;
|
||||
}
|
||||
|
||||
/// The result from [buildInTempDir]. Note that this object should be disposed after use.
|
||||
class DirectoryResult {
|
||||
final Directory directory;
|
||||
final String localBundlePath;
|
||||
|
||||
DirectoryResult(this.directory, this.localBundlePath);
|
||||
|
||||
/// Call this to delete the temporary directory.
|
||||
void dispose() {
|
||||
directory.deleteSync(recursive: true);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<int> build(
|
||||
|
||||
528
packages/flutter_tools/lib/src/ios/device_ios.dart
Normal file
528
packages/flutter_tools/lib/src/ios/device_ios.dart
Normal file
@ -0,0 +1,528 @@
|
||||
// Copyright 2016 The Chromium 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:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../base/logging.dart';
|
||||
import '../base/process.dart';
|
||||
import '../build_configuration.dart';
|
||||
import '../device.dart';
|
||||
import '../toolchain.dart';
|
||||
|
||||
class IOSDevice extends Device {
|
||||
static final String defaultDeviceID = 'default_ios_id';
|
||||
|
||||
static const String _macInstructions =
|
||||
'To work with iOS devices, please install ideviceinstaller. '
|
||||
'If you use homebrew, you can install it with '
|
||||
'"\$ brew install ideviceinstaller".';
|
||||
static const String _linuxInstructions =
|
||||
'To work with iOS devices, please install ideviceinstaller. '
|
||||
'On Ubuntu or Debian, you can install it with '
|
||||
'"\$ apt-get install ideviceinstaller".';
|
||||
|
||||
String _installerPath;
|
||||
String get installerPath => _installerPath;
|
||||
|
||||
String _listerPath;
|
||||
String get listerPath => _listerPath;
|
||||
|
||||
String _informerPath;
|
||||
String get informerPath => _informerPath;
|
||||
|
||||
String _debuggerPath;
|
||||
String get debuggerPath => _debuggerPath;
|
||||
|
||||
String _loggerPath;
|
||||
String get loggerPath => _loggerPath;
|
||||
|
||||
String _pusherPath;
|
||||
String get pusherPath => _pusherPath;
|
||||
|
||||
String _name;
|
||||
String get name => _name;
|
||||
|
||||
factory IOSDevice({String id, String name}) {
|
||||
IOSDevice device = Device.unique(id ?? defaultDeviceID, (String id) => new IOSDevice.fromId(id));
|
||||
device._name = name;
|
||||
return device;
|
||||
}
|
||||
|
||||
IOSDevice.fromId(String id) : super.fromId(id) {
|
||||
_installerPath = _checkForCommand('ideviceinstaller');
|
||||
_listerPath = _checkForCommand('idevice_id');
|
||||
_informerPath = _checkForCommand('ideviceinfo');
|
||||
_debuggerPath = _checkForCommand('idevicedebug');
|
||||
_loggerPath = _checkForCommand('idevicesyslog');
|
||||
_pusherPath = _checkForCommand(
|
||||
'ios-deploy',
|
||||
'To copy files to iOS devices, please install ios-deploy. '
|
||||
'You can do this using homebrew as follows:\n'
|
||||
'\$ brew tap flutter/flutter\n'
|
||||
'\$ brew install ios-deploy');
|
||||
}
|
||||
|
||||
static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
|
||||
List<IOSDevice> devices = [];
|
||||
for (String id in _getAttachedDeviceIDs(mockIOS)) {
|
||||
String name = _getDeviceName(id, mockIOS);
|
||||
devices.add(new IOSDevice(id: id, name: name));
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
static Iterable<String> _getAttachedDeviceIDs([IOSDevice mockIOS]) {
|
||||
String listerPath =
|
||||
(mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id');
|
||||
String output;
|
||||
try {
|
||||
output = runSync([listerPath, '-l']);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
return output.trim()
|
||||
.split('\n')
|
||||
.where((String s) => s != null && s.length > 0);
|
||||
}
|
||||
|
||||
static String _getDeviceName(String deviceID, [IOSDevice mockIOS]) {
|
||||
String informerPath = (mockIOS != null)
|
||||
? mockIOS.informerPath
|
||||
: _checkForCommand('ideviceinfo');
|
||||
return runSync([informerPath, '-k', 'DeviceName', '-u', deviceID]);
|
||||
}
|
||||
|
||||
static final Map<String, String> _commandMap = {};
|
||||
static String _checkForCommand(
|
||||
String command, [
|
||||
String macInstructions = _macInstructions,
|
||||
String linuxInstructions = _linuxInstructions
|
||||
]) {
|
||||
return _commandMap.putIfAbsent(command, () {
|
||||
try {
|
||||
command = runCheckedSync(['which', command]).trim();
|
||||
} catch (e) {
|
||||
if (Platform.isMacOS) {
|
||||
logging.severe(macInstructions);
|
||||
} else if (Platform.isLinux) {
|
||||
logging.severe(linuxInstructions);
|
||||
} else {
|
||||
logging.severe('$command is not available on your platform.');
|
||||
}
|
||||
}
|
||||
return command;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
bool installApp(ApplicationPackage app) {
|
||||
try {
|
||||
if (id == defaultDeviceID) {
|
||||
runCheckedSync([installerPath, '-i', app.localPath]);
|
||||
} else {
|
||||
runCheckedSync([installerPath, '-u', id, '-i', app.localPath]);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool isConnected() {
|
||||
Iterable<String> ids = _getAttachedDeviceIDs();
|
||||
for (String id in ids) {
|
||||
if (id == this.id || this.id == defaultDeviceID) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool isAppInstalled(ApplicationPackage app) {
|
||||
try {
|
||||
String apps = runCheckedSync([installerPath, '--list-apps']);
|
||||
if (new RegExp(app.id, multiLine: true).hasMatch(apps)) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> startApp(
|
||||
ApplicationPackage app,
|
||||
Toolchain toolchain, {
|
||||
String mainPath,
|
||||
String route,
|
||||
bool checked: true,
|
||||
Map<String, dynamic> platformArgs
|
||||
}) async {
|
||||
// TODO: Use checked, mainPath, route
|
||||
logging.fine('Building ${app.name} for $id');
|
||||
|
||||
// Step 1: Install the precompiled application if necessary
|
||||
bool buildResult = await _buildIOSXcodeProject(app, true);
|
||||
|
||||
if (!buildResult) {
|
||||
logging.severe('Could not build the precompiled application for the device');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 2: Check that the application exists at the specified path
|
||||
Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphoneos', 'Runner.app'));
|
||||
|
||||
bool bundleExists = await bundle.exists();
|
||||
if (!bundleExists) {
|
||||
logging.severe('Could not find the built application bundle at ${bundle.path}');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 3: Attempt to install the application on the device
|
||||
int installationResult = await runCommandAndStreamOutput([
|
||||
'/usr/bin/env',
|
||||
'ios-deploy',
|
||||
'--id',
|
||||
id,
|
||||
'--bundle',
|
||||
bundle.path,
|
||||
]);
|
||||
|
||||
if (installationResult != 0) {
|
||||
logging.severe('Could not install ${bundle.path} on $id');
|
||||
return false;
|
||||
}
|
||||
|
||||
logging.fine('Installation successful');
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> stopApp(ApplicationPackage app) async {
|
||||
// Currently we don't have a way to stop an app running on iOS.
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> pushFile(
|
||||
ApplicationPackage app, String localFile, String targetFile) async {
|
||||
if (Platform.isMacOS) {
|
||||
runSync([
|
||||
pusherPath,
|
||||
'-t',
|
||||
'1',
|
||||
'--bundle_id',
|
||||
app.id,
|
||||
'--upload',
|
||||
localFile,
|
||||
'--to',
|
||||
targetFile
|
||||
]);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
TargetPlatform get platform => TargetPlatform.iOS;
|
||||
|
||||
/// Note that clear is not supported on iOS at this time.
|
||||
Future<int> logs({bool clear: false}) async {
|
||||
if (!isConnected()) {
|
||||
return 2;
|
||||
}
|
||||
return await runCommandAndStreamOutput([loggerPath],
|
||||
prefix: 'iOS dev: ', filter: new RegExp(r'.*SkyShell.*'));
|
||||
}
|
||||
}
|
||||
|
||||
class IOSSimulator extends Device {
|
||||
static final String defaultDeviceID = 'default_ios_sim_id';
|
||||
|
||||
static const String _macInstructions =
|
||||
'To work with iOS devices, please install ideviceinstaller. '
|
||||
'If you use homebrew, you can install it with '
|
||||
'"\$ brew install ideviceinstaller".';
|
||||
|
||||
static String _xcrunPath = path.join('/usr', 'bin', 'xcrun');
|
||||
|
||||
String _iOSSimPath;
|
||||
String get iOSSimPath => _iOSSimPath;
|
||||
|
||||
String get xcrunPath => _xcrunPath;
|
||||
|
||||
String _name;
|
||||
String get name => _name;
|
||||
|
||||
factory IOSSimulator({String id, String name, String iOSSimulatorPath}) {
|
||||
IOSSimulator device = Device.unique(id ?? defaultDeviceID, (String id) => new IOSSimulator.fromId(id));
|
||||
device._name = name;
|
||||
if (iOSSimulatorPath == null) {
|
||||
iOSSimulatorPath = path.join(
|
||||
'/Applications', 'iOS Simulator.app', 'Contents', 'MacOS', 'iOS Simulator'
|
||||
);
|
||||
}
|
||||
device._iOSSimPath = iOSSimulatorPath;
|
||||
return device;
|
||||
}
|
||||
|
||||
IOSSimulator.fromId(String id) : super.fromId(id);
|
||||
|
||||
static _IOSSimulatorInfo _getRunningSimulatorInfo([IOSSimulator mockIOS]) {
|
||||
String xcrunPath = mockIOS != null ? mockIOS.xcrunPath : _xcrunPath;
|
||||
String output = runCheckedSync([xcrunPath, 'simctl', 'list', 'devices']);
|
||||
|
||||
Match match;
|
||||
// iPhone 6s Plus (8AC808E1-6BAE-4153-BBC5-77F83814D414) (Booted)
|
||||
Iterable<Match> matches = new RegExp(
|
||||
r'[\W]*(.*) \(([^\)]+)\) \(Booted\)',
|
||||
multiLine: true
|
||||
).allMatches(output);
|
||||
if (matches.length > 1) {
|
||||
// More than one simulator is listed as booted, which is not allowed but
|
||||
// sometimes happens erroneously. Kill them all because we don't know
|
||||
// which one is actually running.
|
||||
logging.warning('Multiple running simulators were detected, '
|
||||
'which is not supposed to happen.');
|
||||
for (Match match in matches) {
|
||||
if (match.groupCount > 0) {
|
||||
// TODO: We're killing simulator devices inside an accessor method;
|
||||
// we probably shouldn't be changing state here.
|
||||
logging.warning('Killing simulator ${match.group(1)}');
|
||||
runSync([xcrunPath, 'simctl', 'shutdown', match.group(2)]);
|
||||
}
|
||||
}
|
||||
} else if (matches.length == 1) {
|
||||
match = matches.first;
|
||||
}
|
||||
|
||||
if (match != null && match.groupCount > 0) {
|
||||
return new _IOSSimulatorInfo(match.group(2), match.group(1));
|
||||
} else {
|
||||
logging.info('No running simulators found');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String _getSimulatorPath() {
|
||||
String deviceID = id == defaultDeviceID ? _getRunningSimulatorInfo()?.id : id;
|
||||
String homeDirectory = path.absolute(Platform.environment['HOME']);
|
||||
if (deviceID == null)
|
||||
return null;
|
||||
return path.join(homeDirectory, 'Library', 'Developer', 'CoreSimulator', 'Devices', deviceID);
|
||||
}
|
||||
|
||||
String _getSimulatorAppHomeDirectory(ApplicationPackage app) {
|
||||
String simulatorPath = _getSimulatorPath();
|
||||
if (simulatorPath == null)
|
||||
return null;
|
||||
return path.join(simulatorPath, 'data');
|
||||
}
|
||||
|
||||
static List<IOSSimulator> getAttachedDevices([IOSSimulator mockIOS]) {
|
||||
List<IOSSimulator> devices = [];
|
||||
_IOSSimulatorInfo deviceInfo = _getRunningSimulatorInfo(mockIOS);
|
||||
if (deviceInfo != null)
|
||||
devices.add(new IOSSimulator(id: deviceInfo.id, name: deviceInfo.name));
|
||||
return devices;
|
||||
}
|
||||
|
||||
Future<bool> boot() async {
|
||||
if (!Platform.isMacOS)
|
||||
return false;
|
||||
if (isConnected())
|
||||
return true;
|
||||
if (id == defaultDeviceID) {
|
||||
runDetached([iOSSimPath]);
|
||||
Future<bool> checkConnection([int attempts = 20]) async {
|
||||
if (attempts == 0) {
|
||||
logging.info('Timed out waiting for iOS Simulator $id to boot.');
|
||||
return false;
|
||||
}
|
||||
if (!isConnected()) {
|
||||
logging.info('Waiting for iOS Simulator $id to boot...');
|
||||
return await new Future.delayed(new Duration(milliseconds: 500),
|
||||
() => checkConnection(attempts - 1));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return await checkConnection();
|
||||
} else {
|
||||
try {
|
||||
runCheckedSync([xcrunPath, 'simctl', 'boot', id]);
|
||||
} catch (e) {
|
||||
logging.warning('Unable to boot iOS Simulator $id: ', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool installApp(ApplicationPackage app) {
|
||||
if (!isConnected())
|
||||
return false;
|
||||
|
||||
try {
|
||||
if (id == defaultDeviceID) {
|
||||
runCheckedSync([xcrunPath, 'simctl', 'install', 'booted', app.localPath]);
|
||||
} else {
|
||||
runCheckedSync([xcrunPath, 'simctl', 'install', id, app.localPath]);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool isConnected() {
|
||||
if (!Platform.isMacOS)
|
||||
return false;
|
||||
_IOSSimulatorInfo deviceInfo = _getRunningSimulatorInfo();
|
||||
if (deviceInfo == null) {
|
||||
return false;
|
||||
} else if (deviceInfo.id == defaultDeviceID) {
|
||||
return true;
|
||||
} else {
|
||||
return _getRunningSimulatorInfo()?.id == id;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool isAppInstalled(ApplicationPackage app) {
|
||||
try {
|
||||
String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
|
||||
return FileSystemEntity.isDirectorySync(simulatorHomeDirectory);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> startApp(
|
||||
ApplicationPackage app,
|
||||
Toolchain toolchain, {
|
||||
String mainPath,
|
||||
String route,
|
||||
bool checked: true,
|
||||
Map<String, dynamic> platformArgs
|
||||
}) async {
|
||||
// TODO: Use checked, mainPath, route
|
||||
logging.fine('Building ${app.name} for $id');
|
||||
|
||||
// Step 1: Build the Xcode project
|
||||
bool buildResult = await _buildIOSXcodeProject(app, false);
|
||||
if (!buildResult) {
|
||||
logging.severe('Could not build the application for the simulator');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 2: Assert that the Xcode project was successfully built
|
||||
Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphonesimulator', 'Runner.app'));
|
||||
bool bundleExists = await bundle.exists();
|
||||
if (!bundleExists) {
|
||||
logging.severe('Could not find the built application bundle at ${bundle.path}');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 3: Install the updated bundle to the simulator
|
||||
int installResult = await runCommandAndStreamOutput([
|
||||
xcrunPath,
|
||||
'simctl',
|
||||
'install',
|
||||
id == defaultDeviceID ? 'booted' : id,
|
||||
path.absolute(bundle.path)
|
||||
]);
|
||||
|
||||
if (installResult != 0) {
|
||||
logging.severe('Could not install the application bundle on the simulator');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 4: Launch the updated application in the simulator
|
||||
int launchResult = await runCommandAndStreamOutput([
|
||||
xcrunPath,
|
||||
'simctl',
|
||||
'launch',
|
||||
id == defaultDeviceID ? 'booted' : id,
|
||||
app.id
|
||||
]);
|
||||
|
||||
if (launchResult != 0) {
|
||||
logging.severe('Could not launch the freshly installed application on the simulator');
|
||||
return false;
|
||||
}
|
||||
|
||||
logging.fine('Successfully started ${app.name} on $id');
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> stopApp(ApplicationPackage app) async {
|
||||
// Currently we don't have a way to stop an app running on iOS.
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> pushFile(
|
||||
ApplicationPackage app, String localFile, String targetFile) async {
|
||||
if (Platform.isMacOS) {
|
||||
String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
|
||||
runCheckedSync(['cp', localFile, path.join(simulatorHomeDirectory, targetFile)]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
TargetPlatform get platform => TargetPlatform.iOSSimulator;
|
||||
|
||||
Future<int> logs({bool clear: false}) async {
|
||||
if (!isConnected())
|
||||
return 2;
|
||||
|
||||
String homeDirectory = path.absolute(Platform.environment['HOME']);
|
||||
String simulatorDeviceID = _getRunningSimulatorInfo().id;
|
||||
String logFilePath = path.join(
|
||||
homeDirectory, 'Library', 'Logs', 'CoreSimulator', simulatorDeviceID, 'system.log'
|
||||
);
|
||||
if (clear)
|
||||
runSync(['rm', logFilePath]);
|
||||
return await runCommandAndStreamOutput(
|
||||
['tail', '-f', logFilePath],
|
||||
prefix: 'iOS sim: ',
|
||||
filter: new RegExp(r'.*SkyShell.*')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IOSSimulatorInfo {
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
_IOSSimulatorInfo(this.id, this.name);
|
||||
}
|
||||
|
||||
Future<bool> _buildIOSXcodeProject(ApplicationPackage app, bool isDevice) async {
|
||||
List<String> command = [
|
||||
'/usr/bin/env', 'xcrun', 'xcodebuild', '-target', 'Runner', '-configuration', 'Release'
|
||||
];
|
||||
|
||||
if (!isDevice) {
|
||||
command.addAll(['-sdk', 'iphonesimulator']);
|
||||
}
|
||||
|
||||
int result = await runCommandAndStreamOutput(command,
|
||||
workingDirectory: app.localPath);
|
||||
return result == 0;
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_tools/src/device.dart';
|
||||
import 'package:flutter_tools/src/android/device_android.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
main() => defineTests();
|
||||
|
||||
@ -2,9 +2,11 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_tools/src/android/device_android.dart';
|
||||
import 'package:flutter_tools/src/application_package.dart';
|
||||
import 'package:flutter_tools/src/build_configuration.dart';
|
||||
import 'package:flutter_tools/src/device.dart';
|
||||
import 'package:flutter_tools/src/ios/device_ios.dart';
|
||||
import 'package:flutter_tools/src/runner/flutter_command.dart';
|
||||
import 'package:flutter_tools/src/toolchain.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user