gradle.dart 15.4 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
import '../android/android_sdk.dart';
8
import '../artifacts.dart';
9
import '../base/common.dart';
10
import '../base/file_system.dart';
11 12
import '../base/logger.dart';
import '../base/os.dart';
13
import '../base/platform.dart';
14 15 16 17 18
import '../base/process.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../cache.dart';
import '../globals.dart';
19
import '../plugins.dart';
20
import 'android_sdk.dart';
21
import 'android_studio.dart';
22 23

const String gradleManifestPath = 'android/app/src/main/AndroidManifest.xml';
24
const String gradleAppOutV1 = 'android/app/build/outputs/apk/app-debug.apk';
25
const String gradleAppOutDirV1 = 'android/app/build/outputs/apk';
26
const String gradleVersion = '3.3';
27
final RegExp _assembleTaskPattern = new 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

bool isProjectUsingGradle() {
40
  return fs.isFileSync('android/build.gradle');
41 42
}

43
FlutterPluginVersion get flutterPluginVersion {
44
  final File plugin = fs.file('android/buildSrc/src/main/groovy/FlutterPlugin.groovy');
45
  if (plugin.existsSync()) {
46
    final String packageLine = plugin.readAsLinesSync().skip(4).first;
47
    if (packageLine == 'package io.flutter.gradle') {
48 49 50
      return FlutterPluginVersion.v2;
    }
    return FlutterPluginVersion.v1;
51
  }
52
  final File appGradle = fs.file('android/app/build.gradle');
53 54
  if (appGradle.existsSync()) {
    for (String line in appGradle.readAsLinesSync()) {
55
      if (line.contains(new RegExp(r'apply from: .*/flutter.gradle'))) {
56 57 58
        return FlutterPluginVersion.managed;
      }
    }
59
  }
60
  return FlutterPluginVersion.none;
61 62
}

63 64
/// Returns the path to the apk file created by [buildGradleProject], relative
/// to current directory.
65
Future<String> getGradleAppOut() async {
66 67 68 69 70
  switch (flutterPluginVersion) {
    case FlutterPluginVersion.none:
      // Fall through. Pretend we're v1, and just go with it.
    case FlutterPluginVersion.v1:
      return gradleAppOutV1;
71 72
    case FlutterPluginVersion.managed:
      // Fall through. The managed plugin matches plugin v2 for now.
73
    case FlutterPluginVersion.v2:
74
      return fs.path.relative(fs.path.join((await _gradleProject()).apkDirectory, 'app.apk'));
75 76 77 78
  }
  return null;
}

79 80 81
Future<GradleProject> _gradleProject() async {
  _cachedGradleProject ??= await _readGradleProject();
  return _cachedGradleProject;
82 83
}

84 85
// Note: Dependencies are resolved and possibly downloaded as a side-effect
// of calculating the app properties using Gradle. This may take minutes.
86
Future<GradleProject> _readGradleProject() async {
87
  final String gradle = await _ensureGradle();
88
  updateLocalProperties();
89
  try {
90 91
    final Status status = logger.startProgress('Resolving dependencies...', expectSlowOperation: true);
    final RunResult runResult = await runCheckedAsync(
92 93
      <String>[gradle, 'app:properties'],
      workingDirectory: 'android',
94
      environment: _gradleEnv,
95
    );
96
    final String properties = runResult.stdout.trim();
97
    final GradleProject project = new GradleProject.fromAppProperties(properties);
98
    status.stop();
99
    return project;
100
  } catch (e) {
101
    if (flutterPluginVersion == FlutterPluginVersion.managed) {
102 103
      // Handle known exceptions. This will exit if handled.
      handleKnownGradleExceptions(e);
104

105 106 107
      // Print a general Gradle error and exit.
      printError('* Error running Gradle:\n$e\n');
      throwToolExit('Please review your Gradle project setup in the android/ folder.');
108
    }
109 110
  }
  // Fall back to the default
111
  return new GradleProject(<String>['debug', 'profile', 'release'], <String>[], gradleAppOutDirV1);
112 113
}

114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 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.
  final String matcher =
    r'You have not accepted the license agreements of the following SDK components:'
    r'\s*\[(.+)\]';
  final RegExp licenseFailure = new RegExp(matcher, multiLine: true);
  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);
  }
}

135
String _locateProjectGradlew({ bool ensureExecutable: true }) {
136
  final String path = fs.path.join(
137 138
    'android',
    platform.isWindows ? 'gradlew.bat' : 'gradlew',
139
  );
140

141
  if (fs.isFileSync(path)) {
142
    final File gradle = fs.file(path);
143
    if (ensureExecutable)
144 145
      os.makeExecutable(gradle);
    return gradle.absolute.path;
146 147 148 149 150
  } else {
    return null;
  }
}

