Commit 89570732 authored by Ralph Bergmann's avatar Ralph Bergmann Committed by Todd Volkert

improve Flutter build commands (#15788)

add --buildNumber and --buildName to flutter build like
flutter build apk --buildNumber=42 --buildName=1.0.42
parent 2c898f68
......@@ -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 @@ void updateLocalProperties({String projectPath, BuildInfo buildInfo}) {
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();
......
......@@ -19,6 +19,8 @@ class BuildInfo {
this.targetPlatform,
this.fileSystemRoots,
this.fileSystemScheme,
this.buildNumber,
this.buildName,
});
final BuildMode mode;
......@@ -52,6 +54,19 @@ class BuildInfo {
/// 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);
......
......@@ -13,6 +13,8 @@ class BuildApkCommand extends BuildSubCommand {
addBuildModeFlags();
usesFlavorOption();
usesPubOption();
usesBuildNumberOption();
usesBuildNameOption();
argParser
..addFlag('preview-dart-2',
......
......@@ -17,6 +17,8 @@ class BuildIOSCommand extends BuildSubCommand {
usesTargetOption();
usesFlavorOption();
usesPubOption();
usesBuildNumberOption();
usesBuildNameOption();
argParser
..addFlag('debug',
negatable: false,
......
......@@ -252,6 +252,53 @@ Future<XcodeBuildResult> buildXcodeProject({
);
}
// 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(
......
......@@ -122,7 +122,26 @@ abstract class FlutterCommand extends Command<Null> {
_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 @@ abstract class FlutterCommand extends Command<Null> {
'--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 @@ abstract class FlutterCommand extends Command<Null> {
? 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,
);
}
......
......@@ -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;
};
......
......@@ -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;
};
......
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