Refactor build-number/build-name logic. (#27743)
This PR aims at several things:
1. Use pub_semver to check a version in pubspec.yaml meets the requirements specified in https://semver.org/.
2. Don't limit build-number/build-name as a fixed format. Instead, validate it according to the target(ios/android).
3. Make sure that build-number/build-name are always validated no matter it's specified by the `flutter command` or version in pubspec.yaml.
Fixes #27589
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index 8ab267f..6f3eea9 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -274,9 +274,9 @@
if (buildInfo != null) {
changeIfNecessary('flutter.buildMode', buildInfo.modeName);
- final String buildName = buildInfo.buildName ?? manifest.buildName;
+ final String buildName = validatedBuildNameForPlatform(TargetPlatform.android_arm, buildInfo.buildName ?? manifest.buildName);
changeIfNecessary('flutter.versionName', buildName);
- final int buildNumber = buildInfo.buildNumber ?? manifest.buildNumber;
+ final String buildNumber = validatedBuildNumberForPlatform(TargetPlatform.android_arm, buildInfo.buildNumber ?? manifest.buildNumber);
changeIfNecessary('flutter.versionCode', buildNumber?.toString());
}
diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart
index 926eccd..309f8ff 100644
--- a/packages/flutter_tools/lib/src/base/build.dart
+++ b/packages/flutter_tools/lib/src/base/build.dart
@@ -353,7 +353,7 @@
@required String outputPath,
@required String compilationTraceFilePath,
@required bool createPatch,
- int buildNumber,
+ String buildNumber,
String baselineDir,
List<String> extraGenSnapshotOptions = const <String>[],
}) async {
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index 0e63342..cd8ce05 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -83,7 +83,7 @@
/// 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;
+ final String 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.
@@ -141,6 +141,83 @@
dynamicRelease
}
+String validatedBuildNumberForPlatform(TargetPlatform targetPlatform, String buildNumber) {
+ if (buildNumber == null) {
+ return null;
+ }
+ if (targetPlatform == TargetPlatform.ios ||
+ targetPlatform == TargetPlatform.darwin_x64) {
+ // See CFBundleVersion at https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+ final RegExp disallowed = RegExp(r'[^\d\.]');
+ String tmpBuildNumber = buildNumber.replaceAll(disallowed, '');
+ final List<String> segments = tmpBuildNumber
+ .split('.')
+ .where((String segment) => segment.isNotEmpty)
+ .toList();
+ if (segments.isEmpty) {
+ segments.add('0');
+ }
+ tmpBuildNumber = segments.join('.');
+ if (tmpBuildNumber != buildNumber) {
+ printTrace('Invalid build-number: $buildNumber for iOS/macOS, overridden by $tmpBuildNumber.\n'
+ 'See CFBundleVersion at https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html');
+ }
+ return tmpBuildNumber;
+ }
+ if (targetPlatform == TargetPlatform.android_arm ||
+ targetPlatform == TargetPlatform.android_arm64 ||
+ targetPlatform == TargetPlatform.android_x64 ||
+ targetPlatform == TargetPlatform.android_x86) {
+ // See versionCode at https://developer.android.com/studio/publish/versioning
+ final RegExp disallowed = RegExp(r'[^\d]');
+ String tmpBuildNumberStr = buildNumber.replaceAll(disallowed, '');
+ int tmpBuildNumberInt = int.tryParse(tmpBuildNumberStr) ?? 0;
+ if (tmpBuildNumberInt < 1) {
+ tmpBuildNumberInt = 1;
+ }
+ tmpBuildNumberStr = tmpBuildNumberInt.toString();
+ if (tmpBuildNumberStr != buildNumber) {
+ printTrace('Invalid build-number: $buildNumber for Android, overridden by $tmpBuildNumberStr.\n'
+ 'See versionCode at https://developer.android.com/studio/publish/versioning');
+ }
+ return tmpBuildNumberStr;
+ }
+ return buildNumber;
+}
+
+String validatedBuildNameForPlatform(TargetPlatform targetPlatform, String buildName) {
+ if (buildName == null) {
+ return null;
+ }
+ if (targetPlatform == TargetPlatform.ios ||
+ targetPlatform == TargetPlatform.darwin_x64) {
+ // See CFBundleShortVersionString at https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+ final RegExp disallowed = RegExp(r'[^\d\.]');
+ String tmpBuildName = buildName.replaceAll(disallowed, '');
+ final List<String> segments = tmpBuildName
+ .split('.')
+ .where((String segment) => segment.isNotEmpty)
+ .toList();
+ while (segments.length < 3) {
+ segments.add('0');
+ }
+ tmpBuildName = segments.join('.');
+ if (tmpBuildName != buildName) {
+ printTrace('Invalid build-name: $buildName for iOS/macOS, overridden by $tmpBuildName.\n'
+ 'See CFBundleShortVersionString at https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html');
+ }
+ return tmpBuildName;
+ }
+ if (targetPlatform == TargetPlatform.android_arm ||
+ targetPlatform == TargetPlatform.android_arm64 ||
+ targetPlatform == TargetPlatform.android_x64 ||
+ targetPlatform == TargetPlatform.android_x86) {
+ // See versionName at https://developer.android.com/studio/publish/versioning
+ return buildName;
+ }
+ return buildName;
+}
+
String getModeName(BuildMode mode) => getEnumName(mode);
String getFriendlyModeName(BuildMode mode) {
diff --git a/packages/flutter_tools/lib/src/bundle.dart b/packages/flutter_tools/lib/src/bundle.dart
index 4b9baed..856d219 100644
--- a/packages/flutter_tools/lib/src/bundle.dart
+++ b/packages/flutter_tools/lib/src/bundle.dart
@@ -61,7 +61,7 @@
bool trackWidgetCreation = false,
String compilationTraceFilePath,
bool createPatch = false,
- int buildNumber,
+ String buildNumber,
String baselineDir,
List<String> extraFrontEndOptions = const <String>[],
List<String> extraGenSnapshotOptions = const <String>[],
diff --git a/packages/flutter_tools/lib/src/commands/build_bundle.dart b/packages/flutter_tools/lib/src/commands/build_bundle.dart
index 2fff12f..fed385d 100644
--- a/packages/flutter_tools/lib/src/commands/build_bundle.dart
+++ b/packages/flutter_tools/lib/src/commands/build_bundle.dart
@@ -4,8 +4,6 @@
import 'dart:async';
-import 'package:args/command_runner.dart';
-
import '../base/common.dart';
import '../build_info.dart';
import '../bundle.dart';
@@ -72,14 +70,7 @@
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);
- }
+ final String buildNumber = argResults['build-number'] != null ? argResults['build-number'] : null;
await build(
platform: platform,
diff --git a/packages/flutter_tools/lib/src/flutter_manifest.dart b/packages/flutter_tools/lib/src/flutter_manifest.dart
index 142b810..d9b878a 100644
--- a/packages/flutter_tools/lib/src/flutter_manifest.dart
+++ b/packages/flutter_tools/lib/src/flutter_manifest.dart
@@ -6,6 +6,7 @@
import 'package:json_schema/json_schema.dart';
import 'package:meta/meta.dart';
+import 'package:pub_semver/pub_semver.dart';
import 'package:yaml/yaml.dart';
import 'base/file_system.dart';
@@ -15,8 +16,6 @@
import 'convert.dart' as convert;
import 'globals.dart';
-final RegExp _versionPattern = RegExp(r'^(\d+)(\.(\d+)(\.(\d+))?)?(\+(\d+))?$');
-
/// A wrapper around the `flutter` section in the `pubspec.yaml` file.
class FlutterManifest {
FlutterManifest._();
@@ -83,16 +82,21 @@
/// The version String from the `pubspec.yaml` file.
/// Can be null if it isn't set or has a wrong format.
String get appVersion {
- final String version = _descriptor['version']?.toString();
- if (version != null) {
- if (_versionPattern.hasMatch(version)) {
- return version;
- } else if (!_hasShowInvalidVersionMsg) {
- printStatus(userMessages.invalidVersionSettingHintMessage(version), emphasis: true);
+ final String verStr = _descriptor['version']?.toString();
+ if (verStr == null) {
+ return null;
+ }
+
+ Version version;
+ try {
+ version = Version.parse(verStr);
+ } on Exception {
+ if (!_hasShowInvalidVersionMsg) {
+ printStatus(userMessages.invalidVersionSettingHintMessage(verStr), emphasis: true);
_hasShowInvalidVersionMsg = true;
}
}
- return null;
+ return version?.toString();
}
/// The build version name from the `pubspec.yaml` file.
@@ -100,16 +104,17 @@
String get buildName {
if (appVersion != null && appVersion.contains('+'))
return appVersion.split('+')?.elementAt(0);
- else
+ else {
return appVersion;
+ }
}
/// The build version number from the `pubspec.yaml` file.
/// Can be null if version isn't set or has a wrong format.
- int get buildNumber {
+ String get buildNumber {
if (appVersion != null && appVersion.contains('+')) {
final String value = appVersion.split('+')?.elementAt(1);
- return value == null ? null : int.tryParse(value);
+ return value;
} else {
return null;
}
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
index 79a02ac..5d7d460 100644
--- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart
+++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -62,12 +62,12 @@
localsBuffer.writeln('FLUTTER_FRAMEWORK_DIR=${flutterFrameworkDir(buildInfo.mode)}');
}
- final String buildName = buildInfo?.buildName ?? project.manifest.buildName;
+ final String buildName = validatedBuildNameForPlatform(TargetPlatform.ios, buildInfo?.buildName ?? project.manifest.buildName);
if (buildName != null) {
localsBuffer.writeln('FLUTTER_BUILD_NAME=$buildName');
}
- final int buildNumber = buildInfo?.buildNumber ?? project.manifest.buildNumber;
+ final String buildNumber = validatedBuildNumberForPlatform(TargetPlatform.ios, buildInfo?.buildNumber ?? project.manifest.buildNumber);
if (buildNumber != null) {
localsBuffer.writeln('FLUTTER_BUILD_NUMBER=$buildNumber');
}
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 17e2a5b..f3c903d 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -199,12 +199,12 @@
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'
+ help: 'An identifier used as an internal version number.\n'
+ 'Each build must have a unique identifier 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() {
@@ -370,15 +370,9 @@
? argResults['track-widget-creation']
: false;
- int buildNumber;
- try {
- buildNumber = argParser.options.containsKey('build-number') && argResults['build-number'] != null
- ? int.parse(argResults['build-number'])
- : null;
- } catch (e) {
- throw UsageException(
- '--build-number (${argResults['build-number']}) must be an int.', null);
- }
+ final String buildNumber = argParser.options.containsKey('build-number') && argResults['build-number'] != null
+ ? argResults['build-number']
+ : null;
int patchNumber;
try {
diff --git a/packages/flutter_tools/test/android/gradle_test.dart b/packages/flutter_tools/test/android/gradle_test.dart
index 9cbf11a..c25d2ad 100644
--- a/packages/flutter_tools/test/android/gradle_test.dart
+++ b/packages/flutter_tools/test/android/gradle_test.dart
@@ -337,7 +337,7 @@
sdk: flutter
flutter:
''';
- const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildNumber: 3);
+ const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildNumber: '3');
await checkBuildVersion(
manifest: manifest,
buildInfo: buildInfo,
@@ -355,7 +355,7 @@
sdk: flutter
flutter:
''';
- const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: 3);
+ const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
await checkBuildVersion(
manifest: manifest,
buildInfo: buildInfo,
@@ -373,7 +373,7 @@
sdk: flutter
flutter:
''';
- const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: 3);
+ const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
await checkBuildVersion(
manifest: manifest,
buildInfo: buildInfo,
@@ -390,7 +390,7 @@
sdk: flutter
flutter:
''';
- const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: 3);
+ const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
await checkBuildVersion(
manifest: manifest,
buildInfo: buildInfo,
@@ -415,13 +415,13 @@
);
await checkBuildVersion(
manifest: manifest,
- buildInfo: const BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: 3),
+ buildInfo: const BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3'),
expectedBuildName: '1.0.2',
expectedBuildNumber: '3',
);
await checkBuildVersion(
manifest: manifest,
- buildInfo: const BuildInfo(BuildMode.release, null, buildName: '1.0.3', buildNumber: 4),
+ buildInfo: const BuildInfo(BuildMode.release, null, buildName: '1.0.3', buildNumber: '4'),
expectedBuildName: '1.0.3',
expectedBuildNumber: '4',
);
diff --git a/packages/flutter_tools/test/base/build_test.dart b/packages/flutter_tools/test/base/build_test.dart
index de2ff43..3fc832e 100644
--- a/packages/flutter_tools/test/base/build_test.dart
+++ b/packages/flutter_tools/test/base/build_test.dart
@@ -860,7 +860,7 @@
outputPath: 'build/foo',
compilationTraceFilePath: kTrace,
createPatch: true,
- buildNumber: 100,
+ buildNumber: '100',
baselineDir: '.baseline',
);
@@ -913,7 +913,7 @@
outputPath: 'build/foo',
compilationTraceFilePath: kTrace,
createPatch: true,
- buildNumber: 100,
+ buildNumber: '100',
baselineDir: '.baseline',
);
@@ -974,7 +974,7 @@
outputPath: 'build/foo',
compilationTraceFilePath: kTrace,
createPatch: true,
- buildNumber: 100,
+ buildNumber: '100',
baselineDir: '.baseline',
);
@@ -1017,7 +1017,7 @@
outputPath: 'build/foo',
compilationTraceFilePath: kTrace,
createPatch: true,
- buildNumber: 100,
+ buildNumber: '100',
baselineDir: '.baseline',
);
diff --git a/packages/flutter_tools/test/build_info_test.dart b/packages/flutter_tools/test/build_info_test.dart
new file mode 100644
index 0000000..acd2a34
--- /dev/null
+++ b/packages/flutter_tools/test/build_info_test.dart
@@ -0,0 +1,56 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+
+import 'package:flutter_tools/src/build_info.dart';
+
+import 'src/common.dart';
+import 'src/context.dart';
+
+void main() {
+ setUpAll(() {
+ });
+
+ group('Validate build number', () {
+ setUp(() async {
+ });
+
+ testUsingContext('CFBundleVersion for iOS', () async {
+ String buildName = validatedBuildNumberForPlatform(TargetPlatform.ios, 'xyz');
+ expect(buildName, '0');
+ buildName = validatedBuildNumberForPlatform(TargetPlatform.ios, '123.xyz');
+ expect(buildName, '123');
+ buildName = validatedBuildNumberForPlatform(TargetPlatform.ios, '123.456.xyz');
+ expect(buildName, '123.456');
+ });
+
+ testUsingContext('versionCode for Android', () async {
+ String buildName = validatedBuildNumberForPlatform(TargetPlatform.android_arm, '123.abc+-');
+ expect(buildName, '123');
+ buildName = validatedBuildNumberForPlatform(TargetPlatform.android_arm, 'abc');
+ expect(buildName, '1');
+ });
+ });
+
+ group('Validate build name', () {
+ setUp(() async {
+ });
+
+ testUsingContext('CFBundleShortVersionString for iOS', () async {
+ String buildName = validatedBuildNameForPlatform(TargetPlatform.ios, 'xyz');
+ expect(buildName, '0.0.0');
+ buildName = validatedBuildNameForPlatform(TargetPlatform.ios, '123.456.xyz');
+ expect(buildName, '123.456.0');
+ buildName = validatedBuildNameForPlatform(TargetPlatform.ios, '123.xyz');
+ expect(buildName, '123.0.0');
+ });
+
+ testUsingContext('versionName for Android', () async {
+ String buildName = validatedBuildNameForPlatform(TargetPlatform.android_arm, '123.abc+-');
+ expect(buildName, '123.abc+-');
+ buildName = validatedBuildNameForPlatform(TargetPlatform.android_arm, 'abc+-');
+ expect(buildName, 'abc+-');
+ });
+ });
+}
diff --git a/packages/flutter_tools/test/flutter_manifest_test.dart b/packages/flutter_tools/test/flutter_manifest_test.dart
index 894a1bc..424c9da 100644
--- a/packages/flutter_tools/test/flutter_manifest_test.dart
+++ b/packages/flutter_tools/test/flutter_manifest_test.dart
@@ -388,7 +388,7 @@
String manifest,
String expectedAppVersion,
String expectedBuildName,
- int expectedBuildNumber,
+ String expectedBuildNumber,
}) async {
final FlutterManifest flutterManifest = await FlutterManifest.createFromString(manifest);
expect(flutterManifest.appVersion, expectedAppVersion);
@@ -396,7 +396,7 @@
expect(flutterManifest.buildNumber, expectedBuildNumber);
}
- test('parses major.minor.patch+build version clause', () async {
+ test('parses major.minor.patch+build version clause 1', () async {
const String manifest = '''
name: test
version: 1.0.0+2
@@ -409,7 +409,24 @@
manifest: manifest,
expectedAppVersion: '1.0.0+2',
expectedBuildName: '1.0.0',
- expectedBuildNumber: 2,
+ expectedBuildNumber: '2',
+ );
+ });
+
+ test('parses major.minor.patch+build version clause 2', () async {
+ const String manifest = '''
+name: test
+version: 1.0.0-beta+exp.sha.5114f85
+dependencies:
+ flutter:
+ sdk: flutter
+flutter:
+''';
+ await checkManifestVersion(
+ manifest: manifest,
+ expectedAppVersion: '1.0.0-beta+exp.sha.5114f85',
+ expectedBuildName: '1.0.0-beta',
+ expectedBuildNumber: 'exp.sha.5114f85',
);
});
@@ -426,41 +443,7 @@
manifest: manifest,
expectedAppVersion: '1.0+2',
expectedBuildName: '1.0',
- expectedBuildNumber: 2,
- );
- });
-
- test('parses major+build version clause', () async {
- const String manifest = '''
-name: test
-version: 1+2
-dependencies:
- flutter:
- sdk: flutter
-flutter:
-''';
- await checkManifestVersion(
- manifest: manifest,
- expectedAppVersion: '1+2',
- expectedBuildName: '1',
- expectedBuildNumber: 2,
- );
- });
-
- test('parses major version clause', () async {
- const String manifest = '''
-name: test
-version: 1
-dependencies:
- flutter:
- sdk: flutter
-flutter:
-''';
- await checkManifestVersion(
- manifest: manifest,
- expectedAppVersion: '1',
- expectedBuildName: '1',
- expectedBuildNumber: null,
+ expectedBuildNumber: '2',
);
});
diff --git a/packages/flutter_tools/test/ios/xcodeproj_test.dart b/packages/flutter_tools/test/ios/xcodeproj_test.dart
index 874f897..2769cdc 100644
--- a/packages/flutter_tools/test/ios/xcodeproj_test.dart
+++ b/packages/flutter_tools/test/ios/xcodeproj_test.dart
@@ -450,7 +450,7 @@
sdk: flutter
flutter:
''';
- const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildNumber: 3);
+ const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildNumber: '3');
await checkBuildVersion(
manifestString: manifest,
buildInfo: buildInfo,
@@ -468,7 +468,7 @@
sdk: flutter
flutter:
''';
- const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: 3);
+ const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
await checkBuildVersion(
manifestString: manifest,
buildInfo: buildInfo,
@@ -486,7 +486,7 @@
sdk: flutter
flutter:
''';
- const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: 3);
+ const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
await checkBuildVersion(
manifestString: manifest,
buildInfo: buildInfo,
@@ -503,7 +503,7 @@
sdk: flutter
flutter:
''';
- const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: 3);
+ const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
await checkBuildVersion(
manifestString: manifest,
buildInfo: buildInfo,