Skip to content

Commit 614df69

Browse files
authored
Android license detector in doctor, take two (flutter#14783)
* Revert "Revert "Add android license verification to doctor and some refactoring" (flutter#14727)" This reverts commit d260294. * Add tests, fix sdkManagerEnv and use it consistently, and rearrange Status object model * AnsiSpinner needs to leave the cursor where it found it. * fix tests * Const constructor warning only shows up on windows...? * Avoid crash if we can't find the home directory * Make pathVarSeparator return a string in the mock * Implement review comments * Fix out-of-order problem on stop
1 parent 97091f5 commit 614df69

File tree

14 files changed

+541
-128
lines changed

14 files changed

+541
-128
lines changed

packages/flutter_tools/lib/runner.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ Future<String> _doctorText() async {
234234

235235
appContext.setVariable(Logger, logger);
236236

237-
await appContext.runInZone(() => doctor.diagnose());
237+
await appContext.runInZone(() => doctor.diagnose(verbose: true));
238238

239239
return logger.statusText;
240240
} catch (error, trace) {

packages/flutter_tools/lib/src/android/android_sdk.dart

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ import '../base/file_system.dart';
1212
import '../base/io.dart' show ProcessResult;
1313
import '../base/os.dart';
1414
import '../base/platform.dart';
15+
import '../base/process.dart';
1516
import '../base/process_manager.dart';
1617
import '../base/version.dart';
1718
import '../globals.dart';
19+
import 'android_studio.dart' as android_studio;
1820

1921
AndroidSdk get androidSdk => context[AndroidSdk];
2022

@@ -63,6 +65,9 @@ class AndroidSdk {
6365
_init();
6466
}
6567

68+
static const String _kJavaHomeEnvironmentVariable = 'JAVA_HOME';
69+
static const String _kJavaExecutable = 'java';
70+
6671
/// The path to the Android SDK.
6772
final String directory;
6873

@@ -291,11 +296,56 @@ class AndroidSdk {
291296
return fs.path.join(directory, 'tools', 'bin', 'sdkmanager');
292297
}
293298

299+
/// First try Java bundled with Android Studio, then sniff JAVA_HOME, then fallback to PATH.
300+
static String findJavaBinary() {
301+
302+
if (android_studio.javaPath != null)
303+
return fs.path.join(android_studio.javaPath, 'bin', 'java');
304+
305+
final String javaHomeEnv = platform.environment[_kJavaHomeEnvironmentVariable];
306+
if (javaHomeEnv != null) {
307+
// Trust JAVA_HOME.
308+
return fs.path.join(javaHomeEnv, 'bin', 'java');
309+
}
310+
311+
// MacOS specific logic to avoid popping up a dialog window.
312+
// See: http://stackoverflow.com/questions/14292698/how-do-i-check-if-the-java-jdk-is-installed-on-mac.
313+
if (platform.isMacOS) {
314+
try {
315+
final String javaHomeOutput = runCheckedSync(<String>['/usr/libexec/java_home'], hideStdout: true);
316+
if (javaHomeOutput != null) {
317+
final List<String> javaHomeOutputSplit = javaHomeOutput.split('\n');
318+
if ((javaHomeOutputSplit != null) && (javaHomeOutputSplit.isNotEmpty)) {
319+
final String javaHome = javaHomeOutputSplit[0].trim();
320+
return fs.path.join(javaHome, 'bin', 'java');
321+
}
322+
}
323+
} catch (_) { /* ignore */ }
324+
}
325+
326+
// Fallback to PATH based lookup.
327+
return os.which(_kJavaExecutable)?.path;
328+
}
329+
330+
Map<String, String> _sdkManagerEnv;
331+
Map<String, String> get sdkManagerEnv {
332+
if (_sdkManagerEnv == null) {
333+
// If we can locate Java, then add it to the path used to run the Android SDK manager.
334+
_sdkManagerEnv = <String, String>{};
335+
final String javaBinary = findJavaBinary();
336+
if (javaBinary != null) {
337+
_sdkManagerEnv['PATH'] =
338+
fs.path.dirname(javaBinary) + os.pathVarSeparator + platform.environment['PATH'];
339+
}
340+
}
341+
return _sdkManagerEnv;
342+
}
343+
294344
/// Returns the version of the Android SDK manager tool or null if not found.
295345
String get sdkManagerVersion {
296346
if (!processManager.canRun(sdkManagerPath))
297347
throwToolExit('Android sdkmanager not found. Update to the latest Android SDK to resolve this.');
298-
final ProcessResult result = processManager.runSync(<String>[sdkManagerPath, '--version']);
348+
final ProcessResult result = processManager.runSync(<String>[sdkManagerPath, '--version'], environment: sdkManagerEnv);
299349
if (result.exitCode != 0) {
300350
throwToolExit('sdkmanager --version failed: ${result.exitCode}', exitCode: result.exitCode);
301351
}

packages/flutter_tools/lib/src/android/android_studio.dart

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -178,14 +178,14 @@ class AndroidStudio implements Comparable<AndroidStudio> {
178178

179179
// Read all $HOME/.AndroidStudio*/system/.home files. There may be several
180180
// pointing to the same installation, so we grab only the latest one.
181-
for (FileSystemEntity entity in fs.directory(homeDirPath).listSync()) {
182-
if (entity is Directory && entity.basename.startsWith('.AndroidStudio')) {
183-
final AndroidStudio studio = new AndroidStudio.fromHomeDot(entity);
184-
if (studio != null &&
185-
!_hasStudioAt(studio.directory, newerThan: studio.version)) {
186-
studios.removeWhere(
187-
(AndroidStudio other) => other.directory == studio.directory);
188-
studios.add(studio);
181+
if (fs.directory(homeDirPath).existsSync()) {
182+
for (FileSystemEntity entity in fs.directory(homeDirPath).listSync()) {
183+
if (entity is Directory && entity.basename.startsWith('.AndroidStudio')) {
184+
final AndroidStudio studio = new AndroidStudio.fromHomeDot(entity);
185+
if (studio != null && !_hasStudioAt(studio.directory, newerThan: studio.version)) {
186+
studios.removeWhere((AndroidStudio other) => other.directory == studio.directory);
187+
studios.add(studio);
188+
}
189189
}
190190
}
191191
}

packages/flutter_tools/lib/src/android/android_workflow.dart

Lines changed: 63 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
// found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:convert';
67

78
import '../base/common.dart';
89
import '../base/context.dart';
9-
import '../base/file_system.dart';
1010
import '../base/io.dart';
11-
import '../base/os.dart';
1211
import '../base/platform.dart';
1312
import '../base/process.dart';
1413
import '../base/process_manager.dart';
@@ -17,10 +16,20 @@ import '../base/version.dart';
1716
import '../doctor.dart';
1817
import '../globals.dart';
1918
import 'android_sdk.dart';
20-
import 'android_studio.dart' as android_studio;
2119

2220
AndroidWorkflow get androidWorkflow => context.putIfAbsent(AndroidWorkflow, () => new AndroidWorkflow());
2321

22+
enum LicensesAccepted {
23+
none,
24+
some,
25+
all,
26+
unknown,
27+
}
28+
29+
final RegExp licenseCounts = new RegExp(r'(\d+) of (\d+) SDK package licenses? not accepted.');
30+
final RegExp licenseNotAccepted = new RegExp(r'licenses? not accepted', caseSensitive: false);
31+
final RegExp licenseAccepted = new RegExp(r'All SDK package licenses accepted.');
32+
2433
class AndroidWorkflow extends DoctorValidator implements Workflow {
2534
AndroidWorkflow() : super('Android toolchain - develop for Android devices');
2635

@@ -33,41 +42,8 @@ class AndroidWorkflow extends DoctorValidator implements Workflow {
3342
@override
3443
bool get canLaunchDevices => androidSdk != null && androidSdk.validateSdkWellFormed().isEmpty;
3544

36-
static const String _kJavaHomeEnvironmentVariable = 'JAVA_HOME';
37-
static const String _kJavaExecutable = 'java';
3845
static const String _kJdkDownload = 'https://www.oracle.com/technetwork/java/javase/downloads/';
3946

40-
/// First try Java bundled with Android Studio, then sniff JAVA_HOME, then fallback to PATH.
41-
static String _findJavaBinary() {
42-
43-
if (android_studio.javaPath != null)
44-
return fs.path.join(android_studio.javaPath, 'bin', 'java');
45-
46-
final String javaHomeEnv = platform.environment[_kJavaHomeEnvironmentVariable];
47-
if (javaHomeEnv != null) {
48-
// Trust JAVA_HOME.
49-
return fs.path.join(javaHomeEnv, 'bin', 'java');
50-
}
51-
52-
// MacOS specific logic to avoid popping up a dialog window.
53-
// See: http://stackoverflow.com/questions/14292698/how-do-i-check-if-the-java-jdk-is-installed-on-mac.
54-
if (platform.isMacOS) {
55-
try {
56-
final String javaHomeOutput = runCheckedSync(<String>['/usr/libexec/java_home'], hideStdout: true);
57-
if (javaHomeOutput != null) {
58-
final List<String> javaHomeOutputSplit = javaHomeOutput.split('\n');
59-
if ((javaHomeOutputSplit != null) && (javaHomeOutputSplit.isNotEmpty)) {
60-
final String javaHome = javaHomeOutputSplit[0].trim();
61-
return fs.path.join(javaHome, 'bin', 'java');
62-
}
63-
}
64-
} catch (_) { /* ignore */ }
65-
}
66-
67-
// Fallback to PATH based lookup.
68-
return os.which(_kJavaExecutable)?.path;
69-
}
70-
7147
/// Returns false if we cannot determine the Java version or if the version
7248
/// is not compatible.
7349
bool _checkJavaVersion(String javaBinary, List<ValidationMessage> messages) {
@@ -154,7 +130,7 @@ class AndroidWorkflow extends DoctorValidator implements Workflow {
154130
}
155131

156132
// Now check for the JDK.
157-
final String javaBinary = _findJavaBinary();
133+
final String javaBinary = AndroidSdk.findJavaBinary();
158134
if (javaBinary == null) {
159135
messages.add(new ValidationMessage.error(
160136
'No Java Development Kit (JDK) found; You must have the environment '
@@ -169,25 +145,66 @@ class AndroidWorkflow extends DoctorValidator implements Workflow {
169145
return new ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
170146
}
171147

148+
// Check for licenses.
149+
switch (await licensesAccepted) {
150+
case LicensesAccepted.all:
151+
messages.add(new ValidationMessage('All Android licenses accepted.'));
152+
break;
153+
case LicensesAccepted.some:
154+
messages.add(new ValidationMessage.hint('Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses'));
155+
return new ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
156+
case LicensesAccepted.none:
157+
messages.add(new ValidationMessage.error('Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses'));
158+
return new ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
159+
case LicensesAccepted.unknown:
160+
messages.add(new ValidationMessage.error('Android license status unknown.'));
161+
return new ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
162+
}
163+
172164
// Success.
173165
return new ValidationResult(ValidationType.installed, messages, statusInfo: sdkVersionText);
174166
}
175167

168+
Future<LicensesAccepted> get licensesAccepted async {
169+
LicensesAccepted status = LicensesAccepted.unknown;
170+
171+
void _onLine(String line) {
172+
if (licenseAccepted.hasMatch(line)) {
173+
status = LicensesAccepted.all;
174+
} else if (licenseCounts.hasMatch(line)) {
175+
final Match match = licenseCounts.firstMatch(line);
176+
if (match.group(1) != match.group(2)) {
177+
status = LicensesAccepted.some;
178+
} else {
179+
status = LicensesAccepted.none;
180+
}
181+
} else if (licenseNotAccepted.hasMatch(line)) {
182+
// In case the format changes, a more general match will keep doctor
183+
// mostly working.
184+
status = LicensesAccepted.none;
185+
}
186+
}
187+
188+
final Process process = await runCommand(<String>[androidSdk.sdkManagerPath, '--licenses'], environment: androidSdk.sdkManagerEnv);
189+
process.stdin.write('n\n');
190+
final Future<void> output = process.stdout.transform(const Utf8Decoder(allowMalformed: true)).transform(const LineSplitter()).listen(_onLine).asFuture<void>(null);
191+
final Future<void> errors = process.stderr.transform(const Utf8Decoder(allowMalformed: true)).transform(const LineSplitter()).listen(_onLine).asFuture<void>(null);
192+
try {
193+
await Future.wait<void>(<Future<void>>[output, errors]).timeout(const Duration(seconds: 30));
194+
} catch (TimeoutException) {
195+
printTrace('Intentionally killing ${androidSdk.sdkManagerPath}');
196+
processManager.killPid(process.pid);
197+
}
198+
return status;
199+
}
200+
176201
/// Run the Android SDK manager tool in order to accept SDK licenses.
177202
static Future<bool> runLicenseManager() async {
178203
if (androidSdk == null) {
179204
printStatus('Unable to locate Android SDK.');
180205
return false;
181206
}
182207

183-
// If we can locate Java, then add it to the path used to run the Android SDK manager.
184-
final Map<String, String> sdkManagerEnv = <String, String>{};
185-
final String javaBinary = _findJavaBinary();
186-
if (javaBinary != null) {
187-
sdkManagerEnv['PATH'] =
188-
fs.path.dirname(javaBinary) + os.pathVarSeparator + platform.environment['PATH'];
189-
}
190-
191208
if (!processManager.canRun(androidSdk.sdkManagerPath))
192209
throwToolExit(
193210
'Android sdkmanager tool not found.\n'
@@ -205,7 +222,7 @@ class AndroidWorkflow extends DoctorValidator implements Workflow {
205222

206223
final Process process = await runCommand(
207224
<String>[androidSdk.sdkManagerPath, '--licenses'],
208-
environment: sdkManagerEnv,
225+
environment: androidSdk.sdkManagerEnv,
209226
);
210227

211228
waitGroup<Null>(<Future<Null>>[

0 commit comments

Comments
 (0)