• 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 '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