android_sdk.dart 18.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7 8
import 'package:meta/meta.dart';

9
import '../base/common.dart';
10
import '../base/file_system.dart';
11
import '../base/os.dart';
12
import '../base/platform.dart';
13
import '../base/process.dart';
14
import '../base/version.dart';
15
import '../convert.dart';
16
import '../globals.dart' as globals;
17
import 'android_studio.dart';
18

19 20
// ANDROID_HOME is deprecated.
// See https://developer.android.com/studio/command-line/variables.html#envar
21
const String kAndroidHome = 'ANDROID_HOME';
22
const String kAndroidSdkRoot = 'ANDROID_SDK_ROOT';
23

24 25
final RegExp _numberedAndroidPlatformRe = RegExp(r'^android-([0-9]+)$');
final RegExp _sdkVersionRe = RegExp(r'^ro.build.version.sdk=([0-9]+)$');
26

27
// Android SDK layout:
28

29
// $ANDROID_SDK_ROOT/platform-tools/adb
30

31 32 33 34 35
// $ANDROID_SDK_ROOT/build-tools/19.1.0/aapt, dx, zipalign
// $ANDROID_SDK_ROOT/build-tools/22.0.1/aapt
// $ANDROID_SDK_ROOT/build-tools/23.0.2/aapt
// $ANDROID_SDK_ROOT/build-tools/24.0.0-preview/aapt
// $ANDROID_SDK_ROOT/build-tools/25.0.2/apksigner
36

37 38 39
// $ANDROID_SDK_ROOT/platforms/android-22/android.jar
// $ANDROID_SDK_ROOT/platforms/android-23/android.jar
// $ANDROID_SDK_ROOT/platforms/android-N/android.jar
40
class AndroidSdk {
41
  AndroidSdk(this.directory) {
42
    reinitialize();
43 44
  }

45 46
  static const String _javaHomeEnvironmentVariable = 'JAVA_HOME';
  static const String _javaExecutable = 'java';
47

48 49
  /// The Android SDK root directory.
  final Directory directory;
50 51 52 53

  List<AndroidSdkVersion> _sdkVersions;
  AndroidSdkVersion _latestVersion;

54
  /// Whether the `platform-tools` or `cmdline-tools` directory exists in the Android SDK.
55 56 57 58 59
  ///
  /// 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.
60 61
  bool get platformToolsAvailable => directory.childDirectory('cmdline-tools').existsSync()
     || directory.childDirectory('platform-tools').existsSync();
62 63 64 65 66 67 68 69

  /// 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.
70
  bool get licensesAvailable => directory.childDirectory('licenses').existsSync();
71

72
  static AndroidSdk locateAndroidSdk() {
73 74
    String findAndroidHomeDir() {
      String androidHomeDir;
75 76 77 78 79 80 81
      if (globals.config.containsKey('android-sdk')) {
        androidHomeDir = globals.config.getValue('android-sdk') as String;
      } else if (globals.platform.environment.containsKey(kAndroidHome)) {
        androidHomeDir = globals.platform.environment[kAndroidHome];
      } else if (globals.platform.environment.containsKey(kAndroidSdkRoot)) {
        androidHomeDir = globals.platform.environment[kAndroidSdkRoot];
      } else if (globals.platform.isLinux) {
82 83 84 85 86 87
        if (globals.fsUtils.homeDirPath != null) {
          androidHomeDir = globals.fs.path.join(
            globals.fsUtils.homeDirPath,
            'Android',
            'Sdk',
          );
88
        }
89
      } else if (globals.platform.isMacOS) {
90 91 92 93 94 95 96
        if (globals.fsUtils.homeDirPath != null) {
          androidHomeDir = globals.fs.path.join(
            globals.fsUtils.homeDirPath,
            'Library',
            'Android',
            'sdk',
          );
97
        }
98
      } else if (globals.platform.isWindows) {
99 100 101 102 103 104 105 106
        if (globals.fsUtils.homeDirPath != null) {
          androidHomeDir = globals.fs.path.join(
            globals.fsUtils.homeDirPath,
            'AppData',
            'Local',
            'Android',
            'sdk',
          );
107
        }
108 109 110
      }

      if (androidHomeDir != null) {
111
        if (validSdkDirectory(androidHomeDir)) {
112
          return androidHomeDir;
113
        }
114 115
        if (validSdkDirectory(globals.fs.path.join(androidHomeDir, 'sdk'))) {
          return globals.fs.path.join(androidHomeDir, 'sdk');
116
        }
117 118 119
      }

      // in build-tools/$version/aapt
120
      final List<File> aaptBins = globals.os.whichAll('aapt');
121 122
      for (File aaptBin in aaptBins) {
        // Make sure we're using the aapt from the SDK.
123
        aaptBin = globals.fs.file(aaptBin.resolveSymbolicLinksSync());
124
        final String dir = aaptBin.parent.parent.parent.path;
125
        if (validSdkDirectory(dir)) {
126
          return dir;
127
        }
128 129 130
      }

      // in platform-tools/adb
131
      final List<File> adbBins = globals.os.whichAll('adb');
132 133
      for (File adbBin in adbBins) {
        // Make sure we're using the adb from the SDK.
134
        adbBin = globals.fs.file(adbBin.resolveSymbolicLinksSync());
135
        final String dir = adbBin.parent.parent.path;
136
        if (validSdkDirectory(dir)) {
137
          return dir;
138
        }
139 140 141 142 143 144 145 146
      }

      return null;
    }

    final String androidHomeDir = findAndroidHomeDir();
    if (androidHomeDir == null) {
      // No dice.
147
      globals.printTrace('Unable to locate an Android SDK.');
148
      return null;
149 150
    }

151
    return AndroidSdk(globals.fs.directory(androidHomeDir));
152 153 154
  }

