From dcf0b7bae64b28d73deb61dda9d4902360d1da5b Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Sat, 13 Feb 2016 12:00:41 -0800 Subject: [PATCH] allow any android sdk version --- .../lib/src/android/android_sdk.dart | 214 ++++++++++++++++++ .../lib/src/android/device_android.dart | 121 +++------- .../flutter_tools/lib/src/base/context.dart | 8 + .../flutter_tools/lib/src/base/globals.dart | 2 + packages/flutter_tools/lib/src/base/os.dart | 18 ++ .../flutter_tools/lib/src/commands/apk.dart | 76 +++---- .../lib/src/commands/daemon.dart | 1 + .../lib/src/commands/refresh.dart | 2 +- .../flutter_tools/lib/src/commands/trace.dart | 2 +- .../flutter_tools/lib/src/ios/device_ios.dart | 2 + .../src/runner/flutter_command_runner.dart | 18 ++ packages/flutter_tools/lib/src/services.dart | 11 +- packages/flutter_tools/pubspec.yaml | 1 + .../test/android_device_test.dart | 2 +- packages/flutter_tools/test/create_test.dart | 2 +- packages/flutter_tools/test/daemon_test.dart | 24 +- packages/flutter_tools/test/device_test.dart | 2 +- packages/flutter_tools/test/install_test.dart | 20 +- packages/flutter_tools/test/list_test.dart | 43 ++-- packages/flutter_tools/test/listen_test.dart | 10 +- packages/flutter_tools/test/logs_test.dart | 8 +- packages/flutter_tools/test/src/common.dart | 11 + .../src/{test_context.dart => context.dart} | 19 +- packages/flutter_tools/test/stop_test.dart | 19 +- packages/flutter_tools/test/trace_test.dart | 10 +- 25 files changed, 426 insertions(+), 220 deletions(-) create mode 100644 packages/flutter_tools/lib/src/android/android_sdk.dart create mode 100644 packages/flutter_tools/test/src/common.dart rename packages/flutter_tools/test/src/{test_context.dart => context.dart} (65%) diff --git a/packages/flutter_tools/lib/src/android/android_sdk.dart b/packages/flutter_tools/lib/src/android/android_sdk.dart new file mode 100644 index 0000000000..0271f3a966 --- /dev/null +++ b/packages/flutter_tools/lib/src/android/android_sdk.dart @@ -0,0 +1,214 @@ +// 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:io'; + +import 'package:path/path.dart' as path; +import 'package:pub_semver/pub_semver.dart'; + +import '../base/globals.dart'; +import '../base/os.dart'; + +// Android SDK layout: +// +// $ANDROID_HOME/platform-tools/adb +// $ANDROID_HOME/build-tools/19.1.0/aapt, dx, zipalign +// $ANDROID_HOME/build-tools/22.0.1/aapt +// $ANDROID_HOME/build-tools/23.0.2/aapt +// $ANDROID_HOME/platforms/android-22/android.jar +// $ANDROID_HOME/platforms/android-23/android.jar + +// TODO(devoncarew): We need a way to locate the Android SDK w/o using an environment variable. +// Perhaps something like `flutter config --android-home=foo/bar`. + +/// Locate ADB. Prefer to use one from an Android SDK, if we can locate that. +String getAdbPath() { + AndroidSdk sdk = AndroidSdk.locateAndroidSdk(); + + if (sdk?.latestVersion == null) { + return os.which('adb')?.path; + } else { + return sdk.adbPath; + } +} + +class AndroidSdk { + AndroidSdk(this.directory) { + _init(); + } + + final String directory; + + List _sdkVersions; + AndroidSdkVersion _latestVersion; + + static AndroidSdk locateAndroidSdk() { + // TODO: Use explicit configuration information from a metadata file? + + if (Platform.environment.containsKey('ANDROID_HOME')) { + String homeDir = Platform.environment['ANDROID_HOME']; + if (validSdkDirectory(homeDir)) + return new AndroidSdk(homeDir); + if (validSdkDirectory(path.join(homeDir, 'sdk'))) + return new AndroidSdk(path.join(homeDir, 'sdk')); + } + + File aaptBin = os.which('aapt'); // in build-tools/$version/aapt + if (aaptBin != null) { + String dir = aaptBin.parent.parent.parent.path; + if (validSdkDirectory(dir)) + return new AndroidSdk(dir); + } + + File adbBin = os.which('adb'); // in platform-tools/adb + if (adbBin != null) { + String dir = adbBin.parent.parent.path; + if (validSdkDirectory(dir)) + return new AndroidSdk(dir); + } + + // No dice. + printTrace('Unable to locate an Android SDK.'); + return null; + } + + static bool validSdkDirectory(String dir) { + return FileSystemEntity.isDirectorySync(path.join(dir, 'platform-tools')); + } + + List get sdkVersions => _sdkVersions; + + AndroidSdkVersion get latestVersion => _latestVersion; + + String get adbPath => getPlatformToolsPath('adb'); + + bool validateSdkWellFormed({ bool complain: false }) { + if (!FileSystemEntity.isFileSync(adbPath)) { + if (complain) + printError('Android SDK file not found: $adbPath.'); + return false; + } + + if (sdkVersions.isEmpty) { + if (complain) + printError('Android SDK does not have the proper build-tools.'); + return false; + } + + return latestVersion.validateSdkWellFormed(complain: complain); + } + + String getPlatformToolsPath(String binaryName) { + return path.join(directory, 'platform-tools', binaryName); + } + + void _init() { + List platforms = []; // android-22, ... + + Directory platformsDir = new Directory(path.join(directory, 'platforms')); + if (platformsDir.existsSync()) { + platforms = platformsDir + .listSync() + .map((FileSystemEntity entity) => path.basename(entity.path)) + .where((String name) => name.startsWith('android-')) + .toList(); + } + + List buildToolsVersions = []; // 19.1.0, 22.0.1, ... + + Directory buildToolsDir = new Directory(path.join(directory, 'build-tools')); + if (buildToolsDir.existsSync()) { + buildToolsVersions = buildToolsDir + .listSync() + .map((FileSystemEntity entity) { + try { + return new Version.parse(path.basename(entity.path)); + } catch (error) { + return null; + } + }) + .where((Version version) => version != null) + .toList(); + } + + // Here we match up platforms with cooresponding build-tools. If we don't + // have a match, we don't return anything for that platform version. So if + // the user only have 'android-22' and 'build-tools/19.0.0', we don't find + // an Android sdk. + _sdkVersions = platforms.map((String platform) { + int sdkVersion; + + try { + sdkVersion = int.parse(platform.substring('android-'.length)); + } catch (error) { + return null; + } + + Version buildToolsVersion = Version.primary(buildToolsVersions.where((Version version) { + return version.major == sdkVersion; + }).toList()); + + if (buildToolsVersion == null) + return null; + + return new AndroidSdkVersion(this, platform, buildToolsVersion.toString()); + }).where((AndroidSdkVersion version) => version != null).toList(); + + _sdkVersions.sort(); + + _latestVersion = _sdkVersions.isEmpty ? null : _sdkVersions.last; + } + + String toString() => 'AndroidSdk: $directory'; +} + +class AndroidSdkVersion implements Comparable { + AndroidSdkVersion(this.sdk, this.androidVersion, this.buildToolsVersion); + + final AndroidSdk sdk; + final String androidVersion; + final String buildToolsVersion; + + int get sdkLevel => int.parse(androidVersion.substring('android-'.length)); + + String get androidJarPath => getPlatformsPath('android.jar'); + + String get aaptPath => getBuildToolsPath('aapt'); + + String get dxPath => getBuildToolsPath('dx'); + + String get zipalignPath => getBuildToolsPath('zipalign'); + + bool validateSdkWellFormed({ bool complain: false }) { + return + _exists(androidJarPath, complain: complain) && + _exists(aaptPath, complain: complain) && + _exists(dxPath, complain: complain) && + _exists(zipalignPath, complain: complain); + } + + String getPlatformsPath(String itemName) { + return path.join(sdk.directory, 'platforms', androidVersion, itemName); + } + + String getBuildToolsPath(String binaryName) { + return path.join(sdk.directory, 'build-tools', buildToolsVersion, binaryName); + } + + int compareTo(AndroidSdkVersion other) { + return sdkLevel - other.sdkLevel; + } + + String toString() => '[${sdk.directory}, SDK version $sdkLevel, build-tools $buildToolsVersion]'; + + bool _exists(String path, { bool complain: false }) { + if (!FileSystemEntity.isFileSync(path)) { + if (complain) + printError('Android SDK file not found: $path.'); + return false; + } + + return true; + } +} diff --git a/packages/flutter_tools/lib/src/android/device_android.dart b/packages/flutter_tools/lib/src/android/device_android.dart index c55e0d4017..0f7c4d0a69 100644 --- a/packages/flutter_tools/lib/src/android/device_android.dart +++ b/packages/flutter_tools/lib/src/android/device_android.dart @@ -50,18 +50,6 @@ class AndroidDevice extends Device { }) : super(id) { if (connected != null) _connected = connected; - - _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) { - printError('Unable to run on Android.'); - } } final String productID; @@ -69,32 +57,9 @@ class AndroidDevice extends Device { final String deviceCodeName; bool _connected; - String _adbPath; - String get adbPath => _adbPath; - bool _hasAdb = false; - bool _hasValidAndroid = false; - - 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 { - printError('Android SDK not found at $androidHomeDir'); - return null; - } - } else { - printError('Android SDK not found. The ANDROID_HOME variable must be set.'); - return null; - } - } List adbCommandForDevice(List args) { - return [adbPath, '-s', id]..addAll(args); + return [androidSdk.adbPath, '-s', id]..addAll(args); } bool _isValidAdbVersion(String adbVersion) { @@ -121,24 +86,19 @@ class AndroidDevice extends Device { return true; } - bool _checkForAdb() { - try { - String adbVersion = runCheckedSync([adbPath, 'version']); - if (_isValidAdbVersion(adbVersion)) { - return true; - } + bool _checkForSupportedAdbVersion() { + if (androidSdk == null) + return false; - String locatedAdbPath = runCheckedSync(['which', 'adb']); - printError('"$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) { - printError('"adb" not found in \$PATH. ' - 'Please install the Android SDK or set ANDROID_HOME ' - 'to the path of your Android SDK install.'); - printTrace('$e'); + try { + String adbVersion = runCheckedSync([androidSdk.adbPath, 'version']); + if (_isValidAdbVersion(adbVersion)) + return true; + printError('The ADB at "${androidSdk.adbPath}" is too old; please install version 1.0.32 or later.'); + } catch (error, trace) { + printError('Error running ADB: $error', trace); } + return false; } @@ -150,34 +110,29 @@ class AndroidDevice extends Device { // * daemon started successfully * runCheckedSync(adbCommandForDevice(['start-server'])); - String ready = runSync(adbCommandForDevice(['shell', 'echo', 'ready'])); - if (ready.trim() != 'ready') { - printTrace('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); + int sdkVersionParsed = int.parse(sdkVersion, onError: (String source) => null); if (sdkVersionParsed == null) { printError('Unexpected response from getprop: "$sdkVersion"'); return false; } + if (sdkVersionParsed < minApiLevel) { printError( '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) { printError('Unexpected failure from adb: $e'); + return false; } - return false; } String _getDeviceSha1Path(ApplicationPackage app) { @@ -220,11 +175,15 @@ class AndroidDevice extends Device { printTrace('Android device not connected. Not installing.'); return false; } + if (!FileSystemEntity.isFileSync(app.localPath)) { printError('"${app.localPath}" does not exist.'); return false; } + if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion()) + return false; + printStatus('Installing ${app.name} on device.'); runCheckedSync(adbCommandForDevice(['install', '-r', app.localPath])); runCheckedSync(adbCommandForDevice(['shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app)])); @@ -306,6 +265,9 @@ class AndroidDevice extends Device { int debugPort: observatoryDefaultPort, Map platformArgs }) async { + if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion()) + return false; + flx.DirectoryResult buildResult = await flx.buildInTempDir( toolchain, mainPath: mainPath @@ -420,7 +382,7 @@ class AndroidDevice extends Device { return null; } - bool isConnected() => _connected ?? _hasValidAndroid; + bool isConnected() => _connected ?? androidSdk != null; void setConnected(bool value) { _connected = value; @@ -447,18 +409,12 @@ class AndroidDevice extends Device { } } -/// The [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. -List getAdbDevices([AndroidDevice mockAndroid]) { - List devices = []; - String adbPath = (mockAndroid != null) ? mockAndroid.adbPath : getAdbPath(); +List getAdbDevices() { + if (androidSdk == null) + return []; - try { - runCheckedSync([adbPath, 'version']); - } catch (e) { - printError('Unable to find adb. Is "adb" in your path?'); - return devices; - } + String adbPath = androidSdk.adbPath; + List devices = []; List output = runSync([adbPath, 'devices', '-l']).trim().split('\n'); @@ -525,25 +481,6 @@ List getAdbDevices([AndroidDevice mockAndroid]) { return devices; } -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 { - printTrace('"adb" not found at\n "$adbPath1" or\n "$adbPath2"\n' + - 'using default path "$_defaultAdbPath"'); - return _defaultAdbPath; - } - } else { - return _defaultAdbPath; - } -} - /// A log reader that logs from `adb logcat`. This will have the same output as /// another copy of [_AdbLogReader], and the two instances will be equivalent. class _AdbLogReader extends DeviceLogReader { diff --git a/packages/flutter_tools/lib/src/base/context.dart b/packages/flutter_tools/lib/src/base/context.dart index 8c1f54d2b3..87b9da4a12 100644 --- a/packages/flutter_tools/lib/src/base/context.dart +++ b/packages/flutter_tools/lib/src/base/context.dart @@ -16,6 +16,14 @@ AppContext get context { class AppContext { Map _instances = {}; + bool isSet(Type type) { + if (_instances.containsKey(type)) + return true; + + AppContext parent = _calcParent(Zone.current); + return parent != null ? parent.isSet(type) : false; + } + dynamic getVariable(Type type) { if (_instances.containsKey(type)) return _instances[type]; diff --git a/packages/flutter_tools/lib/src/base/globals.dart b/packages/flutter_tools/lib/src/base/globals.dart index 5c86fb2def..ebe3c12a84 100644 --- a/packages/flutter_tools/lib/src/base/globals.dart +++ b/packages/flutter_tools/lib/src/base/globals.dart @@ -2,12 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import '../android/android_sdk.dart'; import '../device.dart'; import 'context.dart'; import 'logger.dart'; DeviceManager get deviceManager => context[DeviceManager]; Logger get logger => context[Logger]; +AndroidSdk get androidSdk => context[AndroidSdk]; /// Display an error level message to the user. Commands should use this if they /// fail in some way. diff --git a/packages/flutter_tools/lib/src/base/os.dart b/packages/flutter_tools/lib/src/base/os.dart index df10b82e4a..444991cf96 100644 --- a/packages/flutter_tools/lib/src/base/os.dart +++ b/packages/flutter_tools/lib/src/base/os.dart @@ -18,12 +18,26 @@ abstract class OperatingSystemUtils { /// Make the given file executable. This may be a no-op on some platforms. ProcessResult makeExecutable(File file); + + /// Return the path (with symlinks resolved) to the given executable, or `null` + /// if `which` was not able to locate the binary. + File which(String execName); } class _PosixUtils implements OperatingSystemUtils { ProcessResult makeExecutable(File file) { return Process.runSync('chmod', ['u+x', file.path]); } + + /// Return the path (with symlinks resolved) to the given executable, or `null` + /// if `which` was not able to locate the binary. + File which(String execName) { + ProcessResult result = Process.runSync('which', [execName]); + if (result.exitCode != 0) + return null; + String path = result.stdout.trim().split('\n').first.trim(); + return new File(new File(path).resolveSymbolicLinksSync()); + } } class _WindowsUtils implements OperatingSystemUtils { @@ -31,6 +45,10 @@ class _WindowsUtils implements OperatingSystemUtils { ProcessResult makeExecutable(File file) { return new ProcessResult(0, 0, null, null); } + + File which(String execName) { + throw new UnimplementedError('_WindowsUtils.which'); + } } Future findAvailablePort() async { diff --git a/packages/flutter_tools/lib/src/commands/apk.dart b/packages/flutter_tools/lib/src/commands/apk.dart index 4a9899ea81..e6d928e9bd 100644 --- a/packages/flutter_tools/lib/src/commands/apk.dart +++ b/packages/flutter_tools/lib/src/commands/apk.dart @@ -7,11 +7,12 @@ import 'dart:io'; import 'package:path/path.dart' as path; -import '../android/device_android.dart'; +import '../android/android_sdk.dart'; import '../application_package.dart'; import '../artifacts.dart'; import '../base/file_system.dart'; import '../base/globals.dart'; +import '../base/os.dart'; import '../base/process.dart'; import '../build_configuration.dart'; import '../device.dart'; @@ -34,9 +35,6 @@ const String _kDebugKeystoreKeyAlias = "chromiumdebugkey"; // Password for the Chromium debug keystore const String _kDebugKeystorePassword = "chromium"; -const String _kAndroidPlatformVersion = '22'; -const String _kBuildToolsVersion = '22.0.1'; - /// Copies files into a new directory structure. class _AssetBuilder { final Directory outDir; @@ -59,30 +57,24 @@ class _AssetBuilder { /// Builds an APK package using Android SDK tools. class _ApkBuilder { - final String androidSdk; + final AndroidSdkVersion sdk; File _androidJar; File _aapt; File _dx; File _zipalign; - String _jarsigner; + File _jarsigner; - _ApkBuilder(this.androidSdk) { - _androidJar = new File('$androidSdk/platforms/android-$_kAndroidPlatformVersion/android.jar'); - - String buildTools = '$androidSdk/build-tools/$_kBuildToolsVersion'; - _aapt = new File('$buildTools/aapt'); - _dx = new File('$buildTools/dx'); - _zipalign = new File('$buildTools/zipalign'); - _jarsigner = 'jarsigner'; - } - - bool checkSdkPath() { - return (_androidJar.existsSync() && _aapt.existsSync() && _dx.existsSync() && _zipalign.existsSync()); + _ApkBuilder(this.sdk) { + _androidJar = new File(sdk.androidJarPath); + _aapt = new File(sdk.aaptPath); + _dx = new File(sdk.dxPath); + _zipalign = new File(sdk.zipalignPath); + _jarsigner = os.which('jarsigner'); } void compileClassesDex(File classesDex, List jars) { - List packageArgs = [_dx.path, + List packageArgs = [_dx.path, '--dex', '--force-jumbo', '--output', classesDex.path @@ -94,7 +86,7 @@ class _ApkBuilder { } void package(File outputApk, File androidManifest, Directory assets, Directory artifacts, Directory resources) { - List packageArgs = [_aapt.path, + List packageArgs = [_aapt.path, 'package', '-M', androidManifest.path, '-A', assets.path, @@ -109,7 +101,7 @@ class _ApkBuilder { } void sign(File keystore, String keystorePassword, String keyAlias, String keyPassword, File outputApk) { - runCheckedSync([_jarsigner, + runCheckedSync([_jarsigner.path, '-keystore', keystore.path, '-storepass', keystorePassword, '-keypass', keyPassword, @@ -119,12 +111,11 @@ class _ApkBuilder { } void align(File unalignedApk, File outputApk) { - runCheckedSync([_zipalign.path, '-f', '4', unalignedApk.path, outputApk.path]); + runCheckedSync([_zipalign.path, '-f', '4', unalignedApk.path, outputApk.path]); } } class _ApkComponents { - Directory androidSdk; File manifest; File icuData; List jars; @@ -135,11 +126,12 @@ class _ApkComponents { } class ApkKeystoreInfo { + ApkKeystoreInfo({ this.keystore, this.password, this.keyAlias, this.keyPassword }); + String keystore; String password; String keyAlias; String keyPassword; - ApkKeystoreInfo({ this.keystore, this.password, this.keyAlias, this.keyPassword }); } class ApkCommand extends FlutterCommand { @@ -183,7 +175,19 @@ class ApkCommand extends FlutterCommand { @override Future runInProject() async { + // Validate that we can find an android sdk. + if (androidSdk == null) { + printError('No Android SDK found.'); + return 1; + } + + if (!androidSdk.validateSdkWellFormed(complain: true)) { + printError('Try re-installing or updating your Android SDK.'); + return 1; + } + await downloadToolchain(); + return await buildAndroid( toolchain: toolchain, configs: buildConfigurations, @@ -207,10 +211,8 @@ class ApkCommand extends FlutterCommand { Future<_ApkComponents> _findApkComponents( BuildConfiguration config, String enginePath, String manifest, String resources ) async { - String androidSdkPath; List artifactPaths; if (enginePath != null) { - androidSdkPath = '$enginePath/third_party/android_tools/sdk'; artifactPaths = [ '$enginePath/third_party/icu/android/icudtl.dat', '${config.buildDir}/gen/sky/shell/shell/classes.dex.jar', @@ -218,9 +220,6 @@ Future<_ApkComponents> _findApkComponents( '$enginePath/build/android/ant/chromium-debug.keystore', ]; } else { - androidSdkPath = AndroidDevice.getAndroidSdkPath(); - if (androidSdkPath == null) - return null; List artifactTypes = [ ArtifactType.androidIcuData, ArtifactType.androidClassesJar, @@ -234,7 +233,6 @@ Future<_ApkComponents> _findApkComponents( } _ApkComponents components = new _ApkComponents(); - components.androidSdk = new Directory(androidSdkPath); components.manifest = new File(manifest); components.icuData = new File(artifactPaths[0]); components.jars = [new File(artifactPaths[1])]; @@ -242,11 +240,7 @@ Future<_ApkComponents> _findApkComponents( components.debugKeystore = new File(artifactPaths[3]); components.resources = new Directory(resources); - await parseServiceConfigs( - components.services, - jars: components.jars, - androidSdk: components.androidSdk.path - ); + await parseServiceConfigs(components.services, jars: components.jars); if (!components.resources.existsSync()) { // TODO(eseidel): This level should be higher when path is manually set. @@ -254,16 +248,6 @@ Future<_ApkComponents> _findApkComponents( components.resources = null; } - if (!components.androidSdk.existsSync()) { - printError('Can not locate Android SDK: $androidSdkPath'); - return null; - } - if (!(new _ApkBuilder(components.androidSdk.path).checkSdkPath())) { - printError('Can not locate expected Android SDK tools at $androidSdkPath'); - printError('You must install version $_kAndroidPlatformVersion of the SDK platform'); - printError('and version $_kBuildToolsVersion of the build tools.'); - return null; - } for (File f in [ components.manifest, components.icuData, components.libSkyShell, components.debugKeystore ]..addAll(components.jars)) { @@ -281,7 +265,7 @@ int _buildApk( ) { Directory tempDir = Directory.systemTemp.createTempSync('flutter_tools'); try { - _ApkBuilder builder = new _ApkBuilder(components.androidSdk.path); + _ApkBuilder builder = new _ApkBuilder(androidSdk.latestVersion); File classesDex = new File('${tempDir.path}/classes.dex'); builder.compileClassesDex(classesDex, components.jars); diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 0dee7cc1c1..f95ea65e04 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'dart:io'; import '../android/adb.dart'; +import '../android/android_sdk.dart'; import '../android/device_android.dart'; import '../base/context.dart'; import '../base/globals.dart'; diff --git a/packages/flutter_tools/lib/src/commands/refresh.dart b/packages/flutter_tools/lib/src/commands/refresh.dart index 35ff97245d..d4d5fc108e 100644 --- a/packages/flutter_tools/lib/src/commands/refresh.dart +++ b/packages/flutter_tools/lib/src/commands/refresh.dart @@ -32,7 +32,7 @@ class RefreshCommand extends FlutterCommand { downloadApplicationPackagesAndConnectToDevices(), ], eagerError: true); - if (!devices.android.isConnected()) { + if (devices.android == null || !devices.android.isConnected()) { printError('No device connected.'); return 1; } diff --git a/packages/flutter_tools/lib/src/commands/trace.dart b/packages/flutter_tools/lib/src/commands/trace.dart index a40af6dc53..365d552f4d 100644 --- a/packages/flutter_tools/lib/src/commands/trace.dart +++ b/packages/flutter_tools/lib/src/commands/trace.dart @@ -29,7 +29,7 @@ class TraceCommand extends FlutterCommand { Future runInProject() async { await downloadApplicationPackagesAndConnectToDevices(); - if (!devices.android.isConnected()) { + if (devices.android == null || !devices.android.isConnected()) { printError('No device connected, so no trace was completed.'); return 1; } diff --git a/packages/flutter_tools/lib/src/ios/device_ios.dart b/packages/flutter_tools/lib/src/ios/device_ios.dart index 1e1eba8d3c..4ab15bf6dd 100644 --- a/packages/flutter_tools/lib/src/ios/device_ios.dart +++ b/packages/flutter_tools/lib/src/ios/device_ios.dart @@ -402,6 +402,8 @@ class _IOSDeviceLogReader extends DeviceLogReader { if (!device.isConnected()) return 2; + // TODO(devoncarew): This regex should use the CFBundleIdentifier value from + // the user's plist (instead of `flutter.runner.Runner`). return await runCommandAndStreamOutput( [device.loggerPath], prefix: '[$name] ', diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart index a01c21ffc5..9f0cbc375a 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart @@ -9,7 +9,9 @@ import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:path/path.dart' as path; +import '../android/android_sdk.dart'; import '../artifacts.dart'; +import '../base/context.dart'; import '../base/globals.dart'; import '../base/process.dart'; import '../build_configuration.dart'; @@ -167,6 +169,22 @@ class FlutterCommandRunner extends CommandRunner { // See if the user specified a specific device. deviceManager.specifiedDeviceId = globalResults['device-id']; + // The Android SDK could already have been set by tests. + if (!context.isSet(AndroidSdk)) { + if (enginePath != null) { + context[AndroidSdk] = new AndroidSdk('$enginePath/third_party/android_tools/sdk'); + } else { + context[AndroidSdk] = AndroidSdk.locateAndroidSdk(); + } + } + + if (androidSdk != null) { + printTrace('Using Android SDK at ${androidSdk.directory}.'); + + if (androidSdk.latestVersion != null) + printTrace('${androidSdk.latestVersion}'); + } + ArtifactStore.flutterRoot = path.normalize(path.absolute(globalResults['flutter-root'])); if (globalResults.wasParsed('package-root')) ArtifactStore.packageRoot = path.normalize(path.absolute(globalResults['package-root'])); diff --git a/packages/flutter_tools/lib/src/services.dart b/packages/flutter_tools/lib/src/services.dart index 0225aec264..f23d68a37d 100644 --- a/packages/flutter_tools/lib/src/services.dart +++ b/packages/flutter_tools/lib/src/services.dart @@ -10,6 +10,7 @@ import 'package:path/path.dart' as path; import 'package:yaml/yaml.dart'; import 'artifacts.dart'; +import 'base/globals.dart'; const String _kFlutterManifestPath = 'flutter.yaml'; @@ -23,7 +24,7 @@ dynamic _loadYamlFile(String path) { /// Loads all services specified in `flutter.yaml`. Parses each service config file, /// storing metadata in [services] and the list of jar files in [jars]. Future parseServiceConfigs( - List> services, { List jars, String androidSdk } + List> services, { List jars } ) async { if (!ArtifactStore.isPackageRootValid) return; @@ -49,17 +50,17 @@ Future parseServiceConfigs( if (jars != null) { for (String jar in serviceConfig['jars']) - jars.add(new File(await getServiceFromUrl(jar, serviceRoot, service, androidSdk: androidSdk, unzip: false))); + jars.add(new File(await getServiceFromUrl(jar, serviceRoot, service, unzip: false))); } } } Future getServiceFromUrl( - String url, String rootDir, String serviceName, { String androidSdk, bool unzip: false } + String url, String rootDir, String serviceName, { bool unzip: false } ) async { - if (url.startsWith("android-sdk:")) { + if (url.startsWith("android-sdk:") && androidSdk != null) { // It's something shipped in the standard android SDK. - return url.replaceAll('android-sdk:', '$androidSdk/'); + return url.replaceAll('android-sdk:', '${androidSdk.directory}/'); } else if (url.startsWith("http")) { // It's a regular file to download. return await ArtifactStore.getThirdPartyFile(url, serviceName, unzip); diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 5b8898fe07..be82b6507a 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: den_api: ^0.1.0 mustache4dart: ^1.0.0 path: ^1.3.0 + pub_semver: ^1.0.0 stack_trace: ^1.4.0 test: 0.12.6+1 # see note below yaml: ^2.1.3 diff --git a/packages/flutter_tools/test/android_device_test.dart b/packages/flutter_tools/test/android_device_test.dart index 1d34f1e839..d71878d656 100644 --- a/packages/flutter_tools/test/android_device_test.dart +++ b/packages/flutter_tools/test/android_device_test.dart @@ -5,7 +5,7 @@ import 'package:flutter_tools/src/android/device_android.dart'; import 'package:test/test.dart'; -import 'src/test_context.dart'; +import 'src/context.dart'; main() => defineTests(); diff --git a/packages/flutter_tools/test/create_test.dart b/packages/flutter_tools/test/create_test.dart index 4e736d9f48..00306f3a7e 100644 --- a/packages/flutter_tools/test/create_test.dart +++ b/packages/flutter_tools/test/create_test.dart @@ -11,7 +11,7 @@ import 'package:flutter_tools/src/commands/create.dart'; import 'package:path/path.dart' as path; import 'package:test/test.dart'; -import 'src/test_context.dart'; +import 'src/context.dart'; main() => defineTests(); diff --git a/packages/flutter_tools/test/daemon_test.dart b/packages/flutter_tools/test/daemon_test.dart index 7ad73a1170..e735bd3693 100644 --- a/packages/flutter_tools/test/daemon_test.dart +++ b/packages/flutter_tools/test/daemon_test.dart @@ -16,11 +16,17 @@ import 'src/mocks.dart'; main() => defineTests(); defineTests() { - group('daemon', () { - Daemon daemon; - AppContext appContext; - NotifyingLogger notifyingLogger; + Daemon daemon; + AppContext appContext; + NotifyingLogger notifyingLogger; + void _testUsingContext(String description, dynamic testMethod()) { + test(description, () { + return appContext.runInZone(testMethod); + }); + } + + group('daemon', () { setUp(() { appContext = new AppContext(); notifyingLogger = new NotifyingLogger(); @@ -32,7 +38,7 @@ defineTests() { return daemon.shutdown(); }); - test('daemon.version', () async { + _testUsingContext('daemon.version', () async { StreamController> commands = new StreamController(); StreamController> responses = new StreamController(); daemon = new Daemon( @@ -47,7 +53,7 @@ defineTests() { expect(response['result'] is String, true); }); - test('daemon.logMessage', () { + _testUsingContext('daemon.logMessage', () { return appContext.runInZone(() async { StreamController> commands = new StreamController(); StreamController> responses = new StreamController(); @@ -68,7 +74,7 @@ defineTests() { }); }); - test('daemon.shutdown', () async { + _testUsingContext('daemon.shutdown', () async { StreamController> commands = new StreamController(); StreamController> responses = new StreamController(); daemon = new Daemon( @@ -82,7 +88,7 @@ defineTests() { }); }); - test('daemon.stopAll', () async { + _testUsingContext('daemon.stopAll', () async { DaemonCommand command = new DaemonCommand(); applyMocksToCommand(command); @@ -112,7 +118,7 @@ defineTests() { expect(response['result'], true); }); - test('device.getDevices', () async { + _testUsingContext('device.getDevices', () async { StreamController> commands = new StreamController(); StreamController> responses = new StreamController(); daemon = new Daemon( diff --git a/packages/flutter_tools/test/device_test.dart b/packages/flutter_tools/test/device_test.dart index dd9b5c661e..3ea8f09c5a 100644 --- a/packages/flutter_tools/test/device_test.dart +++ b/packages/flutter_tools/test/device_test.dart @@ -5,7 +5,7 @@ import 'package:flutter_tools/src/device.dart'; import 'package:test/test.dart'; -import 'src/test_context.dart'; +import 'src/context.dart'; main() => defineTests(); diff --git a/packages/flutter_tools/test/install_test.dart b/packages/flutter_tools/test/install_test.dart index f876456bea..e6302bcdfc 100644 --- a/packages/flutter_tools/test/install_test.dart +++ b/packages/flutter_tools/test/install_test.dart @@ -2,18 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:args/command_runner.dart'; import 'package:flutter_tools/src/commands/install.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; +import 'src/common.dart'; +import 'src/context.dart'; import 'src/mocks.dart'; main() => defineTests(); defineTests() { group('install', () { - test('returns 0 when Android is connected and ready for an install', () { + testUsingContext('returns 0 when Android is connected and ready for an install', () { InstallCommand command = new InstallCommand(); applyMocksToCommand(command); MockDeviceStore mockDevices = command.devices; @@ -30,13 +31,12 @@ defineTests() { when(mockDevices.iOSSimulator.isAppInstalled(any)).thenReturn(false); when(mockDevices.iOSSimulator.installApp(any)).thenReturn(false); - - CommandRunner runner = new CommandRunner('test_flutter', '') - ..addCommand(command); - return runner.run(['install']).then((int code) => expect(code, equals(0))); + return createTestCommandRunner(command).run(['install']).then((int code) { + expect(code, equals(0)); + }); }); - test('returns 0 when iOS is connected and ready for an install', () { + testUsingContext('returns 0 when iOS is connected and ready for an install', () { InstallCommand command = new InstallCommand(); applyMocksToCommand(command); MockDeviceStore mockDevices = command.devices; @@ -53,9 +53,9 @@ defineTests() { when(mockDevices.iOSSimulator.isAppInstalled(any)).thenReturn(false); when(mockDevices.iOSSimulator.installApp(any)).thenReturn(false); - CommandRunner runner = new CommandRunner('test_flutter', '') - ..addCommand(command); - return runner.run(['install']).then((int code) => expect(code, equals(0))); + return createTestCommandRunner(command).run(['install']).then((int code) { + expect(code, equals(0)); + }); }); }); } diff --git a/packages/flutter_tools/test/list_test.dart b/packages/flutter_tools/test/list_test.dart index f01949cd10..bff9e948d0 100644 --- a/packages/flutter_tools/test/list_test.dart +++ b/packages/flutter_tools/test/list_test.dart @@ -2,43 +2,34 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io'; - -import 'package:args/command_runner.dart'; +import 'package:flutter_tools/src/android/android_sdk.dart'; import 'package:flutter_tools/src/commands/list.dart'; -import 'package:mockito/mockito.dart'; +import 'package:flutter_tools/src/device.dart'; import 'package:test/test.dart'; -import 'src/mocks.dart'; -import 'src/test_context.dart'; +import 'src/common.dart'; +import 'src/context.dart'; main() => defineTests(); defineTests() { group('list', () { testUsingContext('returns 0 when called', () { - final String mockCommand = Platform.isWindows ? 'cmd /c echo' : 'echo'; - ListCommand command = new ListCommand(); - applyMocksToCommand(command); - MockDeviceStore mockDevices = command.devices; + return createTestCommandRunner(command).run(['list']).then((int code) { + expect(code, equals(0)); + }); + }); - // Avoid relying on adb being installed on the test system. - // Instead, cause the test to run the echo command. - when(mockDevices.android.adbPath).thenReturn(mockCommand); - - // Avoid relying on idevice* being installed on the test system. - // Instead, cause the test to run the echo command. - when(mockDevices.iOS.informerPath).thenReturn(mockCommand); - when(mockDevices.iOS.installerPath).thenReturn(mockCommand); - when(mockDevices.iOS.listerPath).thenReturn(mockCommand); - - // Avoid relying on xcrun being installed on the test system. - // Instead, cause the test to run the echo command. - when(mockDevices.iOSSimulator.xcrunPath).thenReturn(mockCommand); - - CommandRunner runner = new CommandRunner('test_flutter', '')..addCommand(command); - return runner.run(['list']).then((int code) => expect(code, equals(0))); + testUsingContext('no error when no connected devices', () { + ListCommand command = new ListCommand(); + return createTestCommandRunner(command).run(['list']).then((int code) { + expect(code, equals(0)); + expect(testLogger.statusText, contains('No connected devices')); + }); + }, overrides: { + AndroidSdk: null, + DeviceManager: new DeviceManager() }); }); } diff --git a/packages/flutter_tools/test/listen_test.dart b/packages/flutter_tools/test/listen_test.dart index dcbf122599..ddc0bd51f1 100644 --- a/packages/flutter_tools/test/listen_test.dart +++ b/packages/flutter_tools/test/listen_test.dart @@ -2,14 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:args/command_runner.dart'; import 'package:flutter_tools/src/commands/listen.dart'; -import 'package:flutter_tools/src/runner/flutter_command_runner.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; +import 'src/common.dart'; +import 'src/context.dart'; import 'src/mocks.dart'; -import 'src/test_context.dart'; main() => defineTests(); @@ -24,8 +23,9 @@ defineTests() { when(mockDevices.iOS.isConnected()).thenReturn(false); when(mockDevices.iOSSimulator.isConnected()).thenReturn(false); - CommandRunner runner = new FlutterCommandRunner()..addCommand(command); - return runner.run(['listen']).then((int code) => expect(code, equals(0))); + return createTestCommandRunner(command).run(['listen']).then((int code) { + expect(code, equals(0)); + }); }); }); } diff --git a/packages/flutter_tools/test/logs_test.dart b/packages/flutter_tools/test/logs_test.dart index 2dd67d5b16..ba5bdc9932 100644 --- a/packages/flutter_tools/test/logs_test.dart +++ b/packages/flutter_tools/test/logs_test.dart @@ -2,13 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:args/command_runner.dart'; import 'package:flutter_tools/src/commands/logs.dart'; -import 'package:flutter_tools/src/runner/flutter_command_runner.dart'; import 'package:test/test.dart'; +import 'src/common.dart'; +import 'src/context.dart'; import 'src/mocks.dart'; -import 'src/test_context.dart'; main() => defineTests(); @@ -17,8 +16,7 @@ defineTests() { testUsingContext('fail with a bad device id', () { LogsCommand command = new LogsCommand(); applyMocksToCommand(command); - CommandRunner runner = new FlutterCommandRunner()..addCommand(command); - return runner.run(['-d', 'abc123', 'logs']).then((int code) { + return createTestCommandRunner(command).run(['-d', 'abc123', 'logs']).then((int code) { expect(code, equals(1)); }); }); diff --git a/packages/flutter_tools/test/src/common.dart b/packages/flutter_tools/test/src/common.dart new file mode 100644 index 0000000000..5183515d97 --- /dev/null +++ b/packages/flutter_tools/test/src/common.dart @@ -0,0 +1,11 @@ +// 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 'package:args/command_runner.dart'; +import 'package:flutter_tools/src/runner/flutter_command.dart'; +import 'package:flutter_tools/src/runner/flutter_command_runner.dart'; + +CommandRunner createTestCommandRunner(FlutterCommand command) { + return new FlutterCommandRunner()..addCommand(command); +} diff --git a/packages/flutter_tools/test/src/test_context.dart b/packages/flutter_tools/test/src/context.dart similarity index 65% rename from packages/flutter_tools/test/src/test_context.dart rename to packages/flutter_tools/test/src/context.dart index c5a5d9da9f..b1c518c852 100644 --- a/packages/flutter_tools/test/src/test_context.dart +++ b/packages/flutter_tools/test/src/context.dart @@ -9,12 +9,25 @@ import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:test/test.dart'; -void testUsingContext(String description, dynamic testMethod(), { Timeout timeout }) { +/// Return the test logger. This assumes that the current Logger is a BufferLogger. +BufferLogger get testLogger => context[Logger]; + +void testUsingContext(String description, dynamic testMethod(), { + Timeout timeout, + Map overrides: const {} +}) { test(description, () { AppContext testContext = new AppContext(); - testContext[Logger] = new BufferLogger(); - testContext[DeviceManager] = new MockDeviceManager(); + overrides.forEach((Type type, dynamic value) { + testContext[type] = value; + }); + + if (!overrides.containsKey(Logger)) + testContext[Logger] = new BufferLogger(); + + if (!overrides.containsKey(DeviceManager)) + testContext[DeviceManager] = new MockDeviceManager(); return testContext.runInZone(testMethod); }, timeout: timeout); diff --git a/packages/flutter_tools/test/stop_test.dart b/packages/flutter_tools/test/stop_test.dart index f700e3b942..0c0c08e1c4 100644 --- a/packages/flutter_tools/test/stop_test.dart +++ b/packages/flutter_tools/test/stop_test.dart @@ -2,18 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:args/command_runner.dart'; import 'package:flutter_tools/src/commands/stop.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; +import 'src/common.dart'; +import 'src/context.dart'; import 'src/mocks.dart'; main() => defineTests(); defineTests() { group('stop', () { - test('returns 0 when Android is connected and ready to be stopped', () { + testUsingContext('returns 0 when Android is connected and ready to be stopped', () { StopCommand command = new StopCommand(); applyMocksToCommand(command); MockDeviceStore mockDevices = command.devices; @@ -27,12 +28,12 @@ defineTests() { when(mockDevices.iOSSimulator.isConnected()).thenReturn(false); when(mockDevices.iOSSimulator.stopApp(any)).thenReturn(false); - CommandRunner runner = new CommandRunner('test_flutter', '') - ..addCommand(command); - return runner.run(['stop']).then((int code) => expect(code, equals(0))); + return createTestCommandRunner(command).run(['stop']).then((int code) { + expect(code, equals(0)); + }); }); - test('returns 0 when iOS is connected and ready to be stopped', () { + testUsingContext('returns 0 when iOS is connected and ready to be stopped', () { StopCommand command = new StopCommand(); applyMocksToCommand(command); MockDeviceStore mockDevices = command.devices; @@ -46,9 +47,9 @@ defineTests() { when(mockDevices.iOSSimulator.isConnected()).thenReturn(false); when(mockDevices.iOSSimulator.stopApp(any)).thenReturn(false); - CommandRunner runner = new CommandRunner('test_flutter', '') - ..addCommand(command); - return runner.run(['stop']).then((int code) => expect(code, equals(0))); + return createTestCommandRunner(command).run(['stop']).then((int code) { + expect(code, equals(0)); + }); }); }); } diff --git a/packages/flutter_tools/test/trace_test.dart b/packages/flutter_tools/test/trace_test.dart index 279e7a8171..444da3ec60 100644 --- a/packages/flutter_tools/test/trace_test.dart +++ b/packages/flutter_tools/test/trace_test.dart @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:args/command_runner.dart'; import 'package:flutter_tools/src/commands/trace.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; +import 'src/common.dart'; +import 'src/context.dart'; import 'src/mocks.dart'; -import 'src/test_context.dart'; main() => defineTests(); @@ -21,9 +21,9 @@ defineTests() { when(mockDevices.android.isConnected()).thenReturn(false); - CommandRunner runner = new CommandRunner('test_flutter', '') - ..addCommand(command); - return runner.run(['trace']).then((int code) => expect(code, equals(1))); + return createTestCommandRunner(command).run(['trace']).then((int code) { + expect(code, equals(1)); + }); }); }); }