gradle.dart 18.8 KB
Newer Older
1 2 3 4 5 6
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

7 8
import 'package:meta/meta.dart';

9
import '../android/android_sdk.dart';
10
import '../artifacts.dart';
11
import '../base/common.dart';
12
import '../base/file_system.dart';
13 14
import '../base/logger.dart';
import '../base/os.dart';
15
import '../base/platform.dart';
16 17 18 19
import '../base/process.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../cache.dart';
20
import '../flutter_manifest.dart';
21
import '../globals.dart';
22
import '../project.dart';
23
import 'android_sdk.dart';
24
import 'android_studio.dart';
25

26
const String gradleVersion = '4.4';
27
final RegExp _assembleTaskPattern = RegExp(r'assemble([^:]+): task ');
28

29
GradleProject _cachedGradleProject;
30
String _cachedGradleExecutable;
31

32 33 34 35
enum FlutterPluginVersion {
  none,
  v1,
  v2,
36
  managed,
37
}
38

39 40 41
// Investigation documented in #13975 suggests the filter should be a subset
// of the impact of -q, but users insist they see the error message sometimes
// anyway.  If we can prove it really is impossible, delete the filter.
42
final RegExp ndkMessageFilter = RegExp(r'^(?!NDK is missing a ".*" directory'
43 44 45
  r'|If you are not using NDK, unset the NDK variable from ANDROID_NDK_HOME or local.properties to remove this warning'
  r'|If you are using NDK, verify the ndk.dir is set to a valid NDK directory.  It is currently set to .*)');

46
FlutterPluginVersion getFlutterPluginVersion(AndroidProject project) {
47
  final File plugin = project.hostAppGradleRoot.childFile(
48
      fs.path.join('buildSrc', 'src', 'main', 'groovy', 'FlutterPlugin.groovy'));
49
  if (plugin.existsSync()) {
50
    final String packageLine = plugin.readAsLinesSync().skip(4).first;
51
    if (packageLine == 'package io.flutter.gradle') {
52 53 54
      return FlutterPluginVersion.v2;
    }
    return FlutterPluginVersion.v1;
55
  }
56 57
  final File appGradle = project.hostAppGradleRoot.childFile(
      fs.path.join('app', 'build.gradle'));
58 59
  if (appGradle.existsSync()) {
    for (String line in appGradle.readAsLinesSync()) {
60
      if (line.contains(RegExp(r'apply from: .*/flutter.gradle'))) {
61 62
        return FlutterPluginVersion.managed;
      }
63 64 65
      if (line.contains("def flutterPluginVersion = 'managed'")) {
        return FlutterPluginVersion.managed;
      }
66
    }
67
  }
68
  return FlutterPluginVersion.none;
69 70
}

71 72
/// Returns the apk file created by [buildGradleProject]
Future<File> getGradleAppOut(AndroidProject androidProject) async {
73
  switch (getFlutterPluginVersion(androidProject)) {
74 75 76
    case FlutterPluginVersion.none:
      // Fall through. Pretend we're v1, and just go with it.
    case FlutterPluginVersion.v1:
77
      return androidProject.gradleAppOutV1File;
78 79
    case FlutterPluginVersion.managed:
      // Fall through. The managed plugin matches plugin v2 for now.
80
    case FlutterPluginVersion.v2:
81
      return fs.file((await _gradleProject()).apkDirectory.childFile('app.apk'));
82 83 84 85
  }
  return null;
}

86 87 88
Future<GradleProject> _gradleProject() async {
  _cachedGradleProject ??= await _readGradleProject();
  return _cachedGradleProject;
89 90
}

91 92
// Note: Dependencies are resolved and possibly downloaded as a side-effect
// of calculating the app properties using Gradle. This may take minutes.
93
Future<GradleProject> _readGradleProject() async {
94
  final FlutterProject flutterProject = await FlutterProject.current();
95
  final String gradle = await _ensureGradle(flutterProject);
96
  updateLocalProperties(project: flutterProject);
97 98
  final Status status = logger.startProgress('Resolving dependencies...', expectSlowOperation: true);
  GradleProject project;
99
  try {
100
    final RunResult runResult = await runCheckedAsync(
101
      <String>[gradle, 'app:properties'],
102
      workingDirectory: flutterProject.android.hostAppGradleRoot.path,
103
      environment: _gradleEnv,
104
    );
105
    final String properties = runResult.stdout.trim();
106
    project = GradleProject.fromAppProperties(properties);
107
  } catch (exception) {
108
    if (getFlutterPluginVersion(flutterProject.android) == FlutterPluginVersion.managed) {
109
      status.cancel();
110
      // Handle known exceptions. This will exit if handled.
111
      handleKnownGradleExceptions(exception);
112

113
      // Print a general Gradle error and exit.
114
      printError('* Error running Gradle:\n$exception\n');
115
      throwToolExit('Please review your Gradle project setup in the android/ folder.');
116
    }
117
    // Fall back to the default
118
    project = GradleProject(
119 120 121
      <String>['debug', 'profile', 'release'],
      <String>[], flutterProject.android.gradleAppOutV1Directory,
    );
122
  }
123 124
  status.stop();
  return project;
125 126
}

127 128 129 130
void handleKnownGradleExceptions(String exceptionString) {
  // 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.
131
  const String matcher =
132 133
    r'You have not accepted the license agreements of the following SDK components:'
    r'\s*\[(.+)\]';
134
  final RegExp licenseFailure = RegExp(matcher, multiLine: true);
135 136 137 138 139 140 141 142 143 144 145 146 147
  final Match licenseMatch = licenseFailure.firstMatch(exceptionString);
  if (licenseMatch != null) {
    final String missingLicenses = licenseMatch.group(1);
    final String errorMessage =
      '\n\n* Error running Gradle:\n'
      'Unable to download needed Android SDK components, as the following licenses have not been accepted:\n'
      '$missingLicenses\n\n'
      'To resolve this, please run the following command in a Terminal:\n'
      'flutter doctor --android-licenses';
    throwToolExit(errorMessage);
  }
}

148 149
String _locateGradlewExecutable(Directory directory) {
  final File gradle = directory.childFile(
150
    platform.isWindows ? 'gradlew.bat' : 'gradlew',
151
  );
152

153 154
  if (gradle.existsSync()) {
    os.makeExecutable(gradle);
155
    return gradle.absolute.path;
156 157 158 159 160
  } else {
    return null;
  }
}

161 162
Future<String> _ensureGradle(FlutterProject project) async {
  _cachedGradleExecutable ??= await _initializeGradle(project);
163 164 165 166 167
  return _cachedGradleExecutable;
}

// Note: Gradle may be bootstrapped and possibly downloaded as a side-effect
// of validating the Gradle executable. This may take several seconds.
168
Future<String> _initializeGradle(FlutterProject project) async {
169
  final Directory android = project.android.hostAppGradleRoot;
170
  final Status status = logger.startProgress('Initializing gradle...', expectSlowOperation: true);
171
  String gradle = _locateGradlewExecutable(android);
172
  if (gradle == null) {
173 174
    injectGradleWrapper(android);
    gradle = _locateGradlewExecutable(android);
175
  }
176 177 178 179 180 181 182
  if (gradle == null)
    throwToolExit('Unable to locate gradlew script');
  printTrace('Using gradle from $gradle.');
  // Validates the Gradle executable by asking for its version.
  // Makes Gradle Wrapper download and install Gradle distribution, if needed.
  await runCheckedAsync(<String>[gradle, '-v'], environment: _gradleEnv);
  status.stop();
183
  return gradle;
184 185
}

186 187 188 189 190 191 192
/// Injects the Gradle wrapper into the specified directory.
void injectGradleWrapper(Directory directory) {
  copyDirectorySync(cache.getArtifactDirectory('gradle_wrapper'), directory);
  _locateGradlewExecutable(directory);
  final File propertiesFile = directory.childFile(fs.path.join('gradle', 'wrapper', 'gradle-wrapper.properties'));
  if (!propertiesFile.existsSync()) {
    propertiesFile.writeAsStringSync('''
193 194 195 196 197 198 199 200 201 202
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersion-all.zip
''', flush: true,
    );
  }
}

203 204
/// Overwrite local.properties in the specified Flutter project's Android
/// sub-project, if needed.
205
///
206 207
/// If [requireAndroidSdk] is true (the default) and no Android SDK is found,
/// this will fail with a [ToolExit].
208
void updateLocalProperties({
209 210 211
  @required FlutterProject project,
  BuildInfo buildInfo,
  bool requireAndroidSdk = true,
212 213 214
}) {
  if (requireAndroidSdk) {
    _exitIfNoAndroidSdk();
215 216
  }

217
  final File localProperties = project.android.localPropertiesFile;
218 219 220 221
  bool changed = false;

  SettingsFile settings;
  if (localProperties.existsSync()) {
222
    settings = SettingsFile.parseFromFile(localProperties);
223
  } else {
224
    settings = SettingsFile();
225 226 227
    changed = true;
  }

228 229 230 231 232
  void changeIfNecessary(String key, String value) {
    if (settings.values[key] != value) {
      settings.values[key] = value;
      changed = true;
    }
233 234
  }

235
  final FlutterManifest manifest = project.manifest;
236

237 238 239 240 241
  if (androidSdk != null)
    changeIfNecessary('sdk.dir', escapePath(androidSdk.directory));
  changeIfNecessary('flutter.sdk', escapePath(Cache.flutterRoot));
  if (buildInfo != null)
    changeIfNecessary('flutter.buildMode', buildInfo.modeName);
242
  final String buildName = buildInfo?.buildName ?? manifest.buildName;
243 244
  if (buildName != null)
    changeIfNecessary('flutter.versionName', buildName);
245
  final int buildNumber = buildInfo?.buildNumber ?? manifest.buildNumber;
246 247
  if (buildNumber != null)
    changeIfNecessary('flutter.versionCode', '$buildNumber');
248

249 250
  if (changed)
    settings.writeContents(localProperties);
251 252
}

253 254 255 256
/// Writes standard Android local properties to the specified [properties] file.
///
/// Writes the path to the Android SDK, if known.
void writeLocalProperties(File properties) {
257
  final SettingsFile settings = SettingsFile();
258 259 260 261 262 263 264 265 266 267 268 269 270
  if (androidSdk != null) {
    settings.values['sdk.dir'] = escapePath(androidSdk.directory);
  }
  settings.writeContents(properties);
}

/// Throws a ToolExit, if the path to the Android SDK is not known.
void _exitIfNoAndroidSdk() {
  if (androidSdk == null) {
    throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.');
  }
}

271 272 273 274 275
Future<Null> buildGradleProject({
  @required FlutterProject project,
  @required BuildInfo buildInfo,
  @required String target,
}) async {
276
  // Update the local.properties file with the build mode, version name and code.
277 278 279
  // FlutterPlugin v1 reads local.properties to determine build mode. Plugin v2
  // uses the standard Android way to determine what to build, but we still
  // update local.properties, in case we want to use it in the future.
280 281 282 283
  // Version name and number are provided by the pubspec.yaml file
  // and can be overwritten with flutter build command.
  // The default Gradle script reads the version name and number
  // from the local.properties file.
284
  updateLocalProperties(project: project, buildInfo: buildInfo);
285

286
  final String gradle = await _ensureGradle(project);
287

288
  switch (getFlutterPluginVersion(project.android)) {
289 290 291
    case FlutterPluginVersion.none:
      // Fall through. Pretend it's v1, and just go for it.
    case FlutterPluginVersion.v1:
292
      return _buildGradleProjectV1(project, gradle);
293 294
    case FlutterPluginVersion.managed:
      // Fall through. Managed plugin builds the same way as plugin v2.
295
    case FlutterPluginVersion.v2:
296
      return _buildGradleProjectV2(project, gradle, buildInfo, target);
297 298 299
  }
}

300
Future<Null> _buildGradleProjectV1(FlutterProject project, String gradle) async {
301 302 303
  // Run 'gradlew build'.
  final Status status = logger.startProgress('Running \'gradlew build\'...', expectSlowOperation: true);
  final int exitCode = await runCommandAndStreamOutput(
304
    <String>[fs.file(gradle).absolute.path, 'build'],
305
    workingDirectory: project.android.hostAppGradleRoot.path,
306 307
    allowReentrantFlutter: true,
    environment: _gradleEnv,
308
  );
Devon Carew's avatar
Devon Carew committed
309
  status.stop();
310

311 312
  if (exitCode != 0)
    throwToolExit('Gradle build failed: $exitCode', exitCode: exitCode);
313

314
  printStatus('Built ${fs.path.relative(project.android.gradleAppOutV1File.path)}.');
315 316
}

317 318 319 320 321
Future<Null> _buildGradleProjectV2(
    FlutterProject flutterProject,
    String gradle,
    BuildInfo buildInfo,
    String target) async {
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
  final GradleProject project = await _gradleProject();
  final String assembleTask = project.assembleTaskFor(buildInfo);
  if (assembleTask == null) {
    printError('');
    printError('The Gradle project does not define a task suitable for the requested build.');
    if (!project.buildTypes.contains(buildInfo.modeName)) {
      printError('Review the android/app/build.gradle file and ensure it defines a ${buildInfo.modeName} build type.');
    } else {
      if (project.productFlavors.isEmpty) {
        printError('The android/app/build.gradle file does not define any custom product flavors.');
        printError('You cannot use the --flavor option.');
      } else {
        printError('The android/app/build.gradle file defines product flavors: ${project.productFlavors.join(', ')}');
        printError('You must specify a --flavor option to select one of them.');
      }
      throwToolExit('Gradle build aborted.');
    }
  }
340
  final Status status = logger.startProgress('Running \'gradlew $assembleTask\'...', expectSlowOperation: true);
341 342
  final String gradlePath = fs.file(gradle).absolute.path;
  final List<String> command = <String>[gradlePath];
343 344 345
  if (logger.isVerbose) {
    command.add('-Pverbose=true');
  } else {
346 347 348
    command.add('-q');
  }
  if (artifacts is LocalEngineArtifacts) {
349
    final LocalEngineArtifacts localEngineArtifacts = artifacts;
350 351 352
    printTrace('Using local engine: ${localEngineArtifacts.engineOutPath}');
    command.add('-PlocalEngineOut=${localEngineArtifacts.engineOutPath}');
  }
353 354 355
  if (target != null) {
    command.add('-Ptarget=$target');
  }
356 357 358 359 360 361 362 363 364 365 366 367
  if (buildInfo.trackWidgetCreation)
    command.add('-Ptrack-widget-creation=true');
  if (buildInfo.compilationTraceFilePath != null)
    command.add('-Pprecompile=${buildInfo.compilationTraceFilePath}');
  if (buildInfo.extraFrontEndOptions != null)
    command.add('-Pextra-front-end-options=${buildInfo.extraFrontEndOptions}');
  if (buildInfo.extraGenSnapshotOptions != null)
    command.add('-Pextra-gen-snapshot-options=${buildInfo.extraGenSnapshotOptions}');
  if (buildInfo.fileSystemRoots != null && buildInfo.fileSystemRoots.isNotEmpty)
    command.add('-Pfilesystem-roots=${buildInfo.fileSystemRoots.join('|')}');
  if (buildInfo.fileSystemScheme != null)
    command.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}');
368 369
  if (buildInfo.buildSharedLibrary && androidSdk.ndk != null) {
    command.add('-Pbuild-shared-library=true');
370
  }
371 372
  if (buildInfo.targetPlatform != null)
    command.add('-Ptarget-platform=${getNameForTargetPlatform(buildInfo.targetPlatform)}');
373

374
  command.add(assembleTask);
375
  final int exitCode = await runCommandAndStreamOutput(
376
    command,
377
    workingDirectory: flutterProject.android.hostAppGradleRoot.path,
378 379 380
    allowReentrantFlutter: true,
    environment: _gradleEnv,
    filter: logger.isVerbose ? null : ndkMessageFilter,
381 382 383
  );
  status.stop();

384 385
  if (exitCode != 0)
    throwToolExit('Gradle build failed: $exitCode', exitCode: exitCode);
386

387
  final File apkFile = _findApkFile(project, buildInfo);
388 389
  if (apkFile == null)
    throwToolExit('Gradle build failed to produce an Android package.');
390
  // Copy the APK to app.apk, so `flutter run`, `flutter install`, etc. can find it.
391
  apkFile.copySync(project.apkDirectory.childFile('app.apk').path);
392

393
  printTrace('calculateSha: ${project.apkDirectory}/app.apk');
394
  final File apkShaFile = project.apkDirectory.childFile('app.apk.sha1');
395 396
  apkShaFile.writeAsStringSync(calculateSha(apkFile));

397 398 399 400 401 402 403
  String appSize;
  if (buildInfo.mode == BuildMode.debug) {
    appSize = '';
  } else {
    appSize = ' (${getSizeAsMB(apkFile.lengthSync())})';
  }
  printStatus('Built ${fs.path.relative(apkFile.path)}$appSize.');
404 405 406 407 408 409
}

File _findApkFile(GradleProject project, BuildInfo buildInfo) {
  final String apkFileName = project.apkFileFor(buildInfo);
  if (apkFileName == null)
    return null;
410
  File apkFile = fs.file(fs.path.join(project.apkDirectory.path, apkFileName));
411 412
  if (apkFile.existsSync())
    return apkFile;
413 414
  final String modeName = camelCase(buildInfo.modeName);
  apkFile = fs.file(fs.path.join(project.apkDirectory.path, modeName, apkFileName));
415 416
  if (apkFile.existsSync())
    return apkFile;
417 418
  if (buildInfo.flavor != null) {
    // Android Studio Gradle plugin v3 adds flavor to path.
419
    apkFile = fs.file(fs.path.join(project.apkDirectory.path, buildInfo.flavor, modeName, apkFileName));
420 421 422
    if (apkFile.existsSync())
      return apkFile;
  }
423
  return null;
424
}
425 426

Map<String, String> get _gradleEnv {
427
  final Map<String, String> env = Map<String, String>.from(platform.environment);
428 429 430 431 432 433
  if (javaPath != null) {
    // Use java bundled with Android Studio.
    env['JAVA_HOME'] = javaPath;
  }
  return env;
}
434 435 436 437 438 439 440 441 442 443 444 445 446

class GradleProject {
  GradleProject(this.buildTypes, this.productFlavors, this.apkDirectory);

  factory GradleProject.fromAppProperties(String properties) {
    // Extract build directory.
    final String buildDir = properties
        .split('\n')
        .firstWhere((String s) => s.startsWith('buildDir: '))
        .substring('buildDir: '.length)
        .trim();

    // Extract build types and product flavors.
447
    final Set<String> variants = Set<String>();
448
    for (String s in properties.split('\n')) {
449 450 451 452 453 454
      final Match match = _assembleTaskPattern.matchAsPrefix(s);
      if (match != null) {
        final String variant = match.group(1).toLowerCase();
        if (!variant.endsWith('test'))
          variants.add(variant);
      }
455
    }
456 457
    final Set<String> buildTypes = Set<String>();
    final Set<String> productFlavors = Set<String>();
458 459 460 461 462 463 464 465 466 467 468 469 470
    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)) {
            buildTypes.add(buildType);
            productFlavors.add(variant1);
          }
        }
      }
    }
    if (productFlavors.isEmpty)
      buildTypes.addAll(variants);
471
    return GradleProject(
472 473
      buildTypes.toList(),
      productFlavors.toList(),
474
      fs.directory(fs.path.join(buildDir, 'outputs', 'apk')),
475 476 477 478 479
    );
  }

  final List<String> buildTypes;
  final List<String> productFlavors;
480
  final Directory apkDirectory;
481 482

  String _buildTypeFor(BuildInfo buildInfo) {
483 484 485
    final String modeName = camelCase(buildInfo.modeName);
    if (buildTypes.contains(modeName.toLowerCase()))
      return modeName;
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
    return null;
  }

  String _productFlavorFor(BuildInfo buildInfo) {
    if (buildInfo.flavor == null)
      return productFlavors.isEmpty ? '' : null;
    else if (productFlavors.contains(buildInfo.flavor.toLowerCase()))
      return buildInfo.flavor.toLowerCase();
    else
      return null;
  }

  String assembleTaskFor(BuildInfo buildInfo) {
    final String buildType = _buildTypeFor(buildInfo);
    final String productFlavor = _productFlavorFor(buildInfo);
    if (buildType == null || productFlavor == null)
      return null;
    return 'assemble${toTitleCase(productFlavor)}${toTitleCase(buildType)}';
  }

  String apkFileFor(BuildInfo buildInfo) {
    final String buildType = _buildTypeFor(buildInfo);
    final String productFlavor = _productFlavorFor(buildInfo);
    if (buildType == null || productFlavor == null)
      return null;
    final String flavorString = productFlavor.isEmpty ? '' : '-' + productFlavor;
    return 'app$flavorString-$buildType.apk';
  }
}