Unverified Commit 8627ff43 authored by Emmanuel Garcia's avatar Emmanuel Garcia Committed by GitHub

Allow multi-abi shared libraries in APKs and App bundles (#34123)

* Gradle generates ELF shared libraries instead of AOT snapshots.
* `flutter build apk/appbundle` supports multiple `--target-platform` and defaults to `android-arm` and `android-arm64`.
* `flutter build apk` now has a flag called `--split-per-abi`.
parent 72aff8ef
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
...@@ -46,6 +47,263 @@ Future<void> main() async { ...@@ -46,6 +47,263 @@ Future<void> main() async {
print('\nUsing JAVA_HOME=$javaHome'); print('\nUsing JAVA_HOME=$javaHome');
try { try {
await runPluginProjectTest((FlutterPluginProject pluginProject) async {
section('APK content for task assembleDebug without explicit target platform');
await pluginProject.runGradleTask('assembleDebug');
if (!pluginProject.hasDebugApk)
throw TaskResult.failure(
'Gradle did not produce a debug apk file at: ${pluginProject.debugApkPath}');
final Iterable<String> apkFiles = await pluginProject.getFilesInApk(pluginProject.debugApkPath);
_checkItContains<String>(<String>[
'AndroidManifest.xml',
'classes.dex',
'assets/flutter_assets/isolate_snapshot_data',
'assets/flutter_assets/kernel_blob.bin',
'assets/flutter_assets/vm_snapshot_data',
'lib/arm64-v8a/libflutter.so',
'lib/armeabi-v7a/libflutter.so',
// Debug mode intentionally includes `x86` and `x86_64`.
'lib/x86/libflutter.so',
'lib/x86_64/libflutter.so',
], apkFiles);
_checkItDoesNotContain<String>(<String>[
'lib/arm64-v8a/libapp.so',
'lib/armeabi-v7a/libapp.so',
'lib/x86/libapp.so',
'lib/x86_64/libapp.so',
], apkFiles);
});
await runPluginProjectTest((FlutterPluginProject pluginProject) async {
section('APK content for task assembleDebug with target platform = android-arm');
await pluginProject.runGradleTask('assembleDebug',
options: <String>['-Ptarget-platform=android-arm']);
if (!pluginProject.hasDebugApk)
throw TaskResult.failure(
'Gradle did not produce a debug apk file at: ${pluginProject.debugApkPath}');
final Iterable<String> apkFiles = await pluginProject.getFilesInApk(pluginProject.debugApkPath);
_checkItContains<String>(<String>[
'AndroidManifest.xml',
'classes.dex',
'assets/flutter_assets/isolate_snapshot_data',
'assets/flutter_assets/kernel_blob.bin',
'assets/flutter_assets/vm_snapshot_data',
'lib/armeabi-v7a/libflutter.so',
// Debug mode intentionally includes `x86` and `x86_64`.
'lib/x86/libflutter.so',
'lib/x86_64/libflutter.so',
], apkFiles);
_checkItDoesNotContain<String>(<String>[
'lib/armeabi-v7a/libapp.so',
'lib/x86/libapp.so',
'lib/x86_64/libapp.so',
], apkFiles);
});
await runPluginProjectTest((FlutterPluginProject pluginProject) async {
section('APK content for task assembleRelease without explicit target platform');
await pluginProject.runGradleTask('assembleRelease');
if (!pluginProject.hasReleaseApk)
throw TaskResult.failure(
'Gradle did not produce a release apk file at: ${pluginProject.releaseApkPath}');
final Iterable<String> apkFiles = await pluginProject.getFilesInApk(pluginProject.releaseApkPath);
_checkItContains<String>(<String>[
'AndroidManifest.xml',
'classes.dex',
'lib/arm64-v8a/libflutter.so',
'lib/arm64-v8a/libapp.so',
'lib/armeabi-v7a/libflutter.so',
'lib/armeabi-v7a/libapp.so',
], apkFiles);
_checkItDoesNotContain<String>(<String>[
'assets/flutter_assets/isolate_snapshot_data',
'assets/flutter_assets/kernel_blob.bin',
'assets/flutter_assets/vm_snapshot_data',
], apkFiles);
});
await runPluginProjectTest((FlutterPluginProject pluginProject) async {
section('APK content for task assembleRelease with target platform = android-arm');
await pluginProject.runGradleTask('assembleRelease',
options: <String>['-Ptarget-platform=android-arm']);
if (!pluginProject.hasReleaseApk)
throw TaskResult.failure(
'Gradle did not produce a release apk file at: ${pluginProject.releaseApkPath}');
final Iterable<String> apkFiles = await pluginProject.getFilesInApk(pluginProject.releaseApkPath);
_checkItContains<String>(<String>[
'AndroidManifest.xml',
'classes.dex',
'lib/armeabi-v7a/libflutter.so',
'lib/armeabi-v7a/libapp.so',
], apkFiles);
_checkItDoesNotContain<String>(<String>[
'lib/arm64-v8a/libflutter.so',
'lib/arm64-v8a/libapp.so',
'assets/flutter_assets/isolate_snapshot_data',
'assets/flutter_assets/kernel_blob.bin',
'assets/flutter_assets/vm_snapshot_data',
], apkFiles);
});
await runPluginProjectTest((FlutterPluginProject pluginProject) async {
section('APK content for task assembleRelease with target platform = android-arm64');
await pluginProject.runGradleTask('assembleRelease',
options: <String>['-Ptarget-platform=android-arm64']);
if (!pluginProject.hasReleaseApk)
throw TaskResult.failure(
'Gradle did not produce a release apk file at: ${pluginProject.releaseApkPath}');
final Iterable<String> apkFiles = await pluginProject.getFilesInApk(pluginProject.releaseApkPath);
_checkItContains<String>(<String>[
'AndroidManifest.xml',
'classes.dex',
'lib/arm64-v8a/libflutter.so',
'lib/arm64-v8a/libapp.so',
], apkFiles);
_checkItDoesNotContain<String>(<String>[
'lib/armeabi-v7a/libflutter.so',
'lib/armeabi-v7a/libapp.so',
'assets/flutter_assets/isolate_snapshot_data',
'assets/flutter_assets/kernel_blob.bin',
'assets/flutter_assets/vm_snapshot_data',
], apkFiles);
});
await runPluginProjectTest((FlutterPluginProject pluginProject) async {
section('APK content for task assembleRelease with target platform = android-arm, android-arm64');
await pluginProject.runGradleTask('assembleRelease',
options: <String>['-Ptarget-platform=android-arm,android-arm64']);
if (!pluginProject.hasReleaseApk)
throw TaskResult.failure(
'Gradle did not produce a release apk at: ${pluginProject.releaseApkPath}');
final Iterable<String> apkFiles = await pluginProject.getFilesInApk(pluginProject.releaseApkPath);
_checkItContains<String>(<String>[
'AndroidManifest.xml',
'classes.dex',
'lib/armeabi-v7a/libflutter.so',
'lib/armeabi-v7a/libapp.so',
'lib/arm64-v8a/libflutter.so',
'lib/arm64-v8a/libapp.so',
], apkFiles);
_checkItDoesNotContain<String>(<String>[
'assets/flutter_assets/isolate_snapshot_data',
'assets/flutter_assets/kernel_blob.bin',
'assets/flutter_assets/vm_snapshot_data',
], apkFiles);
});
await runPluginProjectTest((FlutterPluginProject pluginProject) async {
section('APK content for task assembleRelease with '
'target platform = android-arm, android-arm64 and split per ABI');
await pluginProject.runGradleTask('assembleRelease',
options: <String>['-Ptarget-platform=android-arm,android-arm64', '-Psplit-per-abi=true']);
if (!pluginProject.hasReleaseArmApk)
throw TaskResult.failure(
'Gradle did not produce a release apk at: ${pluginProject.releaseArmApkPath}');
final Iterable<String> armApkFiles = await pluginProject.getFilesInApk(pluginProject.releaseArmApkPath);
_checkItContains<String>(<String>[
'AndroidManifest.xml',
'classes.dex',
'lib/armeabi-v7a/libflutter.so',
'lib/armeabi-v7a/libapp.so',
], armApkFiles);
_checkItDoesNotContain<String>(<String>[
'assets/flutter_assets/isolate_snapshot_data',
'assets/flutter_assets/kernel_blob.bin',
'assets/flutter_assets/vm_snapshot_data',
], armApkFiles);
if (!pluginProject.hasReleaseArm64Apk)
throw TaskResult.failure(
'Gradle did not produce a release apk at: ${pluginProject.releaseArm64ApkPath}');
final Iterable<String> arm64ApkFiles = await pluginProject.getFilesInApk(pluginProject.releaseArm64ApkPath);
_checkItContains<String>(<String>[
'AndroidManifest.xml',
'classes.dex',
'lib/arm64-v8a/libflutter.so',
'lib/arm64-v8a/libapp.so',
], arm64ApkFiles);
_checkItDoesNotContain<String>(<String>[
'assets/flutter_assets/isolate_snapshot_data',
'assets/flutter_assets/kernel_blob.bin',
'assets/flutter_assets/vm_snapshot_data',
], arm64ApkFiles);
});
await runPluginProjectTest((FlutterPluginProject pluginProject) async {
section('App bundle content for task bundleRelease without explicit target platform');
await pluginProject.runGradleTask('bundleRelease');
if (!pluginProject.hasReleaseBundle)
throw TaskResult.failure(
'Gradle did not produce a release aab file at: ${pluginProject.releaseBundlePath}');
final Iterable<String> bundleFiles = await pluginProject.getFilesInAppBundle(pluginProject.releaseBundlePath);
_checkItContains<String>(<String>[
'base/manifest/AndroidManifest.xml',
'base/dex/classes.dex',
'base/lib/arm64-v8a/libapp.so',
'base/lib/arm64-v8a/libflutter.so',
'base/lib/armeabi-v7a/libapp.so',
'base/lib/armeabi-v7a/libflutter.so',
], bundleFiles);
});
await runPluginProjectTest((FlutterPluginProject pluginProject) async {
section('App bundle content for task bundleRelease with target platform = android-arm');
await pluginProject.runGradleTask('bundleRelease',
options: <String>['-Ptarget-platform=android-arm']);
if (!pluginProject.hasReleaseBundle)
throw TaskResult.failure(
'Gradle did not produce a release aab file at: ${pluginProject.releaseBundlePath}');
final Iterable<String> bundleFiles = await pluginProject.getFilesInAppBundle(pluginProject.releaseBundlePath);
_checkItContains<String>(<String>[
'base/manifest/AndroidManifest.xml',
'base/dex/classes.dex',
'base/lib/armeabi-v7a/libapp.so',
'base/lib/armeabi-v7a/libflutter.so',
], bundleFiles);
_checkItDoesNotContain<String>(<String>[
'base/lib/arm64-v8a/libapp.so',
'base/lib/arm64-v8a/libflutter.so',
], bundleFiles);
});
await runProjectTest((FlutterProject project) async { await runProjectTest((FlutterProject project) async {
section('gradlew assembleDebug'); section('gradlew assembleDebug');
await project.runGradleTask('assembleDebug'); await project.runGradleTask('assembleDebug');
...@@ -80,32 +338,9 @@ Future<void> main() async { ...@@ -80,32 +338,9 @@ Future<void> main() async {
'release', 'release',
targetPlatform); targetPlatform);
final String isolateSnapshotData = final String sharedLibrary = path.join(androidArmSnapshotPath, 'app.so');
path.join(androidArmSnapshotPath, 'isolate_snapshot_data'); if (!File(sharedLibrary).existsSync()) {
if (!File(isolateSnapshotData).existsSync()) { throw TaskResult.failure('Shared library doesn\'t exist');
throw TaskResult.failure(
'Snapshot doesn\'t exist: $isolateSnapshotData');
}
final String isolateSnapshotInstr =
path.join(androidArmSnapshotPath, 'isolate_snapshot_instr');
if (!File(isolateSnapshotInstr).existsSync()) {
throw TaskResult.failure(
'Snapshot doesn\'t exist: $isolateSnapshotInstr');
}
final String vmSnapshotData =
path.join(androidArmSnapshotPath, 'vm_snapshot_data');
if (!File(isolateSnapshotData).existsSync()) {
throw TaskResult.failure(
'Snapshot doesn\'t exist: $vmSnapshotData');
}
final String vmSnapshotInstr =
path.join(androidArmSnapshotPath, 'vm_snapshot_instr');
if (!File(isolateSnapshotData).existsSync()) {
throw TaskResult.failure(
'Snapshot doesn\'t exist: $vmSnapshotInstr');
} }
} }
}); });
...@@ -183,6 +418,23 @@ Future<void> main() async { ...@@ -183,6 +418,23 @@ Future<void> main() async {
}); });
} }
void _checkItContains<T>(Iterable<T> values, Iterable<T> collection) {
for (T value in values) {
if (!collection.contains(value)) {
throw TaskResult.failure('Expected to find `$value` in `$collection`.');
}
}
}
void _checkItDoesNotContain<T>(Iterable<T> values, Iterable<T> collection) {
for (T value in values) {
if (collection.contains(value)) {
throw TaskResult.failure('Did not expect to find `$value` in `$collection`.');
}
}
}
TaskResult _failure(String message, ProcessResult result) { TaskResult _failure(String message, ProcessResult result) {
print('Unexpected process result:'); print('Unexpected process result:');
print('Exit code: ${result.exitCode}'); print('Exit code: ${result.exitCode}');
...@@ -288,12 +540,37 @@ class FlutterPluginProject { ...@@ -288,12 +540,37 @@ class FlutterPluginProject {
String get examplePath => path.join(rootPath, 'example'); String get examplePath => path.join(rootPath, 'example');
String get exampleAndroidPath => path.join(examplePath, 'android'); String get exampleAndroidPath => path.join(examplePath, 'android');
String get debugApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'apk', 'debug', 'app-debug.apk'); String get debugApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'apk', 'debug', 'app-debug.apk');
String get releaseApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'apk', 'release', 'app-release.apk');
String get releaseArmApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'apk', 'release', 'app-armeabi-v7a-release.apk');
String get releaseArm64ApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'apk', 'release', 'app-arm64-v8a-release.apk');
String get releaseBundlePath => path.join(examplePath, 'build', 'app', 'outputs', 'bundle', 'release', 'app.aab');
bool get hasDebugApk => File(debugApkPath).existsSync();
bool get hasReleaseApk => File(releaseApkPath).existsSync();
bool get hasReleaseArmApk => File(releaseArmApkPath).existsSync();
bool get hasReleaseArm64Apk => File(releaseArm64ApkPath).existsSync();
bool get hasReleaseBundle => File(releaseBundlePath).existsSync();
Future<void> runGradleTask(String task, {List<String> options}) async { Future<void> runGradleTask(String task, {List<String> options}) async {
return _runGradleTask(workingDirectory: exampleAndroidPath, task: task, options: options); return _runGradleTask(workingDirectory: exampleAndroidPath, task: task, options: options);
} }
bool get hasDebugApk => File(debugApkPath).existsSync(); Future<Iterable<String>> getFilesInApk(String apk) async {
final Process unzip = await startProcess(
'unzip',
<String>['-v', apk],
isBot: false, // we just want to test the output, not have any debugging info
);
return unzip.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.map((String line) => line.split(' ').last)
.toList();
}
Future<Iterable<String>> getFilesInAppBundle(String bundle) {
return getFilesInApk(bundle);
}
} }
Future<void> _runGradleTask({String workingDirectory, String task, List<String> options}) async { Future<void> _runGradleTask({String workingDirectory, String task, List<String> options}) async {
......
...@@ -351,6 +351,7 @@ class CompileTest { ...@@ -351,6 +351,7 @@ class CompileTest {
break; break;
case DeviceOperatingSystem.android: case DeviceOperatingSystem.android:
options.insert(0, 'apk'); options.insert(0, 'apk');
options.add('--target-platform=android-arm');
watch.start(); watch.start();
await flutter('build', options: options); await flutter('build', options: options);
watch.stop(); watch.stop();
...@@ -387,6 +388,7 @@ class CompileTest { ...@@ -387,6 +388,7 @@ class CompileTest {
break; break;
case DeviceOperatingSystem.android: case DeviceOperatingSystem.android:
options.insert(0, 'apk'); options.insert(0, 'apk');
options.add('--target-platform=android-arm');
break; break;
} }
watch.start(); watch.start();
...@@ -445,23 +447,14 @@ class CompileTest { ...@@ -445,23 +447,14 @@ class CompileTest {
} }
final _UnzipListEntry libflutter = fileToMetadata['lib/armeabi-v7a/libflutter.so']; final _UnzipListEntry libflutter = fileToMetadata['lib/armeabi-v7a/libflutter.so'];
final _UnzipListEntry isolateSnapshotData = fileToMetadata['assets/isolate_snapshot_data']; final _UnzipListEntry libapp = fileToMetadata['lib/armeabi-v7a/libapp.so'];
final _UnzipListEntry isolateSnapshotInstr = fileToMetadata['assets/isolate_snapshot_instr'];
final _UnzipListEntry vmSnapshotData = fileToMetadata['assets/vm_snapshot_data'];
final _UnzipListEntry vmSnapshotInstr = fileToMetadata['assets/vm_snapshot_instr'];
final _UnzipListEntry license = fileToMetadata['assets/flutter_assets/LICENSE']; final _UnzipListEntry license = fileToMetadata['assets/flutter_assets/LICENSE'];
return <String, dynamic>{ return <String, dynamic>{
'libflutter_uncompressed_bytes': libflutter.uncompressedSize, 'libflutter_uncompressed_bytes': libflutter.uncompressedSize,
'libflutter_compressed_bytes': libflutter.compressedSize, 'libflutter_compressed_bytes': libflutter.compressedSize,
'snapshot_uncompressed_bytes': isolateSnapshotData.uncompressedSize + 'libapp_uncompressed_bytes': libapp.uncompressedSize,
isolateSnapshotInstr.uncompressedSize + 'libapp_compressed_bytes': libapp.compressedSize,
vmSnapshotData.uncompressedSize +
vmSnapshotInstr.uncompressedSize,
'snapshot_compressed_bytes': isolateSnapshotData.compressedSize +
isolateSnapshotInstr.compressedSize +
vmSnapshotData.compressedSize +
vmSnapshotInstr.compressedSize,
'license_uncompressed_bytes': license.uncompressedSize, 'license_uncompressed_bytes': license.uncompressedSize,
'license_compressed_bytes': license.compressedSize, 'license_compressed_bytes': license.compressedSize,
}; };
......
...@@ -5,7 +5,7 @@ homepage: https://github.com/flutter/flutter ...@@ -5,7 +5,7 @@ homepage: https://github.com/flutter/flutter
environment: environment:
# The pub client defaults to an <2.0.0 sdk constraint which we need to explicitly overwrite. # The pub client defaults to an <2.0.0 sdk constraint which we need to explicitly overwrite.
sdk: ">=2.2.2 <3.0.0" sdk: ">=2.3.0 <3.0.0"
dependencies: dependencies:
args: 1.5.2 args: 1.5.2
......
...@@ -2,6 +2,7 @@ import java.nio.file.Path ...@@ -2,6 +2,7 @@ import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import com.android.builder.model.AndroidProject import com.android.builder.model.AndroidProject
import com.android.build.OutputFile
import org.apache.tools.ant.taskdefs.condition.Os import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.GradleException import org.gradle.api.GradleException
...@@ -36,6 +37,49 @@ android { ...@@ -36,6 +37,49 @@ android {
apply plugin: FlutterPlugin apply plugin: FlutterPlugin
class FlutterPlugin implements Plugin<Project> { class FlutterPlugin implements Plugin<Project> {
// The platforms that can be passed to the `--Ptarget-platform` flag.
private static final String PLATFORM_ARM32 = "android-arm";
private static final String PLATFORM_ARM64 = "android-arm64";
private static final String PLATFORM_X86 = "android-x86";
private static final String PLATFORM_X86_64 = "android-x64";
// The ABI architectures.
private static final String ARCH_ARM32 = "armeabi-v7a";
private static final String ARCH_ARM64 = "arm64-v8a";
private static final String ARCH_X86 = "x86";
private static final String ARCH_X86_64 = "x86_64";
// Maps platforms to ABI architectures.
private static final Map PLATFORM_ARCH_MAP = [
(PLATFORM_ARM32) : ARCH_ARM32,
(PLATFORM_ARM64) : ARCH_ARM64,
(PLATFORM_X86) : ARCH_X86,
(PLATFORM_X86_64) : ARCH_X86_64,
]
// The version code that gives each ABI a value.
// For each APK variant, use the following versions to override the version of the Universal APK.
// Otherwise, the Play Store will complain that the APK variants have the same version.
private static final Map ABI_VERSION = [
(ARCH_ARM32) : 1,
(ARCH_ARM64) : 2,
(ARCH_X86) : 3,
(ARCH_X86_64) : 4,
]
// When split is enabled, multiple APKs are generated per each ABI.
private static final List DEFAULT_PLATFORMS = [
PLATFORM_ARM32,
PLATFORM_ARM64,
]
// The name prefix for flutter builds. This is used to identify gradle tasks
// where we expect the flutter tool to provide any error output, and skip the
// standard Gradle error output in the FlutterEventLogger. If you change this,
// be sure to change any instances of this string in symbols in the code below
// to match.
static final String FLUTTER_BUILD_PREFIX = "flutterBuild"
private Path baseEnginePath private Path baseEnginePath
private File flutterRoot private File flutterRoot
private File flutterExecutable private File flutterExecutable
...@@ -49,29 +93,6 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -49,29 +93,6 @@ class FlutterPlugin implements Plugin<Project> {
private File dynamicProfileFlutterJar private File dynamicProfileFlutterJar
private File dynamicReleaseFlutterJar private File dynamicReleaseFlutterJar
// The name prefix for flutter builds. This is used to identify gradle tasks
// where we expect the flutter tool to provide any error output, and skip the
// standard Gradle error output in the FlutterEventLogger. If you change this,
// be sure to change any instances of this string in symbols in the code below
// to match.
static final String flutterBuildPrefix = "flutterBuild"
// The platforms (or CPU architectures) for which native code is generated.
static final Map allTargetPlatforms = [
'android-arm': 'armeabi-v7a',
'android-arm64': 'arm64-v8a',
'android-x64': 'x86_64',
'android-x86': 'x86',
]
// Supports ARM 32 and 64 bits.
// When splits are enabled, multiple APKs are generated per each CPU architecture,
// which helps decrease the size of each APK.
static final Set allArmPlatforms = [
'android-arm',
'android-arm64'
]
private Properties readPropertiesIfExist(File propertiesFile) { private Properties readPropertiesIfExist(File propertiesFile) {
Properties result = new Properties() Properties result = new Properties()
if (propertiesFile.exists()) { if (propertiesFile.exists()) {
...@@ -80,11 +101,16 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -80,11 +101,16 @@ class FlutterPlugin implements Plugin<Project> {
return result return result
} }
private String getTargetPlatform(Project project) { private List<String> getTargetPlatforms(Project project) {
if (project.hasProperty('target-platform')) { if (!project.hasProperty('target-platform')) {
return project.property('target-platform') return DEFAULT_PLATFORMS
}
return project.property('target-platform').split(',').collect {
if (!PLATFORM_ARCH_MAP[it]) {
throw new GradleException("Invalid platform: $it.")
}
return it
} }
return 'android-arm-all';
} }
private Boolean getBuildShareLibrary(Project project) { private Boolean getBuildShareLibrary(Project project) {
...@@ -94,6 +120,13 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -94,6 +120,13 @@ class FlutterPlugin implements Plugin<Project> {
return false; return false;
} }
private Boolean splitPerAbi(Project project) {
if (project.hasProperty('split-per-abi')) {
return project.property('split-per-abi').toBoolean()
}
return false;
}
private String resolveProperty(Project project, String name, String defaultValue) { private String resolveProperty(Project project, String name, String defaultValue) {
if (localProperties == null) { if (localProperties == null) {
localProperties = readPropertiesIfExist(new File(project.projectDir.parentFile, "local.properties")) localProperties = readPropertiesIfExist(new File(project.projectDir.parentFile, "local.properties"))
...@@ -111,21 +144,55 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -111,21 +144,55 @@ class FlutterPlugin implements Plugin<Project> {
return result return result
} }
/**
* Returns the platform that is used to extract the `libflutter.so` and the .class files.
*
* Note: This is only needed to add the .class files.
* Unfortunately, the engine artifacts include the .class and libflutter.so files.
*/
private String getBasePlatform(Project project) {
if (PLATFORM_ARM64 in getTargetPlatforms(project)) {
return PLATFORM_ARM64;
}
return PLATFORM_ARM32;
}
@Override @Override
void apply(Project project) { void apply(Project project) {
project.extensions.create("flutter", FlutterExtension) project.extensions.create("flutter", FlutterExtension)
project.afterEvaluate this.&addFlutterTask project.afterEvaluate this.&addFlutterTask
allTargetPlatforms.each { currentTargetPlatformValue, abi -> // By default, assembling APKs generates fat APKs if multiple platforms are passed.
// Configuring split per ABI allows to generate separate APKs for each abi.
// This is a noop when building a bundle.
if (this.splitPerAbi(project)) {
project.android {
splits {
abi {
// Enables building multiple APKs per ABI.
enable true
// Resets the list of ABIs that Gradle should create APKs for to none.
reset()
// Specifies that we do not want to also generate a universal APK that includes all ABIs.
universalApk false
}
}
}
}
this.getTargetPlatforms(project).each { targetArch ->
String abiValue = PLATFORM_ARCH_MAP[targetArch]
project.android { project.android {
packagingOptions { packagingOptions {
pickFirst "lib/${abi}/libflutter.so" pickFirst "lib/${abiValue}/libflutter.so"
// Prevent the ELF library from getting corrupted.
// Disable warning by *-android-strip: File format not recognized doNotStrip "*/${abiValue}/libapp.so"
doNotStrip "*/${abi}/lib_vm_snapshot_data.so" }
doNotStrip "*/${abi}/lib_vm_snapshot_instr.so" if (this.splitPerAbi(project)) {
doNotStrip "*/${abi}/lib_isolate_snapshot_data.so" splits {
doNotStrip "*/${abi}/lib_isolate_snapshot_instr.so" abi {
include abiValue
}
}
} }
} }
} }
...@@ -173,7 +240,7 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -173,7 +240,7 @@ class FlutterPlugin implements Plugin<Project> {
baseEnginePath = Paths.get(engineOut.absolutePath) baseEnginePath = Paths.get(engineOut.absolutePath)
flutterJar = baseEnginePath.resolve("flutter.jar").toFile() flutterJar = baseEnginePath.resolve("flutter.jar").toFile()
if (!flutterJar.isFile()) { if (!flutterJar.isFile()) {
throw new GradleException('File not found: ' + flutterJar) throw new GradleException('Local engine jar not found: ' + flutterJar)
} }
localEngine = engineOut.name localEngine = engineOut.name
...@@ -186,14 +253,14 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -186,14 +253,14 @@ class FlutterPlugin implements Plugin<Project> {
dynamicProfileFlutterJar = flutterJar dynamicProfileFlutterJar = flutterJar
dynamicReleaseFlutterJar = flutterJar dynamicReleaseFlutterJar = flutterJar
} else { } else {
String targetPlatform = getTargetPlatform(project) String basePlatformArch = getBasePlatform(project)
String targetArch = targetPlatform == 'android-arm64' ? 'arm64' : 'arm' // This is meant to include the compiled classes only, however it will include `libflutter.so` as well.
baseEnginePath = Paths.get(flutterRoot.absolutePath, "bin", "cache", "artifacts", "engine") baseEnginePath = Paths.get(flutterRoot.absolutePath, "bin", "cache", "artifacts", "engine")
debugFlutterJar = baseEnginePath.resolve("android-${targetArch}").resolve("flutter.jar").toFile() debugFlutterJar = baseEnginePath.resolve("${basePlatformArch}").resolve("flutter.jar").toFile()
profileFlutterJar = baseEnginePath.resolve("android-${targetArch}-profile").resolve("flutter.jar").toFile() profileFlutterJar = baseEnginePath.resolve("${basePlatformArch}-profile").resolve("flutter.jar").toFile()
releaseFlutterJar = baseEnginePath.resolve("android-${targetArch}-release").resolve("flutter.jar").toFile() releaseFlutterJar = baseEnginePath.resolve("${basePlatformArch}-release").resolve("flutter.jar").toFile()
dynamicProfileFlutterJar = baseEnginePath.resolve("android-${targetArch}-dynamic-profile").resolve("flutter.jar").toFile() dynamicProfileFlutterJar = baseEnginePath.resolve("${basePlatformArch}-dynamic-profile").resolve("flutter.jar").toFile()
dynamicReleaseFlutterJar = baseEnginePath.resolve("android-${targetArch}-dynamic-release").resolve("flutter.jar").toFile() dynamicReleaseFlutterJar = baseEnginePath.resolve("${basePlatformArch}-dynamic-release").resolve("flutter.jar").toFile()
} }
if (!debugFlutterJar.isFile()) { if (!debugFlutterJar.isFile()) {
...@@ -209,7 +276,7 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -209,7 +276,7 @@ class FlutterPlugin implements Plugin<Project> {
// Add x86/x86_64 native library. Debug mode only, for now. // Add x86/x86_64 native library. Debug mode only, for now.
File flutterX86Jar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/flutter-x86.jar") File flutterX86Jar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/flutter-x86.jar")
Task debugX86JarTask = project.tasks.create("${flutterBuildPrefix}X86Jar", Jar) { Task debugX86JarTask = project.tasks.create("${FLUTTER_BUILD_PREFIX}X86Jar", Jar) {
destinationDir flutterX86Jar.parentFile destinationDir flutterX86Jar.parentFile
archiveName flutterX86Jar.name archiveName flutterX86Jar.name
from("${flutterRoot}/bin/cache/artifacts/engine/android-x86/libflutter.so") { from("${flutterRoot}/bin/cache/artifacts/engine/android-x86/libflutter.so") {
...@@ -221,7 +288,7 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -221,7 +288,7 @@ class FlutterPlugin implements Plugin<Project> {
} }
// Add flutter.jar dependencies to all <buildType>Api configurations, including custom ones // Add flutter.jar dependencies to all <buildType>Api configurations, including custom ones
// added after applying the Flutter plugin. // added after applying the Flutter plugin.
project.android.buildTypes.each { project.android.buildTypes.each {
addFlutterJarApiDependency(project, it, debugX86JarTask) addFlutterJarApiDependency(project, it, debugX86JarTask)
} }
project.android.buildTypes.whenObjectAdded { project.android.buildTypes.whenObjectAdded {
...@@ -338,6 +405,19 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -338,6 +405,19 @@ class FlutterPlugin implements Plugin<Project> {
return "release" return "release"
} }
private static String getEngineArtifactDirName(buildType, targetArch) {
if (buildType.name == "profile") {
return "${targetArch}-profile"
} else if (buildType.name == "dynamicProfile") {
return "${targetArch}-dynamic-profile"
} else if (buildType.name == "dynamicRelease") {
return "${targetArch}-dynamic-release"
} else if (buildType.debuggable) {
return "${targetArch}"
}
return "${targetArch}-release"
}
private void addFlutterTask(Project project) { private void addFlutterTask(Project project) {
if (project.state.failure) { if (project.state.failure) {
return return
...@@ -395,13 +475,24 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -395,13 +475,24 @@ class FlutterPlugin implements Plugin<Project> {
extraGenSnapshotOptionsValue = project.property('extra-gen-snapshot-options') extraGenSnapshotOptionsValue = project.property('extra-gen-snapshot-options')
} }
Boolean buildSharedLibraryValue = this.getBuildShareLibrary(project) def targetPlatforms = this.getTargetPlatforms(project)
String targetPlatformValue = this.getTargetPlatform(project)
def addFlutterDeps = { variant -> def addFlutterDeps = { variant ->
String flutterBuildMode = buildModeFor(variant.buildType) if (this.splitPerAbi(project)) {
variant.outputs.each { output ->
// Assigns the new version code to versionCodeOverride, which changes the version code
// for only the output APK, not for the variant itself. Skipping this step simply
// causes Gradle to use the value of variant.versionCode for the APK.
// For more, see https://developer.android.com/studio/build/configure-apk-splits
def abiVersionCode = ABI_VERSION.get(output.getFilter(OutputFile.ABI))
if (abiVersionCode != null) {
output.versionCodeOverride =
abiVersionCode * 1000 + variant.versionCode
}
}
}
if (flutterBuildMode == 'debug' && project.tasks.findByName('${flutterBuildPrefix}X86Jar')) { String flutterBuildMode = buildModeFor(variant.buildType)
if (flutterBuildMode == 'debug' && project.tasks.findByName("${FLUTTER_BUILD_PREFIX}X86Jar")) {
Task task = project.tasks.findByName("compile${variant.name.capitalize()}JavaWithJavac") Task task = project.tasks.findByName("compile${variant.name.capitalize()}JavaWithJavac")
if (task) { if (task) {
task.dependsOn project.flutterBuildX86Jar task.dependsOn project.flutterBuildX86Jar
...@@ -413,22 +504,10 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -413,22 +504,10 @@ class FlutterPlugin implements Plugin<Project> {
} }
def flutterTasks = [] def flutterTasks = []
def targetPlatforms = [] targetPlatforms.each { targetArch ->
String abiValue = PLATFORM_ARCH_MAP[targetArch]
if (targetPlatformValue == 'android-arm-all') { String taskName = "compile${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}${targetArch.replace('android-', '').capitalize()}"
if (flutterBuildMode == 'release') { FlutterTask compileTask = project.tasks.create(name: taskName, type: FlutterTask) {
targetPlatforms.addAll(allArmPlatforms)
} else {
targetPlatforms.add('android-arm')
}
} else {
targetPlatforms.add(targetPlatformValue)
}
targetPlatforms.each { currentTargetPlatformValue ->
String abiValue = allTargetPlatforms[currentTargetPlatformValue]
FlutterTask compileTask = project.tasks.create(name: "compile${flutterBuildPrefix}${variant.name.capitalize()}${currentTargetPlatformValue}",
type: FlutterTask) {
flutterRoot this.flutterRoot flutterRoot this.flutterRoot
flutterExecutable this.flutterExecutable flutterExecutable this.flutterExecutable
buildMode flutterBuildMode buildMode flutterBuildMode
...@@ -444,56 +523,53 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -444,56 +523,53 @@ class FlutterPlugin implements Plugin<Project> {
createPatch createPatchValue createPatch createPatchValue
buildNumber buildNumberValue buildNumber buildNumberValue
baselineDir baselineDirValue baselineDir baselineDirValue
buildSharedLibrary buildSharedLibraryValue targetPlatform targetArch
targetPlatform currentTargetPlatformValue
sourceDir project.file(project.flutter.source) sourceDir project.file(project.flutter.source)
intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/${currentTargetPlatformValue}") intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/${targetArch}")
extraFrontEndOptions extraFrontEndOptionsValue extraFrontEndOptions extraFrontEndOptionsValue
extraGenSnapshotOptions extraGenSnapshotOptionsValue extraGenSnapshotOptions extraGenSnapshotOptionsValue
} }
flutterTasks.add(compileTask) flutterTasks.add(compileTask)
} }
def libJar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/libs.jar")
def libJar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/libs.jar") Task packFlutterSnapshotsAndLibsTask = project.tasks.create(name: "packLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) {
Task packFlutterSnapshotsAndLibsTask = project.tasks.create(name: "packFlutterSnapshotsAndLibs${flutterBuildPrefix}${variant.name.capitalize()}", type: Jar) {
destinationDir libJar.parentFile destinationDir libJar.parentFile
archiveName libJar.name archiveName libJar.name
targetPlatforms.each { targetPlatform -> targetPlatforms.each { targetArch ->
// Include `libflutter.so` for each abi. // This check prevents including `libflutter.so` twice, since it's included in the base platform jar.
// TODO(blasten): The libs should be outside `flutter.jar` when the artifacts are downloaded. // Unfortunately, the `pickFirst` setting in `packagingOptions` does not work when the project `:flutter`
from(project.zipTree("${flutterRoot}/bin/cache/artifacts/engine/${targetPlatform}-release/flutter.jar")) { // is included as an implementation dependency, which causes duplicated `libflutter.so`.
include 'lib/**' if (getBasePlatform(project) != targetArch) {
def engineArtifactSubdir = getEngineArtifactDirName(variant.buildType, targetArch);
// Include `libflutter.so`.
// TODO(blasten): The libs should be outside `flutter.jar` when the artifacts are downloaded.
from(project.zipTree("${flutterRoot}/bin/cache/artifacts/engine/${engineArtifactSubdir}/flutter.jar")) {
include 'lib/**'
}
} }
} }
dependsOn flutterTasks dependsOn flutterTasks
// Add the snapshots and rename them as `lib/{abi}/*.so`. // Add the ELF library.
flutterTasks.each { flutterTask -> flutterTasks.each { flutterTask ->
from(flutterTask.intermediateDir) { from(flutterTask.intermediateDir) {
include 'vm_snapshot_data' include '*.so'
include 'vm_snapshot_instr' rename { String filename ->
include 'isolate_snapshot_data' return "lib/${flutterTask.abi}/lib${filename}"
include 'isolate_snapshot_instr'
rename { String filename ->
return "lib/${flutterTask.abi}/lib_${filename}.so"
} }
} }
} }
} }
// Include the snapshots and libflutter.so in `lib/`. // Include the snapshots and libflutter.so in `lib/`.
if (flutterBuildMode == 'release' && targetPlatformValue == 'android-arm-all') { project.dependencies {
project.dependencies { String configuration;
String configuration; if (project.getConfigurations().findByName("api")) {
if (project.getConfigurations().findByName("api")) { configuration = buildType.name + "Api";
configuration = buildType.name + "Api"; } else {
} else { configuration = buildType.name + "Compile";
configuration = buildType.name + "Compile";
}
add(configuration, project.files {
packFlutterSnapshotsAndLibsTask
})
} }
add(configuration, project.files {
packFlutterSnapshotsAndLibsTask
})
} }
// We know that the flutter app is a subproject in another Android app when these tasks exist. // We know that the flutter app is a subproject in another Android app when these tasks exist.
Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets") Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")
...@@ -510,12 +586,8 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -510,12 +586,8 @@ class FlutterPlugin implements Plugin<Project> {
variant.mergeAssets.mustRunAfter("clean${variant.mergeAssets.name.capitalize()}") variant.mergeAssets.mustRunAfter("clean${variant.mergeAssets.name.capitalize()}")
into variant.mergeAssets.outputDir into variant.mergeAssets.outputDir
} }
flutterTasks.each { flutterTask -> flutterTasks.each { flutterTask ->
with flutterTask.assets with flutterTask.assets
// Include the snapshots in the assets directory.
if (flutterBuildMode != 'release' || targetPlatformValue != 'android-arm-all') {
with flutterTask.snapshots
}
} }
} }
if (packageAssets) { if (packageAssets) {
...@@ -576,8 +648,6 @@ abstract class BaseFlutterTask extends DefaultTask { ...@@ -576,8 +648,6 @@ abstract class BaseFlutterTask extends DefaultTask {
@Optional @Input @Optional @Input
String baselineDir String baselineDir
@Optional @Input @Optional @Input
Boolean buildSharedLibrary
@Optional @Input
String targetPlatform String targetPlatform
@Input @Input
String abi String abi
...@@ -622,8 +692,10 @@ abstract class BaseFlutterTask extends DefaultTask { ...@@ -622,8 +692,10 @@ abstract class BaseFlutterTask extends DefaultTask {
args "build", "aot" args "build", "aot"
args "--suppress-analytics" args "--suppress-analytics"
args "--quiet" args "--quiet"
args "--build-shared-library"
args "--target", targetPath args "--target", targetPath
args "--output-dir", "${intermediateDir}" args "--output-dir", "${intermediateDir}"
args "--target-platform", "${targetPlatform}"
if (trackWidgetCreation) { if (trackWidgetCreation) {
args "--track-widget-creation" args "--track-widget-creation"
} }
...@@ -633,14 +705,6 @@ abstract class BaseFlutterTask extends DefaultTask { ...@@ -633,14 +705,6 @@ abstract class BaseFlutterTask extends DefaultTask {
if (extraGenSnapshotOptions != null) { if (extraGenSnapshotOptions != null) {
args "--extra-gen-snapshot-options", "${extraGenSnapshotOptions}" args "--extra-gen-snapshot-options", "${extraGenSnapshotOptions}"
} }
if (buildSharedLibrary) {
args "--build-shared-library"
}
if (targetPlatform == null) {
args "--target-platform", "android-arm"
} else {
args "--target-platform", "${targetPlatform}"
}
args "--${buildMode}" args "--${buildMode}"
} }
} }
...@@ -655,6 +719,7 @@ abstract class BaseFlutterTask extends DefaultTask { ...@@ -655,6 +719,7 @@ abstract class BaseFlutterTask extends DefaultTask {
args "build", "bundle" args "build", "bundle"
args "--suppress-analytics" args "--suppress-analytics"
args "--target", targetPath args "--target", targetPath
args "--target-platform", "${targetPlatform}"
if (verbose) { if (verbose) {
args "--verbose" args "--verbose"
} }
...@@ -688,9 +753,6 @@ abstract class BaseFlutterTask extends DefaultTask { ...@@ -688,9 +753,6 @@ abstract class BaseFlutterTask extends DefaultTask {
if (extraGenSnapshotOptions != null) { if (extraGenSnapshotOptions != null) {
args "--extra-gen-snapshot-options", "${extraGenSnapshotOptions}" args "--extra-gen-snapshot-options", "${extraGenSnapshotOptions}"
} }
if (targetPlatform != null) {
args "--target-platform", "${targetPlatform}"
}
if (buildMode == "release" || buildMode == "profile") { if (buildMode == "release" || buildMode == "profile") {
args "--precompiled" args "--precompiled"
} else { } else {
...@@ -731,14 +793,7 @@ class FlutterTask extends BaseFlutterTask { ...@@ -731,14 +793,7 @@ class FlutterTask extends BaseFlutterTask {
from "${intermediateDir}" from "${intermediateDir}"
if (buildMode == 'release' || buildMode == 'profile') { if (buildMode == 'release' || buildMode == 'profile') {
if (buildSharedLibrary) { include "app.so"
include "app.so"
} else {
include "vm_snapshot_data"
include "vm_snapshot_instr"
include "isolate_snapshot_data"
include "isolate_snapshot_instr"
}
} }
} }
} }
...@@ -805,7 +860,7 @@ class FlutterEventLogger extends BuildAdapter implements TaskExecutionListener { ...@@ -805,7 +860,7 @@ class FlutterEventLogger extends BuildAdapter implements TaskExecutionListener {
void buildFinished(BuildResult result) { void buildFinished(BuildResult result) {
if (result.failure != null) { if (result.failure != null) {
if (!(result.failure instanceof GradleException) || !mostRecentTask.startsWith(FlutterPlugin.flutterBuildPrefix)) { if (!(result.failure instanceof GradleException) || !mostRecentTask.startsWith(FlutterPlugin.FLUTTER_BUILD_PREFIX)) {
result.rethrowFailure() result.rethrowFailure()
} }
} }
......
...@@ -397,9 +397,24 @@ class AndroidDevice extends Device { ...@@ -397,9 +397,24 @@ class AndroidDevice extends Device {
return LaunchResult.failed(); return LaunchResult.failed();
} }
BuildInfo buildInfo = debuggingOptions.buildInfo; AndroidArch androidArch;
if (buildInfo.targetPlatform == null && devicePlatform == TargetPlatform.android_arm64) switch (devicePlatform) {
buildInfo = buildInfo.withTargetPlatform(TargetPlatform.android_arm64); case TargetPlatform.android_arm:
androidArch = AndroidArch.armeabi_v7a;
break;
case TargetPlatform.android_arm64:
androidArch = AndroidArch.arm64_v8a;
break;
case TargetPlatform.android_x64:
androidArch = AndroidArch.x86_64;
break;
case TargetPlatform.android_x86:
androidArch = AndroidArch.x86;
break;
default:
printError('Android platforms are only supported.');
return LaunchResult.failed();
}
if (!prebuiltApplication || androidSdk.licensesAvailable && androidSdk.latestVersion == null) { if (!prebuiltApplication || androidSdk.licensesAvailable && androidSdk.latestVersion == null) {
printTrace('Building APK'); printTrace('Building APK');
...@@ -407,7 +422,9 @@ class AndroidDevice extends Device { ...@@ -407,7 +422,9 @@ class AndroidDevice extends Device {
await buildApk( await buildApk(
project: project, project: project,
target: mainPath, target: mainPath,
buildInfo: buildInfo, androidBuildInfo: AndroidBuildInfo(debuggingOptions.buildInfo,
targetArchs: <AndroidArch>[androidArch]
),
); );
// Package has been built, so we can get the updated application ID and // Package has been built, so we can get the updated application ID and
// activity name from the .apk. // activity name from the .apk.
......
...@@ -16,7 +16,7 @@ import 'gradle.dart'; ...@@ -16,7 +16,7 @@ import 'gradle.dart';
Future<void> buildApk({ Future<void> buildApk({
@required FlutterProject project, @required FlutterProject project,
@required String target, @required String target,
BuildInfo buildInfo = BuildInfo.debug, @required AndroidBuildInfo androidBuildInfo,
}) async { }) async {
if (!project.android.isUsingGradle) { if (!project.android.isUsingGradle) {
throwToolExit( throwToolExit(
...@@ -33,7 +33,7 @@ Future<void> buildApk({ ...@@ -33,7 +33,7 @@ Future<void> buildApk({
await buildGradleProject( await buildGradleProject(
project: project, project: project,
buildInfo: buildInfo, androidBuildInfo: androidBuildInfo,
target: target, target: target,
isBuildingBundle: false, isBuildingBundle: false,
); );
......
...@@ -17,7 +17,7 @@ import 'gradle.dart'; ...@@ -17,7 +17,7 @@ import 'gradle.dart';
Future<void> buildAppBundle({ Future<void> buildAppBundle({
@required FlutterProject project, @required FlutterProject project,
@required String target, @required String target,
BuildInfo buildInfo = BuildInfo.debug, @required AndroidBuildInfo androidBuildInfo,
}) async { }) async {
if (!project.android.isUsingGradle) { if (!project.android.isUsingGradle) {
throwToolExit( throwToolExit(
...@@ -42,7 +42,7 @@ Future<void> buildAppBundle({ ...@@ -42,7 +42,7 @@ Future<void> buildAppBundle({
return buildGradleProject( return buildGradleProject(
project: project, project: project,
buildInfo: buildInfo, androidBuildInfo: androidBuildInfo,
target: target, target: target,
isBuildingBundle: true, isBuildingBundle: true,
); );
......
...@@ -15,6 +15,7 @@ import '../base/logger.dart'; ...@@ -15,6 +15,7 @@ import '../base/logger.dart';
import '../base/os.dart'; import '../base/os.dart';
import '../base/platform.dart'; import '../base/platform.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../base/terminal.dart';
import '../base/utils.dart'; import '../base/utils.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../cache.dart'; import '../cache.dart';
...@@ -318,7 +319,7 @@ void _exitIfNoAndroidSdk() { ...@@ -318,7 +319,7 @@ void _exitIfNoAndroidSdk() {
Future<void> buildGradleProject({ Future<void> buildGradleProject({
@required FlutterProject project, @required FlutterProject project,
@required BuildInfo buildInfo, @required AndroidBuildInfo androidBuildInfo,
@required String target, @required String target,
@required bool isBuildingBundle, @required bool isBuildingBundle,
}) async { }) async {
...@@ -330,7 +331,7 @@ Future<void> buildGradleProject({ ...@@ -330,7 +331,7 @@ Future<void> buildGradleProject({
// and can be overwritten with flutter build command. // and can be overwritten with flutter build command.
// The default Gradle script reads the version name and number // The default Gradle script reads the version name and number
// from the local.properties file. // from the local.properties file.
updateLocalProperties(project: project, buildInfo: buildInfo); updateLocalProperties(project: project, buildInfo: androidBuildInfo.buildInfo);
final String gradle = await _ensureGradle(project); final String gradle = await _ensureGradle(project);
...@@ -342,7 +343,7 @@ Future<void> buildGradleProject({ ...@@ -342,7 +343,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, isBuildingBundle); return _buildGradleProjectV2(project, gradle, androidBuildInfo, target, isBuildingBundle);
} }
} }
...@@ -391,11 +392,12 @@ String _calculateSha(File file) { ...@@ -391,11 +392,12 @@ String _calculateSha(File file) {
Future<void> _buildGradleProjectV2( Future<void> _buildGradleProjectV2(
FlutterProject flutterProject, FlutterProject flutterProject,
String gradle, String gradle,
BuildInfo buildInfo, AndroidBuildInfo androidBuildInfo,
String target, String target,
bool isBuildingBundle, bool isBuildingBundle,
) async { ) async {
final GradleProject project = await _gradleProject(); final GradleProject project = await _gradleProject();
final BuildInfo buildInfo = androidBuildInfo.buildInfo;
String assembleTask; String assembleTask;
...@@ -453,12 +455,13 @@ Future<void> _buildGradleProjectV2( ...@@ -453,12 +455,13 @@ Future<void> _buildGradleProjectV2(
command.add('-Pfilesystem-roots=${buildInfo.fileSystemRoots.join('|')}'); command.add('-Pfilesystem-roots=${buildInfo.fileSystemRoots.join('|')}');
if (buildInfo.fileSystemScheme != null) if (buildInfo.fileSystemScheme != null)
command.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}'); command.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}');
if (buildInfo.buildSharedLibrary) { if (androidBuildInfo.splitPerAbi)
command.add('-Pbuild-shared-library=true'); command.add('-Psplit-per-abi=true');
if (androidBuildInfo.targetArchs.isNotEmpty) {
final String targetPlatforms = androidBuildInfo.targetArchs
.map(getPlatformNameForAndroidArch).join(',');
command.add('-Ptarget-platform=$targetPlatforms');
} }
if (buildInfo.targetPlatform != null)
command.add('-Ptarget-platform=${getNameForTargetPlatform(buildInfo.targetPlatform)}');
command.add(assembleTask); command.add(assembleTask);
bool potentialAndroidXFailure = false; bool potentialAndroidXFailure = false;
final Stopwatch sw = Stopwatch()..start(); final Stopwatch sw = Stopwatch()..start();
...@@ -508,24 +511,27 @@ Future<void> _buildGradleProjectV2( ...@@ -508,24 +511,27 @@ Future<void> _buildGradleProjectV2(
flutterUsage.sendTiming('build', 'gradle-v2', Duration(milliseconds: sw.elapsedMilliseconds)); flutterUsage.sendTiming('build', 'gradle-v2', Duration(milliseconds: sw.elapsedMilliseconds));
if (!isBuildingBundle) { if (!isBuildingBundle) {
final File apkFile = _findApkFile(project, buildInfo); final Iterable<File> apkFiles = _findApkFiles(project, androidBuildInfo);
if (apkFile == null) if (apkFiles.isEmpty)
throwToolExit('Gradle build failed to produce an Android package.'); throwToolExit('Gradle build failed to produce an Android package.');
// Copy the APK to app.apk, so `flutter run`, `flutter install`, etc. can find it. // Copy the first APK to app.apk, so `flutter run`, `flutter install`, etc. can find it.
apkFile.copySync(project.apkDirectory.childFile('app.apk').path); // TODO(blasten): Handle multiple APKs.
apkFiles.first.copySync(project.apkDirectory.childFile('app.apk').path);
printTrace('calculateSha: ${project.apkDirectory}/app.apk'); printTrace('calculateSha: ${project.apkDirectory}/app.apk');
final File apkShaFile = project.apkDirectory.childFile('app.apk.sha1'); final File apkShaFile = project.apkDirectory.childFile('app.apk.sha1');
apkShaFile.writeAsStringSync(_calculateSha(apkFile)); apkShaFile.writeAsStringSync(_calculateSha(apkFiles.first));
String appSize; for (File apkFile in apkFiles) {
if (buildInfo.mode == BuildMode.debug) { String appSize;
appSize = ''; if (buildInfo.mode == BuildMode.debug) {
} else { appSize = '';
appSize = ' (${getSizeAsMB(apkFile.lengthSync())})'; } else {
appSize = ' (${getSizeAsMB(apkFile.lengthSync())})';
}
printStatus('Built ${fs.path.relative(apkFile.path)}$appSize.',
color: TerminalColor.green);
} }
printStatus('Built ${fs.path.relative(apkFile.path)}$appSize.');
} else { } else {
final File bundleFile = _findBundleFile(project, buildInfo); final File bundleFile = _findBundleFile(project, buildInfo);
if (bundleFile == null) if (bundleFile == null)
...@@ -537,28 +543,38 @@ Future<void> _buildGradleProjectV2( ...@@ -537,28 +543,38 @@ Future<void> _buildGradleProjectV2(
} else { } else {
appSize = ' (${getSizeAsMB(bundleFile.lengthSync())})'; appSize = ' (${getSizeAsMB(bundleFile.lengthSync())})';
} }
printStatus('Built ${fs.path.relative(bundleFile.path)}$appSize.'); printStatus('Built ${fs.path.relative(bundleFile.path)}$appSize.',
color: TerminalColor.green);
} }
} }
File _findApkFile(GradleProject project, BuildInfo buildInfo) { Iterable<File> _findApkFiles(GradleProject project, AndroidBuildInfo androidBuildInfo) {
final String apkFileName = project.apkFileFor(buildInfo); final Iterable<String> apkFileNames = project.apkFilesFor(androidBuildInfo);
if (apkFileName == null) if (apkFileNames.isEmpty)
return null; return const <File>[];
File apkFile = fs.file(fs.path.join(project.apkDirectory.path, apkFileName));
if (apkFile.existsSync()) return apkFileNames.map<File>((String apkFileName) {
return apkFile; File apkFile = project.apkDirectory.childFile(apkFileName);
final String modeName = camelCase(buildInfo.modeName);
apkFile = fs.file(fs.path.join(project.apkDirectory.path, modeName, apkFileName));
if (apkFile.existsSync())
return apkFile;
if (buildInfo.flavor != null) {
// Android Studio Gradle plugin v3 adds flavor to path.
apkFile = fs.file(fs.path.join(project.apkDirectory.path, buildInfo.flavor, modeName, apkFileName));
if (apkFile.existsSync()) if (apkFile.existsSync())
return apkFile; return apkFile;
} final BuildInfo buildInfo = androidBuildInfo.buildInfo;
return null; final String modeName = camelCase(buildInfo.modeName);
apkFile = project.apkDirectory
.childDirectory(modeName)
.childFile(apkFileName);
if (apkFile.existsSync())
return apkFile;
if (buildInfo.flavor != null) {
// Android Studio Gradle plugin v3 adds flavor to path.
apkFile = project.apkDirectory
.childDirectory(buildInfo.flavor)
.childDirectory(modeName)
.childFile(apkFileName);
if (apkFile.existsSync())
return apkFile;
}
return null;
});
} }
File _findBundleFile(GradleProject project, BuildInfo buildInfo) { File _findBundleFile(GradleProject project, BuildInfo buildInfo) {
...@@ -567,12 +583,12 @@ File _findBundleFile(GradleProject project, BuildInfo buildInfo) { ...@@ -567,12 +583,12 @@ File _findBundleFile(GradleProject project, BuildInfo buildInfo) {
if (bundleFileName == null) if (bundleFileName == null)
return null; return null;
final String modeName = camelCase(buildInfo.modeName); final String modeName = camelCase(buildInfo.modeName);
File bundleFile = fs.file(fs.path.join(project.bundleDirectory.path, modeName, bundleFileName)); File bundleFile = project.bundleDirectory.childDirectory(modeName).childFile(bundleFileName);
if (bundleFile.existsSync()) if (bundleFile.existsSync())
return bundleFile; return bundleFile;
if (buildInfo.flavor != null) { 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. // 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)); bundleFile = project.bundleDirectory.childDirectory(buildInfo.flavor + modeName).childFile(bundleFileName);
if (bundleFile.existsSync()) if (bundleFile.existsSync())
return bundleFile; return bundleFile;
} }
...@@ -661,13 +677,20 @@ class GradleProject { ...@@ -661,13 +677,20 @@ class GradleProject {
return 'assemble${toTitleCase(productFlavor)}${toTitleCase(buildType)}'; return 'assemble${toTitleCase(productFlavor)}${toTitleCase(buildType)}';
} }
String apkFileFor(BuildInfo buildInfo) { Iterable<String> apkFilesFor(AndroidBuildInfo androidBuildInfo) {
final String buildType = _buildTypeFor(buildInfo); final String buildType = _buildTypeFor(androidBuildInfo.buildInfo);
final String productFlavor = _productFlavorFor(buildInfo); final String productFlavor = _productFlavorFor(androidBuildInfo.buildInfo);
if (buildType == null || productFlavor == null) if (buildType == null || productFlavor == null)
return null; return const <String>[];
final String flavorString = productFlavor.isEmpty ? '' : '-' + productFlavor; final String flavorString = productFlavor.isEmpty ? '' : '-' + productFlavor;
return 'app$flavorString-$buildType.apk'; 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'];
} }
String bundleTaskFor(BuildInfo buildInfo) { String bundleTaskFor(BuildInfo buildInfo) {
......
...@@ -137,7 +137,7 @@ class AOTSnapshotter { ...@@ -137,7 +137,7 @@ class AOTSnapshotter {
final String aotSharedLibrary = fs.path.join(outputDir.path, 'app.so'); final String aotSharedLibrary = fs.path.join(outputDir.path, 'app.so');
outputPaths.add(aotSharedLibrary); outputPaths.add(aotSharedLibrary);
genSnapshotArgs.add('--snapshot_kind=app-aot-elf'); genSnapshotArgs.add('--snapshot_kind=app-aot-elf');
genSnapshotArgs.add('--assembly=$aotSharedLibrary'); genSnapshotArgs.add('--elf=$aotSharedLibrary');
} else { } else {
// Blob AOT snapshot. // Blob AOT snapshot.
final String vmSnapshotData = fs.path.join(outputDir.path, 'vm_snapshot_data'); final String vmSnapshotData = fs.path.join(outputDir.path, 'vm_snapshot_data');
......
...@@ -17,8 +17,6 @@ class BuildInfo { ...@@ -17,8 +17,6 @@ class BuildInfo {
this.compilationTraceFilePath, this.compilationTraceFilePath,
this.extraFrontEndOptions, this.extraFrontEndOptions,
this.extraGenSnapshotOptions, this.extraGenSnapshotOptions,
this.buildSharedLibrary,
this.targetPlatform,
this.fileSystemRoots, this.fileSystemRoots,
this.fileSystemScheme, this.fileSystemScheme,
this.buildNumber, this.buildNumber,
...@@ -50,12 +48,6 @@ class BuildInfo { ...@@ -50,12 +48,6 @@ class BuildInfo {
/// Extra command-line options for gen_snapshot. /// Extra command-line options for gen_snapshot.
final String extraGenSnapshotOptions; final String extraGenSnapshotOptions;
/// Whether to prefer AOT compiling to a *so file.
final bool buildSharedLibrary;
/// Target platform for the build (e.g. android_arm versus android_arm64).
final TargetPlatform targetPlatform;
/// Internal version number (not displayed to users). /// Internal version number (not displayed to users).
/// Each build must have a unique number to differentiate it from previous builds. /// Each build must have a unique number to differentiate it from previous builds.
/// It is used to determine whether one build is more recent than another, with higher numbers indicating more recent build. /// It is used to determine whether one build is more recent than another, with higher numbers indicating more recent build.
...@@ -98,15 +90,31 @@ class BuildInfo { ...@@ -98,15 +90,31 @@ class BuildInfo {
bool get supportsSimulator => isEmulatorBuildMode(mode); bool get supportsSimulator => isEmulatorBuildMode(mode);
String get modeName => getModeName(mode); String get modeName => getModeName(mode);
String get friendlyModeName => getFriendlyModeName(mode); String get friendlyModeName => getFriendlyModeName(mode);
}
/// Information about an Android build to be performed or used.
class AndroidBuildInfo {
const AndroidBuildInfo(
this.buildInfo, {
this.targetArchs = const <AndroidArch>[
AndroidArch.armeabi_v7a,
AndroidArch.arm64_v8a,
],
this.splitPerAbi = false,
});
BuildInfo withTargetPlatform(TargetPlatform targetPlatform) => // The build info containing the mode and flavor.
BuildInfo(mode, flavor, final BuildInfo buildInfo;
trackWidgetCreation: trackWidgetCreation,
compilationTraceFilePath: compilationTraceFilePath, /// Whether to split the shared library per ABI.
extraFrontEndOptions: extraFrontEndOptions, ///
extraGenSnapshotOptions: extraGenSnapshotOptions, /// When this is false, multiple ABIs will be contained within one primary
buildSharedLibrary: buildSharedLibrary, /// build artifact. When this is true, multiple build artifacts (one per ABI)
targetPlatform: targetPlatform); /// will be produced.
final bool splitPerAbi;
/// The target platforms for the build.
final Iterable<AndroidArch> targetArchs;
} }
/// The type of build. /// The type of build.
...@@ -254,6 +262,13 @@ enum IOSArch { ...@@ -254,6 +262,13 @@ enum IOSArch {
arm64, arm64,
} }
enum AndroidArch {
armeabi_v7a,
arm64_v8a,
x86,
x86_64,
}
/// The default set of iOS device architectures to build for. /// The default set of iOS device architectures to build for.
const List<IOSArch> defaultIOSArchs = <IOSArch>[ const List<IOSArch> defaultIOSArchs = <IOSArch>[
IOSArch.arm64, IOSArch.arm64,
...@@ -335,6 +350,51 @@ TargetPlatform getTargetPlatformForName(String platform) { ...@@ -335,6 +350,51 @@ TargetPlatform getTargetPlatformForName(String platform) {
return null; return null;
} }
AndroidArch getAndroidArchForName(String platform) {
switch (platform) {
case 'android-arm':
return AndroidArch.armeabi_v7a;
case 'android-arm64':
return AndroidArch.arm64_v8a;
case 'android-x64':
return AndroidArch.x86_64;
case 'android-x86':
return AndroidArch.x86;
}
assert(false);
return null;
}
String getNameForAndroidArch(AndroidArch arch) {
switch (arch) {
case AndroidArch.armeabi_v7a:
return 'armeabi-v7a';
case AndroidArch.arm64_v8a:
return 'arm64-v8a';
case AndroidArch.x86_64:
return 'x86_64';
case AndroidArch.x86:
return 'x86';
}
assert(false);
return null;
}
String getPlatformNameForAndroidArch(AndroidArch arch) {
switch (arch) {
case AndroidArch.armeabi_v7a:
return 'android-arm';
case AndroidArch.arm64_v8a:
return 'android-arm64';
case AndroidArch.x86_64:
return 'android-x64';
case AndroidArch.x86:
return 'android-x86';
}
assert(false);
return null;
}
HostPlatform getCurrentHostPlatform() { HostPlatform getCurrentHostPlatform() {
if (platform.isMacOS) if (platform.isMacOS)
return HostPlatform.darwin_x64; return HostPlatform.darwin_x64;
......
...@@ -5,6 +5,9 @@ ...@@ -5,6 +5,9 @@
import 'dart:async'; import 'dart:async';
import '../android/apk.dart'; import '../android/apk.dart';
import '../base/terminal.dart';
import '../build_info.dart';
import '../globals.dart';
import '../project.dart'; import '../project.dart';
import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult; import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult;
import 'build.dart'; import 'build.dart';
...@@ -21,12 +24,14 @@ class BuildApkCommand extends BuildSubCommand { ...@@ -21,12 +24,14 @@ class BuildApkCommand extends BuildSubCommand {
argParser argParser
..addFlag('track-widget-creation', negatable: false, hide: !verboseHelp) ..addFlag('track-widget-creation', negatable: false, hide: !verboseHelp)
..addFlag('build-shared-library', ..addFlag('split-per-abi',
negatable: false, negatable: false,
help: 'Whether to prefer compiling to a *.so file (android only).', help: 'Whether to split the APKs per ABIs.'
'To learn more, see: https://developer.android.com/studio/build/configure-apk-splits#configure-abi-split',
) )
..addOption('target-platform', ..addMultiOption('target-platform',
defaultsTo: 'android-arm', splitCommas: true,
defaultsTo: <String>['android-arm', 'android-arm64'],
allowed: <String>['android-arm', 'android-arm64', 'android-x86', 'android-x64'], allowed: <String>['android-arm', 'android-arm64', 'android-x86', 'android-x64'],
help: 'The target platform for which the app is compiled.', help: 'The target platform for which the app is compiled.',
); );
...@@ -49,10 +54,33 @@ class BuildApkCommand extends BuildSubCommand { ...@@ -49,10 +54,33 @@ class BuildApkCommand extends BuildSubCommand {
@override @override
Future<FlutterCommandResult> runCommand() async { Future<FlutterCommandResult> runCommand() async {
final BuildInfo buildInfo = getBuildInfo();
final AndroidBuildInfo androidBuildInfo = AndroidBuildInfo(buildInfo,
splitPerAbi: argResults['split-per-abi'],
targetArchs: argResults['target-platform'].map<AndroidArch>(getAndroidArchForName)
);
if (buildInfo.isRelease && !androidBuildInfo.splitPerAbi && androidBuildInfo.targetArchs.length > 1) {
final String targetPlatforms = argResults['target-platform'].join(', ');
printStatus('You are building a fat APK that includes binaries for '
'$targetPlatforms.', emphasis: true, color: TerminalColor.green);
printStatus('If you are deploying the app to the Play Store, '
'it\'s recommended to use app bundles or split the APK to reduce the APK size.', emphasis: true);
printStatus('To generate an app bundle, run:', emphasis: true, indent: 4);
printStatus('flutter build appbundle '
'--target-platform ${targetPlatforms.replaceAll(' ', '')}',indent: 8);
printStatus('Learn more on: https://developer.android.com/guide/app-bundle',indent: 8);
printStatus('To split the APKs per ABI, run:', emphasis: true, indent: 4);
printStatus('flutter build apk '
'--target-platform ${targetPlatforms.replaceAll(' ', '')} '
'--split-per-abi', indent: 8);
printStatus('Learn more on: https://developer.android.com/studio/build/configure-apk-splits#configure-abi-split',indent: 8);
}
await buildApk( await buildApk(
project: FlutterProject.current(), project: FlutterProject.current(),
target: targetFile, target: targetFile,
buildInfo: getBuildInfo(), androidBuildInfo: androidBuildInfo,
); );
return null; return null;
} }
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:async'; import 'dart:async';
import '../android/app_bundle.dart'; import '../android/app_bundle.dart';
import '../build_info.dart';
import '../project.dart'; import '../project.dart';
import '../runner/flutter_command.dart' show FlutterCommandResult; import '../runner/flutter_command.dart' show FlutterCommandResult;
import 'build.dart'; import 'build.dart';
...@@ -20,18 +21,11 @@ class BuildAppBundleCommand extends BuildSubCommand { ...@@ -20,18 +21,11 @@ class BuildAppBundleCommand extends BuildSubCommand {
argParser argParser
..addFlag('track-widget-creation', negatable: false, hide: !verboseHelp) ..addFlag('track-widget-creation', negatable: false, hide: !verboseHelp)
..addFlag( ..addMultiOption('target-platform',
'build-shared-library', splitCommas: true,
negatable: false, defaultsTo: <String>['android-arm', 'android-arm64'],
help: 'Whether to prefer compiling to a *.so file (android only).',
)
..addOption(
'target-platform',
allowed: <String>['android-arm', 'android-arm64'], allowed: <String>['android-arm', 'android-arm64'],
help: 'The target platform for which the app is compiled.\n' help: 'The target platform for which the app is compiled.',
'By default, the bundle will include \'arm\' and \'arm64\', '
'which is the recommended configuration for app bundles.\n'
'For more, see https://developer.android.com/distribute/best-practices/develop/64-bit',
); );
} }
...@@ -47,10 +41,13 @@ class BuildAppBundleCommand extends BuildSubCommand { ...@@ -47,10 +41,13 @@ class BuildAppBundleCommand extends BuildSubCommand {
@override @override
Future<FlutterCommandResult> runCommand() async { Future<FlutterCommandResult> runCommand() async {
final AndroidBuildInfo androidBuildInfo = AndroidBuildInfo(getBuildInfo(),
targetArchs: argResults['target-platform'].map<AndroidArch>(getAndroidArchForName)
);
await buildAppBundle( await buildAppBundle(
project: FlutterProject.current(), project: FlutterProject.current(),
target: targetFile, target: targetFile,
buildInfo: getBuildInfo(), androidBuildInfo: androidBuildInfo,
); );
return null; return null;
} }
......
...@@ -387,8 +387,7 @@ class IOSSimulator extends Device { ...@@ -387,8 +387,7 @@ class IOSSimulator extends Device {
final BuildInfo debugBuildInfo = BuildInfo(BuildMode.debug, buildInfo.flavor, final BuildInfo debugBuildInfo = BuildInfo(BuildMode.debug, buildInfo.flavor,
trackWidgetCreation: buildInfo.trackWidgetCreation, trackWidgetCreation: buildInfo.trackWidgetCreation,
extraFrontEndOptions: buildInfo.extraFrontEndOptions, extraFrontEndOptions: buildInfo.extraFrontEndOptions,
extraGenSnapshotOptions: buildInfo.extraGenSnapshotOptions, extraGenSnapshotOptions: buildInfo.extraGenSnapshotOptions);
buildSharedLibrary: buildInfo.buildSharedLibrary);
final XcodeBuildResult buildResult = await buildXcodeProject( final XcodeBuildResult buildResult = await buildXcodeProject(
app: app, app: app,
......
...@@ -320,12 +320,6 @@ abstract class FlutterCommand extends Command<void> { ...@@ -320,12 +320,6 @@ abstract class FlutterCommand extends Command<void> {
} }
BuildInfo getBuildInfo() { BuildInfo getBuildInfo() {
TargetPlatform targetPlatform;
if (argParser.options.containsKey('target-platform') &&
argResults['target-platform'] != 'default') {
targetPlatform = getTargetPlatformForName(argResults['target-platform']);
}
final bool trackWidgetCreation = argParser.options.containsKey('track-widget-creation') final bool trackWidgetCreation = argParser.options.containsKey('track-widget-creation')
? argResults['track-widget-creation'] ? argResults['track-widget-creation']
: false; : false;
...@@ -362,10 +356,6 @@ abstract class FlutterCommand extends Command<void> { ...@@ -362,10 +356,6 @@ abstract class FlutterCommand extends Command<void> {
extraGenSnapshotOptions: argParser.options.containsKey(FlutterOptions.kExtraGenSnapshotOptions) extraGenSnapshotOptions: argParser.options.containsKey(FlutterOptions.kExtraGenSnapshotOptions)
? argResults[FlutterOptions.kExtraGenSnapshotOptions] ? argResults[FlutterOptions.kExtraGenSnapshotOptions]
: null, : null,
buildSharedLibrary: argParser.options.containsKey('build-shared-library')
? argResults['build-shared-library']
: false,
targetPlatform: targetPlatform,
fileSystemRoots: argParser.options.containsKey(FlutterOptions.kFileSystemRoot) fileSystemRoots: argParser.options.containsKey(FlutterOptions.kFileSystemRoot)
? argResults[FlutterOptions.kFileSystemRoot] : null, ? argResults[FlutterOptions.kFileSystemRoot] : null,
fileSystemScheme: argParser.options.containsKey(FlutterOptions.kFileSystemScheme) fileSystemScheme: argParser.options.containsKey(FlutterOptions.kFileSystemScheme)
......
...@@ -157,16 +157,102 @@ someOtherTask ...@@ -157,16 +157,102 @@ someOtherTask
}); });
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'),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.apkFilesFor(const AndroidBuildInfo(BuildInfo.debug)).first, 'app-debug.apk');
expect(project.apkFileFor(BuildInfo.profile), 'app-profile.apk'); expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo.profile)).first, 'app-profile.apk');
expect(project.apkFileFor(BuildInfo.release), 'app-release.apk'); expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo.release)).first, 'app-release.apk');
expect(project.apkFileFor(const BuildInfo(BuildMode.release, 'unknown')), isNull); expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.release, 'unknown'))).isEmpty, isTrue);
}); });
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'),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.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.debug, 'free'))).first, 'app-free-debug.apk');
expect(project.apkFileFor(const BuildInfo(BuildMode.release, 'paid')), 'app-paid-release.apk'); expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.release, 'paid'))).first, 'app-paid-release.apk');
expect(project.apkFileFor(const BuildInfo(BuildMode.release, 'unknown')), isNull); expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.release, 'unknown'))).isEmpty, isTrue);
});
test('should provide apks for default build types and each ABI', () {
final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
expect(project.apkFilesFor(
const AndroidBuildInfo(
BuildInfo.debug,
splitPerAbi: true,
targetArchs: <AndroidArch>[
AndroidArch.armeabi_v7a,
AndroidArch.arm64_v8a,
]
)
),
<String>[
'app-armeabi-v7a-debug.apk',
'app-arm64-v8a-debug.apk',
]);
expect(project.apkFilesFor(
const AndroidBuildInfo(
BuildInfo.release,
splitPerAbi: true,
targetArchs: <AndroidArch>[
AndroidArch.armeabi_v7a,
AndroidArch.arm64_v8a,
]
)
),
<String>[
'app-armeabi-v7a-release.apk',
'app-arm64-v8a-release.apk',
]);
expect(project.apkFilesFor(
const AndroidBuildInfo(
BuildInfo(BuildMode.release, 'unknown'),
splitPerAbi: true,
targetArchs: <AndroidArch>[
AndroidArch.armeabi_v7a,
AndroidArch.arm64_v8a,
]
)
).isEmpty, isTrue);
});
test('should provide apks for each ABI and flavored build types', () {
final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
expect(project.apkFilesFor(
const AndroidBuildInfo(
BuildInfo(BuildMode.debug, 'free'),
splitPerAbi: true,
targetArchs: <AndroidArch>[
AndroidArch.armeabi_v7a,
AndroidArch.arm64_v8a,
]
)
),
<String>[
'app-free-armeabi-v7a-debug.apk',
'app-free-arm64-v8a-debug.apk',
]);
expect(project.apkFilesFor(
const AndroidBuildInfo(
BuildInfo(BuildMode.release, 'paid'),
splitPerAbi: true,
targetArchs: <AndroidArch>[
AndroidArch.armeabi_v7a,
AndroidArch.arm64_v8a,
]
)
),
<String>[
'app-paid-armeabi-v7a-release.apk',
'app-paid-arm64-v8a-release.apk',
]);
expect(project.apkFilesFor(
const AndroidBuildInfo(
BuildInfo(BuildMode.release, 'unknown'),
splitPerAbi: true,
targetArchs: <AndroidArch>[
AndroidArch.armeabi_v7a,
AndroidArch.arm64_v8a,
]
)
).isEmpty, isTrue);
}); });
test('should provide bundle file name for default build types', () { 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')); final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
......
...@@ -80,7 +80,7 @@ void main() { ...@@ -80,7 +80,7 @@ void main() {
}); });
}); });
group('Snapshotter - iOS AOT', () { group('Snapshotter - AOT', () {
const String kSnapshotDart = 'snapshot.dart'; const String kSnapshotDart = 'snapshot.dart';
String skyEnginePath; String skyEnginePath;
...@@ -395,10 +395,11 @@ void main() { ...@@ -395,10 +395,11 @@ void main() {
]); ]);
}, overrides: contextOverrides); }, overrides: contextOverrides);
testUsingContext('returns failure if buildSharedLibrary is true but no NDK is found', () async { testUsingContext('builds shared library for android-arm', () async {
final String outputPath = fs.path.join('build', 'foo'); fs.file('main.dill').writeAsStringSync('binary magic');
when(mockAndroidSdk.ndk).thenReturn(null); final String outputPath = fs.path.join('build', 'foo');
fs.directory(outputPath).createSync(recursive: true);
final int genSnapshotExitCode = await snapshotter.build( final int genSnapshotExitCode = await snapshotter.build(
platform: TargetPlatform.android_arm, platform: TargetPlatform.android_arm,
...@@ -409,8 +410,45 @@ void main() { ...@@ -409,8 +410,45 @@ void main() {
buildSharedLibrary: true, buildSharedLibrary: true,
); );
expect(genSnapshotExitCode, isNot(0)); expect(genSnapshotExitCode, 0);
expect(genSnapshot.callCount, 0); expect(genSnapshot.callCount, 1);
expect(genSnapshot.snapshotType.platform, TargetPlatform.android_arm);
expect(genSnapshot.snapshotType.mode, BuildMode.release);
expect(genSnapshot.additionalArgs, <String>[
'--deterministic',
'--snapshot_kind=app-aot-elf',
'--elf=build/foo/app.so',
'--no-sim-use-hardfp',
'--no-use-integer-division',
'main.dill',
]);
}, overrides: contextOverrides);
testUsingContext('builds shared library for android-arm64', () async {
fs.file('main.dill').writeAsStringSync('binary magic');
final String outputPath = fs.path.join('build', 'foo');
fs.directory(outputPath).createSync(recursive: true);
final int genSnapshotExitCode = await snapshotter.build(
platform: TargetPlatform.android_arm64,
buildMode: BuildMode.release,
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: outputPath,
buildSharedLibrary: true,
);
expect(genSnapshotExitCode, 0);
expect(genSnapshot.callCount, 1);
expect(genSnapshot.snapshotType.platform, TargetPlatform.android_arm64);
expect(genSnapshot.snapshotType.mode, BuildMode.release);
expect(genSnapshot.additionalArgs, <String>[
'--deterministic',
'--snapshot_kind=app-aot-elf',
'--elf=build/foo/app.so',
'main.dill',
]);
}, overrides: contextOverrides); }, overrides: contextOverrides);
testUsingContext('builds Android arm release AOT snapshot', () async { testUsingContext('builds Android arm release AOT snapshot', () async {
......
...@@ -286,7 +286,7 @@ Information about project "Runner": ...@@ -286,7 +286,7 @@ Information about project "Runner":
platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine'); platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm')); when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, targetPlatform: TargetPlatform.ios); const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null);
final FlutterProject project = FlutterProject.fromPath('path/to/project'); final FlutterProject project = FlutterProject.fromPath('path/to/project');
await updateGeneratedXcodeProperties( await updateGeneratedXcodeProperties(
project: project, project: project,
...@@ -304,7 +304,7 @@ Information about project "Runner": ...@@ -304,7 +304,7 @@ Information about project "Runner":
when(mockArtifacts.getArtifactPath(Artifact.flutterFramework, when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine'); platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm')); when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, trackWidgetCreation: true, targetPlatform: TargetPlatform.ios); const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, trackWidgetCreation: true);
final FlutterProject project = FlutterProject.fromPath('path/to/project'); final FlutterProject project = FlutterProject.fromPath('path/to/project');
await updateGeneratedXcodeProperties( await updateGeneratedXcodeProperties(
project: project, project: project,
...@@ -322,7 +322,7 @@ Information about project "Runner": ...@@ -322,7 +322,7 @@ Information about project "Runner":
when(mockArtifacts.getArtifactPath(Artifact.flutterFramework, when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine'); platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm')); when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, targetPlatform: TargetPlatform.ios); const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null);
final FlutterProject project = FlutterProject.fromPath('path/to/project'); final FlutterProject project = FlutterProject.fromPath('path/to/project');
await updateGeneratedXcodeProperties( await updateGeneratedXcodeProperties(
project: project, project: project,
...@@ -340,7 +340,7 @@ Information about project "Runner": ...@@ -340,7 +340,7 @@ Information about project "Runner":
when(mockArtifacts.getArtifactPath(Artifact.flutterFramework, when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine'); platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile')); when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile'));
const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, targetPlatform: TargetPlatform.ios); const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null);
final FlutterProject project = FlutterProject.fromPath('path/to/project'); final FlutterProject project = FlutterProject.fromPath('path/to/project');
await updateGeneratedXcodeProperties( await updateGeneratedXcodeProperties(
......
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