gradle_errors.dart 19.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:meta/meta.dart';

7 8
import '../base/error_handling_io.dart';
import '../base/file_system.dart';
9
import '../base/process.dart';
10
import '../base/terminal.dart';
11
import '../globals.dart' as globals;
12 13
import '../project.dart';
import '../reporting/reporting.dart';
14
import 'android_studio.dart';
15
import 'multidex.dart';
16 17 18 19

typedef GradleErrorTest = bool Function(String);

/// A Gradle error handled by the tool.
20
class GradleHandledError {
21
  const GradleHandledError({
22 23
    required this.test,
    required this.handler,
24 25 26 27 28 29 30 31 32
    this.eventLabel,
  });

  /// The test function.
  /// Returns [true] if the current error message should be handled.
  final GradleErrorTest test;

  /// The handler function.
  final Future<GradleBuildStatus> Function({
33 34 35
    required String line,
    required FlutterProject project,
    required bool usesAndroidX,
36
    required bool multidexEnabled,
37 38
  }) handler;

39
  /// The [BuildEvent] label is named gradle-[eventLabel].
40 41
  /// If not empty, the build event is logged along with
  /// additional metadata such as the attempt number.
42
  final String? eventLabel;
43 44 45
}

/// The status of the Gradle build.
46
enum GradleBuildStatus {
47 48 49 50 51 52
  /// The tool cannot recover from the failure and should exit.
  exit,
  /// The tool can retry the exact same build.
  retry,
}

53 54
/// Returns a simple test function that evaluates to `true` if at least one of
/// `errorMessages` is contained in the error message.
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
GradleErrorTest _lineMatcher(List<String> errorMessages) {
  return (String line) {
    return errorMessages.any((String errorMessage) => line.contains(errorMessage));
  };
}

/// The list of Gradle errors that the tool can handle.
///
/// The handlers are executed in the order in which they appear in the list.
///
/// Only the first error handler for which the [test] function returns [true]
/// is handled. As a result, sort error handlers based on how strict the [test]
/// function is to eliminate false positives.
final List<GradleHandledError> gradleErrors = <GradleHandledError>[
  licenseNotAcceptedHandler,
  networkErrorHandler,
  permissionDeniedErrorHandler,
  flavorUndefinedHandler,
  r8FailureHandler,
74 75 76
  minSdkVersionHandler,
  transformInputIssueHandler,
  lockFileDepMissingHandler,
77
  multidexErrorHandler,
78
  incompatibleKotlinVersionHandler,
79
  minCompileSdkVersionHandler,
80
  jvm11RequiredHandler,
81 82
];

83 84
const String _boxTitle = 'Flutter Fix';

85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
// Multidex error message.
@visibleForTesting
final GradleHandledError multidexErrorHandler = GradleHandledError(
  test: _lineMatcher(const <String>[
    'com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives:',
    'The number of method references in a .dex file cannot exceed 64K.',
  ]),
  handler: ({
    required String line,
    required FlutterProject project,
    required bool usesAndroidX,
    required bool multidexEnabled,
  }) async {
    globals.printStatus('${globals.logger.terminal.warningMark} App requires Multidex support', emphasis: true);
    if (multidexEnabled) {
      globals.printStatus(
        'Multidex support is required for your android app to build since the number of methods has exceeded 64k. '
        "You may pass the --no-multidex flag to skip Flutter's multidex support to use a manual solution.\n",
        indent: 4,
      );
      if (!androidManifestHasNameVariable(project.directory)) {
        globals.printStatus(
          r'Your `android/app/src/main/AndroidManifest.xml` does not contain `android:name="${applicationName}"` '
          'under the `application` element. This may be due to creating your project with an old version of Flutter. '
          'Add the `android:name="\${applicationName}"` attribute to your AndroidManifest.xml to enable Flutter\'s multidex support:\n',
          indent: 4,
        );
        globals.printStatus(r'''
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  ...
  <application
    ...
    android:name=''',
          indent: 8,
          newline: false,
          color: TerminalColor.grey,
        );
        globals.printStatus(r'"${applicationName}"', color: TerminalColor.green, newline: true);
        globals.printStatus(r'''
    ...>
''',
          indent: 8,
          color: TerminalColor.grey,
        );

        globals.printStatus(
          'You may also roll your own multidex support by following the guide at: https://developer.android.com/studio/build/multidex\n',
          indent: 4,
        );
        return GradleBuildStatus.exit;
      }
      if (!multiDexApplicationExists(project.directory)) {
        globals.printStatus(
          'Flutter tool can add multidex support. The following file will be added by flutter:\n',
          indent: 4,
        );
        globals.printStatus(
          'android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java\n',
          indent: 8,
        );
        String selection = 'n';
        // Default to 'no' if no interactive terminal.
        try {
          selection = await globals.terminal.promptForCharInput(
            <String>['y', 'n'],
            logger: globals.logger,
            prompt: 'Do you want to continue with adding multidex support for Android?',
            defaultChoiceIndex: 0,
          );
        } on StateError catch(e) {
          globals.printError(
            e.message,
            indent: 0,
          );
        }
        if (selection == 'y') {
          ensureMultiDexApplicationExists(project.directory);
          globals.printStatus(
            'Multidex enabled. Retrying build.\n',
            indent: 0,
          );
          return GradleBuildStatus.retry;
        }
      }
    } else {
170
      globals.printBox(
171
        'Flutter multidex handling is disabled. If you wish to let the tool configure multidex, use the --multidex flag.',
172
        title: _boxTitle,
173 174 175 176 177 178 179
      );
    }
    return GradleBuildStatus.exit;
  },
  eventLabel: 'multidex-error',
);

