| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | /// @docImport 'application_package.dart'; |
| 6 | library; |
| 7 | |
| 8 | import '../base/common.dart'; |
| 9 | import '../base/config.dart'; |
| 10 | import '../base/file_system.dart'; |
| 11 | import '../base/platform.dart'; |
| 12 | import '../base/process.dart'; |
| 13 | import '../base/version.dart'; |
| 14 | import '../convert.dart'; |
| 15 | import '../globals.dart' as globals; |
| 16 | import 'java.dart'; |
| 17 | |
| 18 | // ANDROID_SDK_ROOT is deprecated. |
| 19 | // See https://developer.android.com/studio/command-line/variables.html#envar |
| 20 | const kAndroidSdkRoot = 'ANDROID_SDK_ROOT' ; |
| 21 | const kAndroidHome = 'ANDROID_HOME' ; |
| 22 | |
| 23 | // No official environment variable for the NDK root is documented: |
| 24 | // https://developer.android.com/tools/variables#envar |
| 25 | // The follow three seem to be most commonly used. |
| 26 | const kAndroidNdkHome = 'ANDROID_NDK_HOME' ; |
| 27 | const kAndroidNdkPath = 'ANDROID_NDK_PATH' ; |
| 28 | const kAndroidNdkRoot = 'ANDROID_NDK_ROOT' ; |
| 29 | |
| 30 | final _numberedAndroidPlatformRe = RegExp(r'^android-([0-9]+)$' ); |
| 31 | final _sdkVersionRe = RegExp(r'^ro.build.version.sdk=([0-9]+)$' ); |
| 32 | |
| 33 | // Android SDK layout: |
| 34 | |
| 35 | // $ANDROID_HOME/platform-tools/adb |
| 36 | |
| 37 | // $ANDROID_HOME/build-tools/19.1.0/aapt, dx, zipalign |
| 38 | // $ANDROID_HOME/build-tools/22.0.1/aapt |
| 39 | // $ANDROID_HOME/build-tools/23.0.2/aapt |
| 40 | // $ANDROID_HOME/build-tools/24.0.0-preview/aapt |
| 41 | // $ANDROID_HOME/build-tools/25.0.2/apksigner |
| 42 | |
| 43 | // $ANDROID_HOME/platforms/android-22/android.jar |
| 44 | // $ANDROID_HOME/platforms/android-23/android.jar |
| 45 | // $ANDROID_HOME/platforms/android-N/android.jar |
| 46 | class AndroidSdk { |
| 47 | AndroidSdk(this.directory, {Java? java, FileSystem? fileSystem}) : _java = java { |
| 48 | reinitialize(fileSystem: fileSystem); |
| 49 | } |
| 50 | |
| 51 | /// The Android SDK root directory. |
| 52 | final Directory directory; |
| 53 | |
| 54 | final Java? _java; |
| 55 | |
| 56 | var _sdkVersions = <AndroidSdkVersion>[]; |
| 57 | AndroidSdkVersion? _latestVersion; |
| 58 | |
| 59 | /// Whether the `cmdline-tools` directory exists in the Android SDK. |
| 60 | /// |
| 61 | /// This is required to use the newest SDK manager which only works with |
| 62 | /// the newer JDK. |
| 63 | bool get cmdlineToolsAvailable => directory.childDirectory('cmdline-tools' ).existsSync(); |
| 64 | |
| 65 | /// Whether the `platform-tools` or `cmdline-tools` directory exists in the Android SDK. |
| 66 | /// |
| 67 | /// It is possible to have an Android SDK folder that is missing this with |
| 68 | /// the expectation that it will be downloaded later, e.g. by gradle or the |
| 69 | /// sdkmanager. The [licensesAvailable] property should be used to determine |
| 70 | /// whether the licenses are at least possibly accepted. |
| 71 | bool get platformToolsAvailable => |
| 72 | cmdlineToolsAvailable || directory.childDirectory('platform-tools' ).existsSync(); |
| 73 | |
| 74 | /// Whether the `licenses` directory exists in the Android SDK. |
| 75 | /// |
| 76 | /// The existence of this folder normally indicates that the SDK licenses have |
| 77 | /// been accepted, e.g. via the sdkmanager, Android Studio, or by copying them |
| 78 | /// from another workstation such as in CI scenarios. If these files are valid |
| 79 | /// gradle or the sdkmanager will be able to download and use other parts of |
| 80 | /// the SDK on demand. |
| 81 | bool get licensesAvailable => directory.childDirectory('licenses' ).existsSync(); |
| 82 | |
| 83 | static AndroidSdk? locateAndroidSdk() { |
| 84 | String? findAndroidHomeDir() { |
| 85 | String? androidHomeDir; |
| 86 | if (globals.config.containsKey('android-sdk' )) { |
| 87 | androidHomeDir = globals.config.getValue('android-sdk' ) as String?; |
| 88 | } else if (globals.platform.environment.containsKey(kAndroidHome)) { |
| 89 | androidHomeDir = globals.platform.environment[kAndroidHome]; |
| 90 | } else if (globals.platform.environment.containsKey(kAndroidSdkRoot)) { |
| 91 | androidHomeDir = globals.platform.environment[kAndroidSdkRoot]; |
| 92 | } else if (globals.platform.isLinux) { |
| 93 | if (globals.fsUtils.homeDirPath != null) { |
| 94 | androidHomeDir = globals.fs.path.join(globals.fsUtils.homeDirPath!, 'Android' , 'Sdk' ); |
| 95 | } |
| 96 | } else if (globals.platform.isMacOS) { |
| 97 | if (globals.fsUtils.homeDirPath != null) { |
| 98 | androidHomeDir = globals.fs.path.join( |
| 99 | globals.fsUtils.homeDirPath!, |
| 100 | 'Library' , |
| 101 | 'Android' , |
| 102 | 'sdk' , |
| 103 | ); |
| 104 | } |
| 105 | } else if (globals.platform.isWindows) { |
| 106 | if (globals.fsUtils.homeDirPath != null) { |
| 107 | androidHomeDir = globals.fs.path.join( |
| 108 | globals.fsUtils.homeDirPath!, |
| 109 | 'AppData' , |
| 110 | 'Local' , |
| 111 | 'Android' , |
| 112 | 'sdk' , |
| 113 | ); |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | if (androidHomeDir != null) { |
| 118 | if (validSdkDirectory(androidHomeDir)) { |
| 119 | return androidHomeDir; |
| 120 | } |
| 121 | if (validSdkDirectory(globals.fs.path.join(androidHomeDir, 'sdk' ))) { |
| 122 | return globals.fs.path.join(androidHomeDir, 'sdk' ); |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | // in build-tools/$version/aapt |
| 127 | final List<File> aaptBins = globals.os.whichAll('aapt' ); |
| 128 | for (var aaptBin in aaptBins) { |
| 129 | // Make sure we're using the aapt from the SDK. |
| 130 | aaptBin = globals.fs.file(aaptBin.resolveSymbolicLinksSync()); |
| 131 | final String dir = aaptBin.parent.parent.parent.path; |
| 132 | if (validSdkDirectory(dir)) { |
| 133 | return dir; |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | // in platform-tools/adb |
| 138 | final List<File> adbBins = globals.os.whichAll('adb' ); |
| 139 | for (var adbBin in adbBins) { |
| 140 | // Make sure we're using the adb from the SDK. |
| 141 | adbBin = globals.fs.file(adbBin.resolveSymbolicLinksSync()); |
| 142 | final String dir = adbBin.parent.parent.path; |
| 143 | if (validSdkDirectory(dir)) { |
| 144 | return dir; |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | return null; |
| 149 | } |
| 150 | |
| 151 | final String? androidHomeDir = findAndroidHomeDir(); |
| 152 | if (androidHomeDir == null) { |
| 153 | // No dice. |
| 154 | globals.printTrace('Unable to locate an Android SDK.' ); |
| 155 | return null; |
| 156 | } |
| 157 | |
| 158 | return AndroidSdk(globals.fs.directory(androidHomeDir)); |
| 159 | } |
| 160 | |
| 161 | static bool validSdkDirectory(String dir) { |
| 162 | return sdkDirectoryHasLicenses(dir) || sdkDirectoryHasPlatformTools(dir); |
| 163 | } |
| 164 | |
| 165 | static bool sdkDirectoryHasPlatformTools(String dir) { |
| 166 | return globals.fs.isDirectorySync(globals.fs.path.join(dir, 'platform-tools' )); |
| 167 | } |
| 168 | |
| 169 | static bool sdkDirectoryHasLicenses(String dir) { |
| 170 | return globals.fs.isDirectorySync(globals.fs.path.join(dir, 'licenses' )); |
| 171 | } |
| 172 | |
| 173 | List<AndroidSdkVersion> get sdkVersions => _sdkVersions; |
| 174 | |
| 175 | AndroidSdkVersion? get latestVersion => _latestVersion; |
| 176 | |
| 177 | late final String? adbPath = getPlatformToolsPath(globals.platform.isWindows ? 'adb.exe' : 'adb' ); |
| 178 | |
| 179 | String? get emulatorPath => getEmulatorPath(); |
| 180 | |
| 181 | String? get avdManagerPath => getAvdManagerPath(); |
| 182 | |
| 183 | /// Locate the path for storing AVD emulator images. Returns null if none found. |
| 184 | String? getAvdPath() { |
| 185 | final String? avdHome = globals.platform.environment['ANDROID_AVD_HOME' ]; |
| 186 | final String? home = globals.platform.environment['HOME' ]; |
| 187 | final searchPaths = <String>[ |
| 188 | if (avdHome != null) avdHome, |
| 189 | if (home != null) globals.fs.path.join(home, '.android' , 'avd' ), |
| 190 | ]; |
| 191 | |
| 192 | if (globals.platform.isWindows) { |
| 193 | final String? homeDrive = globals.platform.environment['HOMEDRIVE' ]; |
| 194 | final String? homePath = globals.platform.environment['HOMEPATH' ]; |
| 195 | |
| 196 | if (homeDrive != null && homePath != null) { |
| 197 | // Can't use path.join for HOMEDRIVE/HOMEPATH |
| 198 | // https://github.com/dart-lang/path/issues/37 |
| 199 | final String home = homeDrive + homePath; |
| 200 | searchPaths.add(globals.fs.path.join(home, '.android' , 'avd' )); |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | for (final searchPath in searchPaths) { |
| 205 | if (globals.fs.directory(searchPath).existsSync()) { |
| 206 | return searchPath; |
| 207 | } |
| 208 | } |
| 209 | return null; |
| 210 | } |
| 211 | |
| 212 | Directory get _platformsDir => directory.childDirectory('platforms' ); |
| 213 | |
| 214 | Iterable<Directory> get _platforms { |
| 215 | Iterable<Directory> platforms = <Directory>[]; |
| 216 | if (_platformsDir.existsSync()) { |
| 217 | platforms = _platformsDir.listSync().whereType<Directory>(); |
| 218 | } |
| 219 | return platforms; |
| 220 | } |
| 221 | |
| 222 | /// Validate the Android SDK. This returns an empty list if there are no |
| 223 | /// issues; otherwise, it returns a list of issues found. |
| 224 | List<String> validateSdkWellFormed() { |
| 225 | if (adbPath == null || !globals.processManager.canRun(adbPath)) { |
| 226 | return <String>['Android SDK file not found: ${adbPath ?? 'adb' }.' ]; |
| 227 | } |
| 228 | |
| 229 | if (sdkVersions.isEmpty || latestVersion == null) { |
| 230 | final msg = StringBuffer('No valid Android SDK platforms found in ${_platformsDir.path}.' ); |
| 231 | if (_platforms.isEmpty) { |
| 232 | msg.write(' Directory was empty.' ); |
| 233 | } else { |
| 234 | msg.write(' Candidates were:\n' ); |
| 235 | msg.write(_platforms.map((Directory dir) => ' - ${dir.basename}' ).join('\n' )); |
| 236 | } |
| 237 | return <String>[msg.toString()]; |
| 238 | } |
| 239 | |
| 240 | if (directory.absolute.path.contains(' ' )) { |
| 241 | final androidSdkSpaceWarning = |
| 242 | 'Android SDK location currently ' |
| 243 | 'contains spaces, which is not supported by the Android SDK as it ' |
| 244 | 'causes problems with NDK tools. Try moving it from ' |
| 245 | ' ${directory.absolute.path} to a path without spaces.' ; |
| 246 | return <String>[androidSdkSpaceWarning]; |
| 247 | } |
| 248 | |
| 249 | return latestVersion!.validateSdkWellFormed(); |
| 250 | } |
| 251 | |
| 252 | String? getPlatformToolsPath(String binaryName) { |
| 253 | final File cmdlineToolsBinary = directory.childDirectory('cmdline-tools' ).childFile(binaryName); |
| 254 | if (cmdlineToolsBinary.existsSync()) { |
| 255 | return cmdlineToolsBinary.path; |
| 256 | } |
| 257 | final File platformToolBinary = directory |
| 258 | .childDirectory('platform-tools' ) |
| 259 | .childFile(binaryName); |
| 260 | if (platformToolBinary.existsSync()) { |
| 261 | return platformToolBinary.path; |
| 262 | } |
| 263 | return null; |
| 264 | } |
| 265 | |
| 266 | String? getEmulatorPath() { |
| 267 | final binaryName = globals.platform.isWindows ? 'emulator.exe' : 'emulator' ; |
| 268 | // Emulator now lives inside "emulator" but used to live inside "tools" so |
| 269 | // try both. |
| 270 | final searchFolders = <String>['emulator' , 'tools' ]; |
| 271 | for (final folder in searchFolders) { |
| 272 | final File file = directory.childDirectory(folder).childFile(binaryName); |
| 273 | if (file.existsSync()) { |
| 274 | return file.path; |
| 275 | } |
| 276 | } |
| 277 | return null; |
| 278 | } |
| 279 | |
| 280 | String? getCmdlineToolsPath(String binaryName, {bool skipOldTools = false}) { |
| 281 | // First look for the latest version of the command-line tools |
| 282 | final File cmdlineToolsLatestBinary = directory |
| 283 | .childDirectory('cmdline-tools' ) |
| 284 | .childDirectory('latest' ) |
| 285 | .childDirectory('bin' ) |
| 286 | .childFile(binaryName); |
| 287 | if (cmdlineToolsLatestBinary.existsSync()) { |
| 288 | return cmdlineToolsLatestBinary.path; |
| 289 | } |
| 290 | |
| 291 | // Next look for the highest version of the command-line tools |
| 292 | final Directory cmdlineToolsDir = directory.childDirectory('cmdline-tools' ); |
| 293 | if (cmdlineToolsDir.existsSync()) { |
| 294 | final List<Version> cmdlineTools = cmdlineToolsDir |
| 295 | .listSync() |
| 296 | .whereType<Directory>() |
| 297 | .map((Directory subDirectory) { |
| 298 | try { |
| 299 | return Version.parse(subDirectory.basename); |
| 300 | } on Exception { |
| 301 | return null; |
| 302 | } |
| 303 | }) |
| 304 | .whereType<Version>() |
| 305 | .toList(); |
| 306 | cmdlineTools.sort(); |
| 307 | |
| 308 | for (final Version cmdlineToolsVersion in cmdlineTools.reversed) { |
| 309 | final File cmdlineToolsBinary = directory |
| 310 | .childDirectory('cmdline-tools' ) |
| 311 | .childDirectory(cmdlineToolsVersion.toString()) |
| 312 | .childDirectory('bin' ) |
| 313 | .childFile(binaryName); |
| 314 | if (cmdlineToolsBinary.existsSync()) { |
| 315 | return cmdlineToolsBinary.path; |
| 316 | } |
| 317 | } |
| 318 | } |
| 319 | if (skipOldTools) { |
| 320 | return null; |
| 321 | } |
| 322 | |
| 323 | // Finally fallback to the old SDK tools |
| 324 | final File toolsBinary = directory |
| 325 | .childDirectory('tools' ) |
| 326 | .childDirectory('bin' ) |
| 327 | .childFile(binaryName); |
| 328 | if (toolsBinary.existsSync()) { |
| 329 | return toolsBinary.path; |
| 330 | } |
| 331 | |
| 332 | return null; |
| 333 | } |
| 334 | |
| 335 | String? getAvdManagerPath() => |
| 336 | getCmdlineToolsPath(globals.platform.isWindows ? 'avdmanager.bat' : 'avdmanager' ); |
| 337 | |
| 338 | /// From https://developer.android.com/ndk/guides/other_build_systems. |
| 339 | static const _llvmHostDirectoryName = <String, String>{ |
| 340 | 'macos' : 'darwin-x86_64' , |
| 341 | 'linux' : 'linux-x86_64' , |
| 342 | 'windows' : 'windows-x86_64' , |
| 343 | }; |
| 344 | |
| 345 | /// Locates the binary path for an NDK binary. |
| 346 | /// |
| 347 | /// The order of resolution is as follows: |
| 348 | /// |
| 349 | /// 1. If [globals.config] defines an `'android-ndk'` use that. |
| 350 | /// 2. If the environment variable `ANDROID_NDK_HOME` is defined, use that. |
| 351 | /// 3. If the environment variable `ANDROID_NDK_PATH` is defined, use that. |
| 352 | /// 4. If the environment variable `ANDROID_NDK_ROOT` is defined, use that. |
| 353 | /// 5. Look for the default install location inside the Android SDK: |
| 354 | /// [directory]/ndk/\<version\>/. If multiple versions exist, use the |
| 355 | /// newest. |
| 356 | String? getNdkBinaryPath(String binaryName, {Platform? platform, Config? config}) { |
| 357 | platform ??= globals.platform; |
| 358 | config ??= globals.config; |
| 359 | Directory? findAndroidNdkHomeDir() { |
| 360 | String? androidNdkHomeDir; |
| 361 | if (config!.containsKey('android-ndk' )) { |
| 362 | androidNdkHomeDir = config.getValue('android-ndk' ) as String?; |
| 363 | } else if (platform!.environment.containsKey(kAndroidNdkHome)) { |
| 364 | androidNdkHomeDir = platform.environment[kAndroidNdkHome]; |
| 365 | } else if (platform.environment.containsKey(kAndroidNdkPath)) { |
| 366 | androidNdkHomeDir = platform.environment[kAndroidNdkPath]; |
| 367 | } else if (platform.environment.containsKey(kAndroidNdkRoot)) { |
| 368 | androidNdkHomeDir = platform.environment[kAndroidNdkRoot]; |
| 369 | } |
| 370 | if (androidNdkHomeDir != null) { |
| 371 | return directory.fileSystem.directory(androidNdkHomeDir); |
| 372 | } |
| 373 | |
| 374 | // Look for the default install location of the NDK inside the Android |
| 375 | // SDK when installed through `sdkmanager` or Android studio. |
| 376 | final Directory ndk = directory.childDirectory('ndk' ); |
| 377 | if (!ndk.existsSync()) { |
| 378 | return null; |
| 379 | } |
| 380 | final List<Version> ndkVersions = |
| 381 | ndk |
| 382 | .listSync() |
| 383 | .map((FileSystemEntity entity) { |
| 384 | try { |
| 385 | return Version.parse(entity.basename); |
| 386 | } on Exception { |
| 387 | return null; |
| 388 | } |
| 389 | }) |
| 390 | .whereType<Version>() |
| 391 | .toList() |
| 392 | // Use latest NDK first. |
| 393 | ..sort((Version a, Version b) => -a.compareTo(b)); |
| 394 | if (ndkVersions.isEmpty) { |
| 395 | return null; |
| 396 | } |
| 397 | return ndk.childDirectory(ndkVersions.first.toString()); |
| 398 | } |
| 399 | |
| 400 | final Directory? androidNdkHomeDir = findAndroidNdkHomeDir(); |
| 401 | if (androidNdkHomeDir == null) { |
| 402 | return null; |
| 403 | } |
| 404 | final File executable = androidNdkHomeDir |
| 405 | .childDirectory('toolchains' ) |
| 406 | .childDirectory('llvm' ) |
| 407 | .childDirectory('prebuilt' ) |
| 408 | .childDirectory(_llvmHostDirectoryName[platform.operatingSystem]!) |
| 409 | .childDirectory('bin' ) |
| 410 | .childFile(binaryName); |
| 411 | if (executable.existsSync()) { |
| 412 | // LLVM missing in this NDK version. |
| 413 | return executable.path; |
| 414 | } |
| 415 | return null; |
| 416 | } |
| 417 | |
| 418 | String? getNdkClangPath({Platform? platform, Config? config}) { |
| 419 | platform ??= globals.platform; |
| 420 | return getNdkBinaryPath( |
| 421 | platform.isWindows ? 'clang.exe' : 'clang' , |
| 422 | platform: platform, |
| 423 | config: config, |
| 424 | ); |
| 425 | } |
| 426 | |
| 427 | String? getNdkArPath({Platform? platform, Config? config}) { |
| 428 | platform ??= globals.platform; |
| 429 | return getNdkBinaryPath( |
| 430 | platform.isWindows ? 'llvm-ar.exe' : 'llvm-ar' , |
| 431 | platform: platform, |
| 432 | config: config, |
| 433 | ); |
| 434 | } |
| 435 | |
| 436 | String? getNdkLdPath({Platform? platform, Config? config}) { |
| 437 | platform ??= globals.platform; |
| 438 | return getNdkBinaryPath( |
| 439 | platform.isWindows ? 'ld.lld.exe' : 'ld.lld' , |
| 440 | platform: platform, |
| 441 | config: config, |
| 442 | ); |
| 443 | } |
| 444 | |
| 445 | /// Sets up various paths used internally. |
| 446 | /// |
| 447 | /// This method should be called in a case where the tooling may have updated |
| 448 | /// SDK artifacts, such as after running a gradle build. |
| 449 | void reinitialize({FileSystem? fileSystem}) { |
| 450 | var buildTools = <Version>[]; // 19.1.0, 22.0.1, ... |
| 451 | |
| 452 | final Directory buildToolsDir = directory.childDirectory('build-tools' ); |
| 453 | if (buildToolsDir.existsSync()) { |
| 454 | buildTools = buildToolsDir |
| 455 | .listSync() |
| 456 | .map((FileSystemEntity entity) { |
| 457 | try { |
| 458 | return Version.parse(entity.basename); |
| 459 | } on Exception { |
| 460 | return null; |
| 461 | } |
| 462 | }) |
| 463 | .whereType<Version>() |
| 464 | .toList(); |
| 465 | } |
| 466 | |
| 467 | // Match up platforms with the best corresponding build-tools. |
| 468 | _sdkVersions = _platforms |
| 469 | .map<AndroidSdkVersion?>((Directory platformDir) { |
| 470 | final String platformName = platformDir.basename; |
| 471 | int platformVersion; |
| 472 | |
| 473 | try { |
| 474 | final Match? numberedVersion = _numberedAndroidPlatformRe.firstMatch(platformName); |
| 475 | if (numberedVersion != null) { |
| 476 | platformVersion = int.parse(numberedVersion.group(1)!); |
| 477 | } else { |
| 478 | final String buildProps = platformDir.childFile('build.prop' ).readAsStringSync(); |
| 479 | final Iterable<Match> versionMatches = const LineSplitter() |
| 480 | .convert(buildProps) |
| 481 | .map<RegExpMatch?>(_sdkVersionRe.firstMatch) |
| 482 | .whereType<Match>(); |
| 483 | |
| 484 | if (versionMatches.isEmpty) { |
| 485 | return null; |
| 486 | } |
| 487 | |
| 488 | final String? versionString = versionMatches.first.group(1); |
| 489 | if (versionString == null) { |
| 490 | return null; |
| 491 | } |
| 492 | platformVersion = int.parse(versionString); |
| 493 | } |
| 494 | } on Exception { |
| 495 | return null; |
| 496 | } |
| 497 | |
| 498 | Version? buildToolsVersion = Version.primary( |
| 499 | buildTools.where((Version version) { |
| 500 | return version.major == platformVersion; |
| 501 | }).toList(), |
| 502 | ); |
| 503 | |
| 504 | buildToolsVersion ??= Version.primary(buildTools); |
| 505 | |
| 506 | if (buildToolsVersion == null) { |
| 507 | return null; |
| 508 | } |
| 509 | |
| 510 | return AndroidSdkVersion._( |
| 511 | this, |
| 512 | sdkLevel: platformVersion, |
| 513 | platformName: platformName, |
| 514 | buildToolsVersion: buildToolsVersion, |
| 515 | fileSystem: fileSystem ?? globals.fs, |
| 516 | ); |
| 517 | }) |
| 518 | .whereType<AndroidSdkVersion>() |
| 519 | .toList(); |
| 520 | |
| 521 | _sdkVersions.sort(); |
| 522 | |
| 523 | _latestVersion = _sdkVersions.isEmpty ? null : _sdkVersions.last; |
| 524 | } |
| 525 | |
| 526 | /// Returns the filesystem path of the Android SDK manager tool. |
| 527 | String? get sdkManagerPath { |
| 528 | final executable = globals.platform.isWindows ? 'sdkmanager.bat' : 'sdkmanager' ; |
| 529 | return getCmdlineToolsPath(executable, skipOldTools: true); |
| 530 | } |
| 531 | |
| 532 | /// Returns the version of the Android SDK manager tool or null if not found. |
| 533 | String? get sdkManagerVersion { |
| 534 | if (sdkManagerPath == null || !globals.processManager.canRun(sdkManagerPath)) { |
| 535 | throwToolExit( |
| 536 | 'Android sdkmanager not found. Update to the latest Android SDK and ensure that ' |
| 537 | 'the cmdline-tools are installed to resolve this.' , |
| 538 | ); |
| 539 | } |
| 540 | final RunResult result = globals.processUtils.runSync(<String>[ |
| 541 | sdkManagerPath!, |
| 542 | '--version' , |
| 543 | ], environment: _java?.environment); |
| 544 | if (result.exitCode != 0) { |
| 545 | globals.printTrace( |
| 546 | 'sdkmanager --version failed: exitCode: ${result.exitCode} stdout: ${result.stdout} stderr: ${result.stderr}' , |
| 547 | ); |
| 548 | return null; |
| 549 | } |
| 550 | return result.stdout.trim(); |
| 551 | } |
| 552 | |
| 553 | @override |
| 554 | String toString() => 'AndroidSdk: $directory' ; |
| 555 | } |
| 556 | |
| 557 | class AndroidSdkVersion implements Comparable<AndroidSdkVersion> { |
| 558 | AndroidSdkVersion._( |
| 559 | this.sdk, { |
| 560 | required this.sdkLevel, |
| 561 | required this.platformName, |
| 562 | required this.buildToolsVersion, |
| 563 | required FileSystem fileSystem, |
| 564 | }) : _fileSystem = fileSystem; |
| 565 | |
| 566 | final AndroidSdk sdk; |
| 567 | final int sdkLevel; |
| 568 | final String platformName; |
| 569 | final Version buildToolsVersion; |
| 570 | |
| 571 | final FileSystem _fileSystem; |
| 572 | |
| 573 | String get buildToolsVersionName => buildToolsVersion.toString(); |
| 574 | |
| 575 | String get androidJarPath => getPlatformsPath('android.jar' ); |
| 576 | |
| 577 | /// Return the path to the android application package tool. |
| 578 | /// |
| 579 | /// This is used to dump the xml in order to launch built android applications. |
| 580 | /// |
| 581 | /// See also: |
| 582 | /// * [AndroidApk.fromApk], which depends on this to determine application identifiers. |
| 583 | String get aaptPath => getBuildToolsPath('aapt' ); |
| 584 | |
| 585 | List<String> validateSdkWellFormed() { |
| 586 | final String? existsAndroidJarPath = _exists(androidJarPath); |
| 587 | if (existsAndroidJarPath != null) { |
| 588 | return <String>[existsAndroidJarPath]; |
| 589 | } |
| 590 | |
| 591 | final String? canRunAaptPath = _canRun(aaptPath); |
| 592 | if (canRunAaptPath != null) { |
| 593 | return <String>[canRunAaptPath]; |
| 594 | } |
| 595 | |
| 596 | return <String>[]; |
| 597 | } |
| 598 | |
| 599 | String getPlatformsPath(String itemName) { |
| 600 | return sdk.directory |
| 601 | .childDirectory('platforms' ) |
| 602 | .childDirectory(platformName) |
| 603 | .childFile(itemName) |
| 604 | .path; |
| 605 | } |
| 606 | |
| 607 | String getBuildToolsPath(String binaryName) { |
| 608 | return sdk.directory |
| 609 | .childDirectory('build-tools' ) |
| 610 | .childDirectory(buildToolsVersionName) |
| 611 | .childFile(binaryName) |
| 612 | .path; |
| 613 | } |
| 614 | |
| 615 | @override |
| 616 | int compareTo(AndroidSdkVersion other) => sdkLevel - other.sdkLevel; |
| 617 | |
| 618 | @override |
| 619 | String toString() => |
| 620 | '[ ${sdk.directory}, SDK version $sdkLevel, build-tools $buildToolsVersionName]' ; |
| 621 | |
| 622 | String? _exists(String path) { |
| 623 | if (!_fileSystem.isFileSync(path)) { |
| 624 | return 'Android SDK file not found: $path.' ; |
| 625 | } |
| 626 | return null; |
| 627 | } |
| 628 | |
| 629 | String? _canRun(String path) { |
| 630 | if (!globals.processManager.canRun(path)) { |
| 631 | return 'Android SDK file not found: $path.' ; |
| 632 | } |
| 633 | return null; |
| 634 | } |
| 635 | } |
| 636 | |