Unverified Commit 368cd7da authored by Pranay Airan's avatar Pranay Airan Committed by GitHub

Adding support for android app bundle - Issue #17829 (#24440)

* adding support for android app bundle.

* removing the debug statement.

* fixing formatting and code review changes.

* Revert "fixing formatting and code review changes."

This reverts commit 2041d459f335242555a0b75e445343134c245494.

* Fixing code formatting issues.

* updating review comments fixing comments and spacing.

* changing and to & to rerun the CI and tests.

* updating the comment to re-run the test

updating the comment to re-run the test

* fixing the formatting.

* updating comments to re-trigger build

updating comments to re-trigger build
parent 8426910a
...@@ -44,5 +44,6 @@ Future<void> buildApk({ ...@@ -44,5 +44,6 @@ Future<void> buildApk({
project: project, project: project,
buildInfo: buildInfo, buildInfo: buildInfo,
target: target, target: target,
isBuildingBundle: false
); );
} }
// Copyright 2015 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';
import 'package:meta/meta.dart';
import '../base/common.dart';
import '../build_info.dart';
import '../globals.dart';
import '../project.dart';
import 'android_sdk.dart';
import 'gradle.dart';
Future<void> buildAppBundle({
@required FlutterProject project,
@required String target,
BuildInfo buildInfo = BuildInfo.debug
}) async {
if (!project.android.isUsingGradle) {
throwToolExit(
'The build process for Android has changed, and the current project configuration\n'
'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.'
);
}
// Validate that we can find an android sdk.
if (androidSdk == null)
throwToolExit('No Android SDK found. Try setting the ANDROID_HOME environment variable.');
final List<String> validationResult = androidSdk.validateSdkWellFormed();
if (validationResult.isNotEmpty) {
for (String message in validationResult) {
printError(message, wrap: false);
}
throwToolExit('Try re-installing or updating your Android SDK.');
}
return buildGradleProject(
project: project,
buildInfo: buildInfo,
target: target,
isBuildingBundle: true
);
}
...@@ -125,6 +125,7 @@ Future<GradleProject> _readGradleProject() async { ...@@ -125,6 +125,7 @@ Future<GradleProject> _readGradleProject() async {
project = GradleProject( project = GradleProject(
<String>['debug', 'profile', 'release'], <String>['debug', 'profile', 'release'],
<String>[], flutterProject.android.gradleAppOutV1Directory, <String>[], flutterProject.android.gradleAppOutV1Directory,
flutterProject.android.gradleAppBundleOutV1Directory
); );
} }
status.stop(); status.stop();
...@@ -284,6 +285,7 @@ Future<void> buildGradleProject({ ...@@ -284,6 +285,7 @@ Future<void> buildGradleProject({
@required FlutterProject project, @required FlutterProject project,
@required BuildInfo buildInfo, @required BuildInfo buildInfo,
@required String target, @required String target,
@required bool isBuildingBundle,
}) async { }) async {
// Update the local.properties file with the build mode, version name and code. // Update the local.properties file with the build mode, version name and code.
// FlutterPlugin v1 reads local.properties to determine build mode. Plugin v2 // FlutterPlugin v1 reads local.properties to determine build mode. Plugin v2
...@@ -305,7 +307,7 @@ Future<void> buildGradleProject({ ...@@ -305,7 +307,7 @@ Future<void> buildGradleProject({
case FlutterPluginVersion.managed: case FlutterPluginVersion.managed:
// Fall through. Managed plugin builds the same way as plugin v2. // Fall through. Managed plugin builds the same way as plugin v2.
case FlutterPluginVersion.v2: case FlutterPluginVersion.v2:
return _buildGradleProjectV2(project, gradle, buildInfo, target); return _buildGradleProjectV2(project, gradle, buildInfo, target, isBuildingBundle);
} }
} }
...@@ -334,9 +336,18 @@ Future<void> _buildGradleProjectV2( ...@@ -334,9 +336,18 @@ Future<void> _buildGradleProjectV2(
FlutterProject flutterProject, FlutterProject flutterProject,
String gradle, String gradle,
BuildInfo buildInfo, BuildInfo buildInfo,
String target) async { String target,
bool isBuildingBundle) async {
final GradleProject project = await _gradleProject(); final GradleProject project = await _gradleProject();
final String assembleTask = project.assembleTaskFor(buildInfo);
String assembleTask;
if (isBuildingBundle) {
assembleTask = project.bundleTaskFor(buildInfo);
} else {
assembleTask = project.assembleTaskFor(buildInfo);
}
if (assembleTask == null) { if (assembleTask == null) {
printError(''); printError('');
printError('The Gradle project does not define a task suitable for the requested build.'); printError('The Gradle project does not define a task suitable for the requested build.');
...@@ -406,6 +417,7 @@ Future<void> _buildGradleProjectV2( ...@@ -406,6 +417,7 @@ Future<void> _buildGradleProjectV2(
if (exitCode != 0) if (exitCode != 0)
throwToolExit('Gradle task $assembleTask failed with exit code $exitCode', exitCode: exitCode); throwToolExit('Gradle task $assembleTask failed with exit code $exitCode', exitCode: exitCode);
if(!isBuildingBundle) {
final File apkFile = _findApkFile(project, buildInfo); final File apkFile = _findApkFile(project, buildInfo);
if (apkFile == null) if (apkFile == null)
throwToolExit('Gradle build failed to produce an Android package.'); throwToolExit('Gradle build failed to produce an Android package.');
...@@ -437,8 +449,7 @@ Future<void> _buildGradleProjectV2( ...@@ -437,8 +449,7 @@ Future<void> _buildGradleProjectV2(
if (buildInfo.createPatch) { if (buildInfo.createPatch) {
final AndroidApk package = AndroidApk.fromApk(apkFile); final AndroidApk package = AndroidApk.fromApk(apkFile);
final Directory baselineDir = fs.directory(buildInfo.baselineDir); final Directory baselineDir = fs.directory(buildInfo.baselineDir);
final File baselineApkFile = baselineDir.childFile('${package.versionCode}.apk'); final File baselineApkFile = baselineDir.childFile('${package.versionCode}.apk');if (!baselineApkFile.existsSync())
if (!baselineApkFile.existsSync())
throwToolExit('Error: Could not find baseline package ${baselineApkFile.path}.'); throwToolExit('Error: Could not find baseline package ${baselineApkFile.path}.');
printStatus('Found baseline package ${baselineApkFile.path}.'); printStatus('Found baseline package ${baselineApkFile.path}.');
...@@ -490,6 +501,27 @@ Future<void> _buildGradleProjectV2( ...@@ -490,6 +501,27 @@ Future<void> _buildGradleProjectV2(
updateFile.writeAsBytesSync(ZipEncoder().encode(update), flush: true); updateFile.writeAsBytesSync(ZipEncoder().encode(update), flush: true);
printStatus('Created dynamic patch ${updateFile.path}.'); printStatus('Created dynamic patch ${updateFile.path}.');
} }
} else {
final File bundleFile = _findBundleFile(project, buildInfo);
if (bundleFile == null)
throwToolExit('Gradle build failed to produce an Android bundle package.');
// Copy the bundle to app.aab, so `flutter run`, `flutter install`, etc. can find it.
bundleFile.copySync(project.bundleDirectory
.childFile('app.aab')
.path);
printTrace('calculateSha: ${project.bundleDirectory}/app.aab');
final File bundleShaFile = project.bundleDirectory.childFile('app.aab.sha1');
bundleShaFile.writeAsStringSync(calculateSha(bundleFile));
String appSize;
if (buildInfo.mode == BuildMode.debug) {
appSize = '';
} else {
appSize = ' (${getSizeAsMB(bundleFile.lengthSync())})';
}
printStatus('Built ${fs.path.relative(bundleFile.path)}$appSize.');
}
} }
File _findApkFile(GradleProject project, BuildInfo buildInfo) { File _findApkFile(GradleProject project, BuildInfo buildInfo) {
...@@ -512,6 +544,28 @@ File _findApkFile(GradleProject project, BuildInfo buildInfo) { ...@@ -512,6 +544,28 @@ File _findApkFile(GradleProject project, BuildInfo buildInfo) {
return null; return null;
} }
File _findBundleFile(GradleProject project, BuildInfo buildInfo) {
final String bundleFileName = project.bundleFileFor(buildInfo);
if (bundleFileName == null)
return null;
File bundleFile = fs.file(fs.path.join(project.bundleDirectory.path, bundleFileName));
if (bundleFile.existsSync()) {
return bundleFile;
}
final String modeName = camelCase(buildInfo.modeName);
bundleFile = fs.file(fs.path.join(project.bundleDirectory.path, modeName, bundleFileName));
if (bundleFile.existsSync())
return bundleFile;
if (buildInfo.flavor != null) {
// Android Studio Gradle plugin v3 adds the flavor to the path. For the bundle the folder name is the flavor plus the mode name.
bundleFile = fs.file(fs.path.join(project.bundleDirectory.path, buildInfo.flavor + modeName, bundleFileName));
if (bundleFile.existsSync())
return bundleFile;
}
return null;
}
Map<String, String> get _gradleEnv { Map<String, String> get _gradleEnv {
final Map<String, String> env = Map<String, String>.from(platform.environment); final Map<String, String> env = Map<String, String>.from(platform.environment);
if (javaPath != null) { if (javaPath != null) {
...@@ -522,7 +576,7 @@ Map<String, String> get _gradleEnv { ...@@ -522,7 +576,7 @@ Map<String, String> get _gradleEnv {
} }
class GradleProject { class GradleProject {
GradleProject(this.buildTypes, this.productFlavors, this.apkDirectory); GradleProject(this.buildTypes, this.productFlavors, this.apkDirectory, this.bundleDirectory);
factory GradleProject.fromAppProperties(String properties, String tasks) { factory GradleProject.fromAppProperties(String properties, String tasks) {
// Extract build directory. // Extract build directory.
...@@ -561,12 +615,14 @@ class GradleProject { ...@@ -561,12 +615,14 @@ class GradleProject {
buildTypes.toList(), buildTypes.toList(),
productFlavors.toList(), productFlavors.toList(),
fs.directory(fs.path.join(buildDir, 'outputs', 'apk')), fs.directory(fs.path.join(buildDir, 'outputs', 'apk')),
fs.directory(fs.path.join(buildDir, 'outputs', 'bundle')),
); );
} }
final List<String> buildTypes; final List<String> buildTypes;
final List<String> productFlavors; final List<String> productFlavors;
final Directory apkDirectory; final Directory apkDirectory;
final Directory bundleDirectory;
String _buildTypeFor(BuildInfo buildInfo) { String _buildTypeFor(BuildInfo buildInfo) {
final String modeName = camelCase(buildInfo.modeName); final String modeName = camelCase(buildInfo.modeName);
...@@ -600,4 +656,18 @@ class GradleProject { ...@@ -600,4 +656,18 @@ class GradleProject {
final String flavorString = productFlavor.isEmpty ? '' : '-' + productFlavor; final String flavorString = productFlavor.isEmpty ? '' : '-' + productFlavor;
return 'app$flavorString-$buildType.apk'; return 'app$flavorString-$buildType.apk';
} }
String bundleTaskFor(BuildInfo buildInfo) {
final String buildType = _buildTypeFor(buildInfo);
final String productFlavor = _productFlavorFor(buildInfo);
if (buildType == null || productFlavor == null)
return null;
return 'bundle${toTitleCase(productFlavor)}${toTitleCase(buildType)}';
}
String bundleFileFor(BuildInfo buildInfo) {
// For app bundle all bundle names are called as app.aab. Product flavors
// & build types are differentiated as folders, where the aab will be added.
return 'app.aab';
}
} }
...@@ -12,6 +12,7 @@ import '../globals.dart'; ...@@ -12,6 +12,7 @@ import '../globals.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import 'build_aot.dart'; import 'build_aot.dart';
import 'build_apk.dart'; import 'build_apk.dart';
import 'build_appbundle.dart';
import 'build_bundle.dart'; import 'build_bundle.dart';
import 'build_flx.dart'; import 'build_flx.dart';
import 'build_ios.dart'; import 'build_ios.dart';
...@@ -19,6 +20,7 @@ import 'build_ios.dart'; ...@@ -19,6 +20,7 @@ import 'build_ios.dart';
class BuildCommand extends FlutterCommand { class BuildCommand extends FlutterCommand {
BuildCommand({bool verboseHelp = false}) { BuildCommand({bool verboseHelp = false}) {
addSubcommand(BuildApkCommand(verboseHelp: verboseHelp)); addSubcommand(BuildApkCommand(verboseHelp: verboseHelp));
addSubcommand(BuildAppBundleCommand(verboseHelp: verboseHelp));
addSubcommand(BuildAotCommand()); addSubcommand(BuildAotCommand());
addSubcommand(BuildIOSCommand()); addSubcommand(BuildIOSCommand());
addSubcommand(BuildFlxCommand()); addSubcommand(BuildFlxCommand());
......
// Copyright 2015 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';
import '../android/app_bundle.dart';
import '../project.dart';
import '../runner/flutter_command.dart' show FlutterCommandResult;
import 'build.dart';
class BuildAppBundleCommand extends BuildSubCommand {
BuildAppBundleCommand({bool verboseHelp = false}) {
usesTargetOption();
addBuildModeFlags();
usesFlavorOption();
usesPubOption();
usesBuildNumberOption();
usesBuildNameOption();
argParser
..addFlag('track-widget-creation', negatable: false, hide: !verboseHelp)
..addFlag('build-shared-library',
negatable: false,
help: 'Whether to prefer compiling to a *.so file (android only).',
)
..addOption('target-platform',
defaultsTo: 'android-arm',
allowed: <String>['android-arm', 'android-arm64']);
}
@override
final String name = 'appbundle';
@override
final String description = 'Build an Android App Bundle file from your app.\n\n'
'This command can build debug and release versions of an app bundle for your application. \'debug\' builds support '
'debugging and a quick development cycle. \'release\' builds don\'t support debugging and are '
'suitable for deploying to app stores. \n app bundle improves your app size';
@override
Future<FlutterCommandResult> runCommand() async {
await super.runCommand();
await buildAppBundle(
project: await FlutterProject.current(),
target: targetFile,
buildInfo: getBuildInfo(),
);
return null;
}
}
...@@ -361,6 +361,10 @@ class AndroidProject { ...@@ -361,6 +361,10 @@ class AndroidProject {
return fs.directory(fs.path.join(hostAppGradleRoot.path, 'app', 'build', 'outputs', 'apk')); return fs.directory(fs.path.join(hostAppGradleRoot.path, 'app', 'build', 'outputs', 'apk'));
} }
Directory get gradleAppBundleOutV1Directory {
return fs.directory(fs.path.join(hostAppGradleRoot.path, 'app', 'build', 'outputs', 'bundle'));
}
bool get isUsingGradle { bool get isUsingGradle {
return hostAppGradleRoot.childFile('build.gradle').existsSync(); return hostAppGradleRoot.childFile('build.gradle').existsSync();
} }
......
...@@ -113,35 +113,65 @@ someOtherTask ...@@ -113,35 +113,65 @@ someOtherTask
expect(project.productFlavors, <String>['free', 'paid']); expect(project.productFlavors, <String>['free', 'paid']);
}); });
test('should provide apk file name for default build types', () { test('should provide apk file name for default build types', () {
final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir')); final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
expect(project.apkFileFor(BuildInfo.debug), 'app-debug.apk'); expect(project.apkFileFor(BuildInfo.debug), 'app-debug.apk');
expect(project.apkFileFor(BuildInfo.profile), 'app-profile.apk'); expect(project.apkFileFor(BuildInfo.profile), 'app-profile.apk');
expect(project.apkFileFor(BuildInfo.release), 'app-release.apk'); expect(project.apkFileFor(BuildInfo.release), 'app-release.apk');
expect(project.apkFileFor(const BuildInfo(BuildMode.release, 'unknown')), isNull); expect(project.apkFileFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
}); });
test('should provide apk file name for flavored build types', () { test('should provide apk file name for flavored build types', () {
final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir')); final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
expect(project.apkFileFor(const BuildInfo(BuildMode.debug, 'free')), 'app-free-debug.apk'); expect(project.apkFileFor(const BuildInfo(BuildMode.debug, 'free')), 'app-free-debug.apk');
expect(project.apkFileFor(const BuildInfo(BuildMode.release, 'paid')), 'app-paid-release.apk'); expect(project.apkFileFor(const BuildInfo(BuildMode.release, 'paid')), 'app-paid-release.apk');
expect(project.apkFileFor(const BuildInfo(BuildMode.release, 'unknown')), isNull); expect(project.apkFileFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
}); });
test('should provide bundle file name for default build types', () {
final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
expect(project.bundleFileFor(BuildInfo.debug), 'app.aab');
expect(project.bundleFileFor(BuildInfo.profile), 'app.aab');
expect(project.bundleFileFor(BuildInfo.release), 'app.aab');
expect(project.bundleFileFor(const BuildInfo(BuildMode.release, 'unknown')), 'app.aab');
});
test('should provide bundle file name for flavored build types', () {
final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
expect(project.bundleFileFor(const BuildInfo(BuildMode.debug, 'free')), 'app.aab');
expect(project.bundleFileFor(const BuildInfo(BuildMode.release, 'paid')), 'app.aab');
expect(project.bundleFileFor(const BuildInfo(BuildMode.release, 'unknown')), 'app.aab');
});
test('should provide assemble task name for default build types', () { test('should provide assemble task name for default build types', () {
final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir')); final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
expect(project.assembleTaskFor(BuildInfo.debug), 'assembleDebug'); expect(project.assembleTaskFor(BuildInfo.debug), 'assembleDebug');
expect(project.assembleTaskFor(BuildInfo.profile), 'assembleProfile'); expect(project.assembleTaskFor(BuildInfo.profile), 'assembleProfile');
expect(project.assembleTaskFor(BuildInfo.release), 'assembleRelease'); expect(project.assembleTaskFor(BuildInfo.release), 'assembleRelease');
expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull); expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
}); });
test('should provide assemble task name for flavored build types', () { test('should provide assemble task name for flavored build types', () {
final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir')); final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
expect(project.assembleTaskFor(const BuildInfo(BuildMode.debug, 'free')), 'assembleFreeDebug'); expect(project.assembleTaskFor(const BuildInfo(BuildMode.debug, 'free')), 'assembleFreeDebug');
expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'paid')), 'assemblePaidRelease'); expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'paid')), 'assemblePaidRelease');
expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull); expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
}); });
test('should respect format of the flavored build types', () { test('should respect format of the flavored build types', () {
final GradleProject project = GradleProject(<String>['debug'], <String>['randomFlavor'], fs.directory('/some/dir')); final GradleProject project = GradleProject(<String>['debug'], <String>['randomFlavor'], fs.directory('/some/dir'),fs.directory('/some/dir'));
expect(project.assembleTaskFor(const BuildInfo(BuildMode.debug, 'randomFlavor')), 'assembleRandomFlavorDebug'); expect(project.assembleTaskFor(const BuildInfo(BuildMode.debug, 'randomFlavor')), 'assembleRandomFlavorDebug');
}); });
test('bundle should provide assemble task name for default build types', () {
final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
expect(project.bundleTaskFor(BuildInfo.debug), 'bundleDebug');
expect(project.bundleTaskFor(BuildInfo.profile), 'bundleProfile');
expect(project.bundleTaskFor(BuildInfo.release), 'bundleRelease');
expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
});
test('bundle should provide assemble task name for flavored build types', () {
final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
expect(project.bundleTaskFor(const BuildInfo(BuildMode.debug, 'free')), 'bundleFreeDebug');
expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'paid')), 'bundlePaidRelease');
expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
});
test('bundle should respect format of the flavored build types', () {
final GradleProject project = GradleProject(<String>['debug'], <String>['randomFlavor'], fs.directory('/some/dir'),fs.directory('/some/dir'));
expect(project.bundleTaskFor(const BuildInfo(BuildMode.debug, 'randomFlavor')), 'bundleRandomFlavorDebug');
});
}); });
group('Gradle local.properties', () { group('Gradle local.properties', () {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment