android_sdk.dart 22.4 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
import 'package:meta/meta.dart';

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

20
AndroidSdk get androidSdk => context.get<AndroidSdk>();
21

22
const String kAndroidHome = 'ANDROID_HOME';
23
const String kAndroidSdkRoot = 'ANDROID_SDK_ROOT';
24

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 = RegExp(r'^android-([0-9]+)$');
final RegExp _sdkVersionRe = 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
String getAdbPath([ AndroidSdk existingSdk ]) {
50 51 52
  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
/// not the full SDK.
66
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
/// 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.
103
String getAvdManagerPath([ AndroidSdk existingSdk ]) {
104 105 106 107
  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
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) {
130
      throw AndroidNdkSearchError('Can not locate NDK because no SDK is found');
131 132 133 134 135
    }

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

141 142
    // Returns list that contains toolchain bin folder and compiler binary name.
    List<String> findToolchainAndCompiler(String ndkDirectory) {
143 144 145 146 147 148
      String directory;
      if (platform.isLinux) {
        directory = 'linux-x86_64';
      } else if (platform.isMacOS) {
        directory = 'darwin-x86_64';
      } else {
149
        throw AndroidNdkSearchError('Only Linux and macOS are supported');
150 151
      }

152
      final String toolchainBin = fs.path.join(ndkDirectory,
153
          'toolchains', 'arm-linux-androideabi-4.9', 'prebuilt', directory,
154 155 156
          'bin');
      final String ndkCompiler = fs.path.join(toolchainBin,
          'arm-linux-androideabi-gcc');
157
      if (!fs.isFileSync(ndkCompiler)) {
158
        throw AndroidNdkSearchError('Can not locate GCC binary, tried $ndkCompiler');
159 160
      }

161
      return <String>[toolchainBin, ndkCompiler];
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
    }

    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) {
199
        throw AndroidNdkSearchError('Can not locate a suitable platform ARM sysroot (need android-9 or newer), tried to look in $platformsDir');
200 201 202 203 204 205 206
      }

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

207 208 209 210 211 212 213 214 215 216 217 218
    int findNdkMajorVersion(String ndkDirectory) {
      final String propertiesFile = fs.path.join(ndkDirectory, 'source.properties');
      if (!fs.isFileSync(propertiesFile)) {
        throw AndroidNdkSearchError('Can not establish ndk-bundle version: $propertiesFile not found');
      }

      // Parse source.properties: each line has Key = Value format.
      final Iterable<String> propertiesFileLines = fs.file(propertiesFile)
          .readAsStringSync()
          .split('\n')
          .map<String>((String line) => line.trim())
          .where((String line) => line.isNotEmpty);
219 220 221 222 223 224 225 226 227
      final Map<String, String> properties = <String, String>{};
      for (String line in propertiesFileLines) {
        final List<String> parts = line.split(' = ');
        if (parts.length == 2) {
          properties[parts[0]] = parts[1];
        } else {
          printError('Malformed line in ndk source.properties: "$line".');
        }
      }
228 229 230 231 232 233 234 235 236

      if (!properties.containsKey('Pkg.Revision')) {
        throw AndroidNdkSearchError('Can not establish ndk-bundle version: $propertiesFile does not contain Pkg.Revision');
      }

      // Extract major version from Pkg.Revision property which looks like <ndk-version>.x.y.
      return int.parse(properties['Pkg.Revision'].split('.').first);
    }

237
    final String ndkDir = findBundle(androidHomeDir);
238 239 240 241
    final int ndkVersion = findNdkMajorVersion(ndkDir);
    final List<String> ndkToolchainAndCompiler = findToolchainAndCompiler(ndkDir);
    final String ndkToolchain = ndkToolchainAndCompiler[0];
    final String ndkCompiler = ndkToolchainAndCompiler[1];
242
    final List<String> ndkCompilerArgs = findSysroot(ndkDir);
243 244 245 246 247 248 249 250 251 252 253
    if (ndkVersion >= 18) {
      // Newer versions of NDK use clang instead of gcc, which falls back to
      // system linker instead of using toolchain linker. Force clang to
      // use appropriate linker by passing -fuse-ld=<path-to-ld> command line
      // flag.
      final String ndkLinker = fs.path.join(ndkToolchain, 'arm-linux-androideabi-ld');
      if (!fs.isFileSync(ndkLinker)) {
        throw AndroidNdkSearchError('Can not locate linker binary, tried $ndkLinker');
      }
      ndkCompilerArgs.add('-fuse-ld=$ndkLinker');
    }
254
    return AndroidNdk._(ndkDir, ndkCompiler, ndkCompilerArgs);
255 256 257 258 259 260 261 262 263 264 265 266 267 268
  }

  /// 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;
    }
  }
}

269
class AndroidSdk {
270
  AndroidSdk(this.directory, [this.ndk]) {
271
    reinitialize();
272 273
  }

274 275
  static const String _javaHomeEnvironmentVariable = 'JAVA_HOME';
  static const String _javaExecutable = 'java';
276

277
  /// The path to the Android SDK.
278 279
  final String directory;

280 281
  /// Android NDK (can be `null`).
  final AndroidNdk ndk;
282

283 284 285
  List<AndroidSdkVersion> _sdkVersions;
  AndroidSdkVersion _latestVersion;

286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
  /// Whether the `platform-tools` directory exists in the Android SDK.
  ///
  /// It is possible to have an Android SDK folder that is missing this with
  /// the expectation that it will be downloaded later, e.g. by gradle or the
  /// sdkmanager. The [licensesAvailable] property should be used to determine
  /// whether the licenses are at least possibly accepted.
  bool get platformToolsAvailable => fs.directory(fs.path.join(directory, 'platform-tools')).existsSync();

  /// Whether the `licenses` directory exists in the Android SDK.
  ///
  /// The existence of this folder normally indicates that the SDK licenses have
  /// been accepted, e.g. via the sdkmanager, Android Studio, or by copying them
  /// from another workstation such as in CI scenarios. If these files are valid
  /// gradle or the sdkmanager will be able to download and use other parts of
  /// the SDK on demand.
  bool get licensesAvailable => fs.directory(fs.path.join(directory, 'licenses')).existsSync();

303
  static AndroidSdk locateAndroidSdk() {
304 305 306 307 308 309
    String findAndroidHomeDir() {
      String androidHomeDir;
      if (config.containsKey('android-sdk')) {
        androidHomeDir = config.getValue('android-sdk');
      } else if (platform.environment.containsKey(kAndroidHome)) {
        androidHomeDir = platform.environment[kAndroidHome];
310 311
      } else if (platform.environment.containsKey(kAndroidSdkRoot)) {
        androidHomeDir = platform.environment[kAndroidSdkRoot];
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
      } 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;
358 359
    }

360
    // Try to find the NDK compiler. If we can't find it, it's also ok.
361 362 363 364 365 366
    AndroidNdk ndk;
    try {
      ndk = AndroidNdk.locateNdk(androidHomeDir);
    } on AndroidNdkSearchError {
      // Ignore AndroidNdkSearchError's but don't ignore any other
      // exceptions.
367 368
    }

369
    return AndroidSdk(androidHomeDir, ndk);
370 371 372
  }

  static bool validSdkDirectory(String dir) {
Lau Ching Jun's avatar
Lau Ching Jun committed
373
    return sdkDirectoryHasLicenses(dir) || sdkDirectoryHasPlatformTools(dir);
374 375 376 377 378 379
  }

  static bool sdkDirectoryHasPlatformTools(String dir) {
    return fs.isDirectorySync(fs.path.join(dir, 'platform-tools'));
  }

Lau Ching Jun's avatar
Lau Ching Jun committed
380
  static bool sdkDirectoryHasLicenses(String dir) {
381
    return fs.isDirectorySync(fs.path.join(dir, 'licenses'));
382 383 384 385 386 387
  }

  List<AndroidSdkVersion> get sdkVersions => _sdkVersions;

  AndroidSdkVersion get latestVersion => _latestVersion;

388
  String get adbPath => getPlatformToolsPath(platform.isWindows ? 'adb.exe' : 'adb');
389

Danny Tuppeny's avatar
Danny Tuppeny committed
390
  String get emulatorPath => getEmulatorPath();
391

392 393
  String get avdManagerPath => getAvdManagerPath();

394 395 396 397 398 399 400 401 402 403 404 405
  Directory get _platformsDir => fs.directory(fs.path.join(directory, 'platforms'));

  Iterable<Directory> get _platforms {
    Iterable<Directory> platforms = <Directory>[];
    if (_platformsDir.existsSync()) {
      platforms = _platformsDir
        .listSync()
        .whereType<Directory>();
    }
    return platforms;
  }

406 407
  /// Validate the Android SDK. This returns an empty list if there are no
  /// issues; otherwise, it returns a list of issues found.
408
  List<String> validateSdkWellFormed() {
409 410
    if (adbPath == null || !processManager.canRun(adbPath))
      return <String>['Android SDK file not found: ${adbPath ?? 'adb'}.'];
411

412 413 414 415 416 417 418 419 420 421 422 423
    if (sdkVersions.isEmpty || latestVersion == null) {
      final StringBuffer msg = StringBuffer('No valid Android SDK platforms found in ${_platformsDir.path}.');
      if (_platforms.isEmpty) {
        msg.write(' Directory was empty.');
      } else {
        msg.write(' Candidates were:\n');
        msg.write(_platforms
          .map((Directory dir) => '  - ${dir.basename}')
          .join('\n'));
      }
      return <String>[msg.toString()];
    }
424

425
    return latestVersion.validateSdkWellFormed();
426 427 428
  }

  String getPlatformToolsPath(String binaryName) {
429 430 431 432
    final String path = fs.path.join(directory, 'platform-tools', binaryName);
    if (fs.file(path).existsSync())
      return path;
    return null;
433 434
  }

Danny Tuppeny's avatar
Danny Tuppeny committed
435 436 437 438 439 440 441 442 443 444 445
  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;
446 447
  }

448 449 450 451 452 453 454 455
  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;
  }

456 457 458 459 460
  /// Sets up various paths used internally.
  ///
  /// This method should be called in a case where the tooling may have updated
  /// SDK artifacts, such as after running a gradle build.
  void reinitialize() {
461
    List<Version> buildTools = <Version>[]; // 19.1.0, 22.0.1, ...
462

463
    final Directory buildToolsDir = fs.directory(fs.path.join(directory, 'build-tools'));
464
    if (buildToolsDir.existsSync()) {
465
      buildTools = buildToolsDir
466 467 468
        .listSync()
        .map((FileSystemEntity entity) {
          try {
469
            return Version.parse(entity.basename);
470 471 472 473 474 475 476 477
          } catch (error) {
            return null;
          }
        })
        .where((Version version) => version != null)
        .toList();
    }

478
    // Match up platforms with the best corresponding build-tools.
479
    _sdkVersions = _platforms.map<AndroidSdkVersion>((Directory platformDir) {
480
      final String platformName = platformDir.basename;
481
      int platformVersion;
482 483

      try {
484 485 486 487 488 489 490
        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)
491
              .map<Match>(_sdkVersionRe.firstMatch)
492 493 494 495
              .firstWhere((Match match) => match != null)
              .group(1);
          platformVersion = int.parse(versionString);
        }
496 497 498 499
      } catch (error) {
        return null;
      }

500 501
      Version buildToolsVersion = Version.primary(buildTools.where((Version version) {
        return version.major == platformVersion;
502 503
      }).toList());

504 505
      buildToolsVersion ??= Version.primary(buildTools);

506 507 508
      if (buildToolsVersion == null)
        return null;

509
      return AndroidSdkVersion._(
510
        this,
511 512 513
        sdkLevel: platformVersion,
        platformName: platformName,
        buildToolsVersion: buildToolsVersion,
514
      );
515 516 517 518 519 520 521
    }).where((AndroidSdkVersion version) => version != null).toList();

    _sdkVersions.sort();

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

522 523 524 525 526
  /// 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');
  }

527 528 529 530 531 532
  /// 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');

533
    final String javaHomeEnv = platform.environment[_javaHomeEnvironmentVariable];
534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554
    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.
555
    return os.which(_javaExecutable)?.path;
556 557 558
  }

  Map<String, String> _sdkManagerEnv;
559 560
  /// Returns an environment with the Java folder added to PATH for use in calling
  /// Java-based Android SDK commands such as sdkmanager and avdmanager.
561 562 563 564 565 566 567 568 569 570 571 572 573
  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;
  }

574 575 576 577
  /// 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.');
578
    final ProcessResult result = processManager.runSync(<String>[sdkManagerPath, '--version'], environment: sdkManagerEnv);
579
    if (result.exitCode != 0) {
580 581
      printTrace('sdkmanager --version failed: exitCode: ${result.exitCode} stdout: ${result.stdout} stderr: ${result.stderr}');
      return null;
582 583 584 585
    }
    return result.stdout.trim();
  }

586
  @override
587 588 589 590
  String toString() => 'AndroidSdk: $directory';
}

class AndroidSdkVersion implements Comparable<AndroidSdkVersion> {
591 592
  AndroidSdkVersion._(
    this.sdk, {
593 594 595 596 597 598
    @required this.sdkLevel,
    @required this.platformName,
    @required this.buildToolsVersion,
  }) : assert(sdkLevel != null),
       assert(platformName != null),
       assert(buildToolsVersion != null);
599 600

  final AndroidSdk sdk;
601 602
  final int sdkLevel;
  final String platformName;
603
  final Version buildToolsVersion;
604

605
  String get buildToolsVersionName => buildToolsVersion.toString();
606 607 608 609 610

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

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

611
  List<String> validateSdkWellFormed() {
612 613 614
    if (_exists(androidJarPath) != null)
      return <String>[_exists(androidJarPath)];

615 616
    if (_canRun(aaptPath) != null)
      return <String>[_canRun(aaptPath)];
617 618

    return <String>[];
619 620 621
  }

  String getPlatformsPath(String itemName) {
622
    return fs.path.join(sdk.directory, 'platforms', platformName, itemName);
623 624
  }

625
  String getBuildToolsPath(String binaryName) {
626
    return fs.path.join(sdk.directory, 'build-tools', buildToolsVersionName, binaryName);
627 628
  }

629
  @override
630
  int compareTo(AndroidSdkVersion other) => sdkLevel - other.sdkLevel;
631

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

635
  String _exists(String path) {
636
    if (!fs.isFileSync(path))
637 638
      return 'Android SDK file not found: $path.';
    return null;
639
  }
640 641 642 643 644 645

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