improve Flutter build commands (#15788)

add --buildNumber and --buildName to flutter build like
flutter build apk --buildNumber=42 --buildName=1.0.42
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index 4f450e1..0caf41f 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -3,6 +3,9 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:convert';
+
+import 'package:meta/meta.dart';
 
 import '../android/android_sdk.dart';
 import '../artifacts.dart';
@@ -223,12 +226,81 @@
     settings.writeContents(localProperties);
 }
 
+Future<Null> findAndReplaceVersionProperties({@required BuildInfo buildInfo}) {
+  assert(buildInfo != null, 'buildInfo can\'t be null');
+  final Completer<Null> completer = new Completer<Null>();
+
+  // early return, if nothing has to be changed
+  if (buildInfo.buildNumber == null && buildInfo.buildName == null) {
+    completer.complete();
+    return completer.future;
+  }
+
+  final File appGradle = fs.file(fs.path.join('android', 'app', 'build.gradle'));
+  final File appGradleTmp = fs.file(fs.path.join('android', 'app', 'build.gradle.tmp'));
+  appGradleTmp.createSync();
+
+  if (appGradle.existsSync() && appGradleTmp.existsSync()) {
+    final Stream<List<int>> inputStream = appGradle.openRead();
+    final IOSink sink = appGradleTmp.openWrite();
+
+    inputStream.transform(utf8.decoder)
+        .transform(const LineSplitter())
+        .map((String line) {
+
+          // find and replace build number
+          if (buildInfo.buildNumber != null) {
+            if (line.contains(new RegExp(r'^[ |\t]*(versionCode)[ =\t]*\d*'))) {
+              return line.splitMapJoin(new RegExp(r'(versionCode)[ =\t]*\d*'), onMatch: (Match m) {
+                return 'versionCode ${buildInfo.buildNumber}';
+              });
+            }
+          }
+
+          // find and replace build name
+          if (buildInfo.buildName != null) {
+            if (line.contains(new RegExp(r'^[ |\t]*(versionName)[ =\t]*\"[0-9.]*"'))) {
+              return line.splitMapJoin(new RegExp(r'(versionName)[ =\t]*\"[0-9.]*"'), onMatch: (Match m) {
+                return 'versionName "${buildInfo.buildName}"';
+              });
+            }
+          }
+          return line;
+        })
+        .listen((String line) {
+            sink.writeln(line);
+          },
+          onDone: () {
+            sink.close();
+            try {
+              final File gradleOld = appGradle.renameSync(fs.path.join('android', 'app', 'build.gradle.old'));
+              appGradleTmp.renameSync(fs.path.join('android', 'app', 'build.gradle'));
+              gradleOld.deleteSync();
+              completer.complete();
+            } catch (error) {
+              printError('Failed to change version properties. $error');
+              completer.completeError('Failed to change version properties. $error');
+            }
+          },
+          onError: (dynamic error, StackTrace stack) {
+            printError('Failed to change version properties. ${error.toString()}');
+            sink.close();
+            appGradleTmp.deleteSync();
+            completer.completeError('Failed to change version properties. ${error.toString()}', stack);
+          },
+    );
+  }
+
+  return completer.future;
+}
+
 Future<Null> buildGradleProject(BuildInfo buildInfo, String target) async {
   // Update the local.properties file with the build mode.
   // FlutterPlugin v1 reads local.properties to determine build mode. Plugin v2
   // uses the standard Android way to determine what to build, but we still
   // update local.properties, in case we want to use it in the future.
   updateLocalProperties(buildInfo: buildInfo);
+  await findAndReplaceVersionProperties(buildInfo: buildInfo);
 
   final String gradle = await _ensureGradle();
 
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index 2f9cabf..f0ecb7e 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -19,6 +19,8 @@
     this.targetPlatform,
     this.fileSystemRoots,
     this.fileSystemScheme,
+    this.buildNumber,
+    this.buildName,
   });
 
   final BuildMode mode;
@@ -52,6 +54,19 @@
   /// Target platform for the build (e.g. android_arm versus android_arm64).
   final TargetPlatform targetPlatform;
 
+  /// Internal version number (not displayed to users).
+  /// Each build must have a unique number to differentiate it from previous builds.
+  /// It is used to determine whether one build is more recent than another, with higher numbers indicating more recent build.
+  /// On Android it is used as versionCode.
+  /// On Xcode builds it is used as CFBundleVersion.
+  final int buildNumber;
+
+  /// A "x.y.z" string used as the version number shown to users.
+  /// For each new version of your app, you will provide a version number to differentiate it from previous versions.
+  /// On Android it is used as versionName.
+  /// On Xcode builds it is used as CFBundleShortVersionString,
+  final String buildName;
+
   static const BuildInfo debug = const BuildInfo(BuildMode.debug, null);
   static const BuildInfo profile = const BuildInfo(BuildMode.profile, null);
   static const BuildInfo release = const BuildInfo(BuildMode.release, null);
diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart
index 6e70e3f..06b5929 100644
--- a/packages/flutter_tools/lib/src/commands/build_apk.dart
+++ b/packages/flutter_tools/lib/src/commands/build_apk.dart
@@ -13,6 +13,8 @@
     addBuildModeFlags();
     usesFlavorOption();
     usesPubOption();
+    usesBuildNumberOption();
+    usesBuildNameOption();
 
     argParser
       ..addFlag('preview-dart-2',
diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart
index 5983690..1738b6f 100644
--- a/packages/flutter_tools/lib/src/commands/build_ios.dart
+++ b/packages/flutter_tools/lib/src/commands/build_ios.dart
@@ -17,6 +17,8 @@
     usesTargetOption();
     usesFlavorOption();
     usesPubOption();
+    usesBuildNumberOption();
+    usesBuildNameOption();
     argParser
       ..addFlag('debug',
         negatable: false,
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index ff3d9d1..1817601 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -252,6 +252,53 @@
     );
   }
 
+  // If buildNumber is not specified, keep the project untouched.
+  if (buildInfo.buildNumber != null) {
+    final Status buildNumberStatus =
+        logger.startProgress('Setting CFBundleVersion...', expectSlowOperation: true);
+    try {
+      final RunResult buildNumberResult = await runAsync(
+        <String>[
+          '/usr/bin/env',
+          'xcrun',
+          'agvtool',
+          'new-version',
+          '-all',
+          buildInfo.buildNumber.toString(),
+        ],
+        workingDirectory: app.appDirectory,
+      );
+      if (buildNumberResult.exitCode != 0) {
+        throwToolExit('Xcode failed to set new version\n${buildNumberResult.stderr}');
+      }
+    } finally {
+      buildNumberStatus.stop();
+    }
+  }
+
+  // If buildName is not specified, keep the project untouched.
+  if (buildInfo.buildName != null) {
+    final Status buildNameStatus =
+        logger.startProgress('Setting CFBundleShortVersionString...', expectSlowOperation: true);
+    try {
+      final RunResult buildNameResult = await runAsync(
+        <String>[
+          '/usr/bin/env',
+          'xcrun',
+          'agvtool',
+          'new-marketing-version',
+          buildInfo.buildName,
+        ],
+        workingDirectory: app.appDirectory,
+      );
+      if (buildNameResult.exitCode != 0) {
+        throwToolExit('Xcode failed to set new marketing version\n${buildNameResult.stderr}');
+      }
+    } finally {
+      buildNameStatus.stop();
+    }
+  }
+
   final Status cleanStatus =
       logger.startProgress('Running Xcode clean...', expectSlowOperation: true);
   final RunResult cleanResult = await runAsync(
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 560b2c2..ebe13e8 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -122,7 +122,26 @@
     _usesPubOption = true;
   }
 
-  void addBuildModeFlags({ bool defaultToRelease: true }) {
+  void usesBuildNumberOption() {
+    argParser.addOption('build-number',
+        help: 'An integer used as an internal version number.\n'
+              'Each build must have a unique number to differentiate it from previous builds.\n'
+              'It is used to determine whether one build is more recent than another, with higher numbers indicating more recent build.\n'
+              'On Android it is used as \'versionCode\'.\n'
+              'On Xcode builds it is used as \'CFBundleVersion\'',
+        valueHelp: 'int');
+  }
+
+  void usesBuildNameOption() {
+    argParser.addOption('build-name',
+        help: 'A "x.y.z" string used as the version number shown to users.\n'
+              'For each new version of your app, you will provide a version number to differentiate it from previous versions.\n'
+              'On Android it is used as \'versionName\'.\n'
+              'On Xcode builds it is used as \'CFBundleShortVersionString\'',
+        valueHelp: 'x.y.z');
+  }
+
+  void addBuildModeFlags({bool defaultToRelease: true}) {
     defaultBuildMode = defaultToRelease ? BuildMode.release : BuildMode.debug;
 
     argParser.addFlag('debug',
@@ -181,6 +200,16 @@
           '--track-widget-creation is valid only when --preview-dart-2 is specified.', null);
     }
 
+    int buildNumber;
+    try {
+      buildNumber = argParser.options.containsKey('build-number') && argResults['build-number'] != null
+          ? int.parse(argResults['build-number'])
+          : null;
+    } catch (e) {
+      throw new UsageException(
+          '--build-number (${argResults['build-number']}) must be an int.', null);
+    }
+
     return new BuildInfo(getBuildMode(),
       argParser.options.containsKey('flavor')
         ? argResults['flavor']
@@ -201,6 +230,10 @@
           ? argResults[FlutterOptions.kFileSystemRoot] : null,
       fileSystemScheme: argParser.options.containsKey(FlutterOptions.kFileSystemScheme)
           ? argResults[FlutterOptions.kFileSystemScheme] : null,
+      buildNumber: buildNumber,
+      buildName: argParser.options.containsKey('build-name')
+          ? argResults['build-name']
+          : null,
     );
   }
 
diff --git a/packages/flutter_tools/templates/create/ios-objc.tmpl/Runner.xcodeproj/project.pbxproj.tmpl b/packages/flutter_tools/templates/create/ios-objc.tmpl/Runner.xcodeproj/project.pbxproj.tmpl
index 333dabe..41e80d2 100644
--- a/packages/flutter_tools/templates/create/ios-objc.tmpl/Runner.xcodeproj/project.pbxproj.tmpl
+++ b/packages/flutter_tools/templates/create/ios-objc.tmpl/Runner.xcodeproj/project.pbxproj.tmpl
@@ -371,6 +371,7 @@
 			buildSettings = {
 				ARCHS = arm64;
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CURRENT_PROJECT_VERSION = 1;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
 					"$(inherited)",
@@ -384,6 +385,7 @@
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = {{iosIdentifier}};
 				PRODUCT_NAME = "$(TARGET_NAME)";
+				VERSIONING_SYSTEM = "apple-generic";
 			};
 			name = Debug;
 		};
@@ -393,6 +395,7 @@
 			buildSettings = {
 				ARCHS = arm64;
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CURRENT_PROJECT_VERSION = 1;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
 					"$(inherited)",
@@ -406,6 +409,7 @@
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = {{iosIdentifier}};
 				PRODUCT_NAME = "$(TARGET_NAME)";
+				VERSIONING_SYSTEM = "apple-generic";
 			};
 			name = Release;
 		};
diff --git a/packages/flutter_tools/templates/create/ios-swift.tmpl/Runner.xcodeproj/project.pbxproj.tmpl b/packages/flutter_tools/templates/create/ios-swift.tmpl/Runner.xcodeproj/project.pbxproj.tmpl
index 859b9b5..24449ff 100644
--- a/packages/flutter_tools/templates/create/ios-swift.tmpl/Runner.xcodeproj/project.pbxproj.tmpl
+++ b/packages/flutter_tools/templates/create/ios-swift.tmpl/Runner.xcodeproj/project.pbxproj.tmpl
@@ -369,6 +369,7 @@
 				ARCHS = arm64;
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = 1;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
 					"$(inherited)",
@@ -386,6 +387,7 @@
 				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
 				SWIFT_SWIFT3_OBJC_INFERENCE = On;
 				SWIFT_VERSION = 4.0;
+				VERSIONING_SYSTEM = "apple-generic";
 			};
 			name = Debug;
 		};
@@ -396,6 +398,7 @@
 				ARCHS = arm64;
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = 1;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
 					"$(inherited)",
@@ -412,6 +415,7 @@
 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
 				SWIFT_SWIFT3_OBJC_INFERENCE = On;
 				SWIFT_VERSION = 4.0;
+				VERSIONING_SYSTEM = "apple-generic";
 			};
 			name = Release;
 		};