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 'package:crypto/crypto.dart'; 8import 'package:meta/meta.dart'; 9 10import '../android/android_sdk.dart'; 11import '../artifacts.dart'; 12import '../base/common.dart'; 13import '../base/file_system.dart'; 14import '../base/logger.dart'; 15import '../base/os.dart'; 16import '../base/platform.dart'; 17import '../base/process.dart'; 18import '../base/terminal.dart'; 19import '../base/utils.dart'; 20import '../base/version.dart'; 21import '../build_info.dart'; 22import '../cache.dart'; 23import '../features.dart'; 24import '../flutter_manifest.dart'; 25import '../globals.dart'; 26import '../project.dart'; 27import '../reporting/reporting.dart'; 28import 'android_sdk.dart'; 29import 'android_studio.dart'; 30 31final RegExp _assembleTaskPattern = RegExp(r'assemble(\S+)'); 32 33GradleProject _cachedGradleAppProject; 34GradleProject _cachedGradleLibraryProject; 35String _cachedGradleExecutable; 36 37enum FlutterPluginVersion { 38 none, 39 v1, 40 v2, 41 managed, 42} 43 44// Investigation documented in #13975 suggests the filter should be a subset 45// of the impact of -q, but users insist they see the error message sometimes 46// anyway. If we can prove it really is impossible, delete the filter. 47// This technically matches everything *except* the NDK message, since it's 48// passed to a function that filters out all lines that don't match a filter. 49final RegExp ndkMessageFilter = RegExp(r'^(?!NDK is missing a ".*" directory' 50 r'|If you are not using NDK, unset the NDK variable from ANDROID_NDK_HOME or local.properties to remove this warning' 51 r'|If you are using NDK, verify the ndk.dir is set to a valid NDK directory. It is currently set to .*)'); 52 53// This regex is intentionally broad. AndroidX errors can manifest in multiple 54// different ways and each one depends on the specific code config and 55// filesystem paths of the project. Throwing the broadest net possible here to 56// catch all known and likely cases. 57// 58// Example stack traces: 59// 60// https://github.com/flutter/flutter/issues/27226 "AAPT: error: resource android:attr/fontVariationSettings not found." 61// https://github.com/flutter/flutter/issues/27106 "Android resource linking failed|Daemon: AAPT2|error: failed linking references" 62// https://github.com/flutter/flutter/issues/27493 "error: cannot find symbol import androidx.annotation.NonNull;" 63// https://github.com/flutter/flutter/issues/23995 "error: package android.support.annotation does not exist import android.support.annotation.NonNull;" 64final RegExp androidXFailureRegex = RegExp(r'(AAPT|androidx|android\.support)'); 65 66final RegExp androidXPluginWarningRegex = RegExp(r'\*{57}' 67 r"|WARNING: This version of (\w+) will break your Android build if it or its dependencies aren't compatible with AndroidX." 68 r'|See https://goo.gl/CP92wY for more information on the problem and how to fix it.' 69 r'|This warning prints for all Android build failures. The real root cause of the error may be unrelated.'); 70 71FlutterPluginVersion getFlutterPluginVersion(AndroidProject project) { 72 final File plugin = project.hostAppGradleRoot.childFile( 73 fs.path.join('buildSrc', 'src', 'main', 'groovy', 'FlutterPlugin.groovy')); 74 if (plugin.existsSync()) { 75 final String packageLine = plugin.readAsLinesSync().skip(4).first; 76 if (packageLine == 'package io.flutter.gradle') { 77 return FlutterPluginVersion.v2; 78 } 79 return FlutterPluginVersion.v1; 80 } 81 final File appGradle = project.hostAppGradleRoot.childFile( 82 fs.path.join('app', 'build.gradle')); 83 if (appGradle.existsSync()) { 84 for (String line in appGradle.readAsLinesSync()) { 85 if (line.contains(RegExp(r'apply from: .*/flutter.gradle'))) { 86 return FlutterPluginVersion.managed; 87 } 88 if (line.contains("def flutterPluginVersion = 'managed'")) { 89 return FlutterPluginVersion.managed; 90 } 91 } 92 } 93 return FlutterPluginVersion.none; 94} 95 96/// Returns the apk file created by [buildGradleProject] 97Future<File> getGradleAppOut(AndroidProject androidProject) async { 98 switch (getFlutterPluginVersion(androidProject)) { 99 case FlutterPluginVersion.none: 100 // Fall through. Pretend we're v1, and just go with it. 101 case FlutterPluginVersion.v1: 102 return androidProject.gradleAppOutV1File; 103 case FlutterPluginVersion.managed: 104 // Fall through. The managed plugin matches plugin v2 for now. 105 case FlutterPluginVersion.v2: 106 return fs.file((await _gradleAppProject()).apkDirectory.childFile('app.apk')); 107 } 108 return null; 109} 110 111Future<GradleProject> _gradleAppProject() async { 112 _cachedGradleAppProject ??= await _readGradleProject(isLibrary: false); 113 return _cachedGradleAppProject; 114} 115 116Future<GradleProject> _gradleLibraryProject() async { 117 _cachedGradleLibraryProject ??= await _readGradleProject(isLibrary: true); 118 return _cachedGradleLibraryProject; 119} 120 121/// Runs `gradlew dependencies`, ensuring that dependencies are resolved and 122/// potentially downloaded. 123Future<void> checkGradleDependencies() async { 124 final Status progress = logger.startProgress('Ensuring gradle dependencies are up to date...', timeout: timeoutConfiguration.slowOperation); 125 final FlutterProject flutterProject = FlutterProject.current(); 126 final String gradle = await _ensureGradle(flutterProject); 127 await runCheckedAsync( 128 <String>[gradle, 'dependencies'], 129 workingDirectory: flutterProject.android.hostAppGradleRoot.path, 130 environment: _gradleEnv, 131 ); 132 androidSdk.reinitialize(); 133 progress.stop(); 134} 135 136/// Tries to create `settings_aar.gradle` in an app project by removing the subprojects 137/// from the existing `settings.gradle` file. This operation will fail if the existing 138/// `settings.gradle` file has local edits. 139void createSettingsAarGradle(Directory androidDirectory) { 140 final File newSettingsFile = androidDirectory.childFile('settings_aar.gradle'); 141 if (newSettingsFile.existsSync()) { 142 return; 143 } 144 final File currentSettingsFile = androidDirectory.childFile('settings.gradle'); 145 if (!currentSettingsFile.existsSync()) { 146 return; 147 } 148 final String currentFileContent = currentSettingsFile.readAsStringSync(); 149 150 final String newSettingsRelativeFile = fs.path.relative(newSettingsFile.path); 151 final Status status = logger.startProgress('✏️ Creating `$newSettingsRelativeFile`...', 152 timeout: timeoutConfiguration.fastOperation); 153 154 final String flutterRoot = fs.path.absolute(Cache.flutterRoot); 155 final File deprecatedFile = fs.file(fs.path.join(flutterRoot, 'packages','flutter_tools', 156 'gradle', 'deprecated_settings.gradle')); 157 assert(deprecatedFile.existsSync()); 158 final String settingsAarContent = fs.file(fs.path.join(flutterRoot, 'packages','flutter_tools', 159 'gradle', 'settings_aar.gradle.tmpl')).readAsStringSync(); 160 161 // Get the `settings.gradle` content variants that should be patched. 162 final List<String> existingVariants = deprecatedFile.readAsStringSync().split(';EOF'); 163 existingVariants.add(settingsAarContent); 164 165 bool exactMatch = false; 166 for (String fileContentVariant in existingVariants) { 167 if (currentFileContent.trim() == fileContentVariant.trim()) { 168 exactMatch = true; 169 break; 170 } 171 } 172 if (!exactMatch) { 173 status.cancel(); 174 printError('*******************************************************************************************'); 175 printError('Flutter tried to create the file `$newSettingsRelativeFile`, but failed.'); 176 // Print how to manually update the file. 177 printError(fs.file(fs.path.join(flutterRoot, 'packages','flutter_tools', 178 'gradle', 'manual_migration_settings.gradle.md')).readAsStringSync()); 179 printError('*******************************************************************************************'); 180 throwToolExit('Please create the file and run this command again.'); 181 } 182 // Copy the new file. 183 newSettingsFile.writeAsStringSync(settingsAarContent); 184 status.stop(); 185 printStatus('✅ `$newSettingsRelativeFile` created successfully.'); 186} 187 188// Note: Dependencies are resolved and possibly downloaded as a side-effect 189// of calculating the app properties using Gradle. This may take minutes. 190Future<GradleProject> _readGradleProject({bool isLibrary = false}) async { 191 final FlutterProject flutterProject = FlutterProject.current(); 192 final String gradle = await _ensureGradle(flutterProject); 193 updateLocalProperties(project: flutterProject); 194 195 final FlutterManifest manifest = flutterProject.manifest; 196 final Directory hostAppGradleRoot = flutterProject.android.hostAppGradleRoot; 197 198 if (featureFlags.isPluginAsAarEnabled && 199 !manifest.isPlugin && !manifest.isModule) { 200 createSettingsAarGradle(hostAppGradleRoot); 201 } 202 if (manifest.isPlugin) { 203 assert(isLibrary); 204 return GradleProject( 205 <String>['debug', 'profile', 'release'], 206 <String>[], // Plugins don't have flavors. 207 flutterProject.directory.childDirectory('build').path, 208 ); 209 } 210 final Status status = logger.startProgress('Resolving dependencies...', timeout: timeoutConfiguration.slowOperation); 211 GradleProject project; 212 // Get the properties and tasks from Gradle, so we can determinate the `buildDir`, 213 // flavors and build types defined in the project. If gradle fails, then check if the failure is due to t 214 try { 215 final RunResult propertiesRunResult = await runCheckedAsync( 216 <String>[gradle, isLibrary ? 'properties' : 'app:properties'], 217 workingDirectory: hostAppGradleRoot.path, 218 environment: _gradleEnv, 219 ); 220 final RunResult tasksRunResult = await runCheckedAsync( 221 <String>[gradle, isLibrary ? 'tasks': 'app:tasks', '--all', '--console=auto'], 222 workingDirectory: hostAppGradleRoot.path, 223 environment: _gradleEnv, 224 ); 225 project = GradleProject.fromAppProperties(propertiesRunResult.stdout, tasksRunResult.stdout); 226 } catch (exception) { 227 if (getFlutterPluginVersion(flutterProject.android) == FlutterPluginVersion.managed) { 228 status.cancel(); 229 // Handle known exceptions. 230 throwToolExitIfLicenseNotAccepted(exception); 231 // Print a general Gradle error and exit. 232 printError('* Error running Gradle:\n$exception\n'); 233 throwToolExit('Please review your Gradle project setup in the android/ folder.'); 234 } 235 // Fall back to the default 236 project = GradleProject( 237 <String>['debug', 'profile', 'release'], 238 <String>[], 239 fs.path.join(flutterProject.android.hostAppGradleRoot.path, 'app', 'build') 240 ); 241 } 242 status.stop(); 243 return project; 244} 245 246/// Handle Gradle error thrown when Gradle needs to download additional 247/// Android SDK components (e.g. Platform Tools), and the license 248/// for that component has not been accepted. 249void throwToolExitIfLicenseNotAccepted(Exception exception) { 250 const String licenseNotAcceptedMatcher = 251 r'You have not accepted the license agreements of the following SDK components:' 252 r'\s*\[(.+)\]'; 253 final RegExp licenseFailure = RegExp(licenseNotAcceptedMatcher, multiLine: true); 254 final Match licenseMatch = licenseFailure.firstMatch(exception.toString()); 255 if (licenseMatch != null) { 256 final String missingLicenses = licenseMatch.group(1); 257 final String errorMessage = 258 '\n\n* Error running Gradle:\n' 259 'Unable to download needed Android SDK components, as the following licenses have not been accepted:\n' 260 '$missingLicenses\n\n' 261 'To resolve this, please run the following command in a Terminal:\n' 262 'flutter doctor --android-licenses'; 263 throwToolExit(errorMessage); 264 } 265} 266 267String _locateGradlewExecutable(Directory directory) { 268 final File gradle = directory.childFile( 269 platform.isWindows ? 'gradlew.bat' : 'gradlew', 270 ); 271 272 if (gradle.existsSync()) { 273 os.makeExecutable(gradle); 274 return gradle.absolute.path; 275 } else { 276 return null; 277 } 278} 279 280Future<String> _ensureGradle(FlutterProject project) async { 281 _cachedGradleExecutable ??= await _initializeGradle(project); 282 return _cachedGradleExecutable; 283} 284 285// Note: Gradle may be bootstrapped and possibly downloaded as a side-effect 286// of validating the Gradle executable. This may take several seconds. 287Future<String> _initializeGradle(FlutterProject project) async { 288 final Directory android = project.android.hostAppGradleRoot; 289 final Status status = logger.startProgress('Initializing gradle...', timeout: timeoutConfiguration.slowOperation); 290 String gradle = _locateGradlewExecutable(android); 291 if (gradle == null) { 292 injectGradleWrapper(android); 293 gradle = _locateGradlewExecutable(android); 294 } 295 if (gradle == null) 296 throwToolExit('Unable to locate gradlew script'); 297 printTrace('Using gradle from $gradle.'); 298 // Validates the Gradle executable by asking for its version. 299 // Makes Gradle Wrapper download and install Gradle distribution, if needed. 300 await runCheckedAsync(<String>[gradle, '-v'], environment: _gradleEnv); 301 status.stop(); 302 return gradle; 303} 304 305/// Injects the Gradle wrapper into the specified directory. 306void injectGradleWrapper(Directory directory) { 307 copyDirectorySync(cache.getArtifactDirectory('gradle_wrapper'), directory); 308 _locateGradlewExecutable(directory); 309 final File propertiesFile = directory.childFile(fs.path.join('gradle', 'wrapper', 'gradle-wrapper.properties')); 310 if (!propertiesFile.existsSync()) { 311 final String gradleVersion = getGradleVersionForAndroidPlugin(directory); 312 propertiesFile.writeAsStringSync(''' 313distributionBase=GRADLE_USER_HOME 314distributionPath=wrapper/dists 315zipStoreBase=GRADLE_USER_HOME 316zipStorePath=wrapper/dists 317distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersion-all.zip 318''', flush: true, 319 ); 320 } 321} 322 323/// Returns true if [targetVersion] is within the range [min] and [max] inclusive. 324bool _isWithinVersionRange(String targetVersion, {String min, String max}) { 325 final Version parsedTargetVersion = Version.parse(targetVersion); 326 return parsedTargetVersion >= Version.parse(min) && 327 parsedTargetVersion <= Version.parse(max); 328} 329 330const String defaultGradleVersion = '4.10.2'; 331 332/// Returns the Gradle version that is required by the given Android Gradle plugin version 333/// by picking the largest compatible version from 334/// https://developer.android.com/studio/releases/gradle-plugin#updating-gradle 335String getGradleVersionFor(String androidPluginVersion) { 336 if (_isWithinVersionRange(androidPluginVersion, min: '1.0.0', max: '1.1.3')) { 337 return '2.3'; 338 } 339 if (_isWithinVersionRange(androidPluginVersion, min: '1.2.0', max: '1.3.1')) { 340 return '2.9'; 341 } 342 if (_isWithinVersionRange(androidPluginVersion, min: '1.5.0', max: '1.5.0')) { 343 return '2.2.1'; 344 } 345 if (_isWithinVersionRange(androidPluginVersion, min: '2.0.0', max: '2.1.2')) { 346 return '2.13'; 347 } 348 if (_isWithinVersionRange(androidPluginVersion, min: '2.1.3', max: '2.2.3')) { 349 return '2.14.1'; 350 } 351 if (_isWithinVersionRange(androidPluginVersion, min: '2.3.0', max: '2.9.9')) { 352 return '3.3'; 353 } 354 if (_isWithinVersionRange(androidPluginVersion, min: '3.0.0', max: '3.0.9')) { 355 return '4.1'; 356 } 357 if (_isWithinVersionRange(androidPluginVersion, min: '3.1.0', max: '3.1.9')) { 358 return '4.4'; 359 } 360 if (_isWithinVersionRange(androidPluginVersion, min: '3.2.0', max: '3.2.1')) { 361 return '4.6'; 362 } 363 if (_isWithinVersionRange(androidPluginVersion, min: '3.3.0', max: '3.3.2')) { 364 return '4.10.2'; 365 } 366 if (_isWithinVersionRange(androidPluginVersion, min: '3.4.0', max: '3.5.0')) { 367 return '5.1.1'; 368 } 369 throwToolExit('Unsuported Android Plugin version: $androidPluginVersion.'); 370 return ''; 371} 372 373final RegExp _androidPluginRegExp = RegExp('com\.android\.tools\.build\:gradle\:(\\d+\.\\d+\.\\d+\)'); 374 375/// Returns the Gradle version that the current Android plugin depends on when found, 376/// otherwise it returns a default version. 377/// 378/// The Android plugin version is specified in the [build.gradle] file within 379/// the project's Android directory. 380String getGradleVersionForAndroidPlugin(Directory directory) { 381 final File buildFile = directory.childFile('build.gradle'); 382 if (!buildFile.existsSync()) { 383 return defaultGradleVersion; 384 } 385 final String buildFileContent = buildFile.readAsStringSync(); 386 final Iterable<Match> pluginMatches = _androidPluginRegExp.allMatches(buildFileContent); 387 388 if (pluginMatches.isEmpty) { 389 return defaultGradleVersion; 390 } 391 final String androidPluginVersion = pluginMatches.first.group(1); 392 return getGradleVersionFor(androidPluginVersion); 393} 394 395/// Overwrite local.properties in the specified Flutter project's Android 396/// sub-project, if needed. 397/// 398/// If [requireAndroidSdk] is true (the default) and no Android SDK is found, 399/// this will fail with a [ToolExit]. 400void updateLocalProperties({ 401 @required FlutterProject project, 402 BuildInfo buildInfo, 403 bool requireAndroidSdk = true, 404}) { 405 if (requireAndroidSdk) { 406 _exitIfNoAndroidSdk(); 407 } 408 409 final File localProperties = project.android.localPropertiesFile; 410 bool changed = false; 411 412 SettingsFile settings; 413 if (localProperties.existsSync()) { 414 settings = SettingsFile.parseFromFile(localProperties); 415 } else { 416 settings = SettingsFile(); 417 changed = true; 418 } 419 420 void changeIfNecessary(String key, String value) { 421 if (settings.values[key] != value) { 422 if (value == null) { 423 settings.values.remove(key); 424 } else { 425 settings.values[key] = value; 426 } 427 changed = true; 428 } 429 } 430 431 final FlutterManifest manifest = project.manifest; 432 433 if (androidSdk != null) 434 changeIfNecessary('sdk.dir', escapePath(androidSdk.directory)); 435 436 changeIfNecessary('flutter.sdk', escapePath(Cache.flutterRoot)); 437 438 if (buildInfo != null) { 439 changeIfNecessary('flutter.buildMode', buildInfo.modeName); 440 final String buildName = validatedBuildNameForPlatform(TargetPlatform.android_arm, buildInfo.buildName ?? manifest.buildName); 441 changeIfNecessary('flutter.versionName', buildName); 442 final String buildNumber = validatedBuildNumberForPlatform(TargetPlatform.android_arm, buildInfo.buildNumber ?? manifest.buildNumber); 443 changeIfNecessary('flutter.versionCode', buildNumber?.toString()); 444 } 445 446 if (changed) 447 settings.writeContents(localProperties); 448} 449 450/// Writes standard Android local properties to the specified [properties] file. 451/// 452/// Writes the path to the Android SDK, if known. 453void writeLocalProperties(File properties) { 454 final SettingsFile settings = SettingsFile(); 455 if (androidSdk != null) { 456 settings.values['sdk.dir'] = escapePath(androidSdk.directory); 457 } 458 settings.writeContents(properties); 459} 460 461/// Throws a ToolExit, if the path to the Android SDK is not known. 462void _exitIfNoAndroidSdk() { 463 if (androidSdk == null) { 464 throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.'); 465 } 466} 467 468Future<void> buildGradleProject({ 469 @required FlutterProject project, 470 @required AndroidBuildInfo androidBuildInfo, 471 @required String target, 472 @required bool isBuildingBundle, 473}) async { 474 // Update the local.properties file with the build mode, version name and code. 475 // FlutterPlugin v1 reads local.properties to determine build mode. Plugin v2 476 // uses the standard Android way to determine what to build, but we still 477 // update local.properties, in case we want to use it in the future. 478 // Version name and number are provided by the pubspec.yaml file 479 // and can be overwritten with flutter build command. 480 // The default Gradle script reads the version name and number 481 // from the local.properties file. 482 updateLocalProperties(project: project, buildInfo: androidBuildInfo.buildInfo); 483 484 final String gradle = await _ensureGradle(project); 485 486 switch (getFlutterPluginVersion(project.android)) { 487 case FlutterPluginVersion.none: 488 // Fall through. Pretend it's v1, and just go for it. 489 case FlutterPluginVersion.v1: 490 return _buildGradleProjectV1(project, gradle); 491 case FlutterPluginVersion.managed: 492 // Fall through. Managed plugin builds the same way as plugin v2. 493 case FlutterPluginVersion.v2: 494 return _buildGradleProjectV2(project, gradle, androidBuildInfo, target, isBuildingBundle); 495 } 496} 497 498Future<void> buildGradleAar({ 499 @required FlutterProject project, 500 @required AndroidBuildInfo androidBuildInfo, 501 @required String target, 502 @required String outputDir, 503}) async { 504 final FlutterManifest manifest = project.manifest; 505 506 GradleProject gradleProject; 507 if (manifest.isModule) { 508 gradleProject = await _gradleAppProject(); 509 } else if (manifest.isPlugin) { 510 gradleProject = await _gradleLibraryProject(); 511 } else { 512 throwToolExit('AARs can only be built for plugin or module projects.'); 513 } 514 515 if (outputDir != null && outputDir.isNotEmpty) { 516 gradleProject.buildDirectory = outputDir; 517 } 518 519 final String aarTask = gradleProject.aarTaskFor(androidBuildInfo.buildInfo); 520 if (aarTask == null) { 521 printUndefinedTask(gradleProject, androidBuildInfo.buildInfo); 522 throwToolExit('Gradle build aborted.'); 523 } 524 final Status status = logger.startProgress( 525 'Running Gradle task \'$aarTask\'...', 526 timeout: timeoutConfiguration.slowOperation, 527 multilineOutput: true, 528 ); 529 530 final String gradle = await _ensureGradle(project); 531 final String gradlePath = fs.file(gradle).absolute.path; 532 final String flutterRoot = fs.path.absolute(Cache.flutterRoot); 533 final String initScript = fs.path.join(flutterRoot, 'packages','flutter_tools', 'gradle', 'aar_init_script.gradle'); 534 final List<String> command = <String>[ 535 gradlePath, 536 '-I=$initScript', 537 '-Pflutter-root=$flutterRoot', 538 '-Poutput-dir=${gradleProject.buildDirectory}', 539 '-Pis-plugin=${manifest.isPlugin}', 540 '-Dbuild-plugins-as-aars=true', 541 ]; 542 543 if (target != null && target.isNotEmpty) { 544 command.add('-Ptarget=$target'); 545 } 546 547 if (androidBuildInfo.targetArchs.isNotEmpty) { 548 final String targetPlatforms = androidBuildInfo.targetArchs 549 .map(getPlatformNameForAndroidArch).join(','); 550 command.add('-Ptarget-platform=$targetPlatforms'); 551 } 552 command.add(aarTask); 553 554 final Stopwatch sw = Stopwatch()..start(); 555 int exitCode = 1; 556 557 try { 558 exitCode = await runCommandAndStreamOutput( 559 command, 560 workingDirectory: project.android.hostAppGradleRoot.path, 561 allowReentrantFlutter: true, 562 environment: _gradleEnv, 563 mapFunction: (String line) { 564 // Always print the full line in verbose mode. 565 if (logger.isVerbose) { 566 return line; 567 } 568 return null; 569 }, 570 ); 571 } finally { 572 status.stop(); 573 } 574 flutterUsage.sendTiming('build', 'gradle-aar', Duration(milliseconds: sw.elapsedMilliseconds)); 575 576 if (exitCode != 0) { 577 throwToolExit('Gradle task $aarTask failed with exit code $exitCode', exitCode: exitCode); 578 } 579 580 final Directory repoDirectory = gradleProject.repoDirectory; 581 if (!repoDirectory.existsSync()) { 582 throwToolExit('Gradle task $aarTask failed to produce $repoDirectory', exitCode: exitCode); 583 } 584 printStatus('Built ${fs.path.relative(repoDirectory.path)}.', color: TerminalColor.green); 585} 586 587Future<void> _buildGradleProjectV1(FlutterProject project, String gradle) async { 588 // Run 'gradlew build'. 589 final Status status = logger.startProgress( 590 'Running \'gradlew build\'...', 591 timeout: timeoutConfiguration.slowOperation, 592 multilineOutput: true, 593 ); 594 final Stopwatch sw = Stopwatch()..start(); 595 final int exitCode = await runCommandAndStreamOutput( 596 <String>[fs.file(gradle).absolute.path, 'build'], 597 workingDirectory: project.android.hostAppGradleRoot.path, 598 allowReentrantFlutter: true, 599 environment: _gradleEnv, 600 ); 601 status.stop(); 602 flutterUsage.sendTiming('build', 'gradle-v1', Duration(milliseconds: sw.elapsedMilliseconds)); 603 604 if (exitCode != 0) 605 throwToolExit('Gradle build failed: $exitCode', exitCode: exitCode); 606 607 printStatus('Built ${fs.path.relative(project.android.gradleAppOutV1File.path)}.'); 608} 609 610String _hex(List<int> bytes) { 611 final StringBuffer result = StringBuffer(); 612 for (int part in bytes) 613 result.write('${part < 16 ? '0' : ''}${part.toRadixString(16)}'); 614 return result.toString(); 615} 616 617String _calculateSha(File file) { 618 final Stopwatch sw = Stopwatch()..start(); 619 final List<int> bytes = file.readAsBytesSync(); 620 printTrace('calculateSha: reading file took ${sw.elapsedMilliseconds}us'); 621 flutterUsage.sendTiming('build', 'apk-sha-read', Duration(milliseconds: sw.elapsedMilliseconds)); 622 sw.reset(); 623 final String sha = _hex(sha1.convert(bytes).bytes); 624 printTrace('calculateSha: computing sha took ${sw.elapsedMilliseconds}us'); 625 flutterUsage.sendTiming('build', 'apk-sha-calc', Duration(milliseconds: sw.elapsedMilliseconds)); 626 return sha; 627} 628 629void printUndefinedTask(GradleProject project, BuildInfo buildInfo) { 630 printError(''); 631 printError('The Gradle project does not define a task suitable for the requested build.'); 632 if (!project.buildTypes.contains(buildInfo.modeName)) { 633 printError('Review the android/app/build.gradle file and ensure it defines a ${buildInfo.modeName} build type.'); 634 return; 635 } 636 if (project.productFlavors.isEmpty) { 637 printError('The android/app/build.gradle file does not define any custom product flavors.'); 638 printError('You cannot use the --flavor option.'); 639 } else { 640 printError('The android/app/build.gradle file defines product flavors: ${project.productFlavors.join(', ')}'); 641 printError('You must specify a --flavor option to select one of them.'); 642 } 643} 644 645Future<void> _buildGradleProjectV2( 646 FlutterProject flutterProject, 647 String gradle, 648 AndroidBuildInfo androidBuildInfo, 649 String target, 650 bool isBuildingBundle, 651) async { 652 final GradleProject project = await _gradleAppProject(); 653 final BuildInfo buildInfo = androidBuildInfo.buildInfo; 654 655 String assembleTask; 656 657 if (isBuildingBundle) { 658 assembleTask = project.bundleTaskFor(buildInfo); 659 } else { 660 assembleTask = project.assembleTaskFor(buildInfo); 661 } 662 if (assembleTask == null) { 663 printUndefinedTask(project, buildInfo); 664 throwToolExit('Gradle build aborted.'); 665 } 666 final Status status = logger.startProgress( 667 'Running Gradle task \'$assembleTask\'...', 668 timeout: timeoutConfiguration.slowOperation, 669 multilineOutput: true, 670 ); 671 final String gradlePath = fs.file(gradle).absolute.path; 672 final List<String> command = <String>[gradlePath]; 673 if (logger.isVerbose) { 674 command.add('-Pverbose=true'); 675 } else { 676 command.add('-q'); 677 } 678 if (artifacts is LocalEngineArtifacts) { 679 final LocalEngineArtifacts localEngineArtifacts = artifacts; 680 printTrace('Using local engine: ${localEngineArtifacts.engineOutPath}'); 681 command.add('-PlocalEngineOut=${localEngineArtifacts.engineOutPath}'); 682 } 683 if (target != null) { 684 command.add('-Ptarget=$target'); 685 } 686 assert(buildInfo.trackWidgetCreation != null); 687 command.add('-Ptrack-widget-creation=${buildInfo.trackWidgetCreation}'); 688 if (buildInfo.extraFrontEndOptions != null) 689 command.add('-Pextra-front-end-options=${buildInfo.extraFrontEndOptions}'); 690 if (buildInfo.extraGenSnapshotOptions != null) 691 command.add('-Pextra-gen-snapshot-options=${buildInfo.extraGenSnapshotOptions}'); 692 if (buildInfo.fileSystemRoots != null && buildInfo.fileSystemRoots.isNotEmpty) 693 command.add('-Pfilesystem-roots=${buildInfo.fileSystemRoots.join('|')}'); 694 if (buildInfo.fileSystemScheme != null) 695 command.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}'); 696 if (androidBuildInfo.splitPerAbi) 697 command.add('-Psplit-per-abi=true'); 698 if (androidBuildInfo.targetArchs.isNotEmpty) { 699 final String targetPlatforms = androidBuildInfo.targetArchs 700 .map(getPlatformNameForAndroidArch).join(','); 701 command.add('-Ptarget-platform=$targetPlatforms'); 702 } 703 if (featureFlags.isPluginAsAarEnabled) { 704 // Pass a system flag instead of a project flag, so this flag can be 705 // read from include_flutter.groovy. 706 command.add('-Dbuild-plugins-as-aars=true'); 707 if (!flutterProject.manifest.isModule) { 708 command.add('--settings-file=settings_aar.gradle'); 709 } 710 } 711 command.add(assembleTask); 712 bool potentialAndroidXFailure = false; 713 final Stopwatch sw = Stopwatch()..start(); 714 int exitCode = 1; 715 try { 716 exitCode = await runCommandAndStreamOutput( 717 command, 718 workingDirectory: flutterProject.android.hostAppGradleRoot.path, 719 allowReentrantFlutter: true, 720 environment: _gradleEnv, 721 // TODO(mklim): if AndroidX warnings are no longer required, this 722 // mapFunction and all its associated variabled can be replaced with just 723 // `filter: ndkMessagefilter`. 724 mapFunction: (String line) { 725 final bool isAndroidXPluginWarning = androidXPluginWarningRegex.hasMatch(line); 726 if (!isAndroidXPluginWarning && androidXFailureRegex.hasMatch(line)) { 727 potentialAndroidXFailure = true; 728 } 729 // Always print the full line in verbose mode. 730 if (logger.isVerbose) { 731 return line; 732 } else if (isAndroidXPluginWarning || !ndkMessageFilter.hasMatch(line)) { 733 return null; 734 } 735 736 return line; 737 }, 738 ); 739 } finally { 740 status.stop(); 741 } 742 743 if (exitCode != 0) { 744 if (potentialAndroidXFailure) { 745 printError('*******************************************************************************************'); 746 printError('The Gradle failure may have been because of AndroidX incompatibilities in this Flutter app.'); 747 printError('See https://goo.gl/CP92wY for more information on the problem and how to fix it.'); 748 printError('*******************************************************************************************'); 749 BuildEvent('android-x-failure').send(); 750 } 751 throwToolExit('Gradle task $assembleTask failed with exit code $exitCode', exitCode: exitCode); 752 } 753 flutterUsage.sendTiming('build', 'gradle-v2', Duration(milliseconds: sw.elapsedMilliseconds)); 754 755 if (!isBuildingBundle) { 756 final Iterable<File> apkFiles = findApkFiles(project, androidBuildInfo); 757 if (apkFiles.isEmpty) 758 throwToolExit('Gradle build failed to produce an Android package.'); 759 // Copy the first APK to app.apk, so `flutter run`, `flutter install`, etc. can find it. 760 // TODO(blasten): Handle multiple APKs. 761 apkFiles.first.copySync(project.apkDirectory.childFile('app.apk').path); 762 763 printTrace('calculateSha: ${project.apkDirectory}/app.apk'); 764 final File apkShaFile = project.apkDirectory.childFile('app.apk.sha1'); 765 apkShaFile.writeAsStringSync(_calculateSha(apkFiles.first)); 766 767 for (File apkFile in apkFiles) { 768 String appSize; 769 if (buildInfo.mode == BuildMode.debug) { 770 appSize = ''; 771 } else { 772 appSize = ' (${getSizeAsMB(apkFile.lengthSync())})'; 773 } 774 printStatus('Built ${fs.path.relative(apkFile.path)}$appSize.', 775 color: TerminalColor.green); 776 } 777 } else { 778 final File bundleFile = findBundleFile(project, buildInfo); 779 if (bundleFile == null) 780 throwToolExit('Gradle build failed to produce an Android bundle package.'); 781 782 String appSize; 783 if (buildInfo.mode == BuildMode.debug) { 784 appSize = ''; 785 } else { 786 appSize = ' (${getSizeAsMB(bundleFile.lengthSync())})'; 787 } 788 printStatus('Built ${fs.path.relative(bundleFile.path)}$appSize.', 789 color: TerminalColor.green); 790 } 791} 792 793@visibleForTesting 794Iterable<File> findApkFiles(GradleProject project, AndroidBuildInfo androidBuildInfo) { 795 final Iterable<String> apkFileNames = project.apkFilesFor(androidBuildInfo); 796 if (apkFileNames.isEmpty) 797 return const <File>[]; 798 799 return apkFileNames.expand<File>((String apkFileName) { 800 File apkFile = project.apkDirectory.childFile(apkFileName); 801 if (apkFile.existsSync()) 802 return <File>[apkFile]; 803 final BuildInfo buildInfo = androidBuildInfo.buildInfo; 804 final String modeName = camelCase(buildInfo.modeName); 805 apkFile = project.apkDirectory 806 .childDirectory(modeName) 807 .childFile(apkFileName); 808 if (apkFile.existsSync()) 809 return <File>[apkFile]; 810 if (buildInfo.flavor != null) { 811 // Android Studio Gradle plugin v3 adds flavor to path. 812 apkFile = project.apkDirectory 813 .childDirectory(buildInfo.flavor) 814 .childDirectory(modeName) 815 .childFile(apkFileName); 816 if (apkFile.existsSync()) 817 return <File>[apkFile]; 818 } 819 return const <File>[]; 820 }); 821} 822 823@visibleForTesting 824File findBundleFile(GradleProject project, BuildInfo buildInfo) { 825 final String bundleFileName = project.bundleFileFor(buildInfo); 826 if (bundleFileName == null) { 827 return null; 828 } 829 File bundleFile = project.bundleDirectory 830 .childDirectory(camelCase(buildInfo.modeName)) 831 .childFile(bundleFileName); 832 if (bundleFile.existsSync()) { 833 return bundleFile; 834 } 835 if (buildInfo.flavor == null) { 836 return null; 837 } 838 // Android Studio Gradle plugin v3 adds the flavor to the path. For the bundle the 839 // folder name is the flavor plus the mode name. On Windows, filenames aren't case sensitive. 840 // For example: foo_barRelease where `foo_bar` is the flavor and `Release` the mode name. 841 final String childDirName = '${buildInfo.flavor}${camelCase('_' + buildInfo.modeName)}'; 842 bundleFile = project.bundleDirectory 843 .childDirectory(childDirName) 844 .childFile(bundleFileName); 845 if (bundleFile.existsSync()) { 846 return bundleFile; 847 } 848 return null; 849} 850 851Map<String, String> get _gradleEnv { 852 final Map<String, String> env = Map<String, String>.from(platform.environment); 853 if (javaPath != null) { 854 // Use java bundled with Android Studio. 855 env['JAVA_HOME'] = javaPath; 856 } 857 // Don't log analytics for downstream Flutter commands. 858 // e.g. `flutter build bundle`. 859 env['FLUTTER_SUPPRESS_ANALYTICS'] = 'true'; 860 return env; 861} 862 863class GradleProject { 864 GradleProject( 865 this.buildTypes, 866 this.productFlavors, 867 this.buildDirectory, 868 ); 869 870 factory GradleProject.fromAppProperties(String properties, String tasks) { 871 // Extract build directory. 872 final String buildDirectory = properties 873 .split('\n') 874 .firstWhere((String s) => s.startsWith('buildDir: ')) 875 .substring('buildDir: '.length) 876 .trim(); 877 878 // Extract build types and product flavors. 879 final Set<String> variants = <String>{}; 880 for (String s in tasks.split('\n')) { 881 final Match match = _assembleTaskPattern.matchAsPrefix(s); 882 if (match != null) { 883 final String variant = match.group(1).toLowerCase(); 884 if (!variant.endsWith('test')) 885 variants.add(variant); 886 } 887 } 888 final Set<String> buildTypes = <String>{}; 889 final Set<String> productFlavors = <String>{}; 890 for (final String variant1 in variants) { 891 for (final String variant2 in variants) { 892 if (variant2.startsWith(variant1) && variant2 != variant1) { 893 final String buildType = variant2.substring(variant1.length); 894 if (variants.contains(buildType)) { 895 buildTypes.add(buildType); 896 productFlavors.add(variant1); 897 } 898 } 899 } 900 } 901 if (productFlavors.isEmpty) 902 buildTypes.addAll(variants); 903 return GradleProject( 904 buildTypes.toList(), 905 productFlavors.toList(), 906 buildDirectory, 907 ); 908 } 909 910 /// The build types such as [release] or [debug]. 911 final List<String> buildTypes; 912 913 /// The product flavors defined in build.gradle. 914 final List<String> productFlavors; 915 916 /// The build directory. This is typically <project>build/. 917 String buildDirectory; 918 919 /// The directory where the APK artifact is generated. 920 Directory get apkDirectory { 921 return fs.directory(fs.path.join(buildDirectory, 'outputs', 'apk')); 922 } 923 924 /// The directory where the app bundle artifact is generated. 925 Directory get bundleDirectory { 926 return fs.directory(fs.path.join(buildDirectory, 'outputs', 'bundle')); 927 } 928 929 /// The directory where the repo is generated. 930 /// Only applicable to AARs. 931 Directory get repoDirectory { 932 return fs.directory(fs.path.join(buildDirectory, 'outputs', 'repo')); 933 } 934 935 String _buildTypeFor(BuildInfo buildInfo) { 936 final String modeName = camelCase(buildInfo.modeName); 937 if (buildTypes.contains(modeName.toLowerCase())) 938 return modeName; 939 return null; 940 } 941 942 String _productFlavorFor(BuildInfo buildInfo) { 943 if (buildInfo.flavor == null) 944 return productFlavors.isEmpty ? '' : null; 945 else if (productFlavors.contains(buildInfo.flavor)) 946 return buildInfo.flavor; 947 else 948 return null; 949 } 950 951 String assembleTaskFor(BuildInfo buildInfo) { 952 final String buildType = _buildTypeFor(buildInfo); 953 final String productFlavor = _productFlavorFor(buildInfo); 954 if (buildType == null || productFlavor == null) 955 return null; 956 return 'assemble${toTitleCase(productFlavor)}${toTitleCase(buildType)}'; 957 } 958 959 Iterable<String> apkFilesFor(AndroidBuildInfo androidBuildInfo) { 960 final String buildType = _buildTypeFor(androidBuildInfo.buildInfo); 961 final String productFlavor = _productFlavorFor(androidBuildInfo.buildInfo); 962 if (buildType == null || productFlavor == null) 963 return const <String>[]; 964 965 final String flavorString = productFlavor.isEmpty ? '' : '-' + productFlavor; 966 if (androidBuildInfo.splitPerAbi) { 967 return androidBuildInfo.targetArchs.map<String>((AndroidArch arch) { 968 final String abi = getNameForAndroidArch(arch); 969 return 'app$flavorString-$abi-$buildType.apk'; 970 }); 971 } 972 return <String>['app$flavorString-$buildType.apk']; 973 } 974 975 String bundleTaskFor(BuildInfo buildInfo) { 976 final String buildType = _buildTypeFor(buildInfo); 977 final String productFlavor = _productFlavorFor(buildInfo); 978 if (buildType == null || productFlavor == null) 979 return null; 980 return 'bundle${toTitleCase(productFlavor)}${toTitleCase(buildType)}'; 981 } 982 983 String aarTaskFor(BuildInfo buildInfo) { 984 final String buildType = _buildTypeFor(buildInfo); 985 final String productFlavor = _productFlavorFor(buildInfo); 986 if (buildType == null || productFlavor == null) 987 return null; 988 return 'assembleAar${toTitleCase(productFlavor)}${toTitleCase(buildType)}'; 989 } 990 991 String bundleFileFor(BuildInfo buildInfo) { 992 // For app bundle all bundle names are called as app.aab. Product flavors 993 // & build types are differentiated as folders, where the aab will be added. 994 return 'app.aab'; 995 } 996} 997