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
This commit is contained in:
KyleWong 2019-02-13 23:48:03 +08:00 committed by Greg Spencer
parent f2004b8f43
commit 4b4a9400b7
13 changed files with 199 additions and 93 deletions

View File

@ -274,9 +274,9 @@ void updateLocalProperties({
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());
}

View File

@ -353,7 +353,7 @@ class JITSnapshotter {
@required String outputPath,
@required String compilationTraceFilePath,
@required bool createPatch,
int buildNumber,
String buildNumber,
String baselineDir,
List<String> extraGenSnapshotOptions = const <String>[],
}) async {

View File

@ -83,7 +83,7 @@ class BuildInfo {
/// 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 @@ enum BuildMode {
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) {

View File

@ -61,7 +61,7 @@ Future<void> build({
bool trackWidgetCreation = false,
String compilationTraceFilePath,
bool createPatch = false,
int buildNumber,
String buildNumber,
String baselineDir,
List<String> extraFrontEndOptions = const <String>[],
List<String> extraGenSnapshotOptions = const <String>[],

View File

@ -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 @@ 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);
}
final String buildNumber = argResults['build-number'] != null ? argResults['build-number'] : null;
await build(
platform: platform,

View File

@ -6,6 +6,7 @@ import 'dart:async';
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 'cache.dart';
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 @@ class FlutterManifest {
/// 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 @@ class FlutterManifest {
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;
}

View File

@ -62,12 +62,12 @@ Future<void> updateGeneratedXcodeProperties({
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');
}

View File

@ -199,12 +199,12 @@ abstract class FlutterCommand extends Command<void> {
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 @@ abstract class FlutterCommand extends Command<void> {
? 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 {

View File

@ -337,7 +337,7 @@ dependencies:
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 @@ dependencies:
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 @@ dependencies:
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 @@ dependencies:
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 @@ flutter:
);
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',
);

View File

@ -860,7 +860,7 @@ void main() {
outputPath: 'build/foo',
compilationTraceFilePath: kTrace,
createPatch: true,
buildNumber: 100,
buildNumber: '100',
baselineDir: '.baseline',
);
@ -913,7 +913,7 @@ void main() {
outputPath: 'build/foo',
compilationTraceFilePath: kTrace,
createPatch: true,
buildNumber: 100,
buildNumber: '100',
baselineDir: '.baseline',
);
@ -974,7 +974,7 @@ void main() {
outputPath: 'build/foo',
compilationTraceFilePath: kTrace,
createPatch: true,
buildNumber: 100,
buildNumber: '100',
baselineDir: '.baseline',
);
@ -1017,7 +1017,7 @@ void main() {
outputPath: 'build/foo',
compilationTraceFilePath: kTrace,
createPatch: true,
buildNumber: 100,
buildNumber: '100',
baselineDir: '.baseline',
);

View File

@ -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+-');
});
});
}

View File

@ -388,7 +388,7 @@ flutter:
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 @@ flutter:
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 @@ flutter:
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 @@ flutter:
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',
);
});

View File

@ -450,7 +450,7 @@ dependencies:
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 @@ dependencies:
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 @@ dependencies:
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 @@ dependencies:
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,