151 152 153 154 155 156 157 158
Future<String> _ensureGradle() async {
  _cachedGradleExecutable ??= await _initializeGradle();
  return _cachedGradleExecutable;
}

// Note: Gradle may be bootstrapped and possibly downloaded as a side-effect
// of validating the Gradle executable. This may take several seconds.
Future<String> _initializeGradle() async {
159
  final Status status = logger.startProgress('Initializing gradle...', expectSlowOperation: true);
160
  String gradle = _locateProjectGradlew();
161
  if (gradle == null) {
162
    _injectGradleWrapper();
163
    gradle = _locateProjectGradlew();
164
  }
165 166 167 168 169 170 171
  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();
172
  return gradle;
173 174
}

175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
void _injectGradleWrapper() {
  copyDirectorySync(cache.getArtifactDirectory('gradle_wrapper'), fs.directory('android'));
  final String propertiesPath = fs.path.join('android', 'gradle', 'wrapper', 'gradle-wrapper.properties');
  if (!fs.file(propertiesPath).existsSync()) {
    fs.file(propertiesPath).writeAsStringSync('''
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,
    );
  }
}

190
/// Create android/local.properties if needed, and update Flutter settings.
191
void updateLocalProperties({String projectPath, BuildInfo buildInfo}) {
192 193 194 195 196 197 198 199 200 201 202 203
  final File localProperties = (projectPath == null)
      ? fs.file(fs.path.join('android', 'local.properties'))
      : fs.file(fs.path.join(projectPath, 'android', 'local.properties'));
  bool changed = false;

  SettingsFile settings;
  if (localProperties.existsSync()) {
    settings = new SettingsFile.parseFromFile(localProperties);
  } else {
    settings = new SettingsFile();
    settings.values['sdk.dir'] = escapePath(androidSdk.directory);
    changed = true;
204
  }
205 206 207 208 209
  final String escapedRoot = escapePath(Cache.flutterRoot);
  if (changed || settings.values['flutter.sdk'] != escapedRoot) {
    settings.values['flutter.sdk'] = escapedRoot;
    changed = true;
  }
210 211
  if (buildInfo != null && settings.values['flutter.buildMode'] != buildInfo.modeName) {
    settings.values['flutter.buildMode'] = buildInfo.modeName;
212 213 214 215 216
    changed = true;
  }

  if (changed)
    settings.writeContents(localProperties);
217 218
}

219
Future<Null> buildGradleProject(BuildInfo buildInfo, String target) async {
220 221 222 223
  // Update the local.properties file with the build mode.
  // 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.
224
  updateLocalProperties(buildInfo: buildInfo);
225

226
  injectPlugins();
227

228
  final String gradle = await _ensureGradle();
229 230 231 232 233

  switch (flutterPluginVersion) {
    case FlutterPluginVersion.none:
      // Fall through. Pretend it's v1, and just go for it.
    case FlutterPluginVersion.v1:
234
      return _buildGradleProjectV1(gradle);
235 236
    case FlutterPluginVersion.managed:
      // Fall through. Managed plugin builds the same way as plugin v2.
237
    case FlutterPluginVersion.v2:
238
      return _buildGradleProjectV2(gradle, buildInfo, target);
239 240 241
  }
}

242 243 244 245
Future<Null> _buildGradleProjectV1(String gradle) async {
  // Run 'gradlew build'.
  final Status status = logger.startProgress('Running \'gradlew build\'...', expectSlowOperation: true);
  final int exitCode = await runCommandAndStreamOutput(
246
    <String>[fs.file(gradle).absolute.path, 'build'],
247
    workingDirectory: 'android',
248 249
    allowReentrantFlutter: true,
    environment: _gradleEnv,
250
  );
Devon Carew's avatar
Devon Carew committed
251
  status.stop();
252

253 254
  if (exitCode != 0)
    throwToolExit('Gradle build failed: $exitCode', exitCode: exitCode);
255

256
  final File apkFile = fs.file(gradleAppOutV1);
257 258 259
  printStatus('Built $gradleAppOutV1 (${getSizeAsMB(apkFile.lengthSync())}).');
}

260
Future<Null> _buildGradleProjectV2(String gradle, BuildInfo buildInfo, String target) async {
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
  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.');
    }
  }
279
  final Status status = logger.startProgress('Running \'gradlew $assembleTask\'...', expectSlowOperation: true);
280 281
  final String gradlePath = fs.file(gradle).absolute.path;
  final List<String> command = <String>[gradlePath];
282 283 284 285
  if (!logger.isVerbose) {
    command.add('-q');
  }
  if (artifacts is LocalEngineArtifacts) {
286
    final LocalEngineArtifacts localEngineArtifacts = artifacts;
287 288 289
    printTrace('Using local engine: ${localEngineArtifacts.engineOutPath}');
    command.add('-PlocalEngineOut=${localEngineArtifacts.engineOutPath}');
  }
290 291 292
  if (target != null) {
    command.add('-Ptarget=$target');
  }
293
  if (buildInfo.previewDart2) {
294
    command.add('-Ppreview-dart-2=true');
295 296 297 298
  if (buildInfo.extraFrontEndOptions != null)
    command.add('-Pextra-front-end-options=${buildInfo.extraFrontEndOptions}');
  if (buildInfo.extraGenSnapshotOptions != null)
    command.add('-Pextra-gen-snapshot-options=${buildInfo.extraGenSnapshotOptions}');
299 300 301 302 303
  }
  if (buildInfo.preferSharedLibrary && androidSdk.ndkCompiler != null) {
    command.add('-Pprefer-shared-library=true');
  }

304
  command.add(assembleTask);
305
  final int exitCode = await runCommandAndStreamOutput(
306 307
      command,
      workingDirectory: 'android',
308 309
      allowReentrantFlutter: true,
      environment: _gradleEnv,
310 311 312
  );
  status.stop();

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

316
  final File apkFile = _findApkFile(project, buildInfo);
317 318
  if (apkFile == null)
    throwToolExit('Gradle build failed to produce an Android package.');
319
  // Copy the APK to app.apk, so `flutter run`, `flutter install`, etc. can find it.
320
  apkFile.copySync(fs.path.join(project.apkDirectory, 'app.apk'));
321

322 323
  printTrace('calculateSha: ${project.apkDirectory}/app.apk');
  final File apkShaFile = fs.file(fs.path.join(project.apkDirectory, 'app.apk.sha1'));
324 325
  apkShaFile.writeAsStringSync(calculateSha(apkFile));

326 327 328 329 330 331 332 333 334 335 336 337 338 339
  printStatus('Built ${fs.path.relative(apkFile.path)} (${getSizeAsMB(apkFile.lengthSync())}).');
}

File _findApkFile(GradleProject project, BuildInfo buildInfo) {
  final String apkFileName = project.apkFileFor(buildInfo);
  if (apkFileName == null)
    return null;
  File apkFile = fs.file(fs.path.join(project.apkDirectory, apkFileName));
  if (apkFile.existsSync())
    return apkFile;
  apkFile = fs.file(fs.path.join(project.apkDirectory, buildInfo.modeName, apkFileName));
  if (apkFile.existsSync())
    return apkFile;
  return null;
340
}
341 342 343 344 345 346 347 348 349

Map<String, String> get _gradleEnv {
  final Map<String, String> env = new Map<String, String>.from(platform.environment);
  if (javaPath != null) {
    // Use java bundled with Android Studio.
    env['JAVA_HOME'] = javaPath;
  }
  return env;
}
350 351 352 353 354 355 356 357 358 359 360 361 362 363

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.
    final Set<String> variants = new Set<String>();
364
    for (String s in properties.split('\n')) {
365 366 367 368 369 370
      final Match match = _assembleTaskPattern.matchAsPrefix(s);
      if (match != null) {
        final String variant = match.group(1).toLowerCase();
        if (!variant.endsWith('test'))
          variants.add(variant);
      }
371
    }
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
    final Set<String> buildTypes = new Set<String>();
    final Set<String> productFlavors = new Set<String>();
    for (final String variant1 in variants) {
      for (final String variant2 in variants) {
        if (variant2.startsWith(variant1) && variant2 != variant1) {
          final String buildType = variant2.substring(variant1.length);
          if (variants.contains(buildType)) {
            buildTypes.add(buildType);
            productFlavors.add(variant1);
          }
        }
      }
    }
    if (productFlavors.isEmpty)
      buildTypes.addAll(variants);
    return new GradleProject(
      buildTypes.toList(),
      productFlavors.toList(),
      fs.path.normalize(fs.path.join(buildDir, 'outputs', 'apk')),
    );
  }

  final List<String> buildTypes;
  final List<String> productFlavors;
  final String apkDirectory;

  String _buildTypeFor(BuildInfo buildInfo) {
    if (buildTypes.contains(buildInfo.modeName))
      return buildInfo.modeName;
    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';
  }
}