gradle.dart 19.2 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.10.2';
27
final RegExp _assembleTaskPattern = RegExp(r'assemble(\S+)');
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 propertiesRunResult = await runCheckedAsync(
101
      <String>[gradle, 'app:properties'],
102
      workingDirectory: flutterProject.android.hostAppGradleRoot.path,
103
      environment: _gradleEnv,
104
    );
105 106 107 108 109 110
    final RunResult tasksRunResult = await runCheckedAsync(
      <String>[gradle, 'app:tasks', '--all'],
      workingDirectory: flutterProject.android.hostAppGradleRoot.path,
      environment: _gradleEnv,
    );
    project = GradleProject.fromAppProperties(propertiesRunResult.stdout, tasksRunResult.stdout);
111
  } catch (exception) {
112
    if (getFlutterPluginVersion(flutterProject.android) == FlutterPluginVersion.managed) {
113
      status.cancel();
114
      // Handle known exceptions. This will exit if handled.
115
      handleKnownGradleExceptions(exception.toString());
116

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

131 132 133 134
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.
135
  const String matcher =
136 137
    r'You have not accepted the license agreements of the following SDK components:'
    r'\s*\[(.+)\]';
138
  final RegExp licenseFailure = RegExp(matcher, multiLine: true);
139 140 141 142 143 144 145 146 147 148 149 150 151
  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);
  }
}

152 153
String _locateGradlewExecutable(Directory directory) {
  final File gradle = directory.childFile(
154
    platform.isWindows ? 'gradlew.bat' : 'gradlew',
155
  );
156

157 158
  if (gradle.existsSync()) {
    os.makeExecutable(gradle);
159
    return gradle.absolute.path;
160 161 162 163 164
  } else {
    return null;
  }
}

165 166
Future<String> _ensureGradle(FlutterProject project) async {
  _cachedGradleExecutable ??= await _initializeGradle(project);
167 168 169 170 171
  return _cachedGradleExecutable;
}

// Note: Gradle may be bootstrapped and possibly downloaded as a side-effect
// of validating the Gradle executable. This may take several seconds.
172
Future<String> _initializeGradle(FlutterProject project) async {
173
  final Directory android = project.android.hostAppGradleRoot;
174
  final Status status = logger.startProgress('Initializing gradle...', expectSlowOperation: true);
175
  String gradle = _locateGradlewExecutable(android);
176
  if (gradle == null) {
177 178
    injectGradleWrapper(android);
    gradle = _locateGradlewExecutable(android);
179
  }
180 181 182 183 184 185 186
  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();
187
  return gradle;
188 189
}

190 191 192 193 194 195 196
/// 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('''
197 198 199 200 201 202 203 204 205 206
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,
    );
  }
}

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

221
  final File localProperties = project.android.localPropertiesFile;
222 223 224 225
  bool changed = false;

  SettingsFile settings;
  if (localProperties.existsSync()) {
226
    settings = SettingsFile.parseFromFile(localProperties);
227
  } else {
228
    settings = SettingsFile();
229 230 231
    changed = true;
  }

232 233 234 235 236
  void changeIfNecessary(String key, String value) {
    if (settings.values[key] != value) {
      settings.values[key] = value;
      changed = true;
    }
237 238
  }

239
  final FlutterManifest manifest = project.manifest;
240

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

253 254
  if (changed)
    settings.writeContents(localProperties);
255 256
}

257 258 259 260
/// Writes standard Android local properties to the specified [properties] file.
///
/// Writes the path to the Android SDK, if known.
void writeLocalProperties(File properties) {
261
  final SettingsFile settings = SettingsFile();
262 263 264 265 266 267 268 269 270 271 272 273 274
  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.');
  }
}

275
Future<void> buildGradleProject({
276 277 278 279
  @required FlutterProject project,
  @required BuildInfo buildInfo,
  @required String target,
}) async {
280
  // Update the local.properties file with the build mode, version name and code.
281 282 283
  // 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.
284 285 286 287
  // 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.
288
  updateLocalProperties(project: project, buildInfo: buildInfo);
289

290
  final String gradle = await _ensureGradle(project);
291

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

304
Future<void> _buildGradleProjectV1(FlutterProject project, String gradle) async {
305
  // Run 'gradlew build'.
306 307 308 309 310
  final Status status = logger.startProgress(
    "Running 'gradlew build'...",
    expectSlowOperation: true,
    multilineOutput: true,
  );
311
  final int exitCode = await runCommandAndStreamOutput(
312
    <String>[fs.file(gradle).absolute.path, 'build'],
313
    workingDirectory: project.android.hostAppGradleRoot.path,
314 315
    allowReentrantFlutter: true,
    environment: _gradleEnv,
316
  );
Devon Carew's avatar
Devon Carew committed
317
  status.stop();
318

319 320
  if (exitCode != 0)
    throwToolExit('Gradle build failed: $exitCode', exitCode: exitCode);
321

322
  printStatus('Built ${fs.path.relative(project.android.gradleAppOutV1File.path)}.');
323 324
}

325
Future<void> _buildGradleProjectV2(
326 327 328 329
    FlutterProject flutterProject,
    String gradle,
    BuildInfo buildInfo,
    String target) async {
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
  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.');
    }
  }
348 349 350 351 352
  final Status status = logger.startProgress(
    "Gradle task '$assembleTask'...",
    expectSlowOperation: true,
    multilineOutput: true,
  );
353 354
  final String gradlePath = fs.file(gradle).absolute.path;
  final List<String> command = <String>[gradlePath];