180 181 182 183 184 185 186
// Permission defined error message.
@visibleForTesting
final GradleHandledError permissionDeniedErrorHandler = GradleHandledError(
  test: _lineMatcher(const <String>[
    'Permission denied',
  ]),
  handler: ({
187 188 189
    required String line,
    required FlutterProject project,
    required bool usesAndroidX,
190
    required bool multidexEnabled,
191
  }) async {
192 193
    globals.printBox(
      '${globals.logger.terminal.warningMark} Gradle does not have execution permission.\n'
194 195
      'You should change the ownership of the project directory to your user, '
      'or move the project to a directory with execute permissions.',
196
      title: _boxTitle,
197 198 199 200 201 202
    );
    return GradleBuildStatus.exit;
  },
  eventLabel: 'permission-denied',
);

203 204 205 206 207 208 209 210
/// Gradle crashes for several known reasons when downloading that are not
/// actionable by Flutter.
///
/// The Gradle cache directory must be deleted, otherwise it may attempt to
/// re-use the bad zip file.
///
/// See also:
///  * https://docs.gradle.org/current/userguide/directory_layout.html#dir:gradle_user_home
211 212 213 214 215 216 217 218 219 220
@visibleForTesting
final GradleHandledError networkErrorHandler = GradleHandledError(
  test: _lineMatcher(const <String>[
    'java.io.FileNotFoundException: https://downloads.gradle.org',
    'java.io.IOException: Unable to tunnel through proxy',
    'java.lang.RuntimeException: Timeout of',
    'java.util.zip.ZipException: error in opening zip file',
    'javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake',
    'java.net.SocketException: Connection reset',
    'java.io.FileNotFoundException',
221
    "> Could not get resource 'http",
222 223
  ]),
  handler: ({
224 225 226
    required String line,
    required FlutterProject project,
    required bool usesAndroidX,
227
    required bool multidexEnabled,
228
  }) async {
229
    globals.printError(
230 231
      '${globals.logger.terminal.warningMark} '
      'Gradle threw an error while downloading artifacts from the network.'
232
    );
233
    try {
234
      final String? homeDir = globals.platform.environment['HOME'];
235 236 237 238 239 240 241
      if (homeDir != null) {
        final Directory directory = globals.fs.directory(globals.fs.path.join(homeDir, '.gradle'));
        ErrorHandlingFileSystem.deleteIfExists(directory, recursive: true);
      }
    } on FileSystemException catch (err) {
      globals.printTrace('Failed to delete Gradle cache: $err');
    }
242 243 244 245 246 247 248 249 250 251 252 253
    return GradleBuildStatus.retry;
  },
  eventLabel: 'network',
);

