android_sdk.dart 18.3 KB
Newer Older
1 2 3 4
// 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.

5 6 7 8
import 'dart:convert';

import 'package:meta/meta.dart';

9
import '../base/common.dart';
10
import '../base/context.dart';
11
import '../base/file_system.dart';
12
import '../base/io.dart' show ProcessResult;
13
import '../base/os.dart';
14
import '../base/platform.dart';
15
import '../base/process.dart';
16
import '../base/process_manager.dart';
17
import '../base/version.dart';
18
import '../globals.dart';
19
import 'android_studio.dart' as android_studio;
20

21 22
AndroidSdk get androidSdk => context[AndroidSdk];

23 24
const String kAndroidHome = 'ANDROID_HOME';

25
// Android SDK layout:
26

27
// $ANDROID_HOME/platform-tools/adb
28

29 30 31
// $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
32
// $ANDROID_HOME/build-tools/24.0.0-preview/aapt
33
// $ANDROID_HOME/build-tools/25.0.2/apksigner
34

35 36
// $ANDROID_HOME/platforms/android-22/android.jar
// $ANDROID_HOME/platforms/android-23/android.jar
37
// $ANDROID_HOME/platforms/android-N/android.jar
38

39 40
final RegExp _numberedAndroidPlatformRe = new RegExp(r'^android-([0-9]+)$');
final RegExp _sdkVersionRe = new RegExp(r'^ro.build.version.sdk=([0-9]+)$');
41

42 43 44
/// The minimum Android SDK version we support.
const int minimumAndroidSdkVersion = 25;

45
/// Locate ADB. Prefer to use one from an Android SDK, if we can locate that.
46 47 48
/// This should be used over accessing androidSdk.adbPath directly because it
/// will work for those users who have Android Platform Tools installed but
/// not the full SDK.
49 50 51 52
String getAdbPath([AndroidSdk existingSdk]) {
  if (existingSdk?.adbPath != null)
    return existingSdk.adbPath;

53
  final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
54 55 56 57 58 59 60 61

  if (sdk?.latestVersion == null) {
    return os.which('adb')?.path;
  } else {
    return sdk.adbPath;
  }
}

62 63 64
/// Locate 'emulator'. Prefer to use one from an Android SDK, if we can locate that.
/// This should be used over accessing androidSdk.emulatorPath directly because it
/// will work for those users who have Android Tools installed but
65 66
/// not the full SDK.
String getEmulatorPath([AndroidSdk existingSdk]) {
67 68
  return existingSdk?.emulatorPath ??
    AndroidSdk.locateAndroidSdk()?.emulatorPath;
69 70
}

71 72
/// Locate the path for storing AVD emulator images. Returns null if none found.
String getAvdPath() {
73

74
  final List<String> searchPaths = <String>[
75
    platform.environment['ANDROID_AVD_HOME']
76
  ];
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92

  if (platform.environment['HOME'] != null)
    searchPaths.add(fs.path.join(platform.environment['HOME'], '.android', 'avd'));

  if (platform.isWindows) {
    final String homeDrive = platform.environment['HOMEDRIVE'];
    final String homePath = platform.environment['HOMEPATH'];

    if (homeDrive != null && homePath != null) {
      // Can't use path.join for HOMEDRIVE/HOMEPATH
      // https://github.com/dart-lang/path/issues/37
      final String home = homeDrive + homePath;
      searchPaths.add(fs.path.join(home, '.android', 'avd'));
    }
  }

93 94 95 96 97 98
  return searchPaths.where((String p) => p != null).firstWhere(
    (String p) => fs.directory(p).existsSync(),
    orElse: () => null,
  );
}

99 100 101 102 103 104 105 106 107
/// Locate 'avdmanager'. Prefer to use one from an Android SDK, if we can locate that.
/// This should be used over accessing androidSdk.avdManagerPath directly because it
/// will work for those users who have Android Tools installed but
/// not the full SDK.
String getAvdManagerPath([AndroidSdk existingSdk]) {
  return existingSdk?.avdManagerPath ??
    AndroidSdk.locateAndroidSdk()?.avdManagerPath;
}

108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
class AndroidNdkSearchError {
  AndroidNdkSearchError(this.reason);

  /// The message explaining why NDK was not found.
  final String reason;
}

class AndroidNdk {
  AndroidNdk._(this.directory, this.compiler, this.compilerArgs);

  /// The path to the NDK.
  final String directory;

  /// The path to the NDK compiler.
  final String compiler;

  /// The mandatory arguments to the NDK compiler.
  final List<String> compilerArgs;

  /// Locate NDK within the given SDK or throw [AndroidNdkSearchError].
  static AndroidNdk locateNdk(String androidHomeDir) {
    if (androidHomeDir == null) {
      throw new AndroidNdkSearchError('Can not locate NDK because no SDK is found');
    }

    String findBundle(String androidHomeDir) {
      final String ndkDirectory = fs.path.join(androidHomeDir, 'ndk-bundle');
      if (!fs.isDirectorySync(ndkDirectory)) {
        throw new AndroidNdkSearchError('Can not locate ndk-bundle, tried: $ndkDirectory');
      }
      return ndkDirectory;
    }

    String findCompiler(String ndkDirectory) {
      String directory;
      if (platform.isLinux) {
        directory = 'linux-x86_64';
      } else if (platform.isMacOS) {
        directory = 'darwin-x86_64';
      } else {
        throw new AndroidNdkSearchError('Only Linux and macOS are supported');
      }

      final String ndkCompiler = fs.path.join(ndkDirectory,
          'toolchains', 'arm-linux-androideabi-4.9', 'prebuilt', directory,
          'bin', 'arm-linux-androideabi-gcc');
      if (!fs.isFileSync(ndkCompiler)) {
        throw new AndroidNdkSearchError('Can not locate GCC binary, tried $ndkCompiler');
      }

      return ndkCompiler;
    }

    List<String> findSysroot(String ndkDirectory) {
      // If entity represents directory with name android-<version> that
      // contains arch-arm subdirectory then returns version, otherwise
      // returns null.
      int toPlatformVersion(FileSystemEntity entry) {
        if (entry is! Directory) {
          return null;
        }

        if (!fs.isDirectorySync(fs.path.join(entry.path, 'arch-arm'))) {
          return null;
        }

        final String name = fs.path.basename(entry.path);

        const String platformPrefix = 'android-';
        if (!name.startsWith(platformPrefix)) {
          return null;
        }

        return int.tryParse(name.substring(platformPrefix.length));
      }

      final String platformsDir = fs.path.join(ndkDirectory, 'platforms');
      final List<int> versions = fs
          .directory(platformsDir)
          .listSync()
          .map(toPlatformVersion)
          .where((int version) => version != null)
          .toList(growable: false);
      versions.sort();

      final int suitableVersion = versions
          .firstWhere((int version) => version >= 9, orElse: () => null);
      if (suitableVersion == null) {
        throw new AndroidNdkSearchError('Can not locate a suitable platform ARM sysroot (need android-9 or newer), tried to look in $platformsDir');
      }

      final String armPlatform = fs.path.join(ndkDirectory, 'platforms',
          'android-$suitableVersion', 'arch-arm');
      return <String>['--sysroot', armPlatform];
    }

    final String ndkDir = findBundle(androidHomeDir);
    final String ndkCompiler = findCompiler(ndkDir);
    final List<String> ndkCompilerArgs = findSysroot(ndkDir);
    return new AndroidNdk._(ndkDir, ndkCompiler, ndkCompilerArgs);
  }

  /// Returns a descriptive message explaining why NDK can not be found within
  /// the given SDK.
  static String explainMissingNdk(String androidHomeDir) {
    try {
      locateNdk(androidHomeDir);
      return 'Unexpected error: found NDK on the second try';
    } on AndroidNdkSearchError catch (e) {
      return e.reason;
    }
  }
}

222
class AndroidSdk {
223
  AndroidSdk(this.directory, [this.ndk]) {
224 225 226
    _init();
  }

227 228
  static const String _javaHomeEnvironmentVariable = 'JAVA_HOME';
  static const String _javaExecutable = 'java';
229

230
  /// The path to the Android SDK.
231 232
  final String directory;

233 234
  /// Android NDK (can be `null`).
  final AndroidNdk ndk;
235

236 237 238 239
  List<AndroidSdkVersion> _sdkVersions;
  AndroidSdkVersion _latestVersion;

  static AndroidSdk locateAndroidSdk() {
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
    String findAndroidHomeDir() {
      String androidHomeDir;
      if (config.containsKey('android-sdk')) {
        androidHomeDir = config.getValue('android-sdk');
      } else if (platform.environment.containsKey(kAndroidHome)) {
        androidHomeDir = platform.environment[kAndroidHome];
      } else if (platform.isLinux) {
        if (homeDirPath != null)
          androidHomeDir = fs.path.join(homeDirPath, 'Android', 'Sdk');
      } else if (platform.isMacOS) {
        if (homeDirPath != null)
          androidHomeDir = fs.path.join(homeDirPath, 'Library', 'Android', 'sdk');
      } else if (platform.isWindows) {
        if (homeDirPath != null)
          androidHomeDir = fs.path.join(homeDirPath, 'AppData', 'Local', 'Android', 'sdk');
      }

      if (androidHomeDir != null) {
        if (validSdkDirectory(androidHomeDir))
          return androidHomeDir;
        if (validSdkDirectory(fs.path.join(androidHomeDir, 'sdk')))
          return fs.path.join(androidHomeDir, 'sdk');
      }

      // in build-tools/$version/aapt
      final List<File> aaptBins = os.whichAll('aapt');
      for (File aaptBin in aaptBins) {
        // Make sure we're using the aapt from the SDK.
        aaptBin = fs.file(aaptBin.resolveSymbolicLinksSync());
        final String dir = aaptBin.parent.parent.parent.path;
        if (validSdkDirectory(dir))
          return dir;
      }

      // in platform-tools/adb
      final List<File> adbBins = os.whichAll('adb');
      for (File adbBin in adbBins) {
        // Make sure we're using the adb from the SDK.
        adbBin = fs.file(adbBin.resolveSymbolicLinksSync());
        final String dir = adbBin.parent.parent.path;
        if (validSdkDirectory(dir))
          return dir;
      }

      return null;
    }

    final String androidHomeDir = findAndroidHomeDir();
    if (androidHomeDir == null) {
      // No dice.
      printTrace('Unable to locate an Android SDK.');
      return null;
292 293
    }

294
    // Try to find the NDK compiler. If we can't find it, it's also ok.
295 296 297 298 299 300
    AndroidNdk ndk;
    try {
      ndk = AndroidNdk.locateNdk(androidHomeDir);
    } on AndroidNdkSearchError {
      // Ignore AndroidNdkSearchError's but don't ignore any other
      // exceptions.
301 302
    }

303
    return new AndroidSdk(androidHomeDir, ndk);
304 305 306
  }

  static bool validSdkDirectory(String dir) {
307
    return fs.isDirectorySync(fs.path.join(dir, 'platform-tools'));
308 309 310 311 312 313 314 315
  }

  List<AndroidSdkVersion> get sdkVersions => _sdkVersions;

  AndroidSdkVersion get latestVersion => _latestVersion;

  String get adbPath => getPlatformToolsPath('adb');

Danny Tuppeny's avatar
Danny Tuppeny committed
316
  String get emulatorPath => getEmulatorPath();
317

318 319
  String get avdManagerPath => getAvdManagerPath();

320 321
  /// Validate the Android SDK. This returns an empty list if there are no
  /// issues; otherwise, it returns a list of issues found.
322
  List<String> validateSdkWellFormed() {
323
    if (!processManager.canRun(adbPath))
324
      return <String>['Android SDK file not found: $adbPath.'];
325

326
    if (sdkVersions.isEmpty || latestVersion == null)
327
      return <String>['Android SDK is missing command line tools; download from https://goo.gl/XxQghQ'];
328

329
    return latestVersion.validateSdkWellFormed();
330 331 332
  }

  String getPlatformToolsPath(String binaryName) {
333
    return fs.path.join(directory, 'platform-tools', binaryName);
334 335
  }

Danny Tuppeny's avatar
Danny Tuppeny committed
336 337 338 339 340 341 342 343 344 345 346
  String getEmulatorPath() {
    final String binaryName = platform.isWindows ? 'emulator.exe' : 'emulator';
    // Emulator now lives inside "emulator" but used to live inside "tools" so
    // try both.
    final List<String> searchFolders = <String>['emulator', 'tools'];
    for (final String folder in searchFolders) {
      final String path = fs.path.join(directory, folder, binaryName);
      if (fs.file(path).existsSync())
        return path;
    }
    return null;
347 348
  }

349 350 351 352 353 354 355 356
  String getAvdManagerPath() {
    final String binaryName = platform.isWindows ? 'avdmanager.bat' : 'avdmanager';
    final String path = fs.path.join(directory, 'tools', 'bin', binaryName);
    if (fs.file(path).existsSync())
      return path;
    return null;
  }

357
  void _init() {
358
    Iterable<Directory> platforms = <Directory>[]; // android-22, ...
359

360
    final Directory platformsDir = fs.directory(fs.path.join(directory, 'platforms'));
361 362 363
    if (platformsDir.existsSync()) {
      platforms = platformsDir
        .listSync()
364 365 366 367 368
        .where((FileSystemEntity entity) => entity is Directory)
        .map<Directory>((FileSystemEntity entity) {
          final Directory dir = entity;
          return dir;
        });
369 370
    }

371
    List<Version> buildTools = <Version>[]; // 19.1.0, 22.0.1, ...
372

373
    final Directory buildToolsDir = fs.directory(fs.path.join(directory, 'build-tools'));
374
    if (buildToolsDir.existsSync()) {
375
      buildTools = buildToolsDir
376 377 378
        .listSync()
        .map((FileSystemEntity entity) {
          try {
379
            return new Version.parse(entity.basename);
380 381 382 383 384 385 386 387
          } catch (error) {
            return null;
          }
        })
        .where((Version version) => version != null)
        .toList();
    }

388
    // Match up platforms with the best corresponding build-tools.
389 390
    _sdkVersions = platforms.map((Directory platformDir) {
      final String platformName = platformDir.basename;
391
      int platformVersion;
392 393

      try {
394 395 396 397 398 399 400 401 402 403 404 405
        final Match numberedVersion = _numberedAndroidPlatformRe.firstMatch(platformName);
        if (numberedVersion != null) {
          platformVersion = int.parse(numberedVersion.group(1));
        } else {
          final String buildProps = platformDir.childFile('build.prop').readAsStringSync();
          final String versionString = const LineSplitter()
              .convert(buildProps)
              .map(_sdkVersionRe.firstMatch)
              .firstWhere((Match match) => match != null)
              .group(1);
          platformVersion = int.parse(versionString);
        }
406 407 408 409
      } catch (error) {
        return null;
      }

410 411
      Version buildToolsVersion = Version.primary(buildTools.where((Version version) {
        return version.major == platformVersion;
412 413
      }).toList());

414 415
      buildToolsVersion ??= Version.primary(buildTools);

416 417 418
      if (buildToolsVersion == null)
        return null;

419
      return new AndroidSdkVersion._(
420
        this,
421 422 423
        sdkLevel: platformVersion,
        platformName: platformName,
        buildToolsVersion: buildToolsVersion,
424
      );
425 426 427 428 429 430 431
    }).where((AndroidSdkVersion version) => version != null).toList();

    _sdkVersions.sort();

    _latestVersion = _sdkVersions.isEmpty ? null : _sdkVersions.last;
  }

432 433 434 435 436
  /// Returns the filesystem path of the Android SDK manager tool or null if not found.
  String get sdkManagerPath {
    return fs.path.join(directory, 'tools', 'bin', 'sdkmanager');
  }

437 438 439 440 441 442
  /// First try Java bundled with Android Studio, then sniff JAVA_HOME, then fallback to PATH.
  static String findJavaBinary() {

    if (android_studio.javaPath != null)
      return fs.path.join(android_studio.javaPath, 'bin', 'java');

443
    final String javaHomeEnv = platform.environment[_javaHomeEnvironmentVariable];
444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464
    if (javaHomeEnv != null) {
      // Trust JAVA_HOME.
      return fs.path.join(javaHomeEnv, 'bin', 'java');
    }

    // MacOS specific logic to avoid popping up a dialog window.
    // See: http://stackoverflow.com/questions/14292698/how-do-i-check-if-the-java-jdk-is-installed-on-mac.
    if (platform.isMacOS) {
      try {
        final String javaHomeOutput = runCheckedSync(<String>['/usr/libexec/java_home'], hideStdout: true);
        if (javaHomeOutput != null) {
          final List<String> javaHomeOutputSplit = javaHomeOutput.split('\n');
          if ((javaHomeOutputSplit != null) && (javaHomeOutputSplit.isNotEmpty)) {
            final String javaHome = javaHomeOutputSplit[0].trim();
            return fs.path.join(javaHome, 'bin', 'java');
          }
        }
      } catch (_) { /* ignore */ }
    }

    // Fallback to PATH based lookup.
465
    return os.which(_javaExecutable)?.path;
466 467 468
  }

  Map<String, String> _sdkManagerEnv;
469 470
  /// Returns an environment with the Java folder added to PATH for use in calling
  /// Java-based Android SDK commands such as sdkmanager and avdmanager.
471 472 473 474 475 476 477 478 479 480 481 482 483
  Map<String, String> get sdkManagerEnv {
    if (_sdkManagerEnv == null) {
      // If we can locate Java, then add it to the path used to run the Android SDK manager.
      _sdkManagerEnv = <String, String>{};
      final String javaBinary = findJavaBinary();
      if (javaBinary != null) {
        _sdkManagerEnv['PATH'] =
            fs.path.dirname(javaBinary) + os.pathVarSeparator + platform.environment['PATH'];
      }
    }
    return _sdkManagerEnv;
  }

484 485 486 487
  /// Returns the version of the Android SDK manager tool or null if not found.
  String get sdkManagerVersion {
    if (!processManager.canRun(sdkManagerPath))
      throwToolExit('Android sdkmanager not found. Update to the latest Android SDK to resolve this.');
488
    final ProcessResult result = processManager.runSync(<String>[sdkManagerPath, '--version'], environment: sdkManagerEnv);
489
    if (result.exitCode != 0) {
490 491
      printTrace('sdkmanager --version failed: exitCode: ${result.exitCode} stdout: ${result.stdout} stderr: ${result.stderr}');
      return null;
492 493 494 495
    }
    return result.stdout.trim();
  }

496
  @override
497 498 499 500
  String toString() => 'AndroidSdk: $directory';
}

class AndroidSdkVersion implements Comparable<AndroidSdkVersion> {
501 502 503 504 505 506 507
  AndroidSdkVersion._(this.sdk, {
    @required this.sdkLevel,
    @required this.platformName,
    @required this.buildToolsVersion,
  }) : assert(sdkLevel != null),
       assert(platformName != null),
       assert(buildToolsVersion != null);
508 509

  final AndroidSdk sdk;
510 511
  final int sdkLevel;
  final String platformName;
512
  final Version buildToolsVersion;
513

514
  String get buildToolsVersionName => buildToolsVersion.toString();
515 516 517 518 519

  String get androidJarPath => getPlatformsPath('android.jar');

  String get aaptPath => getBuildToolsPath('aapt');

520
  List<String> validateSdkWellFormed() {
521 522 523
    if (_exists(androidJarPath) != null)
      return <String>[_exists(androidJarPath)];

524 525
    if (_canRun(aaptPath) != null)
      return <String>[_canRun(aaptPath)];
526 527

    return <String>[];
528 529 530
  }

  String getPlatformsPath(String itemName) {
531
    return fs.path.join(sdk.directory, 'platforms', platformName, itemName);
532 533
  }

534
  String getBuildToolsPath(String binaryName) {
535
    return fs.path.join(sdk.directory, 'build-tools', buildToolsVersionName, binaryName);
536 537
  }

538
  @override
539
  int compareTo(AndroidSdkVersion other) => sdkLevel - other.sdkLevel;
540

541
  @override
542
  String toString() => '[${sdk.directory}, SDK version $sdkLevel, build-tools $buildToolsVersionName]';
543

544
  String _exists(String path) {
545
    if (!fs.isFileSync(path))
546 547
      return 'Android SDK file not found: $path.';
    return null;
548
  }
549 550 551 552 553 554

  String _canRun(String path) {
    if (!processManager.canRun(path))
      return 'Android SDK file not found: $path.';
    return null;
  }
555
}