Commit 4b4a9400 authored by KyleWong's avatar KyleWong Committed by Greg Spencer

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
parent f2004b8f
......@@ -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());
}
......
......@@ -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 {
......
......@@ -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) {
......
......@@ -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>[],
......
......@@ -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,
......
......@@ -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;
}
......
......@@ -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');
}
......
......@@ -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 {
......
......@@ -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',
);
......
......@@ -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',
);
......
// 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+-');
});
});
}
......@@ -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,14 +409,14 @@ flutter:
manifest: manifest,
expectedAppVersion: '1.0.0+2',
expectedBuildName: '1.0.0',
expectedBuildNumber: 2,
expectedBuildNumber: '2',
);
});
test('parses major.minor+build version clause', () async {
test('parses major.minor.patch+build version clause 2', () async {
const String manifest = '''
name: test
version: 1.0+2
version: 1.0.0-beta+exp.sha.5114f85
dependencies:
flutter:
sdk: flutter
......@@ -424,33 +424,16 @@ flutter:
''';
await checkManifestVersion(
manifest: manifest,
expectedAppVersion: '1.0+2',
expectedBuildName: '1.0',
expectedBuildNumber: 2,
expectedAppVersion: '1.0.0-beta+exp.sha.5114f85',
expectedBuildName: '1.0.0-beta',
expectedBuildNumber: 'exp.sha.5114f85',
);
});
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 {
test('parses major.minor+build version clause', () async {
const String manifest = '''
name: test
version: 1
version: 1.0+2
dependencies:
flutter:
sdk: flutter
......@@ -458,9 +441,9 @@ flutter:
''';
await checkManifestVersion(
manifest: manifest,
expectedAppVersion: '1',
expectedBuildName: '1',
expectedBuildNumber: null,
expectedAppVersion: '1.0+2',
expectedBuildName: '1.0',
expectedBuildNumber: '2',
);
});
......
......@@ -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,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment