Support for app flavors in flutter tooling, #11676 retake (#11734)
diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index 6175d96..6f2d0ec 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -338,8 +338,7 @@
@override
Future<LaunchResult> startApp(
- ApplicationPackage package,
- BuildMode mode, {
+ ApplicationPackage package, {
String mainPath,
String route,
DebuggingOptions debuggingOptions,
@@ -352,7 +351,7 @@
if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
return new LaunchResult.failed();
- if (await targetPlatform != TargetPlatform.android_arm && mode != BuildMode.debug) {
+ if (await targetPlatform != TargetPlatform.android_arm && !debuggingOptions.buildInfo.isDebug) {
printError('Profile and release builds are only supported on ARM targets.');
return new LaunchResult.failed();
}
@@ -361,7 +360,7 @@
printTrace('Building APK');
await buildApk(
target: mainPath,
- buildMode: debuggingOptions.buildMode,
+ buildInfo: debuggingOptions.buildInfo,
kernelPath: kernelPath,
);
// Package has been built, so we can get the updated application ID and
@@ -408,7 +407,7 @@
if (debuggingOptions.enableSoftwareRendering)
cmd.addAll(<String>['--ez', 'enable-software-rendering', 'true']);
if (debuggingOptions.debuggingEnabled) {
- if (debuggingOptions.buildMode == BuildMode.debug)
+ if (debuggingOptions.buildInfo.isDebug)
cmd.addAll(<String>['--ez', 'enable-checked-mode', 'true']);
if (debuggingOptions.startPaused)
cmd.addAll(<String>['--ez', 'start-paused', 'true']);
@@ -435,13 +434,13 @@
try {
Uri observatoryUri, diagnosticUri;
- if (debuggingOptions.buildMode == BuildMode.debug) {
+ if (debuggingOptions.buildInfo.isDebug) {
final List<Uri> deviceUris = await Future.wait(
<Future<Uri>>[observatoryDiscovery.uri, diagnosticDiscovery.uri]
);
observatoryUri = deviceUris[0];
diagnosticUri = deviceUris[1];
- } else if (debuggingOptions.buildMode == BuildMode.profile) {
+ } else if (debuggingOptions.buildInfo.isProfile) {
observatoryUri = await observatoryDiscovery.uri;
}
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index 25d4083..05d7c67 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -23,8 +23,9 @@
const String gradleAppOutV1 = 'android/app/build/outputs/apk/app-debug.apk';
const String gradleAppOutDirV1 = 'android/app/build/outputs/apk';
const String gradleVersion = '3.3';
+final RegExp _assembleTaskPattern = new RegExp(r'assemble([^:]+): task ');
-String _cachedGradleAppOutDirV2;
+GradleProject _cachedGradleProject;
String _cachedGradleExecutable;
enum FlutterPluginVersion {
@@ -58,6 +59,8 @@
return FlutterPluginVersion.none;
}
+/// Returns the path to the apk file created by [buildGradleProject], relative
+/// to current directory.
Future<String> getGradleAppOut() async {
switch (flutterPluginVersion) {
case FlutterPluginVersion.none:
@@ -67,19 +70,19 @@
case FlutterPluginVersion.managed:
// Fall through. The managed plugin matches plugin v2 for now.
case FlutterPluginVersion.v2:
- return '${await _getGradleAppOutDirV2()}/app.apk';
+ return fs.path.relative(fs.path.join((await _gradleProject()).apkDirectory, 'app.apk'));
}
return null;
}
-Future<String> _getGradleAppOutDirV2() async {
- _cachedGradleAppOutDirV2 ??= await _calculateGradleAppOutDirV2();
- return _cachedGradleAppOutDirV2;
+Future<GradleProject> _gradleProject() async {
+ _cachedGradleProject ??= await _readGradleProject();
+ return _cachedGradleProject;
}
// Note: Dependencies are resolved and possibly downloaded as a side-effect
// of calculating the app properties using Gradle. This may take minutes.
-Future<String> _calculateGradleAppOutDirV2() async {
+Future<GradleProject> _readGradleProject() async {
final String gradle = await _ensureGradle();
updateLocalProperties();
try {
@@ -90,28 +93,20 @@
environment: _gradleEnv,
);
final String properties = runResult.stdout.trim();
- String buildDir = properties
- .split('\n')
- .firstWhere((String s) => s.startsWith('buildDir: '))
- .substring('buildDir: '.length)
- .trim();
- final String currentDirectory = fs.currentDirectory.path;
- if (buildDir.startsWith(currentDirectory)) {
- // Relativize path, snip current directory + separating '/'.
- buildDir = buildDir.substring(currentDirectory.length + 1);
- }
+ final GradleProject project = new GradleProject.fromAppProperties(properties);
status.stop();
- return '$buildDir/outputs/apk';
+ return project;
} catch (e) {
printError('Error running gradle: $e');
}
// Fall back to the default
- return gradleAppOutDirV1;
+ return new GradleProject(<String>['debug', 'profile', 'release'], <String>[], gradleAppOutDirV1);
}
String _locateProjectGradlew({ bool ensureExecutable: true }) {
final String path = fs.path.join(
- 'android', platform.isWindows ? 'gradlew.bat' : 'gradlew'
+ 'android',
+ platform.isWindows ? 'gradlew.bat' : 'gradlew',
);
if (fs.isFileSync(path)) {
@@ -164,7 +159,7 @@
}
/// Create android/local.properties if needed, and update Flutter settings.
-void updateLocalProperties({String projectPath, String buildMode}) {
+void updateLocalProperties({String projectPath, BuildInfo buildInfo}) {
final File localProperties = (projectPath == null)
? fs.file(fs.path.join('android', 'local.properties'))
: fs.file(fs.path.join(projectPath, 'android', 'local.properties'));
@@ -183,8 +178,8 @@
settings.values['flutter.sdk'] = escapedRoot;
changed = true;
}
- if (buildMode != null && settings.values['flutter.buildMode'] != buildMode) {
- settings.values['flutter.buildMode'] = buildMode;
+ if (buildInfo != null && settings.values['flutter.buildMode'] != buildInfo.modeName) {
+ settings.values['flutter.buildMode'] = buildInfo.modeName;
changed = true;
}
@@ -192,13 +187,12 @@
settings.writeContents(localProperties);
}
-Future<Null> buildGradleProject(BuildMode buildMode, String target, String kernelPath) async {
+Future<Null> buildGradleProject(BuildInfo buildInfo, String target, String kernelPath) 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.
- final String buildModeName = getModeName(buildMode);
- updateLocalProperties(buildMode: buildModeName);
+ updateLocalProperties(buildInfo: buildInfo);
injectPlugins();
@@ -212,7 +206,7 @@
case FlutterPluginVersion.managed:
// Fall through. Managed plugin builds the same way as plugin v2.
case FlutterPluginVersion.v2:
- return _buildGradleProjectV2(gradle, buildModeName, target, kernelPath);
+ return _buildGradleProjectV2(gradle, buildInfo, target, kernelPath);
}
}
@@ -234,21 +228,25 @@
printStatus('Built $gradleAppOutV1 (${getSizeAsMB(apkFile.lengthSync())}).');
}
-File findApkFile(String buildDirectory, String buildModeName) {
- final String apkFilename = 'app-$buildModeName.apk';
- File apkFile = fs.file('$buildDirectory/$apkFilename');
- if (apkFile.existsSync())
- return apkFile;
- apkFile = fs.file('$buildDirectory/$buildModeName/$apkFilename');
- if (apkFile.existsSync())
- return apkFile;
- return null;
-}
-
-Future<Null> _buildGradleProjectV2(String gradle, String buildModeName, String target, String kernelPath) async {
- final String assembleTask = "assemble${toTitleCase(buildModeName)}";
-
- // Run 'gradlew assemble<BuildMode>'.
+Future<Null> _buildGradleProjectV2(String gradle, BuildInfo buildInfo, String target, String kernelPath) async {
+ final GradleProject project = await _gradleProject();
+ final String assembleTask = project.assembleTaskFor(buildInfo);
+ if (assembleTask == null) {
+ printError('');
+ printError('The Gradle project does not define a task suitable for the requested build.');
+ if (!project.buildTypes.contains(buildInfo.modeName)) {
+ printError('Review the android/app/build.gradle file and ensure it defines a ${buildInfo.modeName} build type.');
+ } else {
+ if (project.productFlavors.isEmpty) {
+ printError('The android/app/build.gradle file does not define any custom product flavors.');
+ printError('You cannot use the --flavor option.');
+ } else {
+ printError('The android/app/build.gradle file defines product flavors: ${project.productFlavors.join(', ')}');
+ printError('You must specify a --flavor option to select one of them.');
+ }
+ throwToolExit('Gradle build aborted.');
+ }
+ }
final Status status = logger.startProgress('Running \'gradlew $assembleTask\'...', expectSlowOperation: true);
final String gradlePath = fs.file(gradle).absolute.path;
final List<String> command = <String>[gradlePath];
@@ -266,7 +264,7 @@
if (kernelPath != null)
command.add('-Pkernel=$kernelPath');
command.add(assembleTask);
- final int exitcode = await runCommandAndStreamOutput(
+ final int exitCode = await runCommandAndStreamOutput(
command,
workingDirectory: 'android',
allowReentrantFlutter: true,
@@ -274,21 +272,33 @@
);
status.stop();
- if (exitcode != 0)
- throwToolExit('Gradle build failed: $exitcode', exitCode: exitcode);
+ if (exitCode != 0)
+ throwToolExit('Gradle build failed: $exitCode', exitCode: exitCode);
- final String buildDirectory = await _getGradleAppOutDirV2();
- final File apkFile = findApkFile(buildDirectory, buildModeName);
+ final File apkFile = _findApkFile(project, buildInfo);
if (apkFile == null)
throwToolExit('Gradle build failed to produce an Android package.');
// Copy the APK to app.apk, so `flutter run`, `flutter install`, etc. can find it.
- apkFile.copySync('$buildDirectory/app.apk');
+ apkFile.copySync(fs.path.join(project.apkDirectory, 'app.apk'));
- printTrace('calculateSha: $buildDirectory/app.apk');
- final File apkShaFile = fs.file('$buildDirectory/app.apk.sha1');
+ printTrace('calculateSha: ${project.apkDirectory}/app.apk');
+ final File apkShaFile = fs.file(fs.path.join(project.apkDirectory, 'app.apk.sha1'));
apkShaFile.writeAsStringSync(calculateSha(apkFile));
- printStatus('Built ${apkFile.path} (${getSizeAsMB(apkFile.lengthSync())}).');
+ printStatus('Built ${fs.path.relative(apkFile.path)} (${getSizeAsMB(apkFile.lengthSync())}).');
+}
+
+File _findApkFile(GradleProject project, BuildInfo buildInfo) {
+ final String apkFileName = project.apkFileFor(buildInfo);
+ if (apkFileName == null)
+ return null;
+ File apkFile = fs.file(fs.path.join(project.apkDirectory, apkFileName));
+ if (apkFile.existsSync())
+ return apkFile;
+ apkFile = fs.file(fs.path.join(project.apkDirectory, buildInfo.modeName, apkFileName));
+ if (apkFile.existsSync())
+ return apkFile;
+ return null;
}
Map<String, String> get _gradleEnv {
@@ -299,3 +309,83 @@
}
return env;
}
+
+class GradleProject {
+ GradleProject(this.buildTypes, this.productFlavors, this.apkDirectory);
+
+ factory GradleProject.fromAppProperties(String properties) {
+ // Extract build directory.
+ final String buildDir = properties
+ .split('\n')
+ .firstWhere((String s) => s.startsWith('buildDir: '))
+ .substring('buildDir: '.length)
+ .trim();
+
+ // Extract build types and product flavors.
+ final Set<String> variants = new Set<String>();
+ properties.split('\n').forEach((String s) {
+ final Match match = _assembleTaskPattern.matchAsPrefix(s);
+ if (match != null) {
+ final String variant = match.group(1).toLowerCase();
+ if (!variant.endsWith('test'))
+ variants.add(variant);
+ }
+ });
+ final Set<String> buildTypes = new Set<String>();
+ final Set<String> productFlavors = new Set<String>();
+ for (final String variant1 in variants) {
+ for (final String variant2 in variants) {
+ if (variant2.startsWith(variant1) && variant2 != variant1) {
+ final String buildType = variant2.substring(variant1.length);
+ if (variants.contains(buildType)) {
+ buildTypes.add(buildType);
+ productFlavors.add(variant1);
+ }
+ }
+ }
+ }
+ if (productFlavors.isEmpty)
+ buildTypes.addAll(variants);
+ return new GradleProject(
+ buildTypes.toList(),
+ productFlavors.toList(),
+ fs.path.normalize(fs.path.join(buildDir, 'outputs', 'apk')),
+ );
+ }
+
+ final List<String> buildTypes;
+ final List<String> productFlavors;
+ final String apkDirectory;
+
+ String _buildTypeFor(BuildInfo buildInfo) {
+ if (buildTypes.contains(buildInfo.modeName))
+ return buildInfo.modeName;
+ return null;
+ }
+
+ String _productFlavorFor(BuildInfo buildInfo) {
+ if (buildInfo.flavor == null)
+ return productFlavors.isEmpty ? '' : null;
+ else if (productFlavors.contains(buildInfo.flavor.toLowerCase()))
+ return buildInfo.flavor.toLowerCase();
+ else
+ return null;
+ }
+
+ String assembleTaskFor(BuildInfo buildInfo) {
+ final String buildType = _buildTypeFor(buildInfo);
+ final String productFlavor = _productFlavorFor(buildInfo);
+ if (buildType == null || productFlavor == null)
+ return null;
+ return 'assemble${toTitleCase(productFlavor)}${toTitleCase(buildType)}';
+ }
+
+ String apkFileFor(BuildInfo buildInfo) {
+ final String buildType = _buildTypeFor(buildInfo);
+ final String productFlavor = _productFlavorFor(buildInfo);
+ if (buildType == null || productFlavor == null)
+ return null;
+ final String flavorString = productFlavor.isEmpty ? '' : '-' + productFlavor;
+ return 'app$flavorString-$buildType.apk';
+ }
+}
diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart
index a931d36..f348a40 100644
--- a/packages/flutter_tools/lib/src/application_package.dart
+++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -228,7 +228,7 @@
bool get isSwift => buildSettings?.containsKey('SWIFT_VERSION');
String _buildAppPath(String type) {
- return fs.path.join(getIosBuildDirectory(), 'Release-$type', kBundleName);
+ return fs.path.join(getIosBuildDirectory(), type, kBundleName);
}
}
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index 52dc5cf..c9a5662 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -8,10 +8,42 @@
import 'base/utils.dart';
import 'globals.dart';
-enum BuildType {
- prebuilt,
- release,
- debug,
+/// Information about a build to be performed or used.
+class BuildInfo {
+ const BuildInfo(this.mode, this.flavor);
+
+ final BuildMode mode;
+ /// Represents a custom Android product flavor or an Xcode scheme, null for
+ /// using the default.
+ ///
+ /// If not null, the Gradle build task will be `assembleFlavorMode` (e.g.
+ /// `assemblePaidRelease`), and the Xcode build configuration will be
+ /// Mode-Flavor (e.g. Release-Paid).
+ final String flavor;
+
+ 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);
+
+ /// Returns whether a debug build is requested.
+ ///
+ /// Exactly one of [isDebug], [isProfile], or [isRelease] is true.
+ bool get isDebug => mode == BuildMode.debug;
+
+ /// Returns whether a profile build is requested.
+ ///
+ /// Exactly one of [isDebug], [isProfile], or [isRelease] is true.
+ bool get isProfile => mode == BuildMode.profile;
+
+ /// Returns whether a release build is requested.
+ ///
+ /// Exactly one of [isDebug], [isProfile], or [isRelease] is true.
+ bool get isRelease => mode == BuildMode.release;
+
+ bool get usesAot => isAotBuildMode(mode);
+ bool get supportsEmulator => isEmulatorBuildMode(mode);
+ bool get supportsSimulator => isEmulatorBuildMode(mode);
+ String get modeName => getModeName(mode);
}
/// The type of build - `debug`, `profile`, or `release`.
diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart
index 21e5f5b..791b1cc 100644
--- a/packages/flutter_tools/lib/src/commands/build_apk.dart
+++ b/packages/flutter_tools/lib/src/commands/build_apk.dart
@@ -35,6 +35,7 @@
BuildApkCommand() {
usesTargetOption();
addBuildModeFlags();
+ usesFlavorOption();
usesPubOption();
}
@@ -51,14 +52,14 @@
Future<Null> runCommand() async {
await super.runCommand();
- final BuildMode buildMode = getBuildMode();
- await buildApk(buildMode: buildMode, target: targetFile);
+ final BuildInfo buildInfo = getBuildInfo();
+ await buildApk(buildInfo: buildInfo, target: targetFile);
}
}
Future<Null> buildApk({
String target,
- BuildMode buildMode: BuildMode.debug,
+ BuildInfo buildInfo: BuildInfo.debug,
String kernelPath,
}) async {
if (!isProjectUsingGradle()) {
@@ -80,5 +81,5 @@
throwToolExit('Try re-installing or updating your Android SDK.');
}
- return buildGradleProject(buildMode, target, kernelPath);
+ return buildGradleProject(buildInfo, target, kernelPath);
}
diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart
index cfc0992..2724865 100644
--- a/packages/flutter_tools/lib/src/commands/build_ios.dart
+++ b/packages/flutter_tools/lib/src/commands/build_ios.dart
@@ -15,6 +15,7 @@
class BuildIOSCommand extends BuildSubCommand {
BuildIOSCommand() {
usesTargetOption();
+ usesFlavorOption();
usesPubOption();
argParser.addFlag('debug',
negatable: false,
@@ -56,17 +57,17 @@
printStatus('Warning: Building for device with codesigning disabled. You will '
'have to manually codesign before deploying to device.');
}
-
- if (forSimulator && !isEmulatorBuildMode(getBuildMode()))
- throwToolExit('${toTitleCase(getModeName(getBuildMode()))} mode is not supported for emulators.');
+ final BuildInfo buildInfo = getBuildInfo();
+ if (forSimulator && !buildInfo.supportsSimulator)
+ throwToolExit('${toTitleCase(buildInfo.modeName)} mode is not supported for simulators.');
final String logTarget = forSimulator ? 'simulator' : 'device';
- final String typeName = artifacts.getEngineType(TargetPlatform.ios, getBuildMode());
+ final String typeName = artifacts.getEngineType(TargetPlatform.ios, buildInfo.mode);
printStatus('Building $app for $logTarget ($typeName)...');
final XcodeBuildResult result = await buildXcodeProject(
app: app,
- mode: getBuildMode(),
+ buildInfo: buildInfo,
target: targetFile,
buildForDevice: !forSimulator,
codesign: shouldCodesign
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index 0b6f67c..75f4795 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -182,7 +182,7 @@
updateXcodeGeneratedProperties(
projectPath: appPath,
- mode: BuildMode.debug,
+ buildInfo: BuildInfo.debug,
target: flx.defaultMainPath,
hasPlugins: generatePlugin,
);
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index cb069f1..f5d997f 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -302,6 +302,7 @@
final bool useTestFonts = _getBoolArg(args, 'useTestFonts') ?? false;
final String route = _getStringArg(args, 'route');
final String mode = _getStringArg(args, 'mode');
+ final String flavor = _getStringArg(args, 'flavor');
final String target = _getStringArg(args, 'target');
final bool enableHotReload = _getBoolArg(args, 'hot') ?? kHotReloadDefault;
@@ -312,13 +313,13 @@
if (!fs.isDirectorySync(projectDirectory))
throw "'$projectDirectory' does not exist";
- final BuildMode buildMode = getBuildModeForName(mode) ?? BuildMode.debug;
+ final BuildInfo buildInfo = new BuildInfo(getBuildModeForName(mode) ?? BuildMode.debug, flavor);
DebuggingOptions options;
- if (buildMode == BuildMode.release) {
- options = new DebuggingOptions.disabled(buildMode);
+ if (buildInfo.isRelease) {
+ options = new DebuggingOptions.disabled(buildInfo);
} else {
options = new DebuggingOptions.enabled(
- buildMode,
+ buildInfo,
startPaused: startPaused,
useTestFonts: useTestFonts,
);
@@ -349,8 +350,8 @@
String packagesFilePath,
String projectAssets,
}) async {
- if (await device.isLocalEmulator && !isEmulatorBuildMode(options.buildMode))
- throw '${toTitleCase(getModeName(options.buildMode))} mode is not supported for emulators.';
+ if (await device.isLocalEmulator && !options.buildInfo.supportsEmulator)
+ throw '${toTitleCase(options.buildInfo.modeName)} mode is not supported for emulators.';
// We change the current working directory for the duration of the `start` command.
final Directory cwd = fs.currentDirectory;
diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart
index e9ad417..d465092 100644
--- a/packages/flutter_tools/lib/src/commands/drive.dart
+++ b/packages/flutter_tools/lib/src/commands/drive.dart
@@ -8,7 +8,6 @@
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/process.dart';
-import '../build_info.dart';
import '../cache.dart';
import '../dart/package_map.dart';
import '../dart/sdk.dart';
@@ -28,7 +27,7 @@
/// as the `--target` option (defaults to `lib/main.dart`). It then looks for a
/// corresponding test file within the `test_driver` directory. The test file is
/// expected to have the same name but contain the `_test.dart` suffix. The
-/// `_test.dart` file would generall be a Dart program that uses
+/// `_test.dart` file would generally be a Dart program that uses
/// `package:flutter_driver` and exercises your application. Most commonly it
/// is a test written using `package:test`, but you are free to use something
/// else.
@@ -113,7 +112,7 @@
if (argResults['use-existing-app'] == null) {
printStatus('Starting application: $targetFile');
- if (getBuildMode() == BuildMode.release) {
+ if (getBuildInfo().isRelease) {
// This is because we need VM service to be able to drive the app.
throwToolExit(
'Flutter Driver does not support running in release mode.\n'
@@ -267,11 +266,10 @@
final LaunchResult result = await command.device.startApp(
package,
- command.getBuildMode(),
mainPath: mainPath,
route: command.route,
debuggingOptions: new DebuggingOptions.enabled(
- command.getBuildMode(),
+ command.getBuildInfo(),
startPaused: true,
observatoryPort: command.observatoryPort,
diagnosticPort: command.diagnosticPort,
diff --git a/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart
index 118b418..399618d 100644
--- a/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart
+++ b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart
@@ -122,7 +122,7 @@
flutterDevice.observatoryUris = observatoryUris;
final HotRunner hotRunner = new HotRunner(
<FlutterDevice>[flutterDevice],
- debuggingOptions: new DebuggingOptions.enabled(getBuildMode()),
+ debuggingOptions: new DebuggingOptions.enabled(getBuildInfo()),
target: _target,
projectRootPath: _fuchsiaProjectPath,
packagesFilePath: _dotPackagesPath
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 02a8441..7ca63a0 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -23,6 +23,7 @@
// Used by run and drive commands.
RunCommandBase() {
addBuildModeFlags(defaultToRelease: false);
+ usesFlavorOption();
argParser.addFlag('trace-startup',
negatable: true,
defaultsTo: false,
@@ -208,7 +209,7 @@
bool shouldUseHotMode() {
final bool hotArg = argResults['hot'] ?? false;
final bool shouldUseHotMode = hotArg;
- return (getBuildMode() == BuildMode.debug) && shouldUseHotMode;
+ return getBuildInfo().isDebug && shouldUseHotMode;
}
bool get runningWithPrebuiltApplication =>
@@ -228,11 +229,12 @@
}
DebuggingOptions _createDebuggingOptions() {
- if (getBuildMode() == BuildMode.release) {
- return new DebuggingOptions.disabled(getBuildMode());
+ final BuildInfo buildInfo = getBuildInfo();
+ if (buildInfo.isRelease) {
+ return new DebuggingOptions.disabled(buildInfo);
} else {
return new DebuggingOptions.enabled(
- getBuildMode(),
+ buildInfo,
startPaused: argResults['start-paused'],
useTestFonts: argResults['use-test-fonts'],
enableSoftwareRendering: argResults['enable-software-rendering'],
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index e46519a..52434aa 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -235,8 +235,7 @@
/// for iOS device deployment. Set to false if stdin cannot be read from while
/// attempting to start the app.
Future<LaunchResult> startApp(
- ApplicationPackage package,
- BuildMode mode, {
+ ApplicationPackage package, {
String mainPath,
String route,
DebuggingOptions debuggingOptions,
@@ -316,7 +315,7 @@
}
class DebuggingOptions {
- DebuggingOptions.enabled(this.buildMode, {
+ DebuggingOptions.enabled(this.buildInfo, {
this.startPaused: false,
this.enableSoftwareRendering: false,
this.useTestFonts: false,
@@ -324,7 +323,7 @@
this.diagnosticPort
}) : debuggingEnabled = true;
- DebuggingOptions.disabled(this.buildMode) :
+ DebuggingOptions.disabled(this.buildInfo) :
debuggingEnabled = false,
useTestFonts = false,
startPaused = false,
@@ -334,7 +333,7 @@
final bool debuggingEnabled;
- final BuildMode buildMode;
+ final BuildInfo buildInfo;
final bool startPaused;
final bool enableSoftwareRendering;
final bool useTestFonts;
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
index 9f8a8e0..1587577 100644
--- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
@@ -59,8 +59,7 @@
@override
Future<LaunchResult> startApp(
- ApplicationPackage app,
- BuildMode mode, {
+ ApplicationPackage app, {
String mainPath,
String route,
DebuggingOptions debuggingOptions,
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index f97ef42..5db8514 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -164,8 +164,7 @@
@override
Future<LaunchResult> startApp(
- ApplicationPackage app,
- BuildMode mode, {
+ ApplicationPackage app, {
String mainPath,
String route,
DebuggingOptions debuggingOptions,
@@ -182,7 +181,7 @@
// Step 1: Build the precompiled/DBC application if necessary.
final XcodeBuildResult buildResult = await buildXcodeProject(
app: app,
- mode: mode,
+ buildInfo: debuggingOptions.buildInfo,
target: mainPath,
buildForDevice: true,
usesTerminalUi: usesTerminalUi,
@@ -267,7 +266,7 @@
final Future<Uri> forwardObservatoryUri = observatoryDiscovery.uri;
Future<Uri> forwardDiagnosticUri;
- if (debuggingOptions.buildMode == BuildMode.debug) {
+ if (debuggingOptions.buildInfo.isDebug) {
forwardDiagnosticUri = diagnosticDiscovery.uri;
} else {
forwardDiagnosticUri = new Future<Uri>.value(null);
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index 87c9f15..3885332 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -220,7 +220,7 @@
Future<XcodeBuildResult> buildXcodeProject({
BuildableIOSApp app,
- BuildMode mode,
+ BuildInfo buildInfo,
String target: flx.defaultMainPath,
bool buildForDevice,
bool codesign: true,
@@ -234,6 +234,35 @@
return new XcodeBuildResult(success: false);
}
+ final XcodeProjectInfo projectInfo = new XcodeProjectInfo.fromProjectSync(app.appDirectory);
+ if (!projectInfo.targets.contains('Runner')) {
+ printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.');
+ printError('Open Xcode to fix the problem:');
+ printError(' open ios/Runner.xcworkspace');
+ return new XcodeBuildResult(success: false);
+ }
+ final String scheme = projectInfo.schemeFor(buildInfo);
+ if (scheme == null) {
+ printError('');
+ if (projectInfo.definesCustomSchemes) {
+ printError('The Xcode project defines schemes: ${projectInfo.schemes.join(', ')}');
+ printError('You must specify a --flavor option to select one of them.');
+ } else {
+ printError('The Xcode project does not define custom schemes.');
+ printError('You cannot use the --flavor option.');
+ }
+ return new XcodeBuildResult(success: false);
+ }
+ final String configuration = projectInfo.buildConfigurationFor(buildInfo, scheme);
+ if (configuration == null) {
+ printError('');
+ printError('The Xcode project defines build configurations: ${projectInfo.buildConfigurations.join(', ')}');
+ printError('Flutter expects a build configuration named ${XcodeProjectInfo.expectedBuildConfigurationFor(buildInfo, scheme)} or similar.');
+ printError('Open Xcode to fix the problem:');
+ printError(' open ios/Runner.xcworkspace');
+ return new XcodeBuildResult(success: false);
+ }
+
String developmentTeam;
if (codesign && buildForDevice)
developmentTeam = await getCodeSigningIdentityDevelopmentTeam(iosApp: app, usesTerminalUi: usesTerminalUi);
@@ -247,13 +276,13 @@
if (hasFlutterPlugins)
await cocoaPods.processPods(
appIosDir: appDirectory,
- iosEngineDir: flutterFrameworkDir(mode),
+ iosEngineDir: flutterFrameworkDir(buildInfo.mode),
isSwift: app.isSwift,
);
updateXcodeGeneratedProperties(
projectPath: fs.currentDirectory.path,
- mode: mode,
+ buildInfo: buildInfo,
target: target,
hasPlugins: hasFlutterPlugins
);
@@ -264,7 +293,8 @@
'xcodebuild',
'clean',
'build',
- '-configuration', 'Release',
+ '-configuration', configuration,
+ '-scheme', scheme,
'ONLY_ACTIVE_ARCH=YES',
];
@@ -276,7 +306,6 @@
if (fs.path.extension(entity.path) == '.xcworkspace') {
commands.addAll(<String>[
'-workspace', fs.path.basename(entity.path),
- '-scheme', fs.path.basenameWithoutExtension(entity.path),
"BUILD_DIR=${fs.path.absolute(getIosBuildDirectory())}",
]);
break;
@@ -306,7 +335,6 @@
allowReentrantFlutter: true
);
status.stop();
-
if (result.exitCode != 0) {
printStatus('Failed to build iOS app');
if (result.stderr.isNotEmpty) {
@@ -328,13 +356,18 @@
),
);
} else {
- // Look for 'clean build/Release-iphoneos/Runner.app'.
- final RegExp regexp = new RegExp(r' clean (\S*\.app)$', multiLine: true);
+ // Look for 'clean build/<configuration>-<sdk>/Runner.app'.
+ final RegExp regexp = new RegExp(r' clean (.*\.app)$', multiLine: true);
final Match match = regexp.firstMatch(result.stdout);
String outputDir;
- if (match != null)
- outputDir = fs.path.join(app.appDirectory, match.group(1));
- return new XcodeBuildResult(success:true, output: outputDir);
+ if (match != null) {
+ final String actualOutputDir = match.group(1).replaceAll('\\ ', ' ');
+ // Copy app folder to a place where other tools can find it without knowing
+ // the BuildInfo.
+ outputDir = actualOutputDir.replaceFirst('/$configuration-', '/');
+ copyDirectorySync(fs.directory(actualOutputDir), fs.directory(outputDir));
+ }
+ return new XcodeBuildResult(success: true, output: outputDir);
}
}
@@ -356,7 +389,9 @@
printError(noDevelopmentTeamInstruction, emphasis: true);
return;
}
- if (app.id?.contains('com.yourcompany') ?? false) {
+ if (result.xcodeBuildExecution != null &&
+ result.xcodeBuildExecution.buildForPhysicalDevice &&
+ app.id?.contains('com.yourcompany') ?? false) {
printError('');
printError('It appears that your application still contains the default signing identifier.');
printError("Try replacing 'com.yourcompany' with your signing id in Xcode:");
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index 910d16d..03c73e4 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -306,8 +306,7 @@
@override
Future<LaunchResult> startApp(
- ApplicationPackage app,
- BuildMode mode, {
+ ApplicationPackage app, {
String mainPath,
String route,
DebuggingOptions debuggingOptions,
@@ -321,7 +320,7 @@
printTrace('Building ${app.name} for $id.');
try {
- await _setupUpdatedApplicationBundle(app);
+ await _setupUpdatedApplicationBundle(app, debuggingOptions.buildInfo.flavor);
} on ToolExit catch (e) {
printError(e.message);
return new LaunchResult.failed();
@@ -343,7 +342,7 @@
}
if (debuggingOptions.debuggingEnabled) {
- if (debuggingOptions.buildMode == BuildMode.debug)
+ if (debuggingOptions.buildInfo.isDebug)
args.add('--enable-checked-mode');
if (debuggingOptions.startPaused)
args.add('--start-paused');
@@ -395,17 +394,17 @@
return criteria.reduce((bool a, bool b) => a && b);
}
- Future<Null> _setupUpdatedApplicationBundle(ApplicationPackage app) async {
+ Future<Null> _setupUpdatedApplicationBundle(ApplicationPackage app, String flavor) async {
await _sideloadUpdatedAssetsForInstalledApplicationBundle(app);
if (!await _applicationIsInstalledAndRunning(app))
- return _buildAndInstallApplicationBundle(app);
+ return _buildAndInstallApplicationBundle(app, flavor);
}
- Future<Null> _buildAndInstallApplicationBundle(ApplicationPackage app) async {
+ Future<Null> _buildAndInstallApplicationBundle(ApplicationPackage app, String flavor) async {
// Step 1: Build the Xcode project.
// The build mode for the simulator is always debug.
- final XcodeBuildResult buildResult = await buildXcodeProject(app: app, mode: BuildMode.debug, buildForDevice: false);
+ final XcodeBuildResult buildResult = await buildXcodeProject(app: app, buildInfo: new BuildInfo(BuildMode.debug, flavor), buildForDevice: false);
if (!buildResult.success)
throwToolExit('Could not build the application for the simulator.');
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
index 6b66067..fb65f40 100644
--- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart
+++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -7,6 +7,7 @@
import '../artifacts.dart';
import '../base/file_system.dart';
import '../base/process.dart';
+import '../base/utils.dart';
import '../build_info.dart';
import '../cache.dart';
import '../globals.dart';
@@ -20,7 +21,7 @@
void updateXcodeGeneratedProperties({
@required String projectPath,
- @required BuildMode mode,
+ @required BuildInfo buildInfo,
@required String target,
@required bool hasPlugins,
}) {
@@ -38,21 +39,21 @@
localsBuffer.writeln('FLUTTER_TARGET=$target');
// The runtime mode for the current build.
- localsBuffer.writeln('FLUTTER_BUILD_MODE=${getModeName(mode)}');
+ localsBuffer.writeln('FLUTTER_BUILD_MODE=${buildInfo.modeName}');
// The build outputs directory, relative to FLUTTER_APPLICATION_PATH.
localsBuffer.writeln('FLUTTER_BUILD_DIR=${getBuildDirectory()}');
localsBuffer.writeln('SYMROOT=\${SOURCE_ROOT}/../${getIosBuildDirectory()}');
- localsBuffer.writeln('FLUTTER_FRAMEWORK_DIR=${flutterFrameworkDir(mode)}');
+ localsBuffer.writeln('FLUTTER_FRAMEWORK_DIR=${flutterFrameworkDir(buildInfo.mode)}');
if (artifacts is LocalEngineArtifacts) {
final LocalEngineArtifacts localEngineArtifacts = artifacts;
localsBuffer.writeln('LOCAL_ENGINE=${localEngineArtifacts.engineOutPath}');
}
- // Add dependency to CocoaPods' generated project only if plugns are used.
+ // Add dependency to CocoaPods' generated project only if plugins are used.
if (hasPlugins)
localsBuffer.writeln('#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"');
@@ -74,7 +75,6 @@
return settings;
}
-
/// Substitutes variables in [str] with their values from the specified Xcode
/// project and target.
String substituteXcodeVariables(String str, Map<String, String> xcodeBuildSettings) {
@@ -84,3 +84,109 @@
return str.replaceAllMapped(_varExpr, (Match m) => xcodeBuildSettings[m[1]] ?? m[0]);
}
+
+/// Information about an Xcode project.
+///
+/// Represents the output of `xcodebuild -list`.
+class XcodeProjectInfo {
+ XcodeProjectInfo(this.targets, this.buildConfigurations, this.schemes);
+
+ factory XcodeProjectInfo.fromProjectSync(String projectPath) {
+ final String out = runCheckedSync(<String>[
+ '/usr/bin/xcodebuild', '-list',
+ ], workingDirectory: projectPath);
+ return new XcodeProjectInfo.fromXcodeBuildOutput(out);
+ }
+
+ factory XcodeProjectInfo.fromXcodeBuildOutput(String output) {
+ final List<String> targets = <String>[];
+ final List<String> buildConfigurations = <String>[];
+ final List<String> schemes = <String>[];
+ List<String> collector;
+ for (String line in output.split('\n')) {
+ if (line.isEmpty) {
+ collector = null;
+ continue;
+ } else if (line.endsWith('Targets:')) {
+ collector = targets;
+ continue;
+ } else if (line.endsWith('Build Configurations:')) {
+ collector = buildConfigurations;
+ continue;
+ } else if (line.endsWith('Schemes:')) {
+ collector = schemes;
+ continue;
+ }
+ collector?.add(line.trim());
+ }
+ return new XcodeProjectInfo(targets, buildConfigurations, schemes);
+ }
+
+ final List<String> targets;
+ final List<String> buildConfigurations;
+ final List<String> schemes;
+
+ bool get definesCustomTargets => !(targets.contains('Runner') && targets.length == 1);
+ bool get definesCustomSchemes => !(schemes.contains('Runner') && schemes.length == 1);
+ bool get definesCustomBuildConfigurations {
+ return !(buildConfigurations.contains('Debug') &&
+ buildConfigurations.contains('Release') &&
+ buildConfigurations.length == 2);
+ }
+
+ /// The expected scheme for [buildInfo].
+ static String expectedSchemeFor(BuildInfo buildInfo) {
+ return toTitleCase(buildInfo.flavor ?? 'runner');
+ }
+
+ /// The expected build configuration for [buildInfo] and [scheme].
+ static String expectedBuildConfigurationFor(BuildInfo buildInfo, String scheme) {
+ final String baseConfiguration = _baseConfigurationFor(buildInfo);
+ if (buildInfo.flavor == null)
+ return baseConfiguration;
+ else
+ return baseConfiguration + '-$scheme';
+ }
+
+ /// Returns unique scheme matching [buildInfo], or null, if there is no unique
+ /// best match.
+ String schemeFor(BuildInfo buildInfo) {
+ final String expectedScheme = expectedSchemeFor(buildInfo);
+ if (schemes.contains(expectedScheme))
+ return expectedScheme;
+ return _uniqueMatch(schemes, (String candidate) {
+ return candidate.toLowerCase() == expectedScheme.toLowerCase();
+ });
+ }
+
+ /// Returns unique build configuration matching [buildInfo] and [scheme], or
+ /// null, if there is no unique best match.
+ String buildConfigurationFor(BuildInfo buildInfo, String scheme) {
+ final String expectedConfiguration = expectedBuildConfigurationFor(buildInfo, scheme);
+ if (buildConfigurations.contains(expectedConfiguration))
+ return expectedConfiguration;
+ final String baseConfiguration = _baseConfigurationFor(buildInfo);
+ return _uniqueMatch(buildConfigurations, (String candidate) {
+ candidate = candidate.toLowerCase();
+ if (buildInfo.flavor == null)
+ return candidate == expectedConfiguration.toLowerCase();
+ else
+ return candidate.contains(baseConfiguration.toLowerCase()) && candidate.contains(scheme.toLowerCase());
+ });
+ }
+
+ static String _baseConfigurationFor(BuildInfo buildInfo) => buildInfo.isDebug ? 'Debug' : 'Release';
+
+ static String _uniqueMatch(Iterable<String> strings, bool matches(String s)) {
+ final List<String> options = strings.where(matches).toList();
+ if (options.length == 1)
+ return options.first;
+ else
+ return null;
+ }
+
+ @override
+ String toString() {
+ return 'XcodeProjectInfo($targets, $buildConfigurations, $schemes)';
+ }
+}
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index e2dfd13..ecb404c 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -208,7 +208,7 @@
bool shouldBuild,
}) async {
final bool prebuiltMode = hotRunner.applicationBinary != null;
- final String modeName = getModeName(hotRunner.debuggingOptions.buildMode);
+ final String modeName = hotRunner.debuggingOptions.buildInfo.modeName;
printStatus('Launching ${getDisplayPath(hotRunner.mainPath)} on ${device.name} in $modeName mode...');
final TargetPlatform targetPlatform = await device.targetPlatform;
@@ -234,7 +234,6 @@
final bool hasDirtyDependencies = hotRunner.hasDirtyDependencies(this);
final Future<LaunchResult> futureResult = device.startApp(
package,
- hotRunner.debuggingOptions.buildMode,
mainPath: hotRunner.mainPath,
debuggingOptions: hotRunner.debuggingOptions,
platformArgs: platformArgs,
@@ -268,7 +267,7 @@
applicationBinary: coldRunner.applicationBinary
);
- final String modeName = getModeName(coldRunner.debuggingOptions.buildMode);
+ final String modeName = coldRunner.debuggingOptions.buildInfo.modeName;
final bool prebuiltMode = coldRunner.applicationBinary != null;
if (coldRunner.mainPath == null) {
assert(prebuiltMode);
@@ -295,7 +294,6 @@
final bool hasDirtyDependencies = coldRunner.hasDirtyDependencies(this);
final LaunchResult result = await device.startApp(
package,
- coldRunner.debuggingOptions.buildMode,
mainPath: coldRunner.mainPath,
debuggingOptions: coldRunner.debuggingOptions,
platformArgs: platformArgs,
@@ -378,9 +376,9 @@
AssetBundle _assetBundle;
AssetBundle get assetBundle => _assetBundle;
- bool get isRunningDebug => debuggingOptions.buildMode == BuildMode.debug;
- bool get isRunningProfile => debuggingOptions.buildMode == BuildMode.profile;
- bool get isRunningRelease => debuggingOptions.buildMode == BuildMode.release;
+ bool get isRunningDebug => debuggingOptions.buildInfo.isDebug;
+ bool get isRunningProfile => debuggingOptions.buildInfo.isProfile;
+ bool get isRunningRelease => debuggingOptions.buildInfo.isRelease;
bool get supportsServiceProtocol => isRunningDebug || isRunningProfile;
/// Start the app and keep the process running during its lifetime.
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 56d2bd9..6cecc1f 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -133,6 +133,19 @@
return _defaultBuildMode;
}
+ void usesFlavorOption() {
+ argParser.addOption(
+ 'flavor',
+ help: 'Build a custom app flavor as defined by platform-specific build setup.\n'
+ 'Supports the use of product flavors in Android Gradle scripts.\n'
+ 'Supports the use of custom Xcode schemes.'
+ );
+ }
+
+ BuildInfo getBuildInfo() {
+ return new BuildInfo(getBuildMode(), argResults['flavor']);
+ }
+
void setupApplicationPackages() {
applicationPackages ??= new ApplicationPackageStore();
}
diff --git a/packages/flutter_tools/test/android/gradle_test.dart b/packages/flutter_tools/test/android/gradle_test.dart
index 457ba81..daf399f 100644
--- a/packages/flutter_tools/test/android/gradle_test.dart
+++ b/packages/flutter_tools/test/android/gradle_test.dart
@@ -2,33 +2,89 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'package:file/file.dart';
-import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/gradle.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/build_info.dart';
import 'package:test/test.dart';
-import '../src/context.dart';
const String _kBuildDirectory = '/build/app/outputs';
void main() {
- FileSystem fs;
+ group('gradle project', () {
+ GradleProject projectFrom(String properties) => new GradleProject.fromAppProperties(properties);
- setUp(() {
- fs = new MemoryFileSystem();
- fs.directory('$_kBuildDirectory/release').createSync(recursive: true);
- fs.file('$_kBuildDirectory/app-debug.apk').createSync();
- fs.file('$_kBuildDirectory/release/app-release.apk').createSync();
- });
-
- group('gradle', () {
- testUsingContext('findApkFile', () {
- expect(findApkFile(_kBuildDirectory, 'debug').path,
- '/build/app/outputs/app-debug.apk');
- expect(findApkFile(_kBuildDirectory, 'release').path,
- '/build/app/outputs/release/app-release.apk');
- }, overrides: <Type, Generator>{
- FileSystem: () => fs,
+ test('should extract build directory from app properties', () {
+ final GradleProject project = projectFrom('''
+someProperty: someValue
+buildDir: /Users/some/apps/hello/build/app
+someOtherProperty: someOtherValue
+ ''');
+ expect(project.apkDirectory, fs.path.normalize('/Users/some/apps/hello/build/app/outputs/apk'));
+ });
+ test('should extract default build variants from app properties', () {
+ final GradleProject project = projectFrom('''
+someProperty: someValue
+assemble: task ':app:assemble'
+assembleAndroidTest: task ':app:assembleAndroidTest'
+assembleDebug: task ':app:assembleDebug'
+assembleProfile: task ':app:assembleProfile'
+assembleRelease: task ':app:assembleRelease'
+buildDir: /Users/some/apps/hello/build/app
+someOtherProperty: someOtherValue
+ ''');
+ expect(project.buildTypes, <String>['debug', 'profile', 'release']);
+ expect(project.productFlavors, isEmpty);
+ });
+ test('should extract custom build variants from app properties', () {
+ final GradleProject project = projectFrom('''
+someProperty: someValue
+assemble: task ':app:assemble'
+assembleAndroidTest: task ':app:assembleAndroidTest'
+assembleDebug: task ':app:assembleDebug'
+assembleFree: task ':app:assembleFree'
+assembleFreeAndroidTest: task ':app:assembleFreeAndroidTest'
+assembleFreeDebug: task ':app:assembleFreeDebug'
+assembleFreeProfile: task ':app:assembleFreeProfile'
+assembleFreeRelease: task ':app:assembleFreeRelease'
+assemblePaid: task ':app:assemblePaid'
+assemblePaidAndroidTest: task ':app:assemblePaidAndroidTest'
+assemblePaidDebug: task ':app:assemblePaidDebug'
+assemblePaidProfile: task ':app:assemblePaidProfile'
+assemblePaidRelease: task ':app:assemblePaidRelease'
+assembleProfile: task ':app:assembleProfile'
+assembleRelease: task ':app:assembleRelease'
+buildDir: /Users/some/apps/hello/build/app
+someOtherProperty: someOtherValue
+ ''');
+ expect(project.buildTypes, <String>['debug', 'profile', 'release']);
+ expect(project.productFlavors, <String>['free', 'paid']);
+ });
+ test('should provide apk file name for default build types', () {
+ final GradleProject project = new GradleProject(<String>['debug', 'profile', 'release'], <String>[], '/some/dir');
+ expect(project.apkFileFor(BuildInfo.debug), 'app-debug.apk');
+ expect(project.apkFileFor(BuildInfo.profile), 'app-profile.apk');
+ expect(project.apkFileFor(BuildInfo.release), 'app-release.apk');
+ expect(project.apkFileFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
+ });
+ test('should provide apk file name for flavored build types', () {
+ final GradleProject project = new GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], '/some/dir');
+ expect(project.apkFileFor(const BuildInfo(BuildMode.debug, 'free')), 'app-free-debug.apk');
+ expect(project.apkFileFor(const BuildInfo(BuildMode.release, 'paid')), 'app-paid-release.apk');
+ expect(project.apkFileFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
+ });
+ test('should provide assemble task name for default build types', () {
+ final GradleProject project = new GradleProject(<String>['debug', 'profile', 'release'], <String>[], '/some/dir');
+ expect(project.assembleTaskFor(BuildInfo.debug), 'assembleDebug');
+ expect(project.assembleTaskFor(BuildInfo.profile), 'assembleProfile');
+ expect(project.assembleTaskFor(BuildInfo.release), 'assembleRelease');
+ expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
+ });
+ test('should provide assemble task name for flavored build types', () {
+ final GradleProject project = new GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], '/some/dir');
+ expect(project.assembleTaskFor(const BuildInfo(BuildMode.debug, 'free')), 'assembleFreeDebug');
+ expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'paid')), 'assemblePaidRelease');
+ expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
});
});
}
diff --git a/packages/flutter_tools/test/ios/xcodeproj_test.dart b/packages/flutter_tools/test/ios/xcodeproj_test.dart
new file mode 100644
index 0000000..a8eeb7f
--- /dev/null
+++ b/packages/flutter_tools/test/ios/xcodeproj_test.dart
@@ -0,0 +1,120 @@
+import 'package:test/test.dart';
+
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/ios/xcodeproj.dart';
+
+void main() {
+ group('Xcode project properties', () {
+ test('properties from default project can be parsed', () {
+ final String output = '''
+Information about project "Runner":
+ Targets:
+ Runner
+
+ Build Configurations:
+ Debug
+ Release
+
+ If no build configuration is specified and -scheme is not passed then "Release" is used.
+
+ Schemes:
+ Runner
+
+''';
+ final XcodeProjectInfo info = new XcodeProjectInfo.fromXcodeBuildOutput(output);
+ expect(info.targets, <String>['Runner']);
+ expect(info.schemes, <String>['Runner']);
+ expect(info.buildConfigurations, <String>['Debug', 'Release']);
+ });
+ test('properties from project with custom schemes can be parsed', () {
+ final String output = '''
+Information about project "Runner":
+ Targets:
+ Runner
+
+ Build Configurations:
+ Debug (Free)
+ Debug (Paid)
+ Release (Free)
+ Release (Paid)
+
+ If no build configuration is specified and -scheme is not passed then "Release (Free)" is used.
+
+ Schemes:
+ Free
+ Paid
+
+''';
+ final XcodeProjectInfo info = new XcodeProjectInfo.fromXcodeBuildOutput(output);
+ expect(info.targets, <String>['Runner']);
+ expect(info.schemes, <String>['Free', 'Paid']);
+ expect(info.buildConfigurations, <String>['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)']);
+ });
+ test('expected scheme for non-flavored build is Runner', () {
+ expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.debug), 'Runner');
+ expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.profile), 'Runner');
+ expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.release), 'Runner');
+ });
+ test('expected build configuration for non-flavored build is derived from BuildMode', () {
+ expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug');
+ expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.profile, 'Runner'), 'Release');
+ expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.release, 'Runner'), 'Release');
+ });
+ test('expected scheme for flavored build is the title-cased flavor', () {
+ expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.debug, 'hello')), 'Hello');
+ expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.profile, 'HELLO')), 'HELLO');
+ expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.release, 'Hello')), 'Hello');
+ });
+ test('expected build configuration for flavored build is Mode-Flavor', () {
+ expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.debug, 'hello'), 'Hello'), 'Debug-Hello');
+ expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.profile, 'HELLO'), 'Hello'), 'Release-Hello');
+ expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.release, 'Hello'), 'Hello'), 'Release-Hello');
+ });
+ test('scheme for default project is Runner', () {
+ final XcodeProjectInfo info = new XcodeProjectInfo(<String>['Runner'], <String>['Debug', 'Release'], <String>['Runner']);
+ expect(info.schemeFor(BuildInfo.debug), 'Runner');
+ expect(info.schemeFor(BuildInfo.profile), 'Runner');
+ expect(info.schemeFor(BuildInfo.release), 'Runner');
+ expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown')), isNull);
+ });
+ test('build configuration for default project is matched against BuildMode', () {
+ final XcodeProjectInfo info = new XcodeProjectInfo(<String>['Runner'], <String>['Debug', 'Release'], <String>['Runner']);
+ expect(info.buildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug');
+ expect(info.buildConfigurationFor(BuildInfo.profile, 'Runner'), 'Release');
+ expect(info.buildConfigurationFor(BuildInfo.release, 'Runner'), 'Release');
+ });
+ test('scheme for project with custom schemes is matched against flavor', () {
+ final XcodeProjectInfo info = new XcodeProjectInfo(
+ <String>['Runner'],
+ <String>['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)'],
+ <String>['Free', 'Paid'],
+ );
+ expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'free')), 'Free');
+ expect(info.schemeFor(const BuildInfo(BuildMode.profile, 'Free')), 'Free');
+ expect(info.schemeFor(const BuildInfo(BuildMode.release, 'paid')), 'Paid');
+ expect(info.schemeFor(const BuildInfo(BuildMode.debug, null)), isNull);
+ expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown')), isNull);
+ });
+ test('build configuration for project with custom schemes is matched against BuildMode and flavor', () {
+ final XcodeProjectInfo info = new XcodeProjectInfo(
+ <String>['Runner'],
+ <String>['debug (free)', 'Debug paid', 'release - Free', 'Release-Paid'],
+ <String>['Free', 'Paid'],
+ );
+ expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'free'), 'Free'), 'debug (free)');
+ expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'Paid'), 'Paid'), 'Debug paid');
+ expect(info.buildConfigurationFor(const BuildInfo(BuildMode.profile, 'FREE'), 'Free'), 'release - Free');
+ expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'paid'), 'Paid'), 'Release-Paid');
+ });
+ test('build configuration for project with inconsistent naming is null', () {
+ final XcodeProjectInfo info = new XcodeProjectInfo(
+ <String>['Runner'],
+ <String>['Debug-F', 'Dbg Paid', 'Rel Free', 'Release Full'],
+ <String>['Free', 'Paid'],
+ );
+ expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'Free'), 'Free'), null);
+ expect(info.buildConfigurationFor(const BuildInfo(BuildMode.profile, 'Free'), 'Free'), null);
+ expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'Paid'), 'Paid'), null);
+ });
+ });
+}