Support for app flavors in flutter tooling, #11676 retake (#11734)

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