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

View file

@ -3,10 +3,13 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:archive/archive.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import '../android/android_sdk.dart'; import '../android/android_sdk.dart';
import '../application_package.dart';
import '../artifacts.dart'; import '../artifacts.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
@ -374,8 +377,8 @@ Future<void> _buildGradleProjectV2(
command.add('-Ptrack-widget-creation=${buildInfo.trackWidgetCreation}'); command.add('-Ptrack-widget-creation=${buildInfo.trackWidgetCreation}');
if (buildInfo.compilationTraceFilePath != null) if (buildInfo.compilationTraceFilePath != null)
command.add('-Pprecompile=${buildInfo.compilationTraceFilePath}'); command.add('-Pprecompile=${buildInfo.compilationTraceFilePath}');
if (buildInfo.buildHotUpdate) if (buildInfo.createPatch)
command.add('-Photupdate=true'); command.add('-Ppatch=true');
if (buildInfo.extraFrontEndOptions != null) if (buildInfo.extraFrontEndOptions != null)
command.add('-Pextra-front-end-options=${buildInfo.extraFrontEndOptions}'); command.add('-Pextra-front-end-options=${buildInfo.extraFrontEndOptions}');
if (buildInfo.extraGenSnapshotOptions != null) if (buildInfo.extraGenSnapshotOptions != null)
@ -420,6 +423,71 @@ Future<void> _buildGradleProjectV2(
appSize = ' (${getSizeAsMB(apkFile.lengthSync())})'; appSize = ' (${getSizeAsMB(apkFile.lengthSync())})';
} }
printStatus('Built ${fs.path.relative(apkFile.path)}$appSize.'); 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) { File _findApkFile(GradleProject project, BuildInfo buildInfo) {

View file

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

View file

@ -4,6 +4,8 @@
import 'dart:async'; import 'dart:async';
import 'package:archive/archive.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import '../android/android_sdk.dart'; import '../android/android_sdk.dart';
@ -348,7 +350,9 @@ class JITSnapshotter {
@required String packagesPath, @required String packagesPath,
@required String outputPath, @required String outputPath,
@required String compilationTraceFilePath, @required String compilationTraceFilePath,
@required bool buildHotUpdate, @required bool createPatch,
int buildNumber,
String baselineDir,
List<String> extraGenSnapshotOptions = const <String>[], List<String> extraGenSnapshotOptions = const <String>[],
}) async { }) async {
if (!_isValidJitPlatform(platform)) { if (!_isValidJitPlatform(platform)) {
@ -367,8 +371,73 @@ class JITSnapshotter {
final List<String> inputPaths = <String>[ final List<String> inputPaths = <String>[
mainPath, compilationTraceFilePath, engineVmSnapshotData, engineIsolateSnapshotData, mainPath, compilationTraceFilePath, engineVmSnapshotData, engineIsolateSnapshotData,
]; ];
if (buildHotUpdate) {
if (createPatch) {
inputPaths.add(isolateSnapshotInstructions); 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'); final String depfilePath = fs.path.join(outputDir.path, 'snapshot.d');
@ -385,7 +454,7 @@ class JITSnapshotter {
final Set<String> outputPaths = Set<String>(); final Set<String> outputPaths = Set<String>();
outputPaths.addAll(<String>[isolateSnapshotData]); outputPaths.addAll(<String>[isolateSnapshotData]);
if (!buildHotUpdate) { if (!createPatch) {
outputPaths.add(isolateSnapshotInstructions); outputPaths.add(isolateSnapshotInstructions);
} }
@ -397,7 +466,7 @@ class JITSnapshotter {
'--isolate_snapshot_data=$isolateSnapshotData', '--isolate_snapshot_data=$isolateSnapshotData',
]); ]);
if (!buildHotUpdate) { if (!createPatch) {
genSnapshotArgs.add('--isolate_snapshot_instructions=$isolateSnapshotInstructions'); genSnapshotArgs.add('--isolate_snapshot_instructions=$isolateSnapshotInstructions');
} else { } else {
genSnapshotArgs.add('--reused_instructions=$isolateSnapshotInstructions'); genSnapshotArgs.add('--reused_instructions=$isolateSnapshotInstructions');
@ -429,7 +498,7 @@ class JITSnapshotter {
'buildMode': buildMode.toString(), 'buildMode': buildMode.toString(),
'targetPlatform': platform.toString(), 'targetPlatform': platform.toString(),
'entryPoint': mainPath, 'entryPoint': mainPath,
'buildHotUpdate': buildHotUpdate.toString(), 'createPatch': createPatch.toString(),
'extraGenSnapshotOptions': extraGenSnapshotOptions.join(' '), 'extraGenSnapshotOptions': extraGenSnapshotOptions.join(' '),
}, },
depfilePaths: <String>[], depfilePaths: <String>[],

View file

@ -13,7 +13,11 @@ class BuildInfo {
const BuildInfo(this.mode, this.flavor, { const BuildInfo(this.mode, this.flavor, {
this.trackWidgetCreation = false, this.trackWidgetCreation = false,
this.compilationTraceFilePath, this.compilationTraceFilePath,
this.buildHotUpdate, this.createBaseline,
this.createPatch,
this.patchNumber,
this.patchDir,
this.baselineDir,
this.extraFrontEndOptions, this.extraFrontEndOptions,
this.extraGenSnapshotOptions, this.extraGenSnapshotOptions,
this.buildSharedLibrary, this.buildSharedLibrary,
@ -43,8 +47,24 @@ class BuildInfo {
/// Dart compilation trace file to use for JIT VM snapshot. /// Dart compilation trace file to use for JIT VM snapshot.
final String compilationTraceFilePath; final String compilationTraceFilePath;
/// Save baseline package.
final bool createBaseline;
/// Build differential snapshot. /// 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. /// Extra command-line options for front-end.
final String extraFrontEndOptions; final String extraFrontEndOptions;
@ -92,6 +112,9 @@ class BuildInfo {
/// Exactly one of [isDebug], [isProfile], or [isRelease] is true. /// Exactly one of [isDebug], [isProfile], or [isRelease] is true.
bool get isRelease => mode == BuildMode.release || mode == BuildMode.dynamicRelease; 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 usesAot => isAotBuildMode(mode);
bool get supportsEmulator => isEmulatorBuildMode(mode); bool get supportsEmulator => isEmulatorBuildMode(mode);
bool get supportsSimulator => isEmulatorBuildMode(mode); bool get supportsSimulator => isEmulatorBuildMode(mode);
@ -101,7 +124,7 @@ class BuildInfo {
BuildInfo(mode, flavor, BuildInfo(mode, flavor,
trackWidgetCreation: trackWidgetCreation, trackWidgetCreation: trackWidgetCreation,
compilationTraceFilePath: compilationTraceFilePath, compilationTraceFilePath: compilationTraceFilePath,
buildHotUpdate: buildHotUpdate, createPatch: createPatch,
extraFrontEndOptions: extraFrontEndOptions, extraFrontEndOptions: extraFrontEndOptions,
extraGenSnapshotOptions: extraGenSnapshotOptions, extraGenSnapshotOptions: extraGenSnapshotOptions,
buildSharedLibrary: buildSharedLibrary, buildSharedLibrary: buildSharedLibrary,

View file

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

View file

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

View file

@ -4,6 +4,8 @@
import 'dart:async'; import 'dart:async';
import 'package:args/command_runner.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../bundle.dart'; import '../bundle.dart';
@ -14,7 +16,10 @@ class BuildBundleCommand extends BuildSubCommand {
BuildBundleCommand({bool verboseHelp = false}) { BuildBundleCommand({bool verboseHelp = false}) {
usesTargetOption(); usesTargetOption();
usesFilesystemOptions(hide: !verboseHelp); usesFilesystemOptions(hide: !verboseHelp);
addBuildModeFlags(); usesBuildNumberOption();
addBuildModeFlags(verboseHelp: verboseHelp);
addDynamicModeFlags(verboseHelp: verboseHelp);
addDynamicBaselineFlags(verboseHelp: verboseHelp);
argParser argParser
..addFlag('precompiled', negatable: false) ..addFlag('precompiled', negatable: false)
// This option is still referenced by the iOS build scripts. We should // This option is still referenced by the iOS build scripts. We should
@ -31,23 +36,6 @@ class BuildBundleCommand extends BuildSubCommand {
hide: !verboseHelp, hide: !verboseHelp,
help: 'Track widget creation locations. Requires Dart 2.0 functionality.', 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, ..addMultiOption(FlutterOptions.kExtraFrontEndOptions,
splitCommas: true, splitCommas: true,
hide: true, hide: true,
@ -86,6 +74,15 @@ class BuildBundleCommand extends BuildSubCommand {
final BuildMode buildMode = getBuildMode(); 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( await build(
platform: platform, platform: platform,
buildMode: buildMode, buildMode: buildMode,
@ -98,7 +95,9 @@ class BuildBundleCommand extends BuildSubCommand {
reportLicensedPackages: argResults['report-licensed-packages'], reportLicensedPackages: argResults['report-licensed-packages'],
trackWidgetCreation: argResults['track-widget-creation'], trackWidgetCreation: argResults['track-widget-creation'],
compilationTraceFilePath: argResults['precompile'], compilationTraceFilePath: argResults['precompile'],
buildHotUpdate: argResults['hotupdate'], createPatch: argResults['patch'],
buildNumber: buildNumber,
baselineDir: argResults['baseline-dir'],
extraFrontEndOptions: argResults[FlutterOptions.kExtraFrontEndOptions], extraFrontEndOptions: argResults[FlutterOptions.kExtraFrontEndOptions],
extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions], extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
fileSystemScheme: argResults['filesystem-scheme'], fileSystemScheme: argResults['filesystem-scheme'],

View file

@ -24,6 +24,8 @@ abstract class RunCommandBase extends FlutterCommand {
// Used by run and drive commands. // Used by run and drive commands.
RunCommandBase({ bool verboseHelp = false }) { RunCommandBase({ bool verboseHelp = false }) {
addBuildModeFlags(defaultToRelease: false, verboseHelp: verboseHelp); addBuildModeFlags(defaultToRelease: false, verboseHelp: verboseHelp);
addDynamicModeFlags(verboseHelp: verboseHelp);
addDynamicPatchingFlags(verboseHelp: verboseHelp);
usesFlavorOption(); usesFlavorOption();
argParser argParser
..addFlag('trace-startup', ..addFlag('trace-startup',
@ -104,23 +106,6 @@ class RunCommand extends RunCommandBase {
hide: !verboseHelp, hide: !verboseHelp,
help: 'Specify a pre-built application binary to use when running.', 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', ..addFlag('track-widget-creation',
hide: !verboseHelp, hide: !verboseHelp,
help: 'Track widget creation locations. Requires Dart 2.0 functionality.', 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.'); '--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}) { void usesFuchsiaOptions({bool hide = false}) {
argParser.addOption( argParser.addOption(
'target-model', 'target-model',
@ -308,6 +366,16 @@ abstract class FlutterCommand extends Command<void> {
'--build-number (${argResults['build-number']}) must be an int.', null); '--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(), return BuildInfo(getBuildMode(),
argParser.options.containsKey('flavor') argParser.options.containsKey('flavor')
? argResults['flavor'] ? argResults['flavor']
@ -316,9 +384,19 @@ abstract class FlutterCommand extends Command<void> {
compilationTraceFilePath: argParser.options.containsKey('precompile') compilationTraceFilePath: argParser.options.containsKey('precompile')
? argResults['precompile'] ? argResults['precompile']
: null, : null,
buildHotUpdate: argParser.options.containsKey('hotupdate') createBaseline: argParser.options.containsKey('baseline')
? argResults['hotupdate'] ? argResults['baseline']
: false, : 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) extraFrontEndOptions: argParser.options.containsKey(FlutterOptions.kExtraFrontEndOptions)
? argResults[FlutterOptions.kExtraFrontEndOptions] ? argResults[FlutterOptions.kExtraFrontEndOptions]
: null, : null,
@ -571,15 +649,21 @@ abstract class FlutterCommand extends Command<void> {
? argResults['dynamic'] : false; ? argResults['dynamic'] : false;
final String compilationTraceFilePath = argParser.options.containsKey('precompile') final String compilationTraceFilePath = argParser.options.containsKey('precompile')
? argResults['precompile'] : null; ? argResults['precompile'] : null;
final bool buildHotUpdate = argParser.options.containsKey('hotupdate') final bool createBaseline = argParser.options.containsKey('baseline')
? argResults['hotupdate'] : false; ? argResults['baseline'] : false;
final bool createPatch = argParser.options.containsKey('patch')
? argResults['patch'] : false;
if (compilationTraceFilePath != null && getBuildMode() == BuildMode.debug) if (compilationTraceFilePath != null && getBuildMode() == BuildMode.debug)
throw ToolExit('Error: --precompile is not allowed when --debug is specified.'); throw ToolExit('Error: --precompile is not allowed when --debug is specified.');
if (compilationTraceFilePath != null && !dynamicFlag) if (compilationTraceFilePath != null && !dynamicFlag)
throw ToolExit('Error: --precompile is allowed only when --dynamic is specified.'); throw ToolExit('Error: --precompile is allowed only when --dynamic is specified.');
if (buildHotUpdate && compilationTraceFilePath == null) if (createBaseline && createPatch)
throw ToolExit('Error: --hotupdate is allowed only when --precompile is specified.'); 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; ApplicationPackageStore applicationPackages;

View file

@ -4,6 +4,7 @@
import 'dart:async'; import 'dart:async';
import 'package:archive/archive.dart';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_sdk.dart'; import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/artifacts.dart';
@ -551,7 +552,7 @@ void main() {
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
compilationTraceFilePath: kTrace, compilationTraceFilePath: kTrace,
buildHotUpdate: false, createPatch: false,
), isNot(equals(0))); ), isNot(equals(0)));
}, overrides: contextOverrides); }, overrides: contextOverrides);
@ -573,7 +574,7 @@ void main() {
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
compilationTraceFilePath: kTrace, compilationTraceFilePath: kTrace,
buildHotUpdate: false, createPatch: false,
); );
expect(genSnapshotExitCode, 0); expect(genSnapshotExitCode, 0);
@ -614,7 +615,7 @@ void main() {
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
compilationTraceFilePath: kTrace, compilationTraceFilePath: kTrace,
buildHotUpdate: false, createPatch: false,
); );
expect(genSnapshotExitCode, 0); expect(genSnapshotExitCode, 0);
@ -644,7 +645,7 @@ void main() {
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
compilationTraceFilePath: kTrace, compilationTraceFilePath: kTrace,
buildHotUpdate: false, createPatch: false,
), isNot(equals(0))); ), isNot(equals(0)));
}, overrides: contextOverrides); }, overrides: contextOverrides);
@ -666,7 +667,7 @@ void main() {
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
compilationTraceFilePath: kTrace, compilationTraceFilePath: kTrace,
buildHotUpdate: false, createPatch: false,
); );
expect(genSnapshotExitCode, 0); expect(genSnapshotExitCode, 0);
@ -706,7 +707,7 @@ void main() {
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
compilationTraceFilePath: kTrace, compilationTraceFilePath: kTrace,
buildHotUpdate: false, createPatch: false,
); );
expect(genSnapshotExitCode, 0); expect(genSnapshotExitCode, 0);
@ -735,7 +736,7 @@ void main() {
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
compilationTraceFilePath: kTrace, compilationTraceFilePath: kTrace,
buildHotUpdate: false, createPatch: false,
), isNot(equals(0))); ), isNot(equals(0)));
}, overrides: contextOverrides); }, overrides: contextOverrides);
@ -757,7 +758,7 @@ void main() {
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
compilationTraceFilePath: kTrace, compilationTraceFilePath: kTrace,
buildHotUpdate: false, createPatch: false,
); );
expect(genSnapshotExitCode, 0); expect(genSnapshotExitCode, 0);
@ -797,7 +798,7 @@ void main() {
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
compilationTraceFilePath: kTrace, compilationTraceFilePath: kTrace,
buildHotUpdate: false, createPatch: false,
); );
expect(genSnapshotExitCode, 0); expect(genSnapshotExitCode, 0);
@ -817,16 +818,30 @@ void main() {
]); ]);
}, overrides: contextOverrides); }, 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'); fs.file('main.dill').writeAsStringSync('binary magic');
final String outputPath = fs.path.join('build', 'foo'); final Archive baselineApk = Archive()
fs.directory(outputPath).createSync(recursive: true); ..addFile(ArchiveFile('assets/flutter_assets/isolate_snapshot_instr',
fs.file(fs.path.join(outputPath, 'isolate_snapshot_instr')).createSync(); '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>{ genSnapshot.outputs = <String, String>{
fs.path.join(outputPath, 'isolate_snapshot_data'): '', 'build/foo/isolate_snapshot_data': '',
fs.path.join(outputPath, 'snapshot.d'): '${fs.path.join(outputPath, 'vm_snapshot_data')} : ', 'build/foo/snapshot.d': 'build/foo/vm_snapshot_data : ',
}; };
final int genSnapshotExitCode = await snapshotter.build( final int genSnapshotExitCode = await snapshotter.build(
@ -834,9 +849,11 @@ void main() {
buildMode: BuildMode.release, buildMode: BuildMode.release,
mainPath: 'main.dill', mainPath: 'main.dill',
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: 'build/foo',
compilationTraceFilePath: kTrace, compilationTraceFilePath: kTrace,
buildHotUpdate: true, createPatch: true,
buildNumber: 100,
baselineDir: '.baseline',
); );
expect(genSnapshotExitCode, 0); expect(genSnapshotExitCode, 0);
@ -858,5 +875,148 @@ void main() {
]); ]);
}, overrides: contextOverrides); }, 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( android: AndroidApk(
id: 'io.flutter.android.mock', id: 'io.flutter.android.mock',
file: fs.file('/mock/path/to/android/SkyShell.apk'), file: fs.file('/mock/path/to/android/SkyShell.apk'),
versionCode: 1,
launchActivity: 'io.flutter.android.mock.MockActivity' launchActivity: 'io.flutter.android.mock.MockActivity'
), ),
iOS: BuildableIOSApp(MockIosProject()) iOS: BuildableIOSApp(MockIosProject())