Skip to content

Commit

Permalink
Adding support for android app bundle - Issue #17829 (#24440)
Browse files Browse the repository at this point in the history
* 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 2041d45.

* 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
  • Loading branch information
pranayairan committed Dec 21, 2018
1 parent 8426910 commit 368cd7d
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 76 deletions.
1 change: 1 addition & 0 deletions packages/flutter_tools/lib/src/android/apk.dart
Expand Up @@ -44,5 +44,6 @@ Future<void> buildApk({
project: project,
buildInfo: buildInfo,
target: target,
isBuildingBundle: false
);
}
49 changes: 49 additions & 0 deletions packages/flutter_tools/lib/src/android/app_bundle.dart
@@ -0,0 +1,49 @@
// 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
);
}
212 changes: 141 additions & 71 deletions packages/flutter_tools/lib/src/android/gradle.dart
Expand Up @@ -125,6 +125,7 @@ Future<GradleProject> _readGradleProject() async {
project = GradleProject(
<String>['debug', 'profile', 'release'],
<String>[], flutterProject.android.gradleAppOutV1Directory,
flutterProject.android.gradleAppBundleOutV1Directory
);
}
status.stop();
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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.');
Expand Down Expand Up @@ -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.');
}
}

Expand All @@ -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) {
Expand All @@ -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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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';
}
}
2 changes: 2 additions & 0 deletions packages/flutter_tools/lib/src/commands/build.dart
Expand Up @@ -12,13 +12,15 @@ 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';

class BuildCommand extends FlutterCommand {
BuildCommand({bool verboseHelp = false}) {
addSubcommand(BuildApkCommand(verboseHelp: verboseHelp));
addSubcommand(BuildAppBundleCommand(verboseHelp: verboseHelp));
addSubcommand(BuildAotCommand());
addSubcommand(BuildIOSCommand());
addSubcommand(BuildFlxCommand());
Expand Down

0 comments on commit 368cd7d

Please sign in to comment.