• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 The Chromium 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
5import 'dart:async';
6
7import '../base/common.dart';
8import '../base/context.dart';
9import '../base/io.dart';
10import '../base/platform.dart';
11import '../base/process.dart';
12import '../base/process_manager.dart';
13import '../base/user_messages.dart';
14import '../base/utils.dart';
15import '../base/version.dart';
16import '../convert.dart';
17import '../doctor.dart';
18import '../globals.dart';
19import 'android_sdk.dart';
20
21const int kAndroidSdkMinVersion = 28;
22final Version kAndroidSdkBuildToolsMinVersion = Version(28, 0, 3);
23
24AndroidWorkflow get androidWorkflow => context.get<AndroidWorkflow>();
25AndroidValidator get androidValidator => context.get<AndroidValidator>();
26AndroidLicenseValidator get androidLicenseValidator => context.get<AndroidLicenseValidator>();
27
28enum LicensesAccepted {
29  none,
30  some,
31  all,
32  unknown,
33}
34
35final RegExp licenseCounts = RegExp(r'(\d+) of (\d+) SDK package licenses? not accepted.');
36final RegExp licenseNotAccepted = RegExp(r'licenses? not accepted', caseSensitive: false);
37final RegExp licenseAccepted = RegExp(r'All SDK package licenses accepted.');
38
39class AndroidWorkflow implements Workflow {
40  @override
41  bool get appliesToHostPlatform => true;
42
43  @override
44  bool get canListDevices => getAdbPath(androidSdk) != null;
45
46  @override
47  bool get canLaunchDevices => androidSdk != null && androidSdk.validateSdkWellFormed().isEmpty;
48
49  @override
50  bool get canListEmulators => getEmulatorPath(androidSdk) != null;
51}
52
53class AndroidValidator extends DoctorValidator {
54  AndroidValidator() : super('Android toolchain - develop for Android devices',);
55
56  @override
57  String get slowWarning => '${_task ?? 'This'} is taking a long time...';
58  String _task;
59
60  /// Returns false if we cannot determine the Java version or if the version
61  /// is not compatible.
62  Future<bool> _checkJavaVersion(String javaBinary, List<ValidationMessage> messages) async {
63    _task = 'Checking Java status';
64    try {
65      if (!processManager.canRun(javaBinary)) {
66        messages.add(ValidationMessage.error(userMessages.androidCantRunJavaBinary(javaBinary)));
67        return false;
68      }
69      String javaVersion;
70      try {
71        printTrace('java -version');
72        final ProcessResult result = await processManager.run(<String>[javaBinary, '-version']);
73        if (result.exitCode == 0) {
74          final List<String> versionLines = result.stderr.split('\n');
75          javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
76        }
77      } catch (error) {
78        printTrace(error.toString());
79      }
80      if (javaVersion == null) {
81        // Could not determine the java version.
82        messages.add(ValidationMessage.error(userMessages.androidUnknownJavaVersion));
83        return false;
84      }
85      messages.add(ValidationMessage(userMessages.androidJavaVersion(javaVersion)));
86      // TODO(johnmccutchan): Validate version.
87      return true;
88    } finally {
89      _task = null;
90    }
91  }
92
93  @override
94  Future<ValidationResult> validate() async {
95    final List<ValidationMessage> messages = <ValidationMessage>[];
96
97    if (androidSdk == null) {
98      // No Android SDK found.
99      if (platform.environment.containsKey(kAndroidHome)) {
100        final String androidHomeDir = platform.environment[kAndroidHome];
101        messages.add(ValidationMessage.error(userMessages.androidBadSdkDir(kAndroidHome, androidHomeDir)));
102      } else {
103        messages.add(ValidationMessage.error(userMessages.androidMissingSdkInstructions(kAndroidHome)));
104      }
105      return ValidationResult(ValidationType.missing, messages);
106    }
107
108    if (androidSdk.licensesAvailable && !androidSdk.platformToolsAvailable) {
109      messages.add(ValidationMessage.hint(userMessages.androidSdkLicenseOnly(kAndroidHome)));
110      return ValidationResult(ValidationType.partial, messages);
111    }
112
113    messages.add(ValidationMessage(userMessages.androidSdkLocation(androidSdk.directory)));
114
115    messages.add(ValidationMessage(androidSdk.ndk == null
116          ? userMessages.androidMissingNdk
117          : userMessages.androidNdkLocation(androidSdk.ndk.directory)));
118
119    String sdkVersionText;
120    if (androidSdk.latestVersion != null) {
121      if (androidSdk.latestVersion.sdkLevel < 28 || androidSdk.latestVersion.buildToolsVersion < kAndroidSdkBuildToolsMinVersion) {
122        messages.add(ValidationMessage.error(
123          userMessages.androidSdkBuildToolsOutdated(androidSdk.sdkManagerPath, kAndroidSdkMinVersion, kAndroidSdkBuildToolsMinVersion.toString())),
124        );
125        return ValidationResult(ValidationType.missing, messages);
126      }
127      sdkVersionText = userMessages.androidStatusInfo(androidSdk.latestVersion.buildToolsVersionName);
128
129      messages.add(ValidationMessage(userMessages.androidSdkPlatformToolsVersion(
130        androidSdk.latestVersion.platformName,
131        androidSdk.latestVersion.buildToolsVersionName)));
132    } else {
133      messages.add(ValidationMessage.error(userMessages.androidMissingSdkInstructions(kAndroidHome)));
134    }
135
136    if (platform.environment.containsKey(kAndroidHome)) {
137      final String androidHomeDir = platform.environment[kAndroidHome];
138      messages.add(ValidationMessage('$kAndroidHome = $androidHomeDir'));
139    }
140    if (platform.environment.containsKey(kAndroidSdkRoot)) {
141      final String androidSdkRoot = platform.environment[kAndroidSdkRoot];
142      messages.add(ValidationMessage('$kAndroidSdkRoot = $androidSdkRoot'));
143    }
144
145    final List<String> validationResult = androidSdk.validateSdkWellFormed();
146
147    if (validationResult.isNotEmpty) {
148      // Android SDK is not functional.
149      messages.addAll(validationResult.map<ValidationMessage>((String message) {
150        return ValidationMessage.error(message);
151      }));
152      messages.add(ValidationMessage(userMessages.androidSdkInstallHelp));
153      return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
154    }
155
156    // Now check for the JDK.
157    final String javaBinary = AndroidSdk.findJavaBinary();
158    if (javaBinary == null) {
159      messages.add(ValidationMessage.error(userMessages.androidMissingJdk));
160      return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
161    }
162    messages.add(ValidationMessage(userMessages.androidJdkLocation(javaBinary)));
163
164    // Check JDK version.
165    if (! await _checkJavaVersion(javaBinary, messages)) {
166      return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
167    }
168
169    // Success.
170    return ValidationResult(ValidationType.installed, messages, statusInfo: sdkVersionText);
171  }
172}
173
174class AndroidLicenseValidator extends DoctorValidator {
175  AndroidLicenseValidator() : super('Android license subvalidator',);
176
177  @override
178  String get slowWarning => 'Checking Android licenses is taking an unexpectedly long time...';
179
180  @override
181  Future<ValidationResult> validate() async {
182    final List<ValidationMessage> messages = <ValidationMessage>[];
183
184    // Match pre-existing early termination behavior
185    if (androidSdk == null || androidSdk.latestVersion == null ||
186        androidSdk.validateSdkWellFormed().isNotEmpty ||
187        ! await _checkJavaVersionNoOutput()) {
188      return ValidationResult(ValidationType.missing, messages);
189    }
190
191    final String sdkVersionText = userMessages.androidStatusInfo(androidSdk.latestVersion.buildToolsVersionName);
192
193    // Check for licenses.
194    switch (await licensesAccepted) {
195      case LicensesAccepted.all:
196        messages.add(ValidationMessage(userMessages.androidLicensesAll));
197        break;
198      case LicensesAccepted.some:
199        messages.add(ValidationMessage.hint(userMessages.androidLicensesSome));
200        return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
201      case LicensesAccepted.none:
202        messages.add(ValidationMessage.error(userMessages.androidLicensesNone));
203        return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
204      case LicensesAccepted.unknown:
205        messages.add(ValidationMessage.error(userMessages.androidLicensesUnknown));
206        return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
207    }
208    return ValidationResult(ValidationType.installed, messages, statusInfo: sdkVersionText);
209  }
210
211  Future<bool> _checkJavaVersionNoOutput() async {
212    final String javaBinary = AndroidSdk.findJavaBinary();
213    if (javaBinary == null) {
214      return false;
215    }
216    if (!processManager.canRun(javaBinary)) {
217      return false;
218    }
219    String javaVersion;
220    try {
221      final ProcessResult result = await processManager.run(<String>[javaBinary, '-version']);
222      if (result.exitCode == 0) {
223        final List<String> versionLines = result.stderr.split('\n');
224        javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
225      }
226    } catch (error) {
227      printTrace(error.toString());
228    }
229    if (javaVersion == null) {
230      // Could not determine the java version.
231      return false;
232    }
233    return true;
234  }
235
236  Future<LicensesAccepted> get licensesAccepted async {
237    LicensesAccepted status;
238
239    void _handleLine(String line) {
240      if (licenseCounts.hasMatch(line)) {
241        final Match match = licenseCounts.firstMatch(line);
242        if (match.group(1) != match.group(2)) {
243          status = LicensesAccepted.some;
244        } else {
245          status = LicensesAccepted.none;
246        }
247      } else if (licenseNotAccepted.hasMatch(line)) {
248        // The licenseNotAccepted pattern is trying to match the same line as
249        // licenseCounts, but is more general. In case the format changes, a
250        // more general match may keep doctor mostly working.
251        status = LicensesAccepted.none;
252      } else if (licenseAccepted.hasMatch(line)) {
253        status ??= LicensesAccepted.all;
254      }
255    }
256
257    if (!_canRunSdkManager()) {
258      return LicensesAccepted.unknown;
259    }
260
261    try {
262      final Process process = await runCommand(
263        <String>[androidSdk.sdkManagerPath, '--licenses'],
264        environment: androidSdk.sdkManagerEnv,
265      );
266      process.stdin.write('n\n');
267      // We expect logcat streams to occasionally contain invalid utf-8,
268      // see: https://github.com/flutter/flutter/pull/8864.
269      final Future<void> output = process.stdout
270        .transform<String>(const Utf8Decoder(reportErrors: false))
271        .transform<String>(const LineSplitter())
272        .listen(_handleLine)
273        .asFuture<void>(null);
274      final Future<void> errors = process.stderr
275        .transform<String>(const Utf8Decoder(reportErrors: false))
276        .transform<String>(const LineSplitter())
277        .listen(_handleLine)
278        .asFuture<void>(null);
279      await Future.wait<void>(<Future<void>>[output, errors]);
280      return status ?? LicensesAccepted.unknown;
281    } on ProcessException catch (e) {
282      printTrace('Failed to run Android sdk manager: $e');
283      return LicensesAccepted.unknown;
284    }
285  }
286
287  /// Run the Android SDK manager tool in order to accept SDK licenses.
288  static Future<bool> runLicenseManager() async {
289    if (androidSdk == null) {
290      printStatus(userMessages.androidSdkShort);
291      return false;
292    }
293
294    if (!_canRunSdkManager()) {
295      throwToolExit(userMessages.androidMissingSdkManager(androidSdk.sdkManagerPath));
296    }
297
298    final Version sdkManagerVersion = Version.parse(androidSdk.sdkManagerVersion);
299    if (sdkManagerVersion == null || sdkManagerVersion.major < 26) {
300      // SDK manager is found, but needs to be updated.
301      throwToolExit(userMessages.androidSdkManagerOutdated(androidSdk.sdkManagerPath));
302    }
303
304    try {
305      final Process process = await runCommand(
306        <String>[androidSdk.sdkManagerPath, '--licenses'],
307        environment: androidSdk.sdkManagerEnv,
308      );
309
310      // The real stdin will never finish streaming. Pipe until the child process
311      // finishes.
312      unawaited(process.stdin.addStream(stdin));
313      // Wait for stdout and stderr to be fully processed, because process.exitCode
314      // may complete first.
315      await waitGroup<void>(<Future<void>>[
316        stdout.addStream(process.stdout),
317        stderr.addStream(process.stderr),
318      ]);
319
320      final int exitCode = await process.exitCode;
321      return exitCode == 0;
322    } on ProcessException catch (e) {
323      throwToolExit(userMessages.androidCannotRunSdkManager(
324          androidSdk.sdkManagerPath, e.toString()));
325      return false;
326    }
327  }
328
329  static bool _canRunSdkManager() {
330    assert(androidSdk != null);
331    final String sdkManagerPath = androidSdk.sdkManagerPath;
332    return processManager.canRun(sdkManagerPath);
333  }
334}
335