  static bool validSdkDirectory(String dir) {
Lau Ching Jun's avatar
Lau Ching Jun committed
155
    return sdkDirectoryHasLicenses(dir) || sdkDirectoryHasPlatformTools(dir);
156 157 158
  }

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

Lau Ching Jun's avatar
Lau Ching Jun committed
162
  static bool sdkDirectoryHasLicenses(String dir) {
163
    return globals.fs.isDirectorySync(globals.fs.path.join(dir, 'licenses'));
164 165 166 167 168 169
  }

  List<AndroidSdkVersion> get sdkVersions => _sdkVersions;

  AndroidSdkVersion get latestVersion => _latestVersion;

170 171
  String get adbPath => _adbPath ??= getPlatformToolsPath(globals.platform.isWindows ? 'adb.exe' : 'adb');
  String _adbPath;
172

Danny Tuppeny's avatar
Danny Tuppeny committed
173
  String get emulatorPath => getEmulatorPath();
174

175 176
  String get avdManagerPath => getAvdManagerPath();

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
  /// Locate the path for storing AVD emulator images. Returns null if none found.
  String getAvdPath() {
    final List<String> searchPaths = <String>[
      globals.platform.environment['ANDROID_AVD_HOME'],
      if (globals.platform.environment['HOME'] != null)
        globals.fs.path.join(globals.platform.environment['HOME'], '.android', 'avd'),
    ];

    if (globals.platform.isWindows) {
      final String homeDrive = globals.platform.environment['HOMEDRIVE'];
      final String homePath = globals.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(globals.fs.path.join(home, '.android', 'avd'));
      }
    }

    return searchPaths.where((String p) => p != null).firstWhere(
      (String p) => globals.fs.directory(p).existsSync(),
      orElse: () => null,
    );
  }

203
  Directory get _platformsDir => directory.childDirectory('platforms');
204 205 206 207 208 209 210 211 212 213 214

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

215 216
  /// Validate the Android SDK. This returns an empty list if there are no
  /// issues; otherwise, it returns a list of issues found.
217
  List<String> validateSdkWellFormed() {
218
    if (adbPath == null || !globals.processManager.canRun(adbPath)) {
219
      return <String>['Android SDK file not found: ${adbPath ?? 'adb'}.'];
220
    }
221

222 223 224 225 226 227 228 229 230 231 232 233
    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()];
    }
234

235
    return latestVersion.validateSdkWellFormed();
236 237 238
  }

  String getPlatformToolsPath(String binaryName) {
239 240 241 242 243 244 245
    final File cmdlineToolsBinary = directory.childDirectory('cmdline-tools').childFile(binaryName);
    if (cmdlineToolsBinary.existsSync()) {
      return cmdlineToolsBinary.path;
    }
    final File platformToolBinary = directory.childDirectory('platform-tools').childFile(binaryName);
    if (platformToolBinary.existsSync()) {
      return platformToolBinary.path;
246
    }
247
    return null;
248 249
  }

Danny Tuppeny's avatar
Danny Tuppeny committed
250
  String getEmulatorPath() {
251
    final String binaryName = globals.platform.isWindows ? 'emulator.exe' : 'emulator';
Danny Tuppeny's avatar
Danny Tuppeny committed
252 253 254 255
    // 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) {
256 257 258
      final File file = directory.childDirectory(folder).childFile(binaryName);
      if (file.existsSync()) {
        return file.path;
259
      }
Danny Tuppeny's avatar
Danny Tuppeny committed
260 261
    }
    return null;
262 263
  }

