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