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())