// R8 failure.
@visibleForTesting
final GradleHandledError r8FailureHandler = GradleHandledError(
  test: _lineMatcher(const <String>[
    'com.android.tools.r8',
  ]),
  handler: ({
254 255 256
    required String line,
    required FlutterProject project,
    required bool usesAndroidX,
257
    required bool multidexEnabled,
258
  }) async {
259 260 261 262 263 264
    globals.printBox(
      '${globals.logger.terminal.warningMark} The shrinker may have failed to optimize the Java bytecode.\n'
      'To disable the shrinker, pass the `--no-shrink` flag to this command.\n'
      'To learn more, see: https://developer.android.com/studio/build/shrink-code',
      title: _boxTitle,
    );
265 266 267 268 269 270 271 272 273 274 275 276 277 278
    return GradleBuildStatus.exit;
  },
  eventLabel: 'r8',
);

/// Handle Gradle error thrown when Gradle needs to download additional
/// Android SDK components (e.g. Platform Tools), and the license
/// for that component has not been accepted.
@visibleForTesting
final GradleHandledError licenseNotAcceptedHandler = GradleHandledError(
  test: _lineMatcher(const <String>[
    'You have not accepted the license agreements of the following SDK components',
  ]),
  handler: ({
279 280 281
    required String line,
    required FlutterProject project,
    required bool usesAndroidX,
282
    required bool multidexEnabled,
283 284
  }) async {
    const String licenseNotAcceptedMatcher =
285
      r'You have not accepted the license agreements of the following SDK components:\s*\[(.+)\]';
286 287 288

    final RegExp licenseFailure = RegExp(licenseNotAcceptedMatcher, multiLine: true);
    assert(licenseFailure != null);
289
    final Match? licenseMatch = licenseFailure.firstMatch(line);
290
    globals.printBox(
291
      '${globals.logger.terminal.warningMark} Unable to download needed Android SDK components, as the '
292
      'following licenses have not been accepted: '
293
      '${licenseMatch?.group(1)}\n\n'
294
      'To resolve this, please run the following command in a Terminal:\n'
295 296
      'flutter doctor --android-licenses',
      title: _boxTitle,
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
    );
    return GradleBuildStatus.exit;
  },
  eventLabel: 'license-not-accepted',
);

final RegExp _undefinedTaskPattern = RegExp(r'Task .+ not found in root project.');

final RegExp _assembleTaskPattern = RegExp(r'assemble(\S+)');

/// Handler when a flavor is undefined.
@visibleForTesting
final GradleHandledError flavorUndefinedHandler = GradleHandledError(
  test: (String line) {
    return _undefinedTaskPattern.hasMatch(line);
  },
  handler: ({
314 315 316
    required String line,
    required FlutterProject project,
    required bool usesAndroidX,
317
    required bool multidexEnabled,
318
  }) async {
319
    final RunResult tasksRunResult = await globals.processUtils.run(
320
      <String>[
321
        globals.gradleUtils!.getExecutable(project),
322 323 324 325 326 327
        'app:tasks' ,
        '--all',
        '--console=auto',
      ],
      throwOnError: true,
      workingDirectory: project.android.hostAppGradleRoot.path,
328 329
      environment: <String, String>{
        if (javaPath != null)
330
          'JAVA_HOME': javaPath!,
331
      },
332 333 334
    );
    // Extract build types and product flavors.
    final Set<String> variants = <String>{};
335
    for (final String task in tasksRunResult.stdout.split('\n')) {
336
      final Match? match = _assembleTaskPattern.matchAsPrefix(task);
337
      if (match != null) {
338
        final String variant = match.group(1)!.toLowerCase();
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
        if (!variant.endsWith('test')) {
          variants.add(variant);
        }
      }
    }
    final Set<String> productFlavors = <String>{};
    for (final String variant1 in variants) {
      for (final String variant2 in variants) {
        if (variant2.startsWith(variant1) && variant2 != variant1) {
          final String buildType = variant2.substring(variant1.length);
          if (variants.contains(buildType)) {
            productFlavors.add(variant1);
          }
        }
      }
    }
355 356
    final String errorMessage = '${globals.logger.terminal.warningMark}  Gradle project does not define a task suitable for the requested build.';
    final File buildGradle = project.directory.childDirectory('android').childDirectory('app').childFile('build.gradle');
357
    if (productFlavors.isEmpty) {
358 359 360
      globals.printBox(
        '$errorMessage\n\n'
        'The ${buildGradle.absolute.path} file does not define '
361
        'any custom product flavors. '
362 363
        'You cannot use the --flavor option.',
        title: _boxTitle,
364 365
      );
    } else {
366 367 368 369 370 371
      globals.printBox(
        '$errorMessage\n\n'
        'The ${buildGradle.absolute.path} file defines product '
        'flavors: ${productFlavors.join(', ')}. '
        'You must specify a --flavor option to select one of them.',
        title: _boxTitle,
372 373 374 375 376 377
      );
    }
    return GradleBuildStatus.exit;
  },
  eventLabel: 'flavor-undefined',
);
378 379 380 381 382 383


