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;
};