show build progress; print app sizes (#4263)

* show build progress; print app sizes

* add todo

* review comments

* remove unused import
diff --git a/packages/flutter_tools/lib/src/base/process.dart b/packages/flutter_tools/lib/src/base/process.dart
index d4287a1..c0c672f 100644
--- a/packages/flutter_tools/lib/src/base/process.dart
+++ b/packages/flutter_tools/lib/src/base/process.dart
@@ -164,6 +164,8 @@
   final ProcessResult processResult;
 
   int get exitCode => processResult.exitCode;
+  String get stdout => processResult.stdout;
+  String get stderr => processResult.stderr;
 
   @override
   String toString() {
diff --git a/packages/flutter_tools/lib/src/base/utils.dart b/packages/flutter_tools/lib/src/base/utils.dart
index 052e671..5fb325f 100644
--- a/packages/flutter_tools/lib/src/base/utils.dart
+++ b/packages/flutter_tools/lib/src/base/utils.dart
@@ -71,6 +71,11 @@
   return new JsonEncoder.withIndent('  ').convert(jsonable) + '\n';
 }
 
+/// Return a String - with units - for the size in MB of the given number of bytes.
+String getSizeAsMB(int bytesLength) {
+  return '${(bytesLength / (1024 * 1024)).toStringAsFixed(1)}MB';
+}
+
 /// A class to maintain a list of items, fire events when items are added or
 /// removed, and calculate a diff of changes when a new list of items is
 /// available.
diff --git a/packages/flutter_tools/lib/src/commands/build_aot.dart b/packages/flutter_tools/lib/src/commands/build_aot.dart
index 1813b63..53c78df 100644
--- a/packages/flutter_tools/lib/src/commands/build_aot.dart
+++ b/packages/flutter_tools/lib/src/commands/build_aot.dart
@@ -7,6 +7,7 @@
 
 import 'package:path/path.dart' as path;
 
+import '../base/logger.dart';
 import '../base/process.dart';
 import '../base/utils.dart';
 import '../build_info.dart';
@@ -50,17 +51,22 @@
       printError('Unknown platform: $targetPlatform');
       return 1;
     }
-    String outputPath = buildAotSnapshot(
+
+    String typeName = path.basename(tools.getEngineArtifactsDirectory(platform, getBuildMode()).path);
+    Status status = logger.startProgress('Building AOT snapshot in ${getModeName(getBuildMode())} mode ($typeName)...');
+    String outputPath = await buildAotSnapshot(
       findMainDartFile(argResults['target']),
       platform,
       getBuildMode(),
       outputPath: argResults['output-dir'],
       interpreter: argResults['interpreter']
     );
+    status.stop(showElapsedTime: true);
+
     if (outputPath == null)
       return 1;
 
-    printStatus('Built $outputPath.');
+    printStatus('Built to $outputPath${Platform.pathSeparator}.');
     return 0;
   }
 }
@@ -72,13 +78,13 @@
 
 /// Build an AOT snapshot. Return `null` (and log to `printError`) if the method
 /// fails.
-String buildAotSnapshot(
+Future<String> buildAotSnapshot(
   String mainPath,
   TargetPlatform platform,
   BuildMode buildMode, {
   String outputPath: _kDefaultAotOutputDir,
   bool interpreter: false
-}) {
+}) async {
   try {
     return _buildAotSnapshot(
       mainPath,
@@ -94,13 +100,13 @@
   }
 }
 
-String _buildAotSnapshot(
+Future<String> _buildAotSnapshot(
   String mainPath,
   TargetPlatform platform,
   BuildMode buildMode, {
   String outputPath: _kDefaultAotOutputDir,
   bool interpreter: false
-}) {
+}) async {
   if (!isAotBuildMode(buildMode)) {
     printError('${toTitleCase(getModeName(buildMode))} mode does not support AOT compilation.');
     return null;
@@ -253,10 +259,11 @@
 
   genSnapshotCmd.add(mainPath);
 
-  String typeName = path.basename(tools.getEngineArtifactsDirectory(platform, buildMode).path);
-  printStatus('Building snapshot in ${getModeName(buildMode)} mode ($typeName)...');
-
-  runCheckedSync(genSnapshotCmd, truncateCommand: true);
+  RunResult results = await runAsync(genSnapshotCmd);
+  if (results.exitCode != 0) {
+    printStatus(results.toString());
+    return null;
+  }
 
   // On iOS, we use Xcode to compile the snapshot into a dynamic library that the
   // end-developer can link into their app.
diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart
index 6211482..38f576f 100644
--- a/packages/flutter_tools/lib/src/commands/build_apk.dart
+++ b/packages/flutter_tools/lib/src/commands/build_apk.dart
@@ -11,6 +11,7 @@
 import '../android/android_sdk.dart';
 import '../base/file_system.dart' show ensureDirectoryExists;
 import '../base/os.dart';
+import '../base/logger.dart';
 import '../base/process.dart';
 import '../base/utils.dart';
 import '../build_info.dart';
@@ -347,9 +348,6 @@
     File apkShaFile = new File('$outputFile.sha1');
     apkShaFile.writeAsStringSync(calculateSha(finalApk));
 
-    double size = finalApk.lengthSync() / (1024 * 1024);
-    printStatus('Built ${finalApk.path} (${size.toStringAsFixed(1)}MB).');
-
     return 0;
   } finally {
     tempDir.deleteSync(recursive: true);
@@ -492,7 +490,7 @@
   }
 
   String typeName = path.basename(tools.getEngineArtifactsDirectory(platform, buildMode).path);
-  printStatus('Building APK in ${getModeName(buildMode)} mode ($typeName)...');
+  Status status = logger.startProgress('Building APK in ${getModeName(buildMode)} mode ($typeName)...');
 
   if (flxPath != null && flxPath.isNotEmpty) {
     if (!FileSystemEntity.isFileSync(flxPath)) {
@@ -513,7 +511,7 @@
 
   // Build an AOT snapshot if needed.
   if (isAotBuildMode(buildMode) && aotPath == null) {
-    aotPath = buildAotSnapshot(findMainDartFile(target), platform, buildMode);
+    aotPath = await buildAotSnapshot(findMainDartFile(target), platform, buildMode);
     if (aotPath == null) {
       printError('Failed to build AOT snapshot');
       return 1;
@@ -540,13 +538,19 @@
   }
 
   int result = _buildApk(platform, buildMode, components, flxPath, keystore, outputFile);
+  status.stop(showElapsedTime: true);
+
   if (result == 0) {
+    File apkFile = new File(outputFile);
+    printStatus('Built $outputFile (${getSizeAsMB(apkFile.lengthSync())}).');
+
     _writeBuildMetaEntry(
       path.dirname(outputFile),
       'targetBuildType',
       _getTargetBuildTypeToken(platform, buildMode, new File(outputFile))
     );
   }
+
   return result;
 }
 
diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart
index 0b95ff6..3385b75 100644
--- a/packages/flutter_tools/lib/src/commands/build_ios.dart
+++ b/packages/flutter_tools/lib/src/commands/build_ios.dart
@@ -4,7 +4,10 @@
 
 import 'dart:async';
 
+import 'package:path/path.dart' as path;
+
 import '../application_package.dart';
+import '../base/logger.dart';
 import '../build_info.dart';
 import '../globals.dart';
 import '../ios/mac.dart';
@@ -42,24 +45,26 @@
     bool shouldCodesign = argResults['codesign'];
 
     if (!forSimulator && !shouldCodesign) {
-      printStatus('Warning: Building for device with codesigning disabled.');
-      printStatus('You will have to manually codesign before deploying to device.');
+      printStatus('Warning: Building for device with codesigning disabled. You will '
+        'have to manually codesign before deploying to device.');
     }
 
     String logTarget = forSimulator ? 'simulator' : 'device';
 
-    printStatus('Building $app for $logTarget...');
-
-    bool result = await buildIOSXcodeProject(app, getBuildMode(),
+    String typeName = path.basename(tools.getEngineArtifactsDirectory(TargetPlatform.ios, getBuildMode()).path);
+    Status status = logger.startProgress('Building $app for $logTarget ($typeName)...');
+    XcodeBuildResult result = await buildXcodeProject(app, getBuildMode(),
         buildForDevice: !forSimulator,
         codesign: shouldCodesign);
+    status.stop(showElapsedTime: true);
 
-    if (!result) {
+    if (!result.success) {
       printError('Encountered error while building for $logTarget.');
       return 1;
     }
 
-    printStatus('Built in ios/.generated.');
+    if (result.output != null)
+      printStatus('Built ${result.output}.');
 
     return 0;
   }
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index c86cf80..4a1e72d 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -180,8 +180,8 @@
     printTrace('Building ${app.name} for $id');
 
     // Step 1: Install the precompiled/DBC application if necessary.
-    bool buildResult = await buildIOSXcodeProject(app, mode, buildForDevice: true);
-    if (!buildResult) {
+    XcodeBuildResult buildResult = await buildXcodeProject(app, mode, buildForDevice: true);
+    if (!buildResult.success) {
       printError('Could not build the precompiled application for the device.');
       return new LaunchResult.failed();
     }
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index d6db90d..03ca717 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -97,7 +97,7 @@
   return false;
 }
 
-Future<bool> buildIOSXcodeProject(ApplicationPackage app, BuildMode mode,
+Future<XcodeBuildResult> buildXcodeProject(ApplicationPackage app, BuildMode mode,
     { bool buildForDevice, bool codesign: true }) async {
   String flutterProjectPath = Directory.current.path;
 
@@ -105,17 +105,17 @@
     printTrace('Initializing the Xcode project.');
     if ((await setupXcodeProjectHarness(flutterProjectPath, mode)) != 0) {
       printError('Could not initialize the Xcode project.');
-      return false;
+      return new XcodeBuildResult(false);
     }
   } else {
    updateXcodeLocalProperties(flutterProjectPath);
   }
 
   if (!_validateEngineRevision(app))
-    return false;
+    return new XcodeBuildResult(false);
 
   if (!_checkXcodeVersion())
-    return false;
+    return new XcodeBuildResult(false);
 
   // Before the build, all service definitions must be updated and the dylibs
   // copied over to a location that is suitable for Xcodebuild to find them.
@@ -148,20 +148,30 @@
     commands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
   }
 
-  printTrace(commands.join(' '));
-
-  ProcessResult result = Process.runSync(
-    commands.first, commands.sublist(1), workingDirectory: app.rootPath
-  );
+  RunResult result = await runAsync(commands, workingDirectory: app.rootPath);
 
   if (result.exitCode != 0) {
     if (result.stderr.isNotEmpty)
       printStatus(result.stderr);
     if (result.stdout.isNotEmpty)
       printStatus(result.stdout);
+    return new XcodeBuildResult(false);
+  } else {
+    // Look for 'clean build/Release-iphoneos/Runner.app'.
+    RegExp regexp = new RegExp(r' clean (\S*\.app)$', multiLine: true);
+    Match match = regexp.firstMatch(result.stdout);
+    String outputDir;
+    if (match != null)
+      outputDir = path.join(app.rootPath, match.group(1));
+    return new XcodeBuildResult(true, outputDir);
   }
+}
 
-  return result.exitCode == 0;
+class XcodeBuildResult {
+  XcodeBuildResult(this.success, [this.output]);
+
+  final bool success;
+  final String output;
 }
 
 final RegExp _xcodeVersionRegExp = new RegExp(r'Xcode (\d+)\..*');
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index 58ae15b..6409396 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -548,8 +548,8 @@
   Future<bool> _buildAndInstallApplicationBundle(ApplicationPackage app) async {
     // Step 1: Build the Xcode project.
     // The build mode for the simulator is always debug.
-    bool buildResult = await buildIOSXcodeProject(app, BuildMode.debug, buildForDevice: false);
-    if (!buildResult) {
+    XcodeBuildResult buildResult = await buildXcodeProject(app, BuildMode.debug, buildForDevice: false);
+    if (!buildResult.success) {
       printError('Could not build the application for the simulator.');
       return false;
     }