Flutter tool support for building dynamic updates (#25576)
diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle
index 3cd58a2..6feec96 100644
--- a/packages/flutter_tools/gradle/flutter.gradle
+++ b/packages/flutter_tools/gradle/flutter.gradle
@@ -321,9 +321,17 @@
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 @@
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 @@
@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 @@
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}"
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index 51bd02b..9c4a6de 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -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 @@
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 @@
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) {
diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart
index fc16897..0bb41c2 100644
--- a/packages/flutter_tools/lib/src/application_package.dart
+++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -41,6 +41,7 @@
AndroidApk({
String id,
@required this.file,
+ @required this.versionCode,
@required this.launchActivity
}) : assert(file != null),
assert(launchActivity != null),
@@ -78,6 +79,7 @@
return AndroidApk(
id: data.packageName,
file: apk,
+ versionCode: int.tryParse(data.versionCode),
launchActivity: '${data.packageName}/${data.launchableActivityName}'
);
}
@@ -88,6 +90,9 @@
/// 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 @@
return AndroidApk(
id: packageId,
file: apkFile,
+ versionCode: null,
launchActivity: launchActivity
);
}
@@ -449,8 +455,25 @@
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 @@
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'];
}
diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart
index 009d75c..894c625 100644
--- a/packages/flutter_tools/lib/src/base/build.dart
+++ b/packages/flutter_tools/lib/src/base/build.dart
@@ -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 @@
@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 @@
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 @@
final Set<String> outputPaths = Set<String>();
outputPaths.addAll(<String>[isolateSnapshotData]);
- if (!buildHotUpdate) {
+ if (!createPatch) {
outputPaths.add(isolateSnapshotInstructions);
}
@@ -397,7 +466,7 @@
'--isolate_snapshot_data=$isolateSnapshotData',
]);
- if (!buildHotUpdate) {
+ if (!createPatch) {
genSnapshotArgs.add('--isolate_snapshot_instructions=$isolateSnapshotInstructions');
} else {
genSnapshotArgs.add('--reused_instructions=$isolateSnapshotInstructions');
@@ -429,7 +498,7 @@
'buildMode': buildMode.toString(),
'targetPlatform': platform.toString(),
'entryPoint': mainPath,
- 'buildHotUpdate': buildHotUpdate.toString(),
+ 'createPatch': createPatch.toString(),
'extraGenSnapshotOptions': extraGenSnapshotOptions.join(' '),
},
depfilePaths: <String>[],
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index f731aec..ac12851 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -13,7 +13,11 @@
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 @@
/// 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 @@
/// 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 @@
BuildInfo(mode, flavor,
trackWidgetCreation: trackWidgetCreation,
compilationTraceFilePath: compilationTraceFilePath,
- buildHotUpdate: buildHotUpdate,
+ createPatch: createPatch,
extraFrontEndOptions: extraFrontEndOptions,
extraGenSnapshotOptions: extraGenSnapshotOptions,
buildSharedLibrary: buildSharedLibrary,
diff --git a/packages/flutter_tools/lib/src/bundle.dart b/packages/flutter_tools/lib/src/bundle.dart
index d8515c1..8f3ed1a 100644
--- a/packages/flutter_tools/lib/src/bundle.dart
+++ b/packages/flutter_tools/lib/src/bundle.dart
@@ -60,7 +60,9 @@
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 @@
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');
diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart
index 15474ce..a8f9b72 100644
--- a/packages/flutter_tools/lib/src/commands/build_apk.dart
+++ b/packages/flutter_tools/lib/src/commands/build_apk.dart
@@ -12,7 +12,9 @@
class BuildApkCommand extends BuildSubCommand {
BuildApkCommand({bool verboseHelp = false}) {
usesTargetOption();
- addBuildModeFlags();
+ addBuildModeFlags(verboseHelp: verboseHelp);
+ addDynamicModeFlags(verboseHelp: verboseHelp);
+ addDynamicPatchingFlags(verboseHelp: verboseHelp);
usesFlavorOption();
usesPubOption();
usesBuildNumberOption();
diff --git a/packages/flutter_tools/lib/src/commands/build_bundle.dart b/packages/flutter_tools/lib/src/commands/build_bundle.dart
index edb6d9f..c47c34c 100644
--- a/packages/flutter_tools/lib/src/commands/build_bundle.dart
+++ b/packages/flutter_tools/lib/src/commands/build_bundle.dart
@@ -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 @@
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 @@
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 @@
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 @@
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'],
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index bb5e622..138daa1 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -24,6 +24,8 @@
// 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 @@
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.',
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 8bc1e3d..9f05b00 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -241,6 +241,64 @@
'--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 @@
'--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 @@
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 @@
? 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;
diff --git a/packages/flutter_tools/test/base/build_test.dart b/packages/flutter_tools/test/base/build_test.dart
index 4105db5..f2f79fc 100644
--- a/packages/flutter_tools/test/base/build_test.dart
+++ b/packages/flutter_tools/test/base/build_test.dart
@@ -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 @@
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
- buildHotUpdate: false,
+ createPatch: false,
), isNot(equals(0)));
}, overrides: contextOverrides);
@@ -573,7 +574,7 @@
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
- buildHotUpdate: false,
+ createPatch: false,
);
expect(genSnapshotExitCode, 0);
@@ -614,7 +615,7 @@
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
- buildHotUpdate: false,
+ createPatch: false,
);
expect(genSnapshotExitCode, 0);
@@ -644,7 +645,7 @@
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
- buildHotUpdate: false,
+ createPatch: false,
), isNot(equals(0)));
}, overrides: contextOverrides);
@@ -666,7 +667,7 @@
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
- buildHotUpdate: false,
+ createPatch: false,
);
expect(genSnapshotExitCode, 0);
@@ -706,7 +707,7 @@
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
- buildHotUpdate: false,
+ createPatch: false,
);
expect(genSnapshotExitCode, 0);
@@ -735,7 +736,7 @@
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
- buildHotUpdate: false,
+ createPatch: false,
), isNot(equals(0)));
}, overrides: contextOverrides);
@@ -757,7 +758,7 @@
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
- buildHotUpdate: false,
+ createPatch: false,
);
expect(genSnapshotExitCode, 0);
@@ -797,7 +798,7 @@
packagesPath: '.packages',
outputPath: outputPath,
compilationTraceFilePath: kTrace,
- buildHotUpdate: false,
+ createPatch: false,
);
expect(genSnapshotExitCode, 0);
@@ -817,16 +818,30 @@
]);
}, 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 @@
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 @@
]);
}, 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);
+
});
}
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index 9278c55..1ce3b7f 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -29,6 +29,7 @@
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())