blob: fca9ce737916d5d7edef95dde21e5453c5de8589 [file] [log] [blame]
// Copyright 2014 The Flutter 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';
import 'package:crypto/crypto.dart';
import 'package:meta/meta.dart';
import 'package:xml/xml.dart' as xml;
import '../android/android_sdk.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../base/terminal.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../cache.dart';
import '../flutter_manifest.dart';
import '../globals.dart' as globals;
import '../project.dart';
import '../reporting/reporting.dart';
import 'android_sdk.dart';
import 'gradle_errors.dart';
import 'gradle_utils.dart';
/// The directory where the APK artifact is generated.
@visibleForTesting
Directory getApkDirectory(FlutterProject project) {
return project.isModule
? project.android.buildDirectory
.childDirectory('host')
.childDirectory('outputs')
.childDirectory('apk')
: project.android.buildDirectory
.childDirectory('app')
.childDirectory('outputs')
.childDirectory('apk');
}
/// The directory where the app bundle artifact is generated.
@visibleForTesting
Directory getBundleDirectory(FlutterProject project) {
return project.isModule
? project.android.buildDirectory
.childDirectory('host')
.childDirectory('outputs')
.childDirectory('bundle')
: project.android.buildDirectory
.childDirectory('app')
.childDirectory('outputs')
.childDirectory('bundle');
}
/// The directory where the repo is generated.
/// Only applicable to AARs.
Directory getRepoDirectory(Directory buildDirectory) {
return buildDirectory
.childDirectory('outputs')
.childDirectory('repo');
}
/// Returns the name of Gradle task that starts with [prefix].
String _taskFor(String prefix, BuildInfo buildInfo) {
final String buildType = camelCase(buildInfo.modeName);
final String productFlavor = buildInfo.flavor ?? '';
return '$prefix${toTitleCase(productFlavor)}${toTitleCase(buildType)}';
}
/// Returns the task to build an APK.
@visibleForTesting
String getAssembleTaskFor(BuildInfo buildInfo) {
return _taskFor('assemble', buildInfo);
}
/// Returns the task to build an AAB.
@visibleForTesting
String getBundleTaskFor(BuildInfo buildInfo) {
return _taskFor('bundle', buildInfo);
}
/// Returns the task to build an AAR.
@visibleForTesting
String getAarTaskFor(BuildInfo buildInfo) {
return _taskFor('assembleAar', buildInfo);
}
/// Returns the output APK file names for a given [AndroidBuildInfo].
///
/// For example, when [splitPerAbi] is true, multiple APKs are created.
Iterable<String> _apkFilesFor(AndroidBuildInfo androidBuildInfo) {
final String buildType = camelCase(androidBuildInfo.buildInfo.modeName);
final String productFlavor = androidBuildInfo.buildInfo.flavor ?? '';
final String flavorString = productFlavor.isEmpty ? '' : '-$productFlavor';
if (androidBuildInfo.splitPerAbi) {
return androidBuildInfo.targetArchs.map<String>((AndroidArch arch) {
final String abi = getNameForAndroidArch(arch);
return 'app$flavorString-$abi-$buildType.apk';
});
}
return <String>['app$flavorString-$buildType.apk'];
}
/// Returns true if the current version of the Gradle plugin is supported.
bool _isSupportedVersion(AndroidProject project) {
final File plugin = project.hostAppGradleRoot.childFile(
globals.fs.path.join('buildSrc', 'src', 'main', 'groovy', 'FlutterPlugin.groovy'));
if (plugin.existsSync()) {
return false;
}
final File appGradle = project.hostAppGradleRoot.childFile(
globals.fs.path.join('app', 'build.gradle'));
if (!appGradle.existsSync()) {
return false;
}
for (final String line in appGradle.readAsLinesSync()) {
if (line.contains(RegExp(r'apply from: .*/flutter.gradle')) ||
line.contains("def flutterPluginVersion = 'managed'")) {
return true;
}
}
return false;
}
/// Returns the apk file created by [buildGradleProject]
Future<File> getGradleAppOut(AndroidProject androidProject) async {
if (!_isSupportedVersion(androidProject)) {
_exitWithUnsupportedProjectMessage();
}
return getApkDirectory(androidProject.parent).childFile('app.apk');
}
/// Runs `gradlew dependencies`, ensuring that dependencies are resolved and
/// potentially downloaded.
Future<void> checkGradleDependencies() async {
final Status progress = globals.logger.startProgress(
'Ensuring gradle dependencies are up to date...',
timeout: timeoutConfiguration.slowOperation,
);
final FlutterProject flutterProject = FlutterProject.current();
await processUtils.run(<String>[
gradleUtils.getExecutable(flutterProject),
'dependencies',
],
throwOnError: true,
workingDirectory: flutterProject.android.hostAppGradleRoot.path,
environment: gradleEnvironment,
);
androidSdk?.reinitialize();
progress.stop();
}
/// Tries to create `settings_aar.gradle` in an app project by removing the subprojects
/// from the existing `settings.gradle` file. This operation will fail if the existing
/// `settings.gradle` file has local edits.
@visibleForTesting
void createSettingsAarGradle(Directory androidDirectory) {
final File newSettingsFile = androidDirectory.childFile('settings_aar.gradle');
if (newSettingsFile.existsSync()) {
return;
}
final File currentSettingsFile = androidDirectory.childFile('settings.gradle');
if (!currentSettingsFile.existsSync()) {
return;
}
final String currentFileContent = currentSettingsFile.readAsStringSync();
final String newSettingsRelativeFile = globals.fs.path.relative(newSettingsFile.path);
final Status status = globals.logger.startProgress('✏️ Creating `$newSettingsRelativeFile`...',
timeout: timeoutConfiguration.fastOperation);
final String flutterRoot = globals.fs.path.absolute(Cache.flutterRoot);
final File deprecatedFile = globals.fs.file(globals.fs.path.join(flutterRoot, 'packages','flutter_tools',
'gradle', 'deprecated_settings.gradle'));
assert(deprecatedFile.existsSync());
final String settingsAarContent = globals.fs.file(globals.fs.path.join(flutterRoot, 'packages','flutter_tools',
'gradle', 'settings_aar.gradle.tmpl')).readAsStringSync();
// Get the `settings.gradle` content variants that should be patched.
final List<String> existingVariants = deprecatedFile.readAsStringSync().split(';EOF');
existingVariants.add(settingsAarContent);
bool exactMatch = false;
for (final String fileContentVariant in existingVariants) {
if (currentFileContent.trim() == fileContentVariant.trim()) {
exactMatch = true;
break;
}
}
if (!exactMatch) {
status.cancel();
globals.printStatus('$warningMark Flutter tried to create the file `$newSettingsRelativeFile`, but failed.');
// Print how to manually update the file.
globals.printStatus(globals.fs.file(globals.fs.path.join(flutterRoot, 'packages','flutter_tools',
'gradle', 'manual_migration_settings.gradle.md')).readAsStringSync());
throwToolExit('Please create the file and run this command again.');
}
// Copy the new file.
newSettingsFile.writeAsStringSync(settingsAarContent);
status.stop();
globals.printStatus('$successMark `$newSettingsRelativeFile` created successfully.');
}
/// Builds an app.
///
/// * [project] is typically [FlutterProject.current()].
/// * [androidBuildInfo] is the build configuration.
/// * [target] is the target dart entry point. Typically, `lib/main.dart`.
/// * If [isBuildingBundle] is `true`, then the output artifact is an `*.aab`,
/// otherwise the output artifact is an `*.apk`.
/// * The plugins are built as AARs if [shouldBuildPluginAsAar] is `true`. This isn't set by default
/// because it makes the build slower proportional to the number of plugins.
/// * [retries] is the max number of build retries in case one of the [GradleHandledError] handler
/// returns [GradleBuildStatus.retry] or [GradleBuildStatus.retryWithAarPlugins].
Future<void> buildGradleApp({
@required FlutterProject project,
@required AndroidBuildInfo androidBuildInfo,
@required String target,
@required bool isBuildingBundle,
@required List<GradleHandledError> localGradleErrors,
bool shouldBuildPluginAsAar = false,
int retries = 1,
}) async {
assert(project != null);
assert(androidBuildInfo != null);
assert(target != null);
assert(isBuildingBundle != null);
assert(localGradleErrors != null);
assert(androidSdk != null);
if (!project.android.isUsingGradle) {
_exitWithProjectNotUsingGradleMessage();
}
if (!_isSupportedVersion(project.android)) {
_exitWithUnsupportedProjectMessage();
}
final Directory buildDirectory = project.android.buildDirectory;
final bool usesAndroidX = isAppUsingAndroidX(project.android.hostAppGradleRoot);
if (usesAndroidX) {
BuildEvent('app-using-android-x').send();
} else if (!usesAndroidX) {
BuildEvent('app-not-using-android-x').send();
globals.printStatus("$warningMark Your app isn't using AndroidX.", emphasis: true);
globals.printStatus(
'To avoid potential build failures, you can quickly migrate your app '
'by following the steps on https://goo.gl/CP92wY.',
indent: 4,
);
}
// The default Gradle script reads the version name and number
// from the local.properties file.
updateLocalProperties(project: project, buildInfo: androidBuildInfo.buildInfo);
if (shouldBuildPluginAsAar) {
// Create a settings.gradle that doesn't import the plugins as subprojects.
createSettingsAarGradle(project.android.hostAppGradleRoot);
await buildPluginsAsAar(
project,
androidBuildInfo,
buildDirectory: buildDirectory.childDirectory('app'),
);
}
final BuildInfo buildInfo = androidBuildInfo.buildInfo;
final String assembleTask = isBuildingBundle
? getBundleTaskFor(buildInfo)
: getAssembleTaskFor(buildInfo);
final Status status = globals.logger.startProgress(
"Running Gradle task '$assembleTask'...",
timeout: timeoutConfiguration.slowOperation,
multilineOutput: true,
);
final List<String> command = <String>[
gradleUtils.getExecutable(project),
];
if (globals.logger.isVerbose) {
command.add('-Pverbose=true');
} else {
command.add('-q');
}
if (globals.artifacts is LocalEngineArtifacts) {
final LocalEngineArtifacts localEngineArtifacts = globals.artifacts as LocalEngineArtifacts;
final Directory localEngineRepo = _getLocalEngineRepo(
engineOutPath: localEngineArtifacts.engineOutPath,
androidBuildInfo: androidBuildInfo,
);
globals.printTrace(
'Using local engine: ${localEngineArtifacts.engineOutPath}\n'
'Local Maven repo: ${localEngineRepo.path}'
);
command.add('-Plocal-engine-repo=${localEngineRepo.path}');
command.add('-Plocal-engine-build-mode=${buildInfo.modeName}');
command.add('-Plocal-engine-out=${localEngineArtifacts.engineOutPath}');
command.add('-Ptarget-platform=${_getTargetPlatformByLocalEnginePath(
localEngineArtifacts.engineOutPath)}');
} else if (androidBuildInfo.targetArchs.isNotEmpty) {
final String targetPlatforms = androidBuildInfo
.targetArchs
.map(getPlatformNameForAndroidArch).join(',');
command.add('-Ptarget-platform=$targetPlatforms');
}
if (target != null) {
command.add('-Ptarget=$target');
}
assert(buildInfo.trackWidgetCreation != null);
command.add('-Ptrack-widget-creation=${buildInfo.trackWidgetCreation}');
if (buildInfo.extraFrontEndOptions != null) {
command.add('-Pextra-front-end-options=${buildInfo.extraFrontEndOptions.join(',')}');
}
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}');
}
if (androidBuildInfo.splitPerAbi) {
command.add('-Psplit-per-abi=true');
}
if (androidBuildInfo.shrink) {
command.add('-Pshrink=true');
}
if (androidBuildInfo.buildInfo.dartDefines?.isNotEmpty ?? false) {
command.add('-Pdart-defines=${androidBuildInfo.buildInfo.dartDefines.join(',')}');
}
if (shouldBuildPluginAsAar) {
// Pass a system flag instead of a project flag, so this flag can be
// read from include_flutter.groovy.
command.add('-Dbuild-plugins-as-aars=true');
// Don't use settings.gradle from the current project since it includes the plugins as subprojects.
command.add('--settings-file=settings_aar.gradle');
}
if (androidBuildInfo.fastStart) {
command.add('-Pfast-start=true');
}
if (androidBuildInfo.buildInfo.splitDebugInfoPath != null) {
command.add('-Psplit-debug-info=${androidBuildInfo.buildInfo.splitDebugInfoPath}');
}
if (androidBuildInfo.buildInfo.treeShakeIcons) {
command.add('-Ptree-shake-icons=true');
}
if (androidBuildInfo.buildInfo.dartObfuscation) {
command.add('-Pdart-obfuscation=true');
}
command.add(assembleTask);
GradleHandledError detectedGradleError;
String detectedGradleErrorLine;
String consumeLog(String line) {
// This message was removed from first-party plugins,
// but older plugin versions still display this message.
if (androidXPluginWarningRegex.hasMatch(line)) {
// Don't pipe.
return null;
}
if (detectedGradleError != null) {
// Pipe stdout/stderr from Gradle.
return line;
}
for (final GradleHandledError gradleError in localGradleErrors) {
if (gradleError.test(line)) {
detectedGradleErrorLine = line;
detectedGradleError = gradleError;
// The first error match wins.
break;
}
}
// Pipe stdout/stderr from Gradle.
return line;
}
final Stopwatch sw = Stopwatch()..start();
int exitCode = 1;
try {
exitCode = await processUtils.stream(
command,
workingDirectory: project.android.hostAppGradleRoot.path,
allowReentrantFlutter: true,
environment: gradleEnvironment,
mapFunction: consumeLog,
);
} on ProcessException catch(exception) {
consumeLog(exception.toString());
// Rethrow the exception if the error isn't handled by any of the
// `localGradleErrors`.
if (detectedGradleError == null) {
rethrow;
}
} finally {
status.stop();
}
globals.flutterUsage.sendTiming('build', 'gradle', sw.elapsed);
if (exitCode != 0) {
if (detectedGradleError == null) {
BuildEvent('gradle-unkown-failure').send();
throwToolExit(
'Gradle task $assembleTask failed with exit code $exitCode',
exitCode: exitCode,
);
} else {
final GradleBuildStatus status = await detectedGradleError.handler(
line: detectedGradleErrorLine,
project: project,
usesAndroidX: usesAndroidX,
shouldBuildPluginAsAar: shouldBuildPluginAsAar,
);
if (retries >= 1) {
final String successEventLabel = 'gradle-${detectedGradleError.eventLabel}-success';
switch (status) {
case GradleBuildStatus.retry:
await buildGradleApp(
project: project,
androidBuildInfo: androidBuildInfo,
target: target,
isBuildingBundle: isBuildingBundle,
localGradleErrors: localGradleErrors,
shouldBuildPluginAsAar: shouldBuildPluginAsAar,
retries: retries - 1,
);
BuildEvent(successEventLabel).send();
return;
case GradleBuildStatus.retryWithAarPlugins:
await buildGradleApp(
project: project,
androidBuildInfo: androidBuildInfo,
target: target,
isBuildingBundle: isBuildingBundle,
localGradleErrors: localGradleErrors,
shouldBuildPluginAsAar: true,
retries: retries - 1,
);
BuildEvent(successEventLabel).send();
return;
case GradleBuildStatus.exit:
// noop.
}
}
BuildEvent('gradle-${detectedGradleError.eventLabel}-failure').send();
throwToolExit(
'Gradle task $assembleTask failed with exit code $exitCode',
exitCode: exitCode,
);
}
}
if (isBuildingBundle) {
final File bundleFile = findBundleFile(project, buildInfo);
final String appSize = (buildInfo.mode == BuildMode.debug)
? '' // Don't display the size when building a debug variant.
: ' (${getSizeAsMB(bundleFile.lengthSync())})';
globals.printStatus(
'$successMark Built ${globals.fs.path.relative(bundleFile.path)}$appSize.',
color: TerminalColor.green,
);
return;
}
// Gradle produced an APK.
final Iterable<File> apkFiles = findApkFiles(project, androidBuildInfo);
final Directory apkDirectory = getApkDirectory(project);
// Copy the first APK to app.apk, so `flutter run` can find it.
// TODO(egarciad): Handle multiple APKs.
apkFiles.first.copySync(apkDirectory.childFile('app.apk').path);
globals.printTrace('calculateSha: $apkDirectory/app.apk');
final File apkShaFile = apkDirectory.childFile('app.apk.sha1');
apkShaFile.writeAsStringSync(_calculateSha(apkFiles.first));
for (final File apkFile in apkFiles) {
final String appSize = (buildInfo.mode == BuildMode.debug)
? '' // Don't display the size when building a debug variant.
: ' (${getSizeAsMB(apkFile.lengthSync())})';
globals.printStatus(
'$successMark Built ${globals.fs.path.relative(apkFile.path)}$appSize.',
color: TerminalColor.green,
);
}
}
/// Builds AAR and POM files.
///
/// * [project] is typically [FlutterProject.current()].
/// * [androidBuildInfo] is the build configuration.
/// * [outputDir] is the destination of the artifacts,
/// * [buildNumber] is the build number of the output aar,
Future<void> buildGradleAar({
@required FlutterProject project,
@required AndroidBuildInfo androidBuildInfo,
@required String target,
@required Directory outputDirectory,
@required String buildNumber,
}) async {
assert(project != null);
assert(target != null);
assert(androidBuildInfo != null);
assert(outputDirectory != null);
assert(androidSdk != null);
final FlutterManifest manifest = project.manifest;
if (!manifest.isModule && !manifest.isPlugin) {
throwToolExit('AARs can only be built for plugin or module projects.');
}
final String aarTask = getAarTaskFor(androidBuildInfo.buildInfo);
final Status status = globals.logger.startProgress(
"Running Gradle task '$aarTask'...",
timeout: timeoutConfiguration.slowOperation,
multilineOutput: true,
);
final String flutterRoot = globals.fs.path.absolute(Cache.flutterRoot);
final String initScript = globals.fs.path.join(
flutterRoot,
'packages',
'flutter_tools',
'gradle',
'aar_init_script.gradle',
);
final List<String> command = <String>[
gradleUtils.getExecutable(project),
'-I=$initScript',
'-Pflutter-root=$flutterRoot',
'-Poutput-dir=${outputDirectory.path}',
'-Pis-plugin=${manifest.isPlugin}',
'-PbuildNumber=$buildNumber'
];
if (target != null && target.isNotEmpty) {
command.add('-Ptarget=$target');
}
if (globals.artifacts is LocalEngineArtifacts) {
final LocalEngineArtifacts localEngineArtifacts = globals.artifacts as LocalEngineArtifacts;
final Directory localEngineRepo = _getLocalEngineRepo(
engineOutPath: localEngineArtifacts.engineOutPath,
androidBuildInfo: androidBuildInfo,
);
globals.printTrace(
'Using local engine: ${localEngineArtifacts.engineOutPath}\n'
'Local Maven repo: ${localEngineRepo.path}'
);
command.add('-Plocal-engine-repo=${localEngineRepo.path}');
command.add('-Plocal-engine-build-mode=${androidBuildInfo.buildInfo.modeName}');
command.add('-Plocal-engine-out=${localEngineArtifacts.engineOutPath}');
if (androidBuildInfo.buildInfo.treeShakeIcons) {
command.add('-Pfont-subset=true');
}
// Copy the local engine repo in the output directory.
try {
globals.fsUtils.copyDirectorySync(
localEngineRepo,
getRepoDirectory(outputDirectory),
);
} on FileSystemException catch(_) {
throwToolExit(
'Failed to copy the local engine ${localEngineRepo.path} repo '
'in ${outputDirectory.path}'
);
}
command.add('-Ptarget-platform=${_getTargetPlatformByLocalEnginePath(
localEngineArtifacts.engineOutPath)}');
} else if (androidBuildInfo.targetArchs.isNotEmpty) {
final String targetPlatforms = androidBuildInfo.targetArchs
.map(getPlatformNameForAndroidArch).join(',');
command.add('-Ptarget-platform=$targetPlatforms');
}
command.add(aarTask);
final Stopwatch sw = Stopwatch()..start();
RunResult result;
try {
result = await processUtils.run(
command,
workingDirectory: project.android.hostAppGradleRoot.path,
allowReentrantFlutter: true,
environment: gradleEnvironment,
);
} finally {
status.stop();
}
globals.flutterUsage.sendTiming('build', 'gradle-aar', sw.elapsed);
if (result.exitCode != 0) {
globals.printStatus(result.stdout, wrap: false);
globals.printError(result.stderr, wrap: false);
throwToolExit(
'Gradle task $aarTask failed with exit code $exitCode.',
exitCode: exitCode,
);
}
final Directory repoDirectory = getRepoDirectory(outputDirectory);
if (!repoDirectory.existsSync()) {
globals.printStatus(result.stdout, wrap: false);
globals.printError(result.stderr, wrap: false);
throwToolExit(
'Gradle task $aarTask failed to produce $repoDirectory.',
exitCode: exitCode,
);
}
globals.printStatus(
'$successMark Built ${globals.fs.path.relative(repoDirectory.path)}.',
color: TerminalColor.green,
);
}
/// Prints how to consume the AAR from a host app.
void printHowToConsumeAar({
@required Set<String> buildModes,
@required String androidPackage,
@required Directory repoDirectory,
String buildNumber,
}) {
assert(buildModes != null && buildModes.isNotEmpty);
assert(androidPackage != null);
assert(repoDirectory != null);
buildNumber ??= '1.0';
globals.printStatus('''
${globals.terminal.bolden('Consuming the Module')}
1. Open ${globals.fs.path.join('<host>', 'app', 'build.gradle')}
2. Ensure you have the repositories configured, otherwise add them:
repositories {
maven {
url '${repoDirectory.path}'
}
maven {
url 'https://storage.googleapis.com/download.flutter.io'
}
}
3. Make the host app depend on the Flutter module:
dependencies {''');
for (final String buildMode in buildModes) {
globals.printStatus("""
${buildMode}Implementation '$androidPackage:flutter_$buildMode:$buildNumber'""");
}
globals.printStatus('''
}
''');
if (buildModes.contains('profile')) {
globals.printStatus('''
4. Add the `profile` build type:
android {
buildTypes {
profile {
initWith debug
}
}
}
''');
}
globals.printStatus('To learn more, visit https://flutter.dev/go/build-aar');
}
String _hex(List<int> bytes) {
final StringBuffer result = StringBuffer();
for (final int part in bytes) {
result.write('${part < 16 ? '0' : ''}${part.toRadixString(16)}');
}
return result.toString();
}
String _calculateSha(File file) {
final Stopwatch sw = Stopwatch()..start();
final List<int> bytes = file.readAsBytesSync();
globals.printTrace('calculateSha: reading file took ${sw.elapsedMilliseconds}us');
globals.flutterUsage.sendTiming('build', 'apk-sha-read', sw.elapsed);
sw.reset();
final String sha = _hex(sha1.convert(bytes).bytes);
globals.printTrace('calculateSha: computing sha took ${sw.elapsedMilliseconds}us');
globals.flutterUsage.sendTiming('build', 'apk-sha-calc', sw.elapsed);
return sha;
}
void _exitWithUnsupportedProjectMessage() {
BuildEvent('unsupported-project', eventError: 'gradle-plugin').send();
throwToolExit(
'$warningMark Your app is using an unsupported Gradle project. '
'To fix this problem, create a new project by running `flutter create -t app <app-directory>` '
'and then move the dart code, assets and pubspec.yaml to the new project.',
);
}
void _exitWithProjectNotUsingGradleMessage() {
BuildEvent('unsupported-project', eventError: 'app-not-using-gradle').send();
throwToolExit(
'$warningMark The build process for Android has changed, and the '
'current project configuration is no longer valid. Please consult\n\n'
'https://github.com/flutter/flutter/wiki/Upgrading-Flutter-projects-to-build-with-gradle\n\n'
'for details on how to upgrade the project.'
);
}
/// Returns [true] if the current app uses AndroidX.
// TODO(egarciad): https://github.com/flutter/flutter/issues/40800
// Remove `FlutterManifest.usesAndroidX` and provide a unified `AndroidProject.usesAndroidX`.
bool isAppUsingAndroidX(Directory androidDirectory) {
final File properties = androidDirectory.childFile('gradle.properties');
if (!properties.existsSync()) {
return false;
}
return properties.readAsStringSync().contains('android.useAndroidX=true');
}
/// Builds the plugins as AARs.
@visibleForTesting
Future<void> buildPluginsAsAar(
FlutterProject flutterProject,
AndroidBuildInfo androidBuildInfo, {
Directory buildDirectory,
}) async {
final File flutterPluginFile = flutterProject.flutterPluginsFile;
if (!flutterPluginFile.existsSync()) {
return;
}
final List<String> plugins = flutterPluginFile.readAsStringSync().split('\n');
for (final String plugin in plugins) {
final List<String> pluginParts = plugin.split('=');
if (pluginParts.length != 2) {
continue;
}
final Directory pluginDirectory = globals.fs.directory(pluginParts.last);
assert(pluginDirectory.existsSync());
final String pluginName = pluginParts.first;
final File buildGradleFile = pluginDirectory.childDirectory('android').childFile('build.gradle');
if (!buildGradleFile.existsSync()) {
globals.printTrace("Skipping plugin $pluginName since it doesn't have a android/build.gradle file");
continue;
}
globals.logger.printStatus('Building plugin $pluginName...');
try {
await buildGradleAar(
project: FlutterProject.fromDirectory(pluginDirectory),
androidBuildInfo: AndroidBuildInfo(
BuildInfo(
BuildMode.release, // Plugins are built as release.
null, // Plugins don't define flavors.
treeShakeIcons: androidBuildInfo.buildInfo.treeShakeIcons,
),
),
target: '',
outputDirectory: buildDirectory,
buildNumber: '1.0'
);
} on ToolExit {
// Log the entire plugin entry in `.flutter-plugins` since it
// includes the plugin name and the version.
BuildEvent('gradle-plugin-aar-failure', eventError: plugin).send();
throwToolExit('The plugin $pluginName could not be built due to the issue above.');
}
}
}
/// Returns the APK files for a given [FlutterProject] and [AndroidBuildInfo].
@visibleForTesting
Iterable<File> findApkFiles(
FlutterProject project,
AndroidBuildInfo androidBuildInfo,
) {
final Iterable<String> apkFileNames = _apkFilesFor(androidBuildInfo);
final Directory apkDirectory = getApkDirectory(project);
final Iterable<File> apks = apkFileNames.expand<File>((String apkFileName) {
File apkFile = apkDirectory.childFile(apkFileName);
if (apkFile.existsSync()) {
return <File>[apkFile];
}
final BuildInfo buildInfo = androidBuildInfo.buildInfo;
final String modeName = camelCase(buildInfo.modeName);
apkFile = apkDirectory
.childDirectory(modeName)
.childFile(apkFileName);
if (apkFile.existsSync()) {
return <File>[apkFile];
}
if (buildInfo.flavor != null) {
// Android Studio Gradle plugin v3 adds flavor to path.
apkFile = apkDirectory
.childDirectory(buildInfo.flavor)
.childDirectory(modeName)
.childFile(apkFileName);
if (apkFile.existsSync()) {
return <File>[apkFile];
}
}
return const <File>[];
});
if (apks.isEmpty) {
_exitWithExpectedFileNotFound(
project: project,
fileExtension: '.apk',
);
}
return apks;
}
@visibleForTesting
File findBundleFile(FlutterProject project, BuildInfo buildInfo) {
final List<File> fileCandidates = <File>[
getBundleDirectory(project)
.childDirectory(camelCase(buildInfo.modeName))
.childFile('app.aab'),
getBundleDirectory(project)
.childDirectory(camelCase(buildInfo.modeName))
.childFile('app-${buildInfo.modeName}.aab'),
];
if (buildInfo.flavor != null) {
// The Android Gradle plugin 3.0.0 adds the flavor name to the path.
// For example: In release mode, if the flavor name is `foo_bar`, then
// the directory name is `foo_barRelease`.
fileCandidates.add(
getBundleDirectory(project)
.childDirectory('${buildInfo.flavor}${camelCase('_' + buildInfo.modeName)}')
.childFile('app.aab'));
// The Android Gradle plugin 3.5.0 adds the flavor name to file name.
// For example: In release mode, if the flavor name is `foo_bar`, then
// the file name name is `app-foo_bar-release.aab`.
fileCandidates.add(
getBundleDirectory(project)
.childDirectory('${buildInfo.flavor}${camelCase('_' + buildInfo.modeName)}')
.childFile('app-${buildInfo.flavor}-${buildInfo.modeName}.aab'));
}
for (final File bundleFile in fileCandidates) {
if (bundleFile.existsSync()) {
return bundleFile;
}
}
_exitWithExpectedFileNotFound(
project: project,
fileExtension: '.aab',
);
return null;
}
/// Throws a [ToolExit] exception and logs the event.
void _exitWithExpectedFileNotFound({
@required FlutterProject project,
@required String fileExtension,
}) {
assert(project != null);
assert(fileExtension != null);
final String androidGradlePluginVersion =
getGradleVersionForAndroidPlugin(project.android.hostAppGradleRoot);
BuildEvent('gradle-expected-file-not-found',
settings:
'androidGradlePluginVersion: $androidGradlePluginVersion, '
'fileExtension: $fileExtension'
).send();
throwToolExit(
'Gradle build failed to produce an $fileExtension file. '
"It's likely that this file was generated under ${project.android.buildDirectory.path}, "
"but the tool couldn't find it."
);
}
void _createSymlink(String targetPath, String linkPath) {
final File targetFile = globals.fs.file(targetPath);
if (!targetFile.existsSync()) {
throwToolExit("The file $targetPath wasn't found in the local engine out directory.");
}
final File linkFile = globals.fs.file(linkPath);
final Link symlink = linkFile.parent.childLink(linkFile.basename);
try {
symlink.createSync(targetPath, recursive: true);
} on FileSystemException catch (exception) {
throwToolExit(
'Failed to create the symlink $linkPath->$targetPath: $exception'
);
}
}
String _getLocalArtifactVersion(String pomPath) {
final File pomFile = globals.fs.file(pomPath);
if (!pomFile.existsSync()) {
throwToolExit("The file $pomPath wasn't found in the local engine out directory.");
}
xml.XmlDocument document;
try {
document = xml.parse(pomFile.readAsStringSync());
} on xml.XmlParserException {
throwToolExit(
'Error parsing $pomPath. Please ensure that this is a valid XML document.'
);
} on FileSystemException {
throwToolExit(
'Error reading $pomPath. Please ensure that you have read permission to this '
'file and try again.');
}
final Iterable<xml.XmlElement> project = document.findElements('project');
assert(project.isNotEmpty);
for (final xml.XmlElement versionElement in document.findAllElements('version')) {
if (versionElement.parent == project.first) {
return versionElement.text;
}
}
throwToolExit('Error while parsing the <version> element from $pomPath');
return null;
}
/// Returns the local Maven repository for a local engine build.
/// For example, if the engine is built locally at <home>/engine/src/out/android_release_unopt
/// This method generates symlinks in the temp directory to the engine artifacts
/// following the convention specified on https://maven.apache.org/pom.html#Repositories
Directory _getLocalEngineRepo({
@required String engineOutPath,
@required AndroidBuildInfo androidBuildInfo,
}) {
assert(engineOutPath != null);
assert(androidBuildInfo != null);
final String abi = _getAbiByLocalEnginePath(engineOutPath);
final Directory localEngineRepo = globals.fs.systemTempDirectory
.createTempSync('flutter_tool_local_engine_repo.');
// Remove the local engine repo before the tool exits.
shutdownHooks.addShutdownHook(
() => localEngineRepo.deleteSync(recursive: true),
ShutdownStage.CLEANUP,
);
final String buildMode = androidBuildInfo.buildInfo.modeName;
final String artifactVersion = _getLocalArtifactVersion(
globals.fs.path.join(
engineOutPath,
'flutter_embedding_$buildMode.pom',
)
);
for (final String artifact in const <String>['pom', 'jar']) {
// The Android embedding artifacts.
_createSymlink(
globals.fs.path.join(
engineOutPath,
'flutter_embedding_$buildMode.$artifact',
),
globals.fs.path.join(
localEngineRepo.path,
'io',
'flutter',
'flutter_embedding_$buildMode',
artifactVersion,
'flutter_embedding_$buildMode-$artifactVersion.$artifact',
),
);
// The engine artifacts (libflutter.so).
_createSymlink(
globals.fs.path.join(
engineOutPath,
'${abi}_$buildMode.$artifact',
),
globals.fs.path.join(
localEngineRepo.path,
'io',
'flutter',
'${abi}_$buildMode',
artifactVersion,
'${abi}_$buildMode-$artifactVersion.$artifact',
),
);
}
return localEngineRepo;
}
String _getAbiByLocalEnginePath(String engineOutPath) {
String result = 'armeabi_v7a';
if (engineOutPath.contains('x86')) {
result = 'x86';
} else if (engineOutPath.contains('x64')) {
result = 'x86_64';
} else if (engineOutPath.contains('arm64')) {
result = 'arm64_v8a';
}
return result;
}
String _getTargetPlatformByLocalEnginePath(String engineOutPath) {
String result = 'android-arm';
if (engineOutPath.contains('x86')) {
result = 'android-x86';
} else if (engineOutPath.contains('x64')) {
result = 'android-x64';
} else if (engineOutPath.contains('arm64')) {
result = 'android-arm64';
}
return result;
}