Skip to content

Commit

Permalink
[flutter_tools] require cmdline-tools for android licenses (#82560)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonahwilliams committed May 22, 2021
1 parent c99f60b commit 0f929f9
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 57 deletions.
36 changes: 20 additions & 16 deletions packages/flutter_tools/lib/src/android/android_sdk.dart
Expand Up @@ -47,13 +47,19 @@ class AndroidSdk {
List<AndroidSdkVersion> _sdkVersions = <AndroidSdkVersion>[];
AndroidSdkVersion? _latestVersion;

/// Whether the `cmdline-tools` directory exists in the Android SDK.
///
/// This is required to use the newest SDK manager which only works with
/// the newer JDK.
bool get cmdlineToolsAvailable => directory.childDirectory('cmdline-tools').existsSync();

/// Whether the `platform-tools` or `cmdline-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 => directory.childDirectory('cmdline-tools').existsSync()
bool get platformToolsAvailable => cmdlineToolsAvailable
|| directory.childDirectory('platform-tools').existsSync();

/// Whether the `licenses` directory exists in the Android SDK.
Expand Down Expand Up @@ -262,7 +268,7 @@ class AndroidSdk {
return null;
}

String? getCmdlineToolsPath(String binaryName) {
String? getCmdlineToolsPath(String binaryName, {bool skipOldTools = false}) {
// First look for the latest version of the command-line tools
final File cmdlineToolsLatestBinary = directory
.childDirectory('cmdline-tools')
Expand Down Expand Up @@ -301,6 +307,9 @@ class AndroidSdk {
}
}
}
if (skipOldTools) {
return null;
}

// Finally fallback to the old SDK tools
final File toolsBinary = directory.childDirectory('tools').childDirectory('bin').childFile(binaryName);
Expand Down Expand Up @@ -386,23 +395,15 @@ class AndroidSdk {
}

/// 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.
String get sdkManagerPath {
String? get sdkManagerPath {
final String executable = globals.platform.isWindows
? 'sdkmanager.bat'
: 'sdkmanager';
final String? path = getCmdlineToolsPath(executable);
final String? path = getCmdlineToolsPath(executable, skipOldTools: true);
if (path != null) {
return path;
}
// If no binary was found, return the default location
return directory
.childDirectory('tools')
.childDirectory('bin')
.childFile(executable)
.path;
return null;
}

/// First try Java bundled with Android Studio, then sniff JAVA_HOME, then fallback to PATH.
Expand Down Expand Up @@ -468,11 +469,14 @@ class AndroidSdk {

/// Returns the version of the Android SDK manager tool or null if not found.
String? get sdkManagerVersion {
if (!globals.processManager.canRun(sdkManagerPath)) {
throwToolExit('Android sdkmanager not found. Update to the latest Android SDK to resolve this.');
if (sdkManagerPath == null || !globals.processManager.canRun(sdkManagerPath)) {
throwToolExit(
'Android sdkmanager not found. Update to the latest Android SDK and ensure that '
'the cmdline-tools are installed to resolve this.'
);
}
final RunResult result = globals.processUtils.runSync(
<String>[sdkManagerPath, '--version'],
<String>[sdkManagerPath!, '--version'],
environment: sdkManagerEnv,
);
if (result.exitCode != 0) {
Expand Down
24 changes: 17 additions & 7 deletions packages/flutter_tools/lib/src/android/android_workflow.dart
Expand Up @@ -185,6 +185,10 @@ class AndroidValidator extends DoctorValidator {
}
return ValidationResult(ValidationType.missing, messages);
}
if (!androidSdk.cmdlineToolsAvailable) {
messages.add(const ValidationMessage.error('cmdline-tools component is missing'));
return ValidationResult(ValidationType.missing, messages);
}

if (androidSdk.licensesAvailable && !androidSdk.platformToolsAvailable) {
messages.add(ValidationMessage.hint(_userMessages.androidSdkLicenseOnly(kAndroidHome)));
Expand All @@ -199,7 +203,7 @@ class AndroidValidator extends DoctorValidator {
if (androidSdkLatestVersion.sdkLevel < kAndroidSdkMinVersion || androidSdkLatestVersion.buildToolsVersion < kAndroidSdkBuildToolsMinVersion) {
messages.add(ValidationMessage.error(
_userMessages.androidSdkBuildToolsOutdated(
_androidSdk!.sdkManagerPath,
_androidSdk!.sdkManagerPath!,
kAndroidSdkMinVersion,
kAndroidSdkBuildToolsMinVersion.toString(),
_platform,
Expand Down Expand Up @@ -250,7 +254,7 @@ class AndroidValidator extends DoctorValidator {
messages.add(ValidationMessage(_userMessages.androidJdkLocation(javaBinary)));

// Check JDK version.
if (! await _checkJavaVersion(javaBinary, messages)) {
if (!await _checkJavaVersion(javaBinary, messages)) {
return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
}

Expand Down Expand Up @@ -384,7 +388,7 @@ class AndroidLicenseValidator extends DoctorValidator {

try {
final Process process = await _processManager.start(
<String>[_androidSdk.sdkManagerPath, '--licenses'],
<String>[_androidSdk.sdkManagerPath!, '--licenses'],
environment: _androidSdk.sdkManagerEnv,
);
process.stdin.write('n\n');
Expand Down Expand Up @@ -416,12 +420,15 @@ class AndroidLicenseValidator extends DoctorValidator {
}

if (!_canRunSdkManager()) {
throwToolExit(_userMessages.androidMissingSdkManager(_androidSdk.sdkManagerPath, _platform));
throwToolExit(
'Android sdkmanager not found. Update to the latest Android SDK and ensure that '
'the cmdline-tools are installed to resolve this.'
);
}

try {
final Process process = await _processManager.start(
<String>[_androidSdk.sdkManagerPath, '--licenses'],
<String>[_androidSdk.sdkManagerPath!, '--licenses'],
environment: _androidSdk.sdkManagerEnv,
);

Expand Down Expand Up @@ -452,15 +459,18 @@ class AndroidLicenseValidator extends DoctorValidator {
return exitCode == 0;
} on ProcessException catch (e) {
throwToolExit(_userMessages.androidCannotRunSdkManager(
_androidSdk.sdkManagerPath,
_androidSdk.sdkManagerPath!,
e.toString(),
_platform,
));
}
}

bool _canRunSdkManager() {
final String sdkManagerPath = _androidSdk.sdkManagerPath;
final String? sdkManagerPath = _androidSdk.sdkManagerPath;
if (sdkManagerPath == null) {
return false;
}
return _processManager.canRun(sdkManagerPath);
}
}
Expand Up @@ -80,7 +80,7 @@ void main() {
});

testUsingContext('returns sdkmanager path under cmdline tools (highest version) on Linux/macOS', () {
sdkDir = createSdkDirectory(fileSystem: fileSystem);
sdkDir = createSdkDirectory(fileSystem: fileSystem, withSdkManager: false);
config.setValue('android-sdk', sdkDir.path);

final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
Expand All @@ -99,71 +99,77 @@ void main() {
Config: () => config,
});

testUsingContext('Caches adb location after first access', () {
sdkDir = createSdkDirectory(fileSystem: fileSystem);
testUsingContext('Does not return sdkmanager under deprecated tools component', () {
sdkDir = createSdkDirectory(fileSystem: fileSystem, withSdkManager: false);
config.setValue('android-sdk', sdkDir.path);

final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
final File adbFile = fileSystem.file(
fileSystem.path.join(sdk.directory.path, 'cmdline-tools', 'adb.exe')
)..createSync(recursive: true);

expect(sdk.adbPath, fileSystem.path.join(sdk.directory.path, 'cmdline-tools', 'adb.exe'));

adbFile.deleteSync(recursive: true);
fileSystem.file(
fileSystem.path.join(sdk.directory.path, 'tools/bin/sdkmanager')
).createSync(recursive: true);

expect(sdk.adbPath, fileSystem.path.join(sdk.directory.path, 'cmdline-tools', 'adb.exe'));
expect(sdk.sdkManagerPath, null);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Platform: () => FakePlatform(operatingSystem: 'windows'),
Platform: () => FakePlatform(operatingSystem: 'linux'),
Config: () => config,
});

testUsingContext('returns sdkmanager.bat path under cmdline tools for windows', () {
sdkDir = createSdkDirectory(fileSystem: fileSystem);
testUsingContext('Can look up cmdline tool from deprecated tools path', () {
sdkDir = createSdkDirectory(fileSystem: fileSystem, withSdkManager: false);
config.setValue('android-sdk', sdkDir.path);

final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
fileSystem.file(
fileSystem.path.join(sdk.directory.path, 'cmdline-tools', 'latest', 'bin', 'sdkmanager.bat')
fileSystem.path.join(sdk.directory.path, 'tools/bin/foo')
).createSync(recursive: true);

expect(sdk.sdkManagerPath,
fileSystem.path.join(sdk.directory.path, 'cmdline-tools', 'latest', 'bin', 'sdkmanager.bat'));
expect(sdk.getCmdlineToolsPath('foo', skipOldTools: false), '/.tmp_rand0/flutter_mock_android_sdk.rand0/tools/bin/foo');
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Platform: () => FakePlatform(operatingSystem: 'windows'),
Platform: () => FakePlatform(operatingSystem: 'linux'),
Config: () => config,
});

testUsingContext("returns sdkmanager path under tools if cmdline doesn't exist", () {
testUsingContext('Caches adb location after first access', () {
sdkDir = createSdkDirectory(fileSystem: fileSystem);
config.setValue('android-sdk', sdkDir.path);

final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
final File adbFile = fileSystem.file(
fileSystem.path.join(sdk.directory.path, 'cmdline-tools', 'adb.exe')
)..createSync(recursive: true);

expect(sdk.sdkManagerPath, fileSystem.path.join(sdk.directory.path, 'tools', 'bin', 'sdkmanager'));
expect(sdk.adbPath, fileSystem.path.join(sdk.directory.path, 'cmdline-tools', 'adb.exe'));

adbFile.deleteSync(recursive: true);

expect(sdk.adbPath, fileSystem.path.join(sdk.directory.path, 'cmdline-tools', 'adb.exe'));
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Platform: () => FakePlatform(operatingSystem: 'windows'),
Config: () => config,
Platform: () => FakePlatform(operatingSystem: 'linux'),
});

testUsingContext("returns sdkmanager path under tools if cmdline doesn't exist on windows", () {
testUsingContext('returns sdkmanager.bat path under cmdline tools for windows', () {
sdkDir = createSdkDirectory(fileSystem: fileSystem);
config.setValue('android-sdk', sdkDir.path);

final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
fileSystem.file(
fileSystem.path.join(sdk.directory.path, 'cmdline-tools', 'latest', 'bin', 'sdkmanager.bat')
).createSync(recursive: true);

expect(sdk.sdkManagerPath, fileSystem.path.join(sdk.directory.path, 'tools', 'bin', 'sdkmanager.bat'));
expect(sdk.sdkManagerPath,
fileSystem.path.join(sdk.directory.path, 'cmdline-tools', 'latest', 'bin', 'sdkmanager.bat'));
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Config: () => config,
Platform: () => FakePlatform(operatingSystem: 'windows'),
Config: () => config,
});

testUsingContext('returns sdkmanager version', () {
Expand All @@ -172,7 +178,7 @@ void main() {
processManager.addCommand(
const FakeCommand(
command: <String>[
'/.tmp_rand0/flutter_mock_android_sdk.rand0/tools/bin/sdkmanager',
'/.tmp_rand0/flutter_mock_android_sdk.rand0/cmdline-tools/latest/bin/sdkmanager',
'--version',
],
stdout: '26.1.1\n',
Expand All @@ -193,7 +199,7 @@ void main() {
fileSystem: fileSystem,
);
processManager.addCommand(const FakeCommand(command: <String>[
'/.tmp_rand0/flutter_mock_android_sdk.rand0/tools/bin/sdkmanager',
'/.tmp_rand0/flutter_mock_android_sdk.rand0/cmdline-tools/latest/bin/sdkmanager',
'--version',
]));
config.setValue('android-sdk', sdkDir.path);
Expand All @@ -217,7 +223,7 @@ void main() {
processManager.addCommand(
const FakeCommand(
command: <String>[
'/.tmp_rand0/flutter_mock_android_sdk.rand0/tools/bin/sdkmanager',
'/.tmp_rand0/flutter_mock_android_sdk.rand0/cmdline-tools/latest/bin/sdkmanager',
'--version',
],
stdout: '\n',
Expand All @@ -239,7 +245,7 @@ void main() {
testUsingContext('throws on sdkmanager version check if sdkmanager not found', () {
sdkDir = createSdkDirectory(withSdkManager: false, fileSystem: fileSystem);
config.setValue('android-sdk', sdkDir.path);
processManager.excludedExecutables.add('/.tmp_rand0/flutter_mock_android_sdk.rand0/tools/bin/sdkmanager');
processManager.excludedExecutables.add('/.tmp_rand0/flutter_mock_android_sdk.rand0/cmdline-tools/latest/bin/sdkmanager');
final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();

expect(() => sdk.sdkManagerVersion, throwsToolExit());
Expand Down Expand Up @@ -387,7 +393,7 @@ Directory createSdkDirectory({
}

if (withSdkManager) {
_createSdkFile(dir, 'tools/bin/sdkmanager$bat');
_createSdkFile(dir, 'cmdline-tools/latest/bin/sdkmanager$bat');
}
return dir;
}
Expand Down
Expand Up @@ -289,9 +289,11 @@ Review licenses that have not been accepted (y/N)?
expect(licenseValidator.runLicenseManager(), throwsToolExit());
});

testWithoutContext('detects license-only SDK installation', () async {
sdk.licensesAvailable = true;
sdk.platformToolsAvailable = false;
testWithoutContext('detects license-only SDK installation with cmdline-tools', () async {
sdk
..licensesAvailable = true
..platformToolsAvailable = false
..cmdlineToolsAvailable = true;
final ValidationResult validationResult = await AndroidValidator(
androidStudio: null,
androidSdk: sdk,
Expand All @@ -304,8 +306,8 @@ Review licenses that have not been accepted (y/N)?

expect(validationResult.type, ValidationType.partial);
expect(
validationResult.messages.last.message,
UserMessages().androidSdkLicenseOnly(kAndroidHome),
validationResult.messages.map((ValidationMessage message) => message.message),
contains(contains(UserMessages().androidSdkLicenseOnly(kAndroidHome))),
);
});

Expand All @@ -323,6 +325,7 @@ Review licenses that have not been accepted (y/N)?
sdk
..licensesAvailable = true
..platformToolsAvailable = true
..cmdlineToolsAvailable = true
// Test with invalid SDK and build tools
..directory = fileSystem.directory('/foo/bar')
..sdkManagerPath = '/foo/bar/sdkmanager'
Expand Down Expand Up @@ -376,6 +379,30 @@ Review licenses that have not been accepted (y/N)?
);
});

testWithoutContext('detects missing cmdline tools', () async {
sdk
..licensesAvailable = true
..platformToolsAvailable = true
..cmdlineToolsAvailable = false;

final AndroidValidator androidValidator = AndroidValidator(
androidStudio: null,
androidSdk: sdk,
fileSystem: fileSystem,
logger: logger,
processManager: processManager,
platform: FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
userMessages: UserMessages(),
);

final ValidationResult validationResult = await androidValidator.validate();
expect(validationResult.type, ValidationType.missing);
expect(
validationResult.messages.last.message,
'cmdline-tools component is missing',
);
});

testWithoutContext('detects minimum required java version', () async {
// Test with older version of JDK
const String javaVersionText = 'openjdk version "1.7.0_212"';
Expand All @@ -393,6 +420,7 @@ Review licenses that have not been accepted (y/N)?
sdk
..licensesAvailable = true
..platformToolsAvailable = true
..cmdlineToolsAvailable = true
..directory = fileSystem.directory('/foo/bar')
..sdkManagerPath = '/foo/bar/sdkmanager';
sdk.latestVersion = sdkVersion;
Expand Down Expand Up @@ -457,6 +485,9 @@ class FakeAndroidSdk extends Fake implements AndroidSdk {
@override
bool platformToolsAvailable;

@override
bool cmdlineToolsAvailable;

@override
Directory directory;

Expand Down

0 comments on commit 0f929f9

Please sign in to comment.