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({
project: project,
buildInfo: buildInfo,
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 {
project = GradleProject(
<String>['debug', 'profile', 'release'],
<String>[], flutterProject.android.gradleAppOutV1Directory,
flutterProject.android.gradleAppBundleOutV1Directory
);
}
status.stop();
......@@ -284,6 +285,7 @@ Future<void> buildGradleProject({
@required FlutterProject project,
@required BuildInfo buildInfo,
@required String target,
@required bool isBuildingBundle,
}) async {
// Update the local.properties file with the build mode, version name and code.
// FlutterPlugin v1 reads local.properties to determine build mode. Plugin v2
......@@ -305,7 +307,7 @@ Future<void> buildGradleProject({
case FlutterPluginVersion.managed:
// Fall through. Managed plugin builds the same way as plugin v2.
case FlutterPluginVersion.v2:
return _buildGradleProjectV2(project, gradle, buildInfo, target);
return _buildGradleProjectV2(project, gradle, buildInfo, target, isBuildingBundle);
}
}
......@@ -334,9 +336,18 @@ Future<void> _buildGradleProjectV2(
FlutterProject flutterProject,
String gradle,
BuildInfo buildInfo,
String target) async {
String target,
bool isBuildingBundle) async {
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) {
printError('');
printError('The Gradle project does not define a task suitable for the requested build.');
......@@ -406,89 +417,110 @@ Future<void> _buildGradleProjectV2(
if (exitCode != 0)
throwToolExit('Gradle task $assembleTask failed with exit code $exitCode', exitCode: exitCode);
final File apkFile = _findApkFile(project, buildInfo);
if (apkFile == null)
throwToolExit('Gradle build failed to produce an Android package.');
// Copy the APK to app.apk, so `flutter run`, `flutter install`, etc. can find it.
apkFile.copySync(project.apkDirectory.childFile('app.apk').path);
if(!isBuildingBundle) {
final File apkFile = _findApkFile(project, buildInfo);
if (apkFile == null)
throwToolExit('Gradle build failed to produce an Android package.');
// Copy the APK to app.apk, so `flutter run`, `flutter install`, etc. can find it.
apkFile.copySync(project.apkDirectory.childFile('app.apk').path);
printTrace('calculateSha: ${project.apkDirectory}/app.apk');
final File apkShaFile = project.apkDirectory.childFile('app.apk.sha1');
apkShaFile.writeAsStringSync(calculateSha(apkFile));
printTrace('calculateSha: ${project.apkDirectory}/app.apk');
final File apkShaFile = project.apkDirectory.childFile('app.apk.sha1');
apkShaFile.writeAsStringSync(calculateSha(apkFile));
String appSize;
if (buildInfo.mode == BuildMode.debug) {
appSize = '';
} else {
appSize = ' (${getSizeAsMB(apkFile.lengthSync())})';
}
printStatus('Built ${fs.path.relative(apkFile.path)}$appSize.');
if (buildInfo.createBaseline) {
// Save baseline apk for generating dynamic patches in later builds.
final AndroidApk package = AndroidApk.fromApk(apkFile);
final Directory baselineDir = fs.directory(buildInfo.baselineDir);
final File baselineApkFile = baselineDir.childFile('${package.versionCode}.apk');
baselineApkFile.parent.createSync(recursive: true);
apkFile.copySync(baselineApkFile.path);
printStatus('Saved baseline package ${baselineApkFile.path}.');
}
String appSize;
if (buildInfo.mode == BuildMode.debug) {
appSize = '';
} else {
appSize = ' (${getSizeAsMB(apkFile.lengthSync())})';
}
printStatus('Built ${fs.path.relative(apkFile.path)}$appSize.');
if (buildInfo.createBaseline) {
// Save baseline apk for generating dynamic patches in later builds.
final AndroidApk package = AndroidApk.fromApk(apkFile);
final Directory baselineDir = fs.directory(buildInfo.baselineDir);
final File baselineApkFile = baselineDir.childFile('${package.versionCode}.apk');
baselineApkFile.parent.createSync(recursive: true);
apkFile.copySync(baselineApkFile.path);
printStatus('Saved baseline package ${baselineApkFile.path}.');
}
if (buildInfo.createPatch) {
final AndroidApk package = AndroidApk.fromApk(apkFile);
if (buildInfo.createPatch) {
final AndroidApk package = AndroidApk.fromApk(apkFile);
final Directory baselineDir = fs.directory(buildInfo.baselineDir);
final File baselineApkFile = baselineDir.childFile('${package.versionCode}.apk');
if (!baselineApkFile.existsSync())
throwToolExit('Error: Could not find baseline package ${baselineApkFile.path}.');
final File baselineApkFile = baselineDir.childFile('${package.versionCode}.apk');if (!baselineApkFile.existsSync())
throwToolExit('Error: Could not find baseline package ${baselineApkFile.path}.');
printStatus('Found baseline package ${baselineApkFile.path}.');
final Archive newApk = ZipDecoder().decodeBytes(apkFile.readAsBytesSync());
final Archive oldApk = ZipDecoder().decodeBytes(baselineApkFile.readAsBytesSync());
printStatus('Found baseline package ${baselineApkFile.path}.');
final Archive newApk = ZipDecoder().decodeBytes(apkFile.readAsBytesSync());
final Archive oldApk = ZipDecoder().decodeBytes(baselineApkFile.readAsBytesSync());
final Archive update = Archive();
for (ArchiveFile newFile in newApk) {
if (!newFile.isFile || !newFile.name.startsWith('assets/flutter_assets/'))
continue;
final Archive update = Archive();
for (ArchiveFile newFile in newApk) {
if (!newFile.isFile || !newFile.name.startsWith('assets/flutter_assets/'))
continue;
final ArchiveFile oldFile = oldApk.findFile(newFile.name);
if (oldFile != null && oldFile.crc32 == newFile.crc32)
continue;
final ArchiveFile oldFile = oldApk.findFile(newFile.name);
if (oldFile != null && oldFile.crc32 == newFile.crc32)
continue;
final String name = fs.path.relative(newFile.name, from: 'assets/');
update.addFile(ArchiveFile(name, newFile.content.length, newFile.content));
}
final String name = fs.path.relative(newFile.name, from: 'assets/');
update.addFile(ArchiveFile(name, newFile.content.length, newFile.content));
}
final File updateFile = fs.directory(buildInfo.patchDir)
.childFile('${package.versionCode}-${buildInfo.patchNumber}.zip');
final File updateFile = fs.directory(buildInfo.patchDir)
.childFile('${package.versionCode}-${buildInfo.patchNumber}.zip');
if (update.files.isEmpty) {
printStatus('No changes detected relative to baseline build.');
if (update.files.isEmpty) {
printStatus('No changes detected relative to baseline build.');
if (updateFile.existsSync()) {
updateFile.deleteSync();
printStatus('Deleted dynamic patch ${updateFile.path}.');
if (updateFile.existsSync()) {
updateFile.deleteSync();
printStatus('Deleted dynamic patch ${updateFile.path}.');
}
return;
}
return;
}
final ArchiveFile oldFile = oldApk.findFile('assets/flutter_assets/isolate_snapshot_data');
if (oldFile == null)
throwToolExit('Error: Could not find baseline assets/flutter_assets/isolate_snapshot_data.');
final ArchiveFile oldFile = oldApk.findFile('assets/flutter_assets/isolate_snapshot_data');
if (oldFile == null)
throwToolExit('Error: Could not find baseline assets/flutter_assets/isolate_snapshot_data.');
final int baselineChecksum = getCrc32(oldFile.content);
final Map<String, dynamic> manifest = <String, dynamic>{
'baselineChecksum': baselineChecksum,
'buildNumber': package.versionCode,
'patchNumber': buildInfo.patchNumber,
};
final int baselineChecksum = getCrc32(oldFile.content);
final Map<String, dynamic> manifest = <String, dynamic>{
'baselineChecksum': baselineChecksum,
'buildNumber': package.versionCode,
'patchNumber': buildInfo.patchNumber,
};
const JsonEncoder encoder = JsonEncoder.withIndent(' ');
final String manifestJson = encoder.convert(manifest);
update.addFile(ArchiveFile('manifest.json', manifestJson.length, manifestJson.codeUnits));
const JsonEncoder encoder = JsonEncoder.withIndent(' ');
final String manifestJson = encoder.convert(manifest);
update.addFile(ArchiveFile('manifest.json', manifestJson.length, manifestJson.codeUnits));
updateFile.parent.createSync(recursive: true);
updateFile.writeAsBytesSync(ZipEncoder().encode(update), flush: true);
printStatus('Created dynamic patch ${updateFile.path}.');
updateFile.parent.createSync(recursive: true);
updateFile.writeAsBytesSync(ZipEncoder().encode(update), flush: true);
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.');
}
}
......@@ -512,6 +544,28 @@ File _findApkFile(GradleProject project, BuildInfo buildInfo) {
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 {
final Map<String, String> env = Map<String, String>.from(platform.environment);
if (javaPath != null) {
......@@ -522,7 +576,7 @@ Map<String, String> get _gradleEnv {
}
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) {
// Extract build directory.
......@@ -561,12 +615,14 @@ class GradleProject {
buildTypes.toList(),
productFlavors.toList(),
fs.directory(fs.path.join(buildDir, 'outputs', 'apk')),
fs.directory(fs.path.join(buildDir, 'outputs', 'bundle')),
);
}
final List<String> buildTypes;
final List<String> productFlavors;
final Directory apkDirectory;
final Directory bundleDirectory;
String _buildTypeFor(BuildInfo buildInfo) {
final String modeName = camelCase(buildInfo.modeName);
......@@ -600,4 +656,18 @@ class GradleProject {
final String flavorString = productFlavor.isEmpty ? '' : '-' + productFlavor;
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';
import '../runner/flutter_command.dart';
import 'build_aot.dart';
import 'build_apk.dart';
import 'build_appbundle.dart';
import 'build_bundle.dart';
import 'build_flx.dart';
import 'build_ios.dart';
......@@ -19,6 +20,7 @@ import 'build_ios.dart';
class BuildCommand extends FlutterCommand {
BuildCommand({bool verboseHelp = false}) {
addSubcommand(BuildApkCommand(verboseHelp: verboseHelp));
addSubcommand(BuildAppBundleCommand(verboseHelp: verboseHelp));
addSubcommand(BuildAotCommand());
addSubcommand(BuildIOSCommand());
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 {
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 {
return hostAppGradleRoot.childFile('build.gradle').existsSync();
}
......
......@@ -113,35 +113,65 @@ someOtherTask
expect(project.productFlavors, <String>['free', 'paid']);
});
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.profile), 'app-profile.apk');
expect(project.apkFileFor(BuildInfo.release), 'app-release.apk');
expect(project.apkFileFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
});
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.release, 'paid')), 'app-paid-release.apk');
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', () {
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.profile), 'assembleProfile');
expect(project.assembleTaskFor(BuildInfo.release), 'assembleRelease');
expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
});
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.release, 'paid')), 'assemblePaidRelease');
expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
});
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');
});
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', () {
......
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