264 265 266
  String getCmdlineToolsPath(String binaryName) {
    // First look for the latest version of the command-line tools
    final File cmdlineToolsLatestBinary = directory
267 268 269 270
      .childDirectory('cmdline-tools')
      .childDirectory('latest')
      .childDirectory('bin')
      .childFile(binaryName);
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
    if (cmdlineToolsLatestBinary.existsSync()) {
      return cmdlineToolsLatestBinary.path;
    }

    // Next look for the highest version of the command-line tools
    final Directory cmdlineToolsDir = directory.childDirectory('cmdline-tools');
    if (cmdlineToolsDir.existsSync()) {
      final List<Version> cmdlineTools = cmdlineToolsDir
        .listSync()
        .whereType<Directory>()
        .map((Directory subDirectory) {
          try {
            return Version.parse(subDirectory.basename);
          } on Exception {
            return null;
          }
        })
        .where((Version version) => version != null)
        .toList();
      cmdlineTools.sort();

      for (final Version cmdlineToolsVersion in cmdlineTools.reversed) {
        final File cmdlineToolsBinary = directory
          .childDirectory('cmdline-tools')
          .childDirectory(cmdlineToolsVersion.toString())
          .childDirectory('bin')
          .childFile(binaryName);
        if (cmdlineToolsBinary.existsSync()) {
          return cmdlineToolsBinary.path;
        }
      }
302
    }
303 304

    // Finally fallback to the old SDK tools
305 306 307
    final File toolsBinary = directory.childDirectory('tools').childDirectory('bin').childFile(binaryName);
    if (toolsBinary.existsSync()) {
      return toolsBinary.path;
308
    }
309

310 311 312
    return null;
  }

313 314
  String getAvdManagerPath() => getCmdlineToolsPath(globals.platform.isWindows ? 'avdmanager.bat' : 'avdmanager');

315 316 317 318 319
  /// 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() {
320
    List<Version> buildTools = <Version>[]; // 19.1.0, 22.0.1, ...
321

322
    final Directory buildToolsDir = directory.childDirectory('build-tools');
323
    if (buildToolsDir.existsSync()) {
324
      buildTools = buildToolsDir
325 326 327
        .listSync()
        .map((FileSystemEntity entity) {
          try {
328
            return Version.parse(entity.basename);
329
          } on Exception {
330 331 332 333 334 335 336
            return null;
          }
        })
        .where((Version version) => version != null)
        .toList();
    }

337
    // Match up platforms with the best corresponding build-tools.
338
    _sdkVersions = _platforms.map<AndroidSdkVersion>((Directory platformDir) {
339
      final String platformName = platformDir.basename;
340
      int platformVersion;
341 342

      try {
343 344 345 346 347 348 349
        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)
350
              .map<Match>(_sdkVersionRe.firstMatch)
351 352 353 354
              .firstWhere((Match match) => match != null)
              .group(1);
          platformVersion = int.parse(versionString);
        }
355
      } on Exception {
356 357 358
        return null;
      }

359 360
      Version buildToolsVersion = Version.primary(buildTools.where((Version version) {
        return version.major == platformVersion;
361 362
      }).toList());

363 364
      buildToolsVersion ??= Version.primary(buildTools);

365
      if (buildToolsVersion == null) {
366
        return null;
367
      }
368

369
      return AndroidSdkVersion._(
370
        this,
371 372 373
        sdkLevel: platformVersion,
        platformName: platformName,
        buildToolsVersion: buildToolsVersion,
374
        fileSystem: globals.fs,
375
      );
376 377 378 379 380 381 382
    }).where((AndroidSdkVersion version) => version != null).toList();

    _sdkVersions.sort();

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

383 384 385 386
  /// Returns the filesystem path of the Android SDK manager tool.
  ///
  /// The sdkmanager was previously in the tools directory but this component
  /// was marked as obsolete in 3.6.
387
  String get sdkManagerPath {
388 389 390
    final String executable = globals.platform.isWindows
      ? 'sdkmanager.bat'
      : 'sdkmanager';
391 392 393
    final String path = getCmdlineToolsPath(executable);
    if (path != null) {
      return path;
394
    }
395
    // If no binary was found, return the default location
396 397 398 399 400
    return directory
      .childDirectory('tools')
      .childDirectory('bin')
      .childFile(executable)
      .path;
401 402
  }

403
  /// First try Java bundled with Android Studio, then sniff JAVA_HOME, then fallback to PATH.
