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';
6library;
7
8import '../base/common.dart';
9import '../base/config.dart';
10import '../base/file_system.dart';
11import '../base/platform.dart';
12import '../base/process.dart';
13import '../base/version.dart';
14import '../convert.dart';
15import '../globals.dart' as globals;
16import 'java.dart';
17
18// ANDROID_SDK_ROOT is deprecated.
19// See https://developer.android.com/studio/command-line/variables.html#envar
20const kAndroidSdkRoot = 'ANDROID_SDK_ROOT';
21const 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.
26const kAndroidNdkHome = 'ANDROID_NDK_HOME';
27const kAndroidNdkPath = 'ANDROID_NDK_PATH';
28const kAndroidNdkRoot = 'ANDROID_NDK_ROOT';
29
30final _numberedAndroidPlatformRe = RegExp(r'^android-([0-9]+)$');
31final _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
46class 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
557class 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