diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle index 3cd58a2e0b0..6feec96094e 100644 --- a/packages/flutter_tools/gradle/flutter.gradle +++ b/packages/flutter_tools/gradle/flutter.gradle @@ -321,9 +321,17 @@ class FlutterPlugin implements Plugin { 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 { 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}" diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index 51bd02b5fac..9c4a6de614c 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 @@ Future _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 _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 manifest = { + '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 fc168971ef7..0bb41c27ec3 100644 --- a/packages/flutter_tools/lib/src/application_package.dart +++ b/packages/flutter_tools/lib/src/application_package.dart @@ -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 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> map = >{}; map['package'] = {'name': packageName}; + map['version-code'] = {'name': versionCode.toString()}; map['launchable-activity'] = {'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']; } diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart index 009d75cd7cb..894c6254ef5 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 @@ class JITSnapshotter { @required String packagesPath, @required String outputPath, @required String compilationTraceFilePath, - @required bool buildHotUpdate, + @required bool createPatch, + int buildNumber, + String baselineDir, List extraGenSnapshotOptions = const [], }) async { if (!_isValidJitPlatform(platform)) { @@ -367,8 +371,73 @@ class JITSnapshotter { final List inputPaths = [ 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().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().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 outputPaths = Set(); outputPaths.addAll([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: [], diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index f731aec6d75..ac1285120a8 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -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, diff --git a/packages/flutter_tools/lib/src/bundle.dart b/packages/flutter_tools/lib/src/bundle.dart index d8515c17f60..8f3ed1a98c3 100644 --- a/packages/flutter_tools/lib/src/bundle.dart +++ b/packages/flutter_tools/lib/src/bundle.dart @@ -60,7 +60,9 @@ Future build({ bool reportLicensedPackages = false, bool trackWidgetCreation = false, String compilationTraceFilePath, - bool buildHotUpdate = false, + bool createPatch = false, + int buildNumber, + String baselineDir, List extraFrontEndOptions = const [], List extraGenSnapshotOptions = const [], List fileSystemRoots, @@ -108,7 +110,9 @@ Future 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'); diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart index 15474ce515b..a8f9b72462a 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 @@ 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(); diff --git a/packages/flutter_tools/lib/src/commands/build_bundle.dart b/packages/flutter_tools/lib/src/commands/build_bundle.dart index edb6d9fec00..c47c34c7c7f 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 @@ 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'], diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index bb5e622e705..138daa16b9e 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -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.', diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index 8bc1e3dc1f2..9f05b0019d9 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 @@ abstract class FlutterCommand extends Command { '--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 { '--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 { 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 { ? 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 4105db5b87a..f2f79fc9543 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 @@ 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 = { - 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 = { + '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, [ + '--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 = { + '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 = { + '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 9278c554e9f..1ce3b7f4e78 100644 --- a/packages/flutter_tools/test/src/mocks.dart +++ b/packages/flutter_tools/test/src/mocks.dart @@ -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())