355 356 357
  if (logger.isVerbose) {
    command.add('-Pverbose=true');
  } else {
358 359 360
    command.add('-q');
  }
  if (artifacts is LocalEngineArtifacts) {
361
    final LocalEngineArtifacts localEngineArtifacts = artifacts;
362 363 364
    printTrace('Using local engine: ${localEngineArtifacts.engineOutPath}');
    command.add('-PlocalEngineOut=${localEngineArtifacts.engineOutPath}');
  }
365 366 367
  if (target != null) {
    command.add('-Ptarget=$target');
  }
368 369
  assert(buildInfo.trackWidgetCreation != null);
  command.add('-Ptrack-widget-creation=${buildInfo.trackWidgetCreation}');
370 371
  if (buildInfo.compilationTraceFilePath != null)
    command.add('-Pprecompile=${buildInfo.compilationTraceFilePath}');
372 373
  if (buildInfo.buildHotUpdate)
    command.add('-Photupdate=true');
374 375 376 377 378 379 380 381
  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}');
382 383
  if (buildInfo.buildSharedLibrary && androidSdk.ndk != null) {
    command.add('-Pbuild-shared-library=true');
384
  }
385 386
  if (buildInfo.targetPlatform != null)
    command.add('-Ptarget-platform=${getNameForTargetPlatform(buildInfo.targetPlatform)}');
387

388
  command.add(assembleTask);
389
  final int exitCode = await runCommandAndStreamOutput(
390
    command,
391
    workingDirectory: flutterProject.android.hostAppGradleRoot.path,
392 393 394
    allowReentrantFlutter: true,
    environment: _gradleEnv,
    filter: logger.isVerbose ? null : ndkMessageFilter,
395 396 397
  );
  status.stop();

398
  if (exitCode != 0)
399
    throwToolExit('Gradle task $assembleTask failed with exit code $exitCode', exitCode: exitCode);
400

401
  final File apkFile = _findApkFile(project, buildInfo);
402 403
  if (apkFile == null)
    throwToolExit('Gradle build failed to produce an Android package.');
404
  // Copy the APK to app.apk, so `flutter run`, `flutter install`, etc. can find it.
405
  apkFile.copySync(project.apkDirectory.childFile('app.apk').path);
406

407
  printTrace('calculateSha: ${project.apkDirectory}/app.apk');
408
  final File apkShaFile = project.apkDirectory.childFile('app.apk.sha1');
409 410
  apkShaFile.writeAsStringSync(calculateSha(apkFile));

411 412 413 414 415 416 417
  String appSize;
  if (buildInfo.mode == BuildMode.debug) {
    appSize = '';
  } else {
    appSize = ' (${getSizeAsMB(apkFile.lengthSync())})';
  }
  printStatus('Built ${fs.path.relative(apkFile.path)}$appSize.');
418 419 420 421 422 423
}

File _findApkFile(GradleProject project, BuildInfo buildInfo) {
  final String apkFileName = project.apkFileFor(buildInfo);
  if (apkFileName == null)
    return null;
424
  File apkFile = fs.file(fs.path.join(project.apkDirectory.path, apkFileName));
425 426
  if (apkFile.existsSync())
    return apkFile;
427 428
  final String modeName = camelCase(buildInfo.modeName);
  apkFile = fs.file(fs.path.join(project.apkDirectory.path, modeName, apkFileName));
429 430
  if (apkFile.existsSync())
    return apkFile;
431 432
  if (buildInfo.flavor != null) {
    // Android Studio Gradle plugin v3 adds flavor to path.
433
    apkFile = fs.file(fs.path.join(project.apkDirectory.path, buildInfo.flavor, modeName, apkFileName));
434 435 436
    if (apkFile.existsSync())
      return apkFile;
  }
437
  return null;
438
}
439 440

Map<String, String> get _gradleEnv {
441
  final Map<String, String> env = Map<String, String>.from(platform.environment);
442 443 444 445 446 447
  if (javaPath != null) {
    // Use java bundled with Android Studio.
    env['JAVA_HOME'] = javaPath;
  }
  return env;
}
448 449 450 451

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

452
  factory GradleProject.fromAppProperties(String properties, String tasks) {
453 454 455 456 457 458 459 460
    // 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.
461
    final Set<String> variants = Set<String>();
462
    for (String s in tasks.split('\n')) {
463 464 465 466 467 468
      final Match match = _assembleTaskPattern.matchAsPrefix(s);
      if (match != null) {
        final String variant = match.group(1).toLowerCase();
        if (!variant.endsWith('test'))
          variants.add(variant);
      }
469
    }
470 471
    final Set<String> buildTypes = Set<String>();
    final Set<String> productFlavors = Set<String>();
472 473 474 475 476 477 478 479 480 481 482 483 484
    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);
485
    return GradleProject(
486 487
      buildTypes.toList(),
      productFlavors.toList(),
488
      fs.directory(fs.path.join(buildDir, 'outputs', 'apk')),
489 490 491 492 493
    );
  }

  final List<String> buildTypes;
  final List<String> productFlavors;
494
  final Directory apkDirectory;
495 496

  String _buildTypeFor(BuildInfo buildInfo) {
497 498 499
    final String modeName = camelCase(buildInfo.modeName);
    if (buildTypes.contains(modeName.toLowerCase()))
      return modeName;
500 501 502 503 504 505
    return null;
  }

  String _productFlavorFor(BuildInfo buildInfo) {
    if (buildInfo.flavor == null)
      return productFlavors.isEmpty ? '' : null;
506 507
    else if (productFlavors.contains(buildInfo.flavor))
      return buildInfo.flavor;
508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528
    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';
  }
}