final RegExp _minSdkVersionPattern = RegExp(r'uses-sdk:minSdkVersion ([0-9]+) cannot be smaller than version ([0-9]+) declared in library \[\:(.+)\]');

/// Handler when a plugin requires a higher Android API level.
@visibleForTesting
384
final GradleHandledError minSdkVersionHandler = GradleHandledError(
385 386 387 388
  test: (String line) {
    return _minSdkVersionPattern.hasMatch(line);
  },
  handler: ({
389 390 391
    required String line,
    required FlutterProject project,
    required bool usesAndroidX,
392
    required bool multidexEnabled,
393
  }) async {
394
    final File gradleFile = project.directory
395
        .childDirectory('android')
396 397
        .childDirectory('app')
        .childFile('build.gradle');
398

399 400
    final Match? minSdkVersionMatch = _minSdkVersionPattern.firstMatch(line);
    assert(minSdkVersionMatch?.groupCount == 3);
401

402
    final String textInBold = globals.logger.terminal.bolden(
403 404 405 406 407 408
      'Fix this issue by adding the following to the file ${gradleFile.path}:\n'
      'android {\n'
      '  defaultConfig {\n'
      '    minSdkVersion ${minSdkVersionMatch?.group(2)}\n'
      '  }\n'
      '}\n'
409
    );
410
    globals.printBox(
411
      'The plugin ${minSdkVersionMatch?.group(3)} requires a higher Android SDK version.\n'
412
      '$textInBold\n'
413
      "Note that your app won't be available to users running Android SDKs below ${minSdkVersionMatch?.group(2)}.\n"
414 415
      'Alternatively, try to find a version of this plugin that supports these lower versions of the Android SDK.\n'
      'For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration',
416
      title: _boxTitle,
417 418 419 420 421
    );
    return GradleBuildStatus.exit;
  },
  eventLabel: 'plugin-min-sdk',
);
422 423 424 425

/// Handler when https://issuetracker.google.com/issues/141126614 or
/// https://github.com/flutter/flutter/issues/58247 is triggered.
@visibleForTesting
426
final GradleHandledError transformInputIssueHandler = GradleHandledError(
427 428 429 430
  test: (String line) {
    return line.contains('https://issuetracker.google.com/issues/158753935');
  },
  handler: ({
431 432 433
    required String line,
    required FlutterProject project,
    required bool usesAndroidX,
434
    required bool multidexEnabled,
435 436 437 438 439
  }) async {
    final File gradleFile = project.directory
        .childDirectory('android')
        .childDirectory('app')
        .childFile('build.gradle');
440
    final String textInBold = globals.logger.terminal.bolden(
441 442 443 444 445 446 447
      'Fix this issue by adding the following to the file ${gradleFile.path}:\n'
      'android {\n'
      '  lintOptions {\n'
      '    checkReleaseBuilds false\n'
      '  }\n'
      '}'
    );
448
    globals.printBox(
449
      'This issue appears to be https://github.com/flutter/flutter/issues/58247.\n'
450 451
      '$textInBold',
      title: _boxTitle,
452 453 454 455 456
    );
    return GradleBuildStatus.exit;
  },
  eventLabel: 'transform-input-issue',
);
457 458 459

/// Handler when a dependency is missing in the lockfile.
@visibleForTesting
460
final GradleHandledError lockFileDepMissingHandler = GradleHandledError(
461 462 463 464
  test: (String line) {
    return line.contains('which is not part of the dependency lock state');
  },
  handler: ({
465 466 467
    required String line,
    required FlutterProject project,
    required bool usesAndroidX,
468
    required bool multidexEnabled,
469 470 471 472
  }) async {
    final File gradleFile = project.directory
        .childDirectory('android')
        .childFile('build.gradle');
473
    final String textInBold = globals.logger.terminal.bolden(
474
      'To regenerate the lockfiles run: `./gradlew :generateLockfiles` in ${gradleFile.path}\n'
475
      'To remove dependency locking, remove the `dependencyLocking` from ${gradleFile.path}'
476
    );
477
    globals.printBox(
478
      'You need to update the lockfile, or disable Gradle dependency locking.\n'
479 480
      '$textInBold',
      title: _boxTitle,
481 482 483 484 485
    );
    return GradleBuildStatus.exit;
  },
  eventLabel: 'lock-dep-issue',
);
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500

@visibleForTesting
final GradleHandledError incompatibleKotlinVersionHandler = GradleHandledError(
  test: _lineMatcher(const <String>[
    'Module was compiled with an incompatible version of Kotlin',
  ]),
  handler: ({
    required String line,
    required FlutterProject project,
    required bool usesAndroidX,
    required bool multidexEnabled,
  }) async {
    final File gradleFile = project.directory
        .childDirectory('android')
        .childFile('build.gradle');
501 502 503
    globals.printBox(
      '${globals.logger.terminal.warningMark} Your project requires a newer version of the Kotlin Gradle plugin.\n'
      'Find the latest version on https://kotlinlang.org/docs/gradle.html#plugin-and-versions, then update ${gradleFile.path}:\n'
504
      "ext.kotlin_version = '<latest-version>'",
505
      title: _boxTitle,
506 507 508 509 510
    );
    return GradleBuildStatus.exit;
  },
  eventLabel: 'incompatible-kotlin-version',
);
511 512 513 514 515 516 517 518 519 520 521 522

final RegExp _minCompileSdkVersionPattern = RegExp(r'The minCompileSdk \(([0-9]+)\) specified in a');

@visibleForTesting
final GradleHandledError minCompileSdkVersionHandler = GradleHandledError(
  test: _minCompileSdkVersionPattern.hasMatch,
  handler: ({
    required String line,
    required FlutterProject project,
    required bool usesAndroidX,
    required bool multidexEnabled,
  }) async {
523 524
    final Match? minCompileSdkVersionMatch = _minCompileSdkVersionPattern.firstMatch(line);
    assert(minCompileSdkVersionMatch?.groupCount == 1);
525 526 527 528 529 530 531 532 533

    final File gradleFile = project.directory
        .childDirectory('android')
        .childDirectory('app')
        .childFile('build.gradle');
    globals.printBox(
      '${globals.logger.terminal.warningMark} Your project requires a higher compileSdkVersion.\n'
      'Fix this issue by bumping the compileSdkVersion in ${gradleFile.path}:\n'
      'android {\n'
534
      '  compileSdkVersion ${minCompileSdkVersionMatch?.group(1)}\n'
535 536 537 538 539 540 541
      '}',
      title: _boxTitle,
    );
    return GradleBuildStatus.exit;
  },
  eventLabel: 'min-compile-sdk-version',
);
542 543

@visibleForTesting
544
final GradleHandledError jvm11RequiredHandler = GradleHandledError(
545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563
  test: (String line) {
    return line.contains('Android Gradle plugin requires Java 11 to run');
  },
  handler: ({
    required String line,
    required FlutterProject project,
    required bool usesAndroidX,
    required bool multidexEnabled,
  }) async {
    globals.printBox(
      '${globals.logger.terminal.warningMark} You need Java 11 or higher to build your app with this version of Gradle.\n\n'
      'To get Java 11, update to the latest version of Android Studio on https://developer.android.com/studio/install.\n\n'
      'To check the Java version used by Flutter, run `flutter doctor -v`.',
      title: _boxTitle,
    );
    return GradleBuildStatus.exit;
  },
  eventLabel: 'java11-required',
);