Unverified Commit 0f929f9f authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] require cmdline-tools for android licenses (#82560)

parent c99f60b8
...@@ -47,13 +47,19 @@ class AndroidSdk { ...@@ -47,13 +47,19 @@ class AndroidSdk {
List<AndroidSdkVersion> _sdkVersions = <AndroidSdkVersion>[]; List<AndroidSdkVersion> _sdkVersions = <AndroidSdkVersion>[];
AndroidSdkVersion? _latestVersion; 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. /// 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 /// 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 /// the expectation that it will be downloaded later, e.g. by gradle or the
/// sdkmanager. The [licensesAvailable] property should be used to determine /// sdkmanager. The [licensesAvailable] property should be used to determine
/// whether the licenses are at least possibly accepted. /// 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(); || directory.childDirectory('platform-tools').existsSync();
/// Whether the `licenses` directory exists in the Android SDK. /// Whether the `licenses` directory exists in the Android SDK.
...@@ -262,7 +268,7 @@ class AndroidSdk { ...@@ -262,7 +268,7 @@ class AndroidSdk {
return null; return null;
} }
String? getCmdlineToolsPath(String binaryName) { String? getCmdlineToolsPath(String binaryName, {bool skipOldTools = false}) {
// First look for the latest version of the command-line tools // First look for the latest version of the command-line tools
final File cmdlineToolsLatestBinary = directory final File cmdlineToolsLatestBinary = directory
.childDirectory('cmdline-tools') .childDirectory('cmdline-tools')
...@@ -301,6 +307,9 @@ class AndroidSdk { ...@@ -301,6 +307,9 @@ class AndroidSdk {
} }
} }
} }
if (skipOldTools) {
return null;
}
// Finally fallback to the old SDK tools // Finally fallback to the old SDK tools
final File toolsBinary = directory.childDirectory('tools').childDirectory('bin').childFile(binaryName); final File toolsBinary = directory.childDirectory('tools').childDirectory('bin').childFile(binaryName);
...@@ -386,23 +395,15 @@ class AndroidSdk { ...@@ -386,23 +395,15 @@ class AndroidSdk {
} }
/// Returns the filesystem path of the Android SDK manager tool. /// Returns the filesystem path of the Android SDK manager tool.
/// String? get sdkManagerPath {
/// The sdkmanager was previously in the tools directory but this component
/// was marked as obsolete in 3.6.
String get sdkManagerPath {
final String executable = globals.platform.isWindows final String executable = globals.platform.isWindows
? 'sdkmanager.bat' ? 'sdkmanager.bat'
: 'sdkmanager'; : 'sdkmanager';
final String? path = getCmdlineToolsPath(executable); final String? path = getCmdlineToolsPath(executable, skipOldTools: true);
if (path != null) { if (path != null) {
return path; return path;
} }
// If no binary was found, return the default location return null;
return directory
.childDirectory('tools')
.childDirectory('bin')
.childFile(executable)
.path;
} }
/// First try Java bundled with Android Studio, then sniff JAVA_HOME, then fallback to PATH. /// First try Java bundled with Android Studio, then sniff JAVA_HOME, then fallback to PATH.
...@@ -468,11 +469,14 @@ class AndroidSdk { ...@@ -468,11 +469,14 @@ class AndroidSdk {
/// Returns the version of the Android SDK manager tool or null if not found. /// Returns the version of the Android SDK manager tool or null if not found.
String? get sdkManagerVersion { String? get sdkManagerVersion {
if (!globals.processManager.canRun(sdkManagerPath)) { if (sdkManagerPath == null || !globals.processManager.canRun(sdkManagerPath)) {
throwToolExit('Android sdkmanager not found. Update to the latest Android SDK to resolve this.'); 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( final RunResult result = globals.processUtils.runSync(
<String>[sdkManagerPath, '--version'], <String>[sdkManagerPath!, '--version'],
environment: sdkManagerEnv, environment: sdkManagerEnv,
); );
if (result.exitCode != 0) { if (result.exitCode != 0) {
......
...@@ -185,6 +185,10 @@ class AndroidValidator extends DoctorValidator { ...@@ -185,6 +185,10 @@ class AndroidValidator extends DoctorValidator {
} }
return ValidationResult(ValidationType.missing, messages); 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) { if (androidSdk.licensesAvailable && !androidSdk.platformToolsAvailable) {
messages.add(ValidationMessage.hint(_userMessages.androidSdkLicenseOnly(kAndroidHome))); messages.add(ValidationMessage.hint(_userMessages.androidSdkLicenseOnly(kAndroidHome)));
...@@ -199,7 +203,7 @@ class AndroidValidator extends DoctorValidator { ...@@ -199,7 +203,7 @@ class AndroidValidator extends DoctorValidator {
if (androidSdkLatestVersion.sdkLevel < kAndroidSdkMinVersion || androidSdkLatestVersion.buildToolsVersion < kAndroidSdkBuildToolsMinVersion) { if (androidSdkLatestVersion.sdkLevel < kAndroidSdkMinVersion || androidSdkLatestVersion.buildToolsVersion < kAndroidSdkBuildToolsMinVersion) {
messages.add(ValidationMessage.error( messages.add(ValidationMessage.error(
_userMessages.androidSdkBuildToolsOutdated( _userMessages.androidSdkBuildToolsOutdated(
_androidSdk!.sdkManagerPath, _androidSdk!.sdkManagerPath!,
kAndroidSdkMinVersion, kAndroidSdkMinVersion,
kAndroidSdkBuildToolsMinVersion.toString(), kAndroidSdkBuildToolsMinVersion.toString(),
_platform, _platform,
...@@ -250,7 +254,7 @@ class AndroidValidator extends DoctorValidator { ...@@ -250,7 +254,7 @@ class AndroidValidator extends DoctorValidator {
messages.add(ValidationMessage(_userMessages.androidJdkLocation(javaBinary))); messages.add(ValidationMessage(_userMessages.androidJdkLocation(javaBinary)));
// Check JDK version. // Check JDK version.
if (! await _checkJavaVersion(javaBinary, messages)) { if (!await _checkJavaVersion(javaBinary, messages)) {
return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText); return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
} }
...@@ -384,7 +388,7 @@ class AndroidLicenseValidator extends DoctorValidator { ...@@ -384,7 +388,7 @@ class AndroidLicenseValidator extends DoctorValidator {
try { try {
final Process process = await _processManager.start( final Process process = await _processManager.start(
<String>[_androidSdk.sdkManagerPath, '--licenses'], <String>[_androidSdk.sdkManagerPath!, '--licenses'],
environment: _androidSdk.sdkManagerEnv, environment: _androidSdk.sdkManagerEnv,
); );
process.stdin.write('n\n'); process.stdin.write('n\n');
...@@ -416,12 +420,15 @@ class AndroidLicenseValidator extends DoctorValidator { ...@@ -416,12 +420,15 @@ class AndroidLicenseValidator extends DoctorValidator {
} }
if (!_canRunSdkManager()) { 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 { try {
final Process process = await _processManager.start( final Process process = await _processManager.start(
<String>[_androidSdk.sdkManagerPath, '--licenses'], <String>[_androidSdk.sdkManagerPath!, '--licenses'],
environment: _androidSdk.sdkManagerEnv, environment: _androidSdk.sdkManagerEnv,
); );
...@@ -452,7 +459,7 @@ class AndroidLicenseValidator extends DoctorValidator { ...@@ -452,7 +459,7 @@ class AndroidLicenseValidator extends DoctorValidator {
return exitCode == 0; return exitCode == 0;
} on ProcessException catch (e) { } on ProcessException catch (e) {
throwToolExit(_userMessages.androidCannotRunSdkManager( throwToolExit(_userMessages.androidCannotRunSdkManager(
_androidSdk.sdkManagerPath, _androidSdk.sdkManagerPath!,
e.toString(), e.toString(),
_platform, _platform,
)); ));
...@@ -460,7 +467,10 @@ class AndroidLicenseValidator extends DoctorValidator { ...@@ -460,7 +467,10 @@ class AndroidLicenseValidator extends DoctorValidator {
} }
bool _canRunSdkManager() { bool _canRunSdkManager() {
final String sdkManagerPath = _androidSdk.sdkManagerPath; final String? sdkManagerPath = _androidSdk.sdkManagerPath;
if (sdkManagerPath == null) {
return false;
}
return _processManager.canRun(sdkManagerPath); return _processManager.canRun(sdkManagerPath);
} }
} }
...@@ -80,7 +80,7 @@ void main() { ...@@ -80,7 +80,7 @@ void main() {
}); });
testUsingContext('returns sdkmanager path under cmdline tools (highest version) on Linux/macOS', () { 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); config.setValue('android-sdk', sdkDir.path);
final AndroidSdk sdk = AndroidSdk.locateAndroidSdk(); final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
...@@ -99,71 +99,77 @@ void main() { ...@@ -99,71 +99,77 @@ void main() {
Config: () => config, Config: () => config,
}); });
testUsingContext('Caches adb location after first access', () { testUsingContext('Does not return sdkmanager under deprecated tools component', () {
sdkDir = createSdkDirectory(fileSystem: fileSystem); sdkDir = createSdkDirectory(fileSystem: fileSystem, withSdkManager: false);
config.setValue('android-sdk', sdkDir.path); config.setValue('android-sdk', sdkDir.path);
final AndroidSdk sdk = AndroidSdk.locateAndroidSdk(); final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
final File adbFile = fileSystem.file( fileSystem.file(
fileSystem.path.join(sdk.directory.path, 'cmdline-tools', 'adb.exe') fileSystem.path.join(sdk.directory.path, 'tools/bin/sdkmanager')
)..createSync(recursive: true); ).createSync(recursive: true);
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')); expect(sdk.sdkManagerPath, null);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => fileSystem, FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
Platform: () => FakePlatform(operatingSystem: 'windows'), Platform: () => FakePlatform(operatingSystem: 'linux'),
Config: () => config, Config: () => config,
}); });
testUsingContext('returns sdkmanager.bat path under cmdline tools for windows', () { testUsingContext('Can look up cmdline tool from deprecated tools path', () {
sdkDir = createSdkDirectory(fileSystem: fileSystem); sdkDir = createSdkDirectory(fileSystem: fileSystem, withSdkManager: false);
config.setValue('android-sdk', sdkDir.path); config.setValue('android-sdk', sdkDir.path);
final AndroidSdk sdk = AndroidSdk.locateAndroidSdk(); final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
fileSystem.file( 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); ).createSync(recursive: true);
expect(sdk.sdkManagerPath, expect(sdk.getCmdlineToolsPath('foo', skipOldTools: false), '/.tmp_rand0/flutter_mock_android_sdk.rand0/tools/bin/foo');
fileSystem.path.join(sdk.directory.path, 'cmdline-tools', 'latest', 'bin', 'sdkmanager.bat'));
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => fileSystem, FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
Platform: () => FakePlatform(operatingSystem: 'windows'), Platform: () => FakePlatform(operatingSystem: 'linux'),
Config: () => config, Config: () => config,
}); });
testUsingContext("returns sdkmanager path under tools if cmdline doesn't exist", () { testUsingContext('Caches adb location after first access', () {
sdkDir = createSdkDirectory(fileSystem: fileSystem); sdkDir = createSdkDirectory(fileSystem: fileSystem);
config.setValue('android-sdk', sdkDir.path); config.setValue('android-sdk', sdkDir.path);
final AndroidSdk sdk = AndroidSdk.locateAndroidSdk(); 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>{ }, overrides: <Type, Generator>{
FileSystem: () => fileSystem, FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
Platform: () => FakePlatform(operatingSystem: 'windows'),
Config: () => config, 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); sdkDir = createSdkDirectory(fileSystem: fileSystem);
config.setValue('android-sdk', sdkDir.path); config.setValue('android-sdk', sdkDir.path);
final AndroidSdk sdk = AndroidSdk.locateAndroidSdk(); 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>{ }, overrides: <Type, Generator>{
FileSystem: () => fileSystem, FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
Config: () => config,
Platform: () => FakePlatform(operatingSystem: 'windows'), Platform: () => FakePlatform(operatingSystem: 'windows'),
Config: () => config,
}); });
testUsingContext('returns sdkmanager version', () { testUsingContext('returns sdkmanager version', () {
...@@ -172,7 +178,7 @@ void main() { ...@@ -172,7 +178,7 @@ void main() {
processManager.addCommand( processManager.addCommand(
const FakeCommand( const FakeCommand(
command: <String>[ 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', '--version',
], ],
stdout: '26.1.1\n', stdout: '26.1.1\n',
...@@ -193,7 +199,7 @@ void main() { ...@@ -193,7 +199,7 @@ void main() {
fileSystem: fileSystem, fileSystem: fileSystem,
); );
processManager.addCommand(const FakeCommand(command: <String>[ 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', '--version',
])); ]));
config.setValue('android-sdk', sdkDir.path); config.setValue('android-sdk', sdkDir.path);
...@@ -217,7 +223,7 @@ void main() { ...@@ -217,7 +223,7 @@ void main() {
processManager.addCommand( processManager.addCommand(
const FakeCommand( const FakeCommand(
command: <String>[ 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', '--version',
], ],
stdout: '\n', stdout: '\n',
...@@ -239,7 +245,7 @@ void main() { ...@@ -239,7 +245,7 @@ void main() {
testUsingContext('throws on sdkmanager version check if sdkmanager not found', () { testUsingContext('throws on sdkmanager version check if sdkmanager not found', () {
sdkDir = createSdkDirectory(withSdkManager: false, fileSystem: fileSystem); sdkDir = createSdkDirectory(withSdkManager: false, fileSystem: fileSystem);
config.setValue('android-sdk', sdkDir.path); 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(); final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
expect(() => sdk.sdkManagerVersion, throwsToolExit()); expect(() => sdk.sdkManagerVersion, throwsToolExit());
...@@ -387,7 +393,7 @@ Directory createSdkDirectory({ ...@@ -387,7 +393,7 @@ Directory createSdkDirectory({
} }
if (withSdkManager) { if (withSdkManager) {
_createSdkFile(dir, 'tools/bin/sdkmanager$bat'); _createSdkFile(dir, 'cmdline-tools/latest/bin/sdkmanager$bat');
} }
return dir; return dir;
} }
......
...@@ -289,9 +289,11 @@ Review licenses that have not been accepted (y/N)? ...@@ -289,9 +289,11 @@ Review licenses that have not been accepted (y/N)?
expect(licenseValidator.runLicenseManager(), throwsToolExit()); expect(licenseValidator.runLicenseManager(), throwsToolExit());
}); });
testWithoutContext('detects license-only SDK installation', () async { testWithoutContext('detects license-only SDK installation with cmdline-tools', () async {
sdk.licensesAvailable = true; sdk
sdk.platformToolsAvailable = false; ..licensesAvailable = true
..platformToolsAvailable = false
..cmdlineToolsAvailable = true;
final ValidationResult validationResult = await AndroidValidator( final ValidationResult validationResult = await AndroidValidator(
androidStudio: null, androidStudio: null,
androidSdk: sdk, androidSdk: sdk,
...@@ -304,8 +306,8 @@ Review licenses that have not been accepted (y/N)? ...@@ -304,8 +306,8 @@ Review licenses that have not been accepted (y/N)?
expect(validationResult.type, ValidationType.partial); expect(validationResult.type, ValidationType.partial);
expect( expect(
validationResult.messages.last.message, validationResult.messages.map((ValidationMessage message) => message.message),
UserMessages().androidSdkLicenseOnly(kAndroidHome), contains(contains(UserMessages().androidSdkLicenseOnly(kAndroidHome))),
); );
}); });
...@@ -323,6 +325,7 @@ Review licenses that have not been accepted (y/N)? ...@@ -323,6 +325,7 @@ Review licenses that have not been accepted (y/N)?
sdk sdk
..licensesAvailable = true ..licensesAvailable = true
..platformToolsAvailable = true ..platformToolsAvailable = true
..cmdlineToolsAvailable = true
// Test with invalid SDK and build tools // Test with invalid SDK and build tools
..directory = fileSystem.directory('/foo/bar') ..directory = fileSystem.directory('/foo/bar')
..sdkManagerPath = '/foo/bar/sdkmanager' ..sdkManagerPath = '/foo/bar/sdkmanager'
...@@ -376,6 +379,30 @@ Review licenses that have not been accepted (y/N)? ...@@ -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 { testWithoutContext('detects minimum required java version', () async {
// Test with older version of JDK // Test with older version of JDK
const String javaVersionText = 'openjdk version "1.7.0_212"'; const String javaVersionText = 'openjdk version "1.7.0_212"';
...@@ -393,6 +420,7 @@ Review licenses that have not been accepted (y/N)? ...@@ -393,6 +420,7 @@ Review licenses that have not been accepted (y/N)?
sdk sdk
..licensesAvailable = true ..licensesAvailable = true
..platformToolsAvailable = true ..platformToolsAvailable = true
..cmdlineToolsAvailable = true
..directory = fileSystem.directory('/foo/bar') ..directory = fileSystem.directory('/foo/bar')
..sdkManagerPath = '/foo/bar/sdkmanager'; ..sdkManagerPath = '/foo/bar/sdkmanager';
sdk.latestVersion = sdkVersion; sdk.latestVersion = sdkVersion;
...@@ -457,6 +485,9 @@ class FakeAndroidSdk extends Fake implements AndroidSdk { ...@@ -457,6 +485,9 @@ class FakeAndroidSdk extends Fake implements AndroidSdk {
@override @override
bool platformToolsAvailable; bool platformToolsAvailable;
@override
bool cmdlineToolsAvailable;
@override @override
Directory directory; Directory directory;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment