Break up and show more progression during an Xcode build (#14715)

* Created plumbing but has stream problem

* testing with makePipe

* Trying pipe but not really getting anywhere

* works by repeatedly reading line

* Minor cleanup

* works

* Clean up pipe after use.

* Move the last status forward

* Make sure failed script commands bubble up
diff --git a/packages/flutter_tools/bin/xcode_backend.sh b/packages/flutter_tools/bin/xcode_backend.sh
index fc1539a..2d0ae18 100755
--- a/packages/flutter_tools/bin/xcode_backend.sh
+++ b/packages/flutter_tools/bin/xcode_backend.sh
@@ -11,6 +11,14 @@
   return $?
 }
 
+# When provided with a pipe by the host Flutter build process, output to the
+# pipe goes to stdout of the Flutter build process directly.
+StreamOutput() {
+  if [[ -n "$SCRIPT_OUTPUT_STREAM_FILE" ]]; then
+    echo "$1" > $SCRIPT_OUTPUT_STREAM_FILE
+  fi
+}
+
 EchoError() {
   echo "$@" 1>&2
 }
@@ -103,18 +111,20 @@
       aot_flags="--${build_mode}"
     fi
 
+    StreamOutput " ├─Building Dart code..."
     RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics build aot \
       --output-dir="${build_dir}/aot"                                       \
       --target-platform=ios                                                 \
       --target="${target_path}"                                             \
       ${aot_flags}                                                          \
       ${local_engine_flag}                                                  \
-      ${preview_dart_2_flag}                                                \
+      ${preview_dart_2_flag}
 
     if [[ $? -ne 0 ]]; then
       EchoError "Failed to build ${project_path}."
       exit -1
     fi
+    StreamOutput "done"
 
     RunCommand cp -r -- "${build_dir}/aot/App.framework" "${derived_dir}"
   else
@@ -133,6 +143,7 @@
     precompilation_flag="--precompiled"
   fi
 
+  StreamOutput " ├─Assembling Flutter resources..."
   RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics build flx \
     --target="${target_path}"                                             \
     --output-file="${derived_dir}/app.flx"                                \
@@ -141,12 +152,14 @@
     --working-dir="${derived_dir}/flutter_assets"                         \
     ${precompilation_flag}                                                \
     ${local_engine_flag}                                                  \
-    ${preview_dart_2_flag}                                                \
+    ${preview_dart_2_flag}
 
   if [[ $? -ne 0 ]]; then
     EchoError "Failed to package ${project_path}."
     exit -1
   fi
+  StreamOutput "done"
+  StreamOutput " └─Compiling, linking and signing..."
 
   RunCommand popd > /dev/null
 
diff --git a/packages/flutter_tools/lib/src/base/logger.dart b/packages/flutter_tools/lib/src/base/logger.dart
index 0c13133..0493edb 100644
--- a/packages/flutter_tools/lib/src/base/logger.dart
+++ b/packages/flutter_tools/lib/src/base/logger.dart
@@ -40,7 +40,15 @@
   ///
   /// [message] is the message to display to the user; [progressId] provides an ID which can be
   /// used to identify this type of progress (`hot.reload`, `hot.restart`, ...).
-  Status startProgress(String message, { String progressId, bool expectSlowOperation: false });
+  ///
+  /// [progressIndicatorPadding] can optionally be used to specify spacing
+  /// between the [message] and the progress indicator.
+  Status startProgress(
+    String message, {
+    String progressId,
+    bool expectSlowOperation: false,
+    int progressIndicatorPadding: 52,
+  });
 }
 
 class Status {
@@ -96,13 +104,23 @@
   void printTrace(String message) { }
 
   @override
-  Status startProgress(String message, { String progressId, bool expectSlowOperation: false }) {
+  Status startProgress(
+    String message, {
+    String progressId,
+    bool expectSlowOperation: false,
+    int progressIndicatorPadding: 52,
+  }) {
     if (_status != null) {
       // Ignore nested progresses; return a no-op status object.
       return new Status();
     } else {
       if (supportsColor) {
-        _status = new _AnsiStatus(message, expectSlowOperation, () { _status = null; });
+        _status = new _AnsiStatus(
+          message,
+          expectSlowOperation,
+          () { _status = null; },
+          progressIndicatorPadding,
+        );
         return _status;
       } else {
         printStatus(message);
@@ -163,7 +181,12 @@
   void printTrace(String message) => _trace.writeln(message);
 
   @override
-  Status startProgress(String message, { String progressId, bool expectSlowOperation: false }) {
+  Status startProgress(
+    String message, {
+    String progressId,
+    bool expectSlowOperation: false,
+    int progressIndicatorPadding: 52,
+  }) {
     printStatus(message);
     return new Status();
   }
@@ -208,7 +231,12 @@
   }
 
   @override
-  Status startProgress(String message, { String progressId, bool expectSlowOperation: false }) {
+  Status startProgress(
+    String message, {
+    String progressId,
+    bool expectSlowOperation: false,
+    int progressIndicatorPadding: 52,
+  }) {
     printStatus(message);
     return new Status();
   }
@@ -253,10 +281,10 @@
 }
 
 class _AnsiStatus extends Status {
-  _AnsiStatus(this.message, this.expectSlowOperation, this.onFinish) {
+  _AnsiStatus(this.message, this.expectSlowOperation, this.onFinish, int padding) {
     stopwatch = new Stopwatch()..start();
 
-    stdout.write('${message.padRight(52)}     ');
+    stdout.write('${message.padRight(padding)}     ');
     stdout.write('${_progress[0]}');
 
     timer = new Timer.periodic(const Duration(milliseconds: 100), _callback);
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index 9d16715..7c9dbba 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -763,7 +763,12 @@
   }
 
   @override
-  Status startProgress(String message, { String progressId, bool expectSlowOperation: false }) {
+  Status startProgress(
+    String message, {
+    String progressId,
+    bool expectSlowOperation: false,
+    int progressIndicatorPadding: 52,
+  }) {
     printStatus(message);
     return new Status();
   }
@@ -863,7 +868,12 @@
   Status _status;
 
   @override
-  Status startProgress(String message, { String progressId, bool expectSlowOperation: false }) {
+  Status startProgress(
+    String message, {
+    String progressId,
+    bool expectSlowOperation: false,
+    int progressIndicatorPadding: 52,
+  }) {
     // Ignore nested progresses; return a no-op status object.
     if (_status != null)
       return new Status();
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index 970def0..5a24e31 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -8,6 +8,7 @@
 import '../application_package.dart';
 import '../base/file_system.dart';
 import '../base/io.dart';
+import '../base/logger.dart';
 import '../base/platform.dart';
 import '../base/port_scanner.dart';
 import '../base/process.dart';
@@ -239,6 +240,8 @@
     int installationResult = -1;
     Uri localObservatoryUri;
 
+    final Status installStatus =
+        logger.startProgress('Installing and launching...', expectSlowOperation: true);
     if (!debuggingOptions.debuggingEnabled) {
       // If debugging is not enabled, just launch the application and continue.
       printTrace('Debugging is not enabled');
@@ -247,6 +250,7 @@
         mapFunction: monitorInstallationFailure,
         trace: true,
       );
+      installStatus.stop();
     } else {
       // Debugging is enabled, look for the observatory server port post launch.
       printTrace('Debugging is enabled, connecting to observatory');
@@ -282,6 +286,7 @@
         observatoryDiscovery.cancel();
       });
     }
+    installStatus.stop();
 
     if (installationResult != 0) {
       printError('Could not install ${bundle.path} on $id.');
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index 621186d..7044ef3 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -13,9 +13,11 @@
 import '../base/file_system.dart';
 import '../base/io.dart';
 import '../base/logger.dart';
+import '../base/os.dart';
 import '../base/platform.dart';
 import '../base/process.dart';
 import '../base/process_manager.dart';
+import '../base/utils.dart';
 import '../build_info.dart';
 import '../flx.dart' as flx;
 import '../globals.dart';
@@ -346,14 +348,58 @@
     );
   }
 
-  final Status buildStatus =
-      logger.startProgress('Running Xcode build...', expectSlowOperation: true);
+  Status buildSubStatus;
+  Status initialBuildStatus;
+  Directory scriptOutputPipeTempDirectory;
+
+  if (logger.supportsColor) {
+    scriptOutputPipeTempDirectory = fs.systemTempDirectory
+        .createTempSync('flutter_build_log_pipe');
+    final File scriptOutputPipeFile =
+        scriptOutputPipeTempDirectory.childFile('pipe_to_stdout');
+    os.makePipe(scriptOutputPipeFile.path);
+
+    Future<void> listenToScriptOutputLine() async {
+      final List<String> lines = await scriptOutputPipeFile.readAsLines();
+      for (String line in lines) {
+        if (line == 'done') {
+          buildSubStatus?.stop();
+          buildSubStatus = null;
+        } else {
+          initialBuildStatus.cancel();
+          buildSubStatus = logger.startProgress(
+            line,
+            expectSlowOperation: true,
+            progressIndicatorPadding: 45,
+          );
+        }
+      }
+      return listenToScriptOutputLine();
+    }
+
+    // Trigger the start of the pipe -> stdout loop. Ignore exceptions.
+    listenToScriptOutputLine(); // ignore: unawaited_futures
+
+    buildCommands.add('SCRIPT_OUTPUT_STREAM_FILE=${scriptOutputPipeFile.absolute.path}');
+  }
+
+  final Stopwatch buildStopwatch = new Stopwatch()..start();
+  initialBuildStatus = logger.startProgress('Starting Xcode build...');
   final RunResult buildResult = await runAsync(
     buildCommands,
     workingDirectory: app.appDirectory,
     allowReentrantFlutter: true
   );
-  buildStatus.stop();
+  buildSubStatus?.stop();
+  initialBuildStatus?.cancel();
+  buildStopwatch.stop();
+  // Free pipe file.
+  scriptOutputPipeTempDirectory?.deleteSync(recursive: true);
+  printStatus(
+    'Xcode build done',
+    ansiAlternative: 'Xcode build done'.padRight(53)
+        + '${getElapsedAsSeconds(buildStopwatch.elapsed).padLeft(5)}',
+  );
 
   // Run -showBuildSettings again but with the exact same parameters as the build.
   final Map<String, String> buildSettings = parseXcodeBuildSettings(runCheckedSync(