Flutter tool support for building dynamic updates (#25576)

This commit is contained in:
Stanislav Baranov 2018-12-19 16:27:47 -08:00 committed by GitHub
parent a282058d69
commit 55f3da7afc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 520 additions and 79 deletions

View file

@ -321,9 +321,17 @@ class FlutterPlugin implements Plugin<Project> {
if (project.hasProperty('precompile')) {
compilationTraceFilePathValue = project.property('precompile')
}
Boolean buildHotUpdateValue = false
if (project.hasProperty('hotupdate')) {
buildHotUpdateValue = project.property('hotupdate').toBoolean()
Boolean createPatchValue = false
if (project.hasProperty('patch')) {
createPatchValue = project.property('patch').toBoolean()
}
Integer buildNumberValue = null
if (project.hasProperty('build-number')) {
buildNumberValue = project.property('build-number').toInteger()
}
String baselineDirValue = null
if (project.hasProperty('baseline-dir')) {
baselineDirValue = project.property('baseline-dir')
}
String extraFrontEndOptionsValue = null
if (project.hasProperty('extra-front-end-options')) {
@ -367,7 +375,9 @@ class FlutterPlugin implements Plugin<Project> {
fileSystemScheme fileSystemSchemeValue
trackWidgetCreation trackWidgetCreationValue
compilationTraceFilePath compilationTraceFilePathValue
buildHotUpdate buildHotUpdateValue
createPatch createPatchValue
buildNumber buildNumberValue
baselineDir baselineDirValue
buildSharedLibrary buildSharedLibraryValue
targetPlatform targetPlatformValue
sourceDir project.file(project.flutter.source)
@ -428,7 +438,11 @@ abstract class BaseFlutterTask extends DefaultTask {
@Optional @Input
String compilationTraceFilePath
@Optional @Input
Boolean buildHotUpdate
Boolean createPatch
@Optional @Input
Integer buildNumber
@Optional @Input
String baselineDir
@Optional @Input
Boolean buildSharedLibrary
@Optional @Input
@ -523,8 +537,15 @@ abstract class BaseFlutterTask extends DefaultTask {
if (compilationTraceFilePath != null) {
args "--precompile", compilationTraceFilePath
}
if (buildHotUpdate) {
args "--hotupdate"
if (createPatch) {
args "--patch"
args "--build-number", project.android.defaultConfig.versionCode
if (buildNumber != null) {
assert buildNumber == project.android.defaultConfig.versionCode
}
}
if (baselineDir != null) {
args "--baseline-dir", baselineDir
}
if (extraFrontEndOptions != null) {
args "--extra-front-end-options", "${extraFrontEndOptions}"

View file

@ -3,10 +3,13 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'package:archive/archive.dart';
import 'package:meta/meta.dart';
import '../android/android_sdk.dart';
import '../application_package.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/file_system.dart';
@ -374,8 +377,8 @@ Future<void> _buildGradleProjectV2(
command.add('-Ptrack-widget-creation=${buildInfo.trackWidgetCreation}');
if (buildInfo.compilationTraceFilePath != null)
command.add('-Pprecompile=${buildInfo.compilationTraceFilePath}');
if (buildInfo.buildHotUpdate)
command.add('-Photupdate=true');
if (buildInfo.createPatch)
command.add('-Ppatch=true');
if (buildInfo.extraFrontEndOptions != null)
command.add('-Pextra-front-end-options=${buildInfo.extraFrontEndOptions}');
if (buildInfo.extraGenSnapshotOptions != null)
@ -420,6 +423,71 @@ Future<void> _buildGradleProjectV2(
appSize = ' (${getSizeAsMB(apkFile.lengthSync())})';
}
printStatus('Built ${fs.path.relative(apkFile.path)}$appSize.');
final AndroidApk package = AndroidApk.fromApk(apkFile);
final File baselineApkFile =
fs.directory(buildInfo.baselineDir).childFile('${package.versionCode}.apk');
if (buildInfo.createBaseline) {
// Save baseline apk for generating dynamic patches in later builds.
baselineApkFile.parent.createSync(recursive: true);
apkFile.copySync(baselineApkFile.path);
printStatus('Saved baseline package ${baselineApkFile.path}.');
}
if (buildInfo.createPatch) {
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());
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 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');
if (update.files.isEmpty) {
printStatus('No changes detected relative to baseline build.');
if (updateFile.existsSync()) {
updateFile.deleteSync();
printStatus('Deleted dynamic patch ${updateFile.path}.');
}
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 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));
updateFile.parent.createSync(recursive: true);
updateFile.writeAsBytesSync(ZipEncoder().encode(update), flush: true);
printStatus('Created dynamic patch ${updateFile.path}.');
}
}
File _findApkFile(GradleProject project, BuildInfo buildInfo) {

View file

@ -41,6 +41,7 @@ class AndroidApk extends ApplicationPackage {
AndroidApk({
String id,
@required this.file,
@required this.versionCode,
@required this.launchActivity
}) : assert(file != null),
assert(launchActivity != null),
@ -78,6 +79,7 @@ class AndroidApk extends ApplicationPackage {
return AndroidApk(
id: data.packageName,
file: apk,
versionCode: int.tryParse(data.versionCode),
launchActivity: '${data.packageName}/${data.launchableActivityName}'
);
}
@ -88,6 +90,9 @@ class AndroidApk extends ApplicationPackage {
/// The path to the activity that should be launched.
final String launchActivity;
/// The version code of the APK.
final int versionCode;
/// Creates a new AndroidApk based on the information in the Android manifest.
static Future<AndroidApk> fromAndroidProject(AndroidProject androidProject) async {
File apkFile;
@ -138,6 +143,7 @@ class AndroidApk extends ApplicationPackage {
return AndroidApk(
id: packageId,
file: apkFile,
versionCode: null,
launchActivity: launchActivity
);
}
@ -449,8 +455,25 @@ class ApkManifestData {
final String activityName = nameAttribute
.value.substring(1, nameAttribute.value.indexOf('" '));
// Example format: (type 0x10)0x1
final _Attribute versionCodeAttr = manifest.firstAttribute('android:versionCode');
if (versionCodeAttr == null) {
printError('Error running $packageName. Manifest versionCode not found');
return null;
}
if (!versionCodeAttr.value.startsWith('(type 0x10)')) {
printError('Error running $packageName. Manifest versionCode invalid');
return null;
}
final int versionCode = int.tryParse(versionCodeAttr.value.substring(11));
if (versionCode == null) {
printError('Error running $packageName. Manifest versionCode invalid');
return null;
}
final Map<String, Map<String, String>> map = <String, Map<String, String>>{};
map['package'] = <String, String>{'name': packageName};
map['version-code'] = <String, String>{'name': versionCode.toString()};
map['launchable-activity'] = <String, String>{'name': activityName};
return ApkManifestData._(map);
@ -464,6 +487,8 @@ class ApkManifestData {
String get packageName => _data['package'] == null ? null : _data['package']['name'];
String get versionCode => _data['version-code'] == null ? null : _data['version-code']['name'];
String get launchableActivityName {
return _data['launchable-activity'] == null ? null : _data['launchable-activity']['name'];
}

View file

@ -4,6 +4,8 @@
import 'dart:async';
import 'package:archive/archive.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import '../android/android_sdk.dart';
@ -348,7 +350,9 @@ class JITSnapshotter {
@required String packagesPath,
@required String outputPath,
@required String compilationTraceFilePath,
@required bool buildHotUpdate,
@required bool createPatch,
int buildNumber,
String baselineDir,
List<String> extraGenSnapshotOptions = const <String>[],
}) async {
if (!_isValidJitPlatform(platform)) {
@ -367,8 +371,73 @@ class JITSnapshotter {
final List<String> inputPaths = <String>[
mainPath, compilationTraceFilePath, engineVmSnapshotData, engineIsolateSnapshotData,
];
if (buildHotUpdate) {
if (createPatch) {
inputPaths.add(isolateSnapshotInstructions);
if (buildNumber == null) {
printError('Error: Dynamic patching requires --build-number specified');
return 1;
}
if (baselineDir == null) {
printError('Error: Dynamic patching requires --baseline-dir specified');
return 1;
}
final File baselineApk = fs.directory(baselineDir).childFile('$buildNumber.apk');
if (!baselineApk.existsSync()) {
printError('Error: Could not find baseline package ${baselineApk.path}.');
return 1;
}
final Archive baselinePkg = ZipDecoder().decodeBytes(baselineApk.readAsBytesSync());
{
final File f = fs.file(isolateSnapshotInstructions);
final ArchiveFile af = baselinePkg.findFile(
fs.path.join('assets/flutter_assets/isolate_snapshot_instr'));
if (af == null) {
printError('Error: Invalid baseline package ${baselineApk.path}.');
return 1;
}
// When building an update, gen_snapshot expects to find the original isolate
// snapshot instructions from the previous full build, so we need to extract
// it from saves baseline APK.
if (!f.existsSync()) {
f.writeAsBytesSync(af.content, flush: true);
} else {
// But if this file is already extracted, we make sure that it's identical.
final Function contentEquals = const ListEquality<int>().equals;
if (!contentEquals(f.readAsBytesSync(), af.content)) {
printError('Error: Detected changes unsupported by dynamic patching.');
return 1;
}
}
}
{
final File f = fs.file(engineVmSnapshotData);
final ArchiveFile af = baselinePkg.findFile(
fs.path.join('assets/flutter_assets/vm_snapshot_data'));
if (af == null) {
printError('Error: Invalid baseline package ${baselineApk.path}.');
return 1;
}
// If engine snapshot artifact doesn't exist, gen_snapshot below will fail
// with a friendly error, so we don't need to handle this case here too.
if (f.existsSync()) {
// But if engine snapshot exists, its content must match the engine snapshot
// in baseline APK. Otherwise, we're trying to build an update at an engine
// version that might be binary incompatible with baseline APK.
final Function contentEquals = const ListEquality<int>().equals;
if (!contentEquals(f.readAsBytesSync(), af.content)) {
printError('Error: Detected engine changes unsupported by dynamic patching.');
return 1;
}
}
}
}
final String depfilePath = fs.path.join(outputDir.path, 'snapshot.d');
@ -385,7 +454,7 @@ class JITSnapshotter {
final Set<String> outputPaths = Set<String>();
outputPaths.addAll(<String>[isolateSnapshotData]);
if (!buildHotUpdate) {
if (!createPatch) {
outputPaths.add(isolateSnapshotInstructions);
}
@ -397,7 +466,7 @@ class JITSnapshotter {
'--isolate_snapshot_data=$isolateSnapshotData',
]);
if (!buildHotUpdate) {
if (!createPatch) {
genSnapshotArgs.add('--isolate_snapshot_instructions=$isolateSnapshotInstructions');
} else {
genSnapshotArgs.add('--reused_instructions=$isolateSnapshotInstructions');
@ -429,7 +498,7 @@ class JITSnapshotter {
'buildMode': buildMode.toString(),
'targetPlatform': platform.toString(),
'entryPoint': mainPath,
'buildHotUpdate': buildHotUpdate.toString(),
'createPatch': createPatch.toString(),
'extraGenSnapshotOptions': extraGenSnapshotOptions.join(' '),
},
depfilePaths: <String>[],

View file

@ -13,7 +13,11 @@ class BuildInfo {
const BuildInfo(this.mode, this.flavor, {
this.trackWidgetCreation = false,
this.compilationTraceFilePath,
this.buildHotUpdate,
this.createBaseline,
this.createPatch,
this.patchNumber,
this.patchDir,
this.baselineDir,
this.extraFrontEndOptions,
this.extraGenSnapshotOptions,
this.buildSharedLibrary,
@ -43,8 +47,24 @@ class BuildInfo {
/// Dart compilation trace file to use for JIT VM snapshot.
final String compilationTraceFilePath;
/// Save baseline package.
final bool createBaseline;
/// Build differential snapshot.
final bool buildHotUpdate;
final bool createPatch;
/// Internal version number of dynamic patch (not displayed to users).
/// Each patch should have a unique number to differentiate from previous
/// patches for the same versionCode on Android or CFBundleVersion on iOS.
final int patchNumber;
/// The directory where to store generated dynamic patches.
final String patchDir;
/// The directory where to store generated baseline packages.
/// Built packages, such as APK files on Android, are saved and can be used
/// to generate dynamic patches in later builds.
final String baselineDir;
/// Extra command-line options for front-end.
final String extraFrontEndOptions;
@ -92,6 +112,9 @@ class BuildInfo {
/// Exactly one of [isDebug], [isProfile], or [isRelease] is true.
bool get isRelease => mode == BuildMode.release || mode == BuildMode.dynamicRelease;
/// Returns whether a dynamic build is requested.
bool get isDynamic => mode == BuildMode.dynamicProfile || mode == BuildMode.dynamicRelease;
bool get usesAot => isAotBuildMode(mode);
bool get supportsEmulator => isEmulatorBuildMode(mode);
bool get supportsSimulator => isEmulatorBuildMode(mode);
@ -101,7 +124,7 @@ class BuildInfo {
BuildInfo(mode, flavor,
trackWidgetCreation: trackWidgetCreation,
compilationTraceFilePath: compilationTraceFilePath,
buildHotUpdate: buildHotUpdate,
createPatch: createPatch,
extraFrontEndOptions: extraFrontEndOptions,
extraGenSnapshotOptions: extraGenSnapshotOptions,
buildSharedLibrary: buildSharedLibrary,

View file

@ -60,7 +60,9 @@ Future<void> build({
bool reportLicensedPackages = false,
bool trackWidgetCreation = false,
String compilationTraceFilePath,
bool buildHotUpdate = false,
bool createPatch = false,
int buildNumber,
String baselineDir,
List<String> extraFrontEndOptions = const <String>[],
List<String> extraGenSnapshotOptions = const <String>[],
List<String> fileSystemRoots,
@ -108,7 +110,9 @@ Future<void> build({
packagesPath: packagesPath,
compilationTraceFilePath: compilationTraceFilePath,
extraGenSnapshotOptions: extraGenSnapshotOptions,
buildHotUpdate: buildHotUpdate,
createPatch: createPatch,
buildNumber: buildNumber,
baselineDir: baselineDir,
);
if (snapshotExitCode != 0) {
throwToolExit('Snapshotting exited with non-zero exit code: $snapshotExitCode');

View file

@ -12,7 +12,9 @@ import 'build.dart';
class BuildApkCommand extends BuildSubCommand {
BuildApkCommand({bool verboseHelp = false}) {
usesTargetOption();
addBuildModeFlags();
addBuildModeFlags(verboseHelp: verboseHelp);
addDynamicModeFlags(verboseHelp: verboseHelp);
addDynamicPatchingFlags(verboseHelp: verboseHelp);
usesFlavorOption();
usesPubOption();
usesBuildNumberOption();

View file

@ -4,6 +4,8 @@
import 'dart:async';
import 'package:args/command_runner.dart';
import '../base/common.dart';
import '../build_info.dart';
import '../bundle.dart';
@ -14,7 +16,10 @@ class BuildBundleCommand extends BuildSubCommand {
BuildBundleCommand({bool verboseHelp = false}) {
usesTargetOption();
usesFilesystemOptions(hide: !verboseHelp);
addBuildModeFlags();
usesBuildNumberOption();
addBuildModeFlags(verboseHelp: verboseHelp);
addDynamicModeFlags(verboseHelp: verboseHelp);
addDynamicBaselineFlags(verboseHelp: verboseHelp);
argParser
..addFlag('precompiled', negatable: false)
// This option is still referenced by the iOS build scripts. We should
@ -31,23 +36,6 @@ class BuildBundleCommand extends BuildSubCommand {
hide: !verboseHelp,
help: 'Track widget creation locations. Requires Dart 2.0 functionality.',
)
..addOption('precompile',
hide: !verboseHelp,
help: 'Precompile functions specified in input file. This flag is only '
'allowed when using --dynamic. It takes a Dart compilation trace '
'file produced by the training run of the application. With this '
'flag, instead of using default Dart VM snapshot provided by the '
'engine, the application will use its own snapshot that includes '
'additional compiled functions.'
)
..addFlag('hotupdate',
hide: !verboseHelp,
help: 'Build differential snapshot based on the last state of the build '
'tree and any changes to the application source code since then. '
'This flag is only allowed when using --dynamic. With this flag, '
'a partial VM snapshot is generated that is loaded on top of the '
'original VM snapshot that contains precompiled code.'
)
..addMultiOption(FlutterOptions.kExtraFrontEndOptions,
splitCommas: true,
hide: true,
@ -86,6 +74,15 @@ class BuildBundleCommand extends BuildSubCommand {
final BuildMode buildMode = getBuildMode();
int buildNumber;
try {
buildNumber = argResults['build-number'] != null
? int.parse(argResults['build-number']) : null;
} catch (e) {
throw UsageException(
'--build-number (${argResults['build-number']}) must be an int.', null);
}
await build(
platform: platform,
buildMode: buildMode,
@ -98,7 +95,9 @@ class BuildBundleCommand extends BuildSubCommand {
reportLicensedPackages: argResults['report-licensed-packages'],
trackWidgetCreation: argResults['track-widget-creation'],
compilationTraceFilePath: argResults['precompile'],
buildHotUpdate: argResults['hotupdate'],
createPatch: argResults['patch'],
buildNumber: buildNumber,
baselineDir: argResults['baseline-dir'],
extraFrontEndOptions: argResults[FlutterOptions.kExtraFrontEndOptions],
extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
fileSystemScheme: argResults['filesystem-scheme'],

View file

@ -24,6 +24,8 @@ abstract class RunCommandBase extends FlutterCommand {
// Used by run and drive commands.
RunCommandBase({ bool verboseHelp = false }) {
addBuildModeFlags(defaultToRelease: false, verboseHelp: verboseHelp);
addDynamicModeFlags(verboseHelp: verboseHelp);
addDynamicPatchingFlags(verboseHelp: verboseHelp);
usesFlavorOption();
argParser
..addFlag('trace-startup',
@ -104,23 +106,6 @@ class RunCommand extends RunCommandBase {
hide: !verboseHelp,
help: 'Specify a pre-built application binary to use when running.',
)
..addOption('precompile',
hide: !verboseHelp,
help: 'Precompile functions specified in input file. This flag is only '
'allowed when using --dynamic. It takes a Dart compilation trace '
'file produced by the training run of the application. With this '
'flag, instead of using default Dart VM snapshot provided by the '
'engine, the application will use its own snapshot that includes '
'additional functions.'
)
..addFlag('hotupdate',
hide: !verboseHelp,
help: 'Build differential snapshot based on the last state of the build '
'tree and any changes to the application source code since then. '
'This flag is only allowed when using --dynamic. With this flag, '
'a partial VM snapshot is generated that is loaded on top of the '
'original VM snapshot that contains precompiled code.'
)
..addFlag('track-widget-creation',
hide: !verboseHelp,
help: 'Track widget creation locations. Requires Dart 2.0 functionality.',

View file

@ -241,6 +241,64 @@ abstract class FlutterCommand extends Command<void> {
'--release or --profile; --debug always has this enabled.');
}
void addDynamicModeFlags({bool verboseHelp = false}) {
argParser.addOption('precompile',
hide: !verboseHelp,
help: 'Precompile functions specified in input file. This flag is only '
'allowed when using --dynamic. It takes a Dart compilation trace '
'file produced by the training run of the application. With this '
'flag, instead of using default Dart VM snapshot provided by the '
'engine, the application will use its own snapshot that includes '
'additional compiled functions.'
);
argParser.addFlag('patch',
hide: !verboseHelp,
negatable: false,
help: 'Generate dynamic patch for current changes from baseline.\n'
'Dynamic patch is generated relative to baseline package.\n'
'This flag is only allowed when using --dynamic.\n'
);
}
void addDynamicPatchingFlags({bool verboseHelp = false}) {
argParser.addOption('patch-number',
defaultsTo: '1',
hide: !verboseHelp,
help: 'An integer used as an internal version number for dynamic patch.\n'
'Each update should have a unique number to differentiate from previous '
'patches for same \'versionCode\' on Android or \'CFBundleVersion\' on iOS.\n'
'This flag is only used when --dynamic --patch is specified.\n'
);
argParser.addOption('patch-dir',
defaultsTo: 'public',
hide: !verboseHelp,
help: 'The directory where to store generated dynamic patches.\n'
'This directory can be deployed to a CDN such as Firebase Hosting.\n'
'It is recommended to store this directory in version control.\n'
'This flag is only used when --dynamic --patch is specified.\n'
);
argParser.addFlag('baseline',
hide: !verboseHelp,
negatable: false,
help: 'Save built package as baseline for future dynamic patching.\n'
'Built package, such as APK file on Android, is saved and '
'can be used to generate dynamic patches in later builds.\n'
'This flag is only allowed when using --dynamic.\n'
);
addDynamicBaselineFlags(verboseHelp: verboseHelp);
}
void addDynamicBaselineFlags({bool verboseHelp = false}) {
argParser.addOption('baseline-dir',
defaultsTo: '.baseline',
hide: !verboseHelp,
help: 'The directory where to store and find generated baseline packages.\n'
'It is recommended to store this directory in version control.\n'
'This flag is only used when --dynamic --baseline is specified.\n'
);
}
void usesFuchsiaOptions({bool hide = false}) {
argParser.addOption(
'target-model',
@ -308,6 +366,16 @@ abstract class FlutterCommand extends Command<void> {
'--build-number (${argResults['build-number']}) must be an int.', null);
}
int patchNumber;
try {
patchNumber = argParser.options.containsKey('patch-number') && argResults['patch-number'] != null
? int.parse(argResults['patch-number'])
: null;
} catch (e) {
throw UsageException(
'--patch-number (${argResults['patch-number']}) must be an int.', null);
}
return BuildInfo(getBuildMode(),
argParser.options.containsKey('flavor')
? argResults['flavor']
@ -316,9 +384,19 @@ abstract class FlutterCommand extends Command<void> {
compilationTraceFilePath: argParser.options.containsKey('precompile')
? argResults['precompile']
: null,
buildHotUpdate: argParser.options.containsKey('hotupdate')
? argResults['hotupdate']
createBaseline: argParser.options.containsKey('baseline')
? argResults['baseline']
: false,
createPatch: argParser.options.containsKey('patch')
? argResults['patch']
: false,
patchNumber: patchNumber,
patchDir: argParser.options.containsKey('patch-dir')
? argResults['patch-dir']
: null,
baselineDir: argParser.options.containsKey('baseline-dir')
? argResults['baseline-dir']
: null,
extraFrontEndOptions: argParser.options.containsKey(FlutterOptions.kExtraFrontEndOptions)
? argResults[FlutterOptions.kExtraFrontEndOptions]
: null,
@ -571,15 +649,21 @@ abstract class FlutterCommand extends Command<void> {
? argResults['dynamic'] : false;
final String compilationTraceFilePath = argParser.options.containsKey('precompile')
? argResults['precompile'] : null;
final bool buildHotUpdate = argParser.options.containsKey('hotupdate')
? argResults['hotupdate'] : false;
final bool createBaseline = argParser.options.containsKey('baseline')
? argResults['baseline'] : false;
final bool createPatch = argParser.options.containsKey('patch')
? argResults['patch'] : false;
if (compilationTraceFilePath != null && getBuildMode() == BuildMode.debug)
throw ToolExit('Error: --precompile is not allowed when --debug is specified.');
if (compilationTraceFilePath != null && !dynamicFlag)
throw ToolExit('Error: --precompile is allowed only when --dynamic is specified.');
if (buildHotUpdate && compilationTraceFilePath == null)
throw ToolExit('Error: --hotupdate is allowed only when --precompile is specified.');
if (createBaseline && createPatch)
throw ToolExit('Error: Only one of --baseline, --patch is allowed.');
if (createBaseline && compilationTraceFilePath == null)
throw ToolExit('Error: --baseline is allowed only when --precompile is specified.');
if (createPatch && compilationTraceFilePath == null)
throw ToolExit('Error: --patch is allowed only when --precompile is specified.');
}
ApplicationPackageStore applicationPackages;

View file

@ -4,6 +4,7 @@
import 'dart:async';
import 'package:archive/archive.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/artifacts.dart';
@ -551,7 +552,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
buildHotUpdate: false,
createPatch: false,
), isNot(equals(0)));
}, overrides: contextOverrides);
@ -573,7 +574,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
buildHotUpdate: false,
createPatch: false,
);
expect(genSnapshotExitCode, 0);
@ -614,7 +615,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
buildHotUpdate: false,
createPatch: false,
);
expect(genSnapshotExitCode, 0);
@ -644,7 +645,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
buildHotUpdate: false,
createPatch: false,
), isNot(equals(0)));
}, overrides: contextOverrides);
@ -666,7 +667,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
buildHotUpdate: false,
createPatch: false,
);
expect(genSnapshotExitCode, 0);
@ -706,7 +707,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
buildHotUpdate: false,
createPatch: false,
);
expect(genSnapshotExitCode, 0);
@ -735,7 +736,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
buildHotUpdate: false,
createPatch: false,
), isNot(equals(0)));
}, overrides: contextOverrides);
@ -757,7 +758,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
buildHotUpdate: false,
createPatch: false,
);
expect(genSnapshotExitCode, 0);
@ -797,7 +798,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
buildHotUpdate: false,
createPatch: false,
);
expect(genSnapshotExitCode, 0);
@ -817,16 +818,30 @@ void main() {
]);
}, overrides: contextOverrides);
testUsingContext('builds Android arm release JIT snapshot for hot update', () async {
testUsingContext('builds Android release JIT dynamic patch - existing snapshot', () async {
fs.file('main.dill').writeAsStringSync('binary magic');
final String outputPath = fs.path.join('build', 'foo');
fs.directory(outputPath).createSync(recursive: true);
fs.file(fs.path.join(outputPath, 'isolate_snapshot_instr')).createSync();
final Archive baselineApk = Archive()
..addFile(ArchiveFile('assets/flutter_assets/isolate_snapshot_instr',
'isolateSnapshotInstr'.length, 'isolateSnapshotInstr'.codeUnits))
..addFile(ArchiveFile('assets/flutter_assets/vm_snapshot_data',
'engineVmSnapshotData'.length, 'engineVmSnapshotData'.codeUnits));
fs.file('.baseline/100.apk')
..createSync(recursive: true)
..writeAsBytesSync(ZipEncoder().encode(baselineApk), flush: true);
fs.file('engine_vm_snapshot_data')
..createSync(recursive: true)
..writeAsStringSync('engineVmSnapshotData', flush: true);
fs.file('build/foo/isolate_snapshot_instr')
..createSync(recursive: true)
..writeAsStringSync('isolateSnapshotInstr', flush: true);
genSnapshot.outputs = <String, String>{
fs.path.join(outputPath, 'isolate_snapshot_data'): '',
fs.path.join(outputPath, 'snapshot.d'): '${fs.path.join(outputPath, 'vm_snapshot_data')} : ',
'build/foo/isolate_snapshot_data': '',
'build/foo/snapshot.d': 'build/foo/vm_snapshot_data : ',
};
final int genSnapshotExitCode = await snapshotter.build(
@ -834,9 +849,11 @@ void main() {
buildMode: BuildMode.release,
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: outputPath,
outputPath: 'build/foo',
compilationTraceFilePath: kTrace,
buildHotUpdate: true,
createPatch: true,
buildNumber: 100,
baselineDir: '.baseline',
);
expect(genSnapshotExitCode, 0);
@ -858,5 +875,148 @@ void main() {
]);
}, overrides: contextOverrides);
testUsingContext('builds Android release JIT dynamic patch - extracts snapshot', () async {
fs.file('main.dill').writeAsStringSync('binary magic');
final Archive baselineApk = Archive()
..addFile(ArchiveFile('assets/flutter_assets/isolate_snapshot_instr',
'isolateSnapshotInstr'.length, 'isolateSnapshotInstr'.codeUnits))
..addFile(ArchiveFile('assets/flutter_assets/vm_snapshot_data',
'engineVmSnapshotData'.length, 'engineVmSnapshotData'.codeUnits));
fs.file('.baseline/100.apk')
..createSync(recursive: true)
..writeAsBytesSync(ZipEncoder().encode(baselineApk), flush: true);
fs.file('engine_vm_snapshot_data')
..createSync(recursive: true)
..writeAsStringSync('engineVmSnapshotData', flush: true);
genSnapshot.outputs = <String, String>{
'build/foo/isolate_snapshot_data': '',
'build/foo/snapshot.d': 'build/foo/vm_snapshot_data : ',
};
final int genSnapshotExitCode = await snapshotter.build(
platform: TargetPlatform.android_arm,
buildMode: BuildMode.release,
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: 'build/foo',
compilationTraceFilePath: kTrace,
createPatch: true,
buildNumber: 100,
baselineDir: '.baseline',
);
// The file was extracted from baseline APK.
expect(fs.file('build/foo/isolate_snapshot_instr').existsSync(), true);
expect(fs.file('build/foo/isolate_snapshot_instr').readAsStringSync(), 'isolateSnapshotInstr');
expect(genSnapshotExitCode, 0);
expect(genSnapshot.callCount, 1);
expect(genSnapshot.snapshotType.platform, TargetPlatform.android_arm);
expect(genSnapshot.snapshotType.mode, BuildMode.release);
expect(genSnapshot.packagesPath, '.packages');
expect(genSnapshot.additionalArgs, <String>[
'--deterministic',
'--snapshot_kind=app-jit',
'--load_compilation_trace=$kTrace',
'--load_vm_snapshot_data=$kEngineVmSnapshotData',
'--load_isolate_snapshot_data=$kEngineIsolateSnapshotData',
'--isolate_snapshot_data=build/foo/isolate_snapshot_data',
'--reused_instructions=build/foo/isolate_snapshot_instr',
'--no-sim-use-hardfp',
'--no-use-integer-division',
'main.dill',
]);
}, overrides: contextOverrides);
testUsingContext('builds Android release JIT dynamic patch - mismatched snapshot 1', () async {
fs.file('main.dill').writeAsStringSync('binary magic');
final Archive baselineApk = Archive()
..addFile(ArchiveFile('assets/flutter_assets/isolate_snapshot_instr',
'isolateSnapshotInstr'.length, 'isolateSnapshotInstr'.codeUnits))
..addFile(ArchiveFile('assets/flutter_assets/vm_snapshot_data',
'engineVmSnapshotData'.length, 'engineVmSnapshotData'.codeUnits));
fs.file('.baseline/100.apk')
..createSync(recursive: true)
..writeAsBytesSync(ZipEncoder().encode(baselineApk), flush: true);
fs.file('engine_vm_snapshot_data')
..createSync(recursive: true)
..writeAsStringSync('mismatchedEngineVmSnapshotData', flush: true);
fs.file('build/foo/isolate_snapshot_instr')
..createSync(recursive: true)
..writeAsStringSync('isolateSnapshotInstr', flush: true);
genSnapshot.outputs = <String, String>{
'build/foo/isolate_snapshot_data': '',
'build/foo/snapshot.d': 'build/foo/vm_snapshot_data : ',
};
final int genSnapshotExitCode = await snapshotter.build(
platform: TargetPlatform.android_arm,
buildMode: BuildMode.release,
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: 'build/foo',
compilationTraceFilePath: kTrace,
createPatch: true,
buildNumber: 100,
baselineDir: '.baseline',
);
expect(genSnapshotExitCode, 1);
expect(genSnapshot.callCount, 0);
}, overrides: contextOverrides);
testUsingContext('builds Android release JIT dynamic patch - mismatched snapshot 2', () async {
fs.file('main.dill').writeAsStringSync('binary magic');
final Archive baselineApk = Archive()
..addFile(ArchiveFile('assets/flutter_assets/isolate_snapshot_instr',
'isolateSnapshotInstr'.length, 'isolateSnapshotInstr'.codeUnits))
..addFile(ArchiveFile('assets/flutter_assets/vm_snapshot_data',
'engineVmSnapshotData'.length, 'engineVmSnapshotData'.codeUnits));
fs.file('.baseline/100.apk')
..createSync(recursive: true)
..writeAsBytesSync(ZipEncoder().encode(baselineApk), flush: true);
fs.file('engine_vm_snapshot_data')
..createSync(recursive: true)
..writeAsStringSync('engineVmSnapshotData', flush: true);
fs.file('build/foo/isolate_snapshot_instr')
..createSync(recursive: true)
..writeAsStringSync('mismatchedIsolateSnapshotInstr', flush: true);
genSnapshot.outputs = <String, String>{
'build/foo/isolate_snapshot_data': '',
'build/foo/snapshot.d': 'build/foo/vm_snapshot_data : ',
};
final int genSnapshotExitCode = await snapshotter.build(
platform: TargetPlatform.android_arm,
buildMode: BuildMode.release,
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: 'build/foo',
compilationTraceFilePath: kTrace,
createPatch: true,
buildNumber: 100,
baselineDir: '.baseline',
);
expect(genSnapshotExitCode, 1);
expect(genSnapshot.callCount, 0);
}, overrides: contextOverrides);
});
}

View file

@ -29,6 +29,7 @@ class MockApplicationPackageStore extends ApplicationPackageStore {
android: AndroidApk(
id: 'io.flutter.android.mock',
file: fs.file('/mock/path/to/android/SkyShell.apk'),
versionCode: 1,
launchActivity: 'io.flutter.android.mock.MockActivity'
),
iOS: BuildableIOSApp(MockIosProject())