404 405 406 407 408 409 410 411
  static String findJavaBinary({
    @required AndroidStudio androidStudio,
    @required FileSystem fileSystem,
    @required OperatingSystemUtils operatingSystemUtils,
    @required Platform platform,
  }) {
    if (androidStudio?.javaPath != null) {
      return fileSystem.path.join(androidStudio.javaPath, 'bin', 'java');
412
    }
413

414
    final String javaHomeEnv = platform.environment[_javaHomeEnvironmentVariable];
415 416
    if (javaHomeEnv != null) {
      // Trust JAVA_HOME.
417
      return fileSystem.path.join(javaHomeEnv, 'bin', 'java');
418 419 420 421
    }

    // 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.
422
    if (platform.isMacOS) {
423
      try {
424
        final String javaHomeOutput = globals.processUtils.runSync(
425
          <String>['/usr/libexec/java_home', '-v', '1.8'],
426 427 428
          throwOnError: true,
          hideStdout: true,
        ).stdout.trim();
429
        if (javaHomeOutput != null) {
430 431
          if ((javaHomeOutput != null) && (javaHomeOutput.isNotEmpty)) {
            final String javaHome = javaHomeOutput.split('\n').last.trim();
432
            return fileSystem.path.join(javaHome, 'bin', 'java');
433 434
          }
        }
435
      } on Exception { /* ignore */ }
436 437 438
    }

    // Fallback to PATH based lookup.
439
    return operatingSystemUtils.which(_javaExecutable)?.path;
440 441 442
  }

  Map<String, String> _sdkManagerEnv;
443 444
  /// Returns an environment with the Java folder added to PATH for use in calling
  /// Java-based Android SDK commands such as sdkmanager and avdmanager.
445 446 447 448
  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>{};
449 450 451 452 453 454
      final String javaBinary = findJavaBinary(
        androidStudio: globals.androidStudio,
        fileSystem: globals.fs,
        operatingSystemUtils: globals.os,
        platform: globals.platform,
      );
455
      if (javaBinary != null) {
456 457 458
        _sdkManagerEnv['PATH'] = globals.fs.path.dirname(javaBinary) +
                                 globals.os.pathVarSeparator +
                                 globals.platform.environment['PATH'];
459 460 461 462 463
      }
    }
    return _sdkManagerEnv;
  }

464 465
  /// Returns the version of the Android SDK manager tool or null if not found.
  String get sdkManagerVersion {
466
    if (!globals.processManager.canRun(sdkManagerPath)) {
467
      throwToolExit('Android sdkmanager not found. Update to the latest Android SDK to resolve this.');
468
    }
469
    final RunResult result = globals.processUtils.runSync(
470 471 472
      <String>[sdkManagerPath, '--version'],
      environment: sdkManagerEnv,
    );
473
    if (result.exitCode != 0) {
474
      globals.printTrace('sdkmanager --version failed: exitCode: ${result.exitCode} stdout: ${result.stdout} stderr: ${result.stderr}');
475
      return null;
476 477 478 479
    }
    return result.stdout.trim();
  }

480
  @override
481 482 483 484
  String toString() => 'AndroidSdk: $directory';
}

class AndroidSdkVersion implements Comparable<AndroidSdkVersion> {
485 486
  AndroidSdkVersion._(
    this.sdk, {
487 488 489
    @required this.sdkLevel,
    @required this.platformName,
    @required this.buildToolsVersion,
490
    @required FileSystem fileSystem,
491 492
  }) : assert(sdkLevel != null),
       assert(platformName != null),
493 494
       assert(buildToolsVersion != null),
       _fileSystem = fileSystem;
495 496

  final AndroidSdk sdk;
497 498
  final int sdkLevel;
  final String platformName;
499
  final Version buildToolsVersion;
500

501 502
  final FileSystem _fileSystem;

503
  String get buildToolsVersionName => buildToolsVersion.toString();
504 505 506

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

507 508 509 510 511 512
  /// Return the path to the android application package tool.
  ///
  /// This is used to dump the xml in order to launch built android applications.
  ///
  /// See also:
  ///   * [AndroidApk.fromApk], which depends on this to determine application identifiers.
513 514
  String get aaptPath => getBuildToolsPath('aapt');

515
  List<String> validateSdkWellFormed() {
516
    if (_exists(androidJarPath) != null) {
517
      return <String>[_exists(androidJarPath)];
518
    }
519

520
    if (_canRun(aaptPath) != null) {
521
      return <String>[_canRun(aaptPath)];
522
    }
523 524

    return <String>[];
525 526 527
  }

  String getPlatformsPath(String itemName) {
528
    return sdk.directory.childDirectory('platforms').childDirectory(platformName).childFile(itemName).path;
529 530
  }

531
  String getBuildToolsPath(String binaryName) {
532
   return sdk.directory.childDirectory('build-tools').childDirectory(buildToolsVersionName).childFile(binaryName).path;
533 534
  }

535
  @override
536
  int compareTo(AndroidSdkVersion other) => sdkLevel - other.sdkLevel;
537

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

541
  String _exists(String path) {
542
    if (!_fileSystem.isFileSync(path)) {
543
      return 'Android SDK file not found: $path.';
544
    }
545
    return null;
546
  }
547 548

  String _canRun(String path) {
549
    if (!globals.processManager.canRun(path)) {
550
      return 'Android SDK file not found: $path.';
551
    }
552 553
    return null;
  }
554
}