Support for macOS release mode (1 of 3) (#37425)

diff --git a/packages/flutter_tools/bin/macos_build_flutter_assets.sh b/packages/flutter_tools/bin/macos_build_flutter_assets.sh
index d576a87..3ee697c 100755
--- a/packages/flutter_tools/bin/macos_build_flutter_assets.sh
+++ b/packages/flutter_tools/bin/macos_build_flutter_assets.sh
@@ -33,18 +33,6 @@
     target_path="${FLUTTER_TARGET}"
 fi
 
-# Set the track widget creation flag.
-track_widget_creation_flag=""
-if [[ -n "$TRACK_WIDGET_CREATION" ]]; then
-  track_widget_creation_flag="--track-widget-creation"
-fi
-
-# Copy the framework and handle local engine builds.
-framework_name="FlutterMacOS.framework"
-ephemeral_dir="${SOURCE_ROOT}/Flutter/ephemeral"
-framework_path="${FLUTTER_ROOT}/bin/cache/artifacts/engine/darwin-x64"
-flutter_framework="${framework_path}/${framework_name}"
-
 if [[ -n "$FLUTTER_ENGINE" ]]; then
   flutter_engine_flag="--local-engine-src-path=${FLUTTER_ENGINE}"
 fi
@@ -63,22 +51,29 @@
     exit -1
   fi
   local_engine_flag="--local-engine=${LOCAL_ENGINE}"
-  flutter_framework="${FLUTTER_ENGINE}/out/${LOCAL_ENGINE}/${framework_name}"
 fi
 
-RunCommand mkdir -p -- "$ephemeral_dir"
-RunCommand rm -rf -- "${ephemeral_dir}/${framework_name}"
-RunCommand cp -Rp -- "${flutter_framework}" "${ephemeral_dir}"
-
 # Set the build mode
 build_mode="$(echo "${FLUTTER_BUILD_MODE:-${CONFIGURATION}}" | tr "[:upper:]" "[:lower:]")"
 
+# The path where the input/output xcfilelists are stored. These are used by xcode
+# to conditionally skip this script phase if neither have changed.
+ephemeral_dir="${SOURCE_ROOT}/Flutter/ephemeral"
+build_inputs_path="${ephemeral_dir}/FlutterInputs.xcfilelist"
+build_outputs_path="${ephemeral_dir}/FlutterOutputs.xcfilelist"
+
+# TODO(jonahwilliams): connect AOT rules once engine artifacts are published.
+# The build mode is currently hard-coded to debug only. Since this does not yet
+# support AOT, we need to ensure that we compile the kernel file in debug so that
+# the VM can load it.
 RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics               \
     ${verbose_flag}                                                         \
-    build bundle                                                            \
-    --target-platform=darwin-x64                                            \
-    --target="${target_path}"                                               \
-    --${build_mode}                                                         \
-    ${track_widget_creation_flag}                                           \
     ${flutter_engine_flag}                                                  \
-    ${local_engine_flag}
+    ${local_engine_flag}                                                    \
+    assemble                                                                \
+    -dTargetPlatform=darwin-x64                                             \
+    -dTargetFile="${target_path}"                                           \
+    -dBuildMode=debug                                                       \
+    --build-inputs="${build_inputs_path}"                                   \
+    --build-outputs="${build_outputs_path}"                                 \
+   debug_bundle_flutter_assets
diff --git a/packages/flutter_tools/lib/src/build_system/build_system.dart b/packages/flutter_tools/lib/src/build_system/build_system.dart
index c3cf8cd..e60c444 100644
--- a/packages/flutter_tools/lib/src/build_system/build_system.dart
+++ b/packages/flutter_tools/lib/src/build_system/build_system.dart
@@ -485,16 +485,18 @@
     // timestamps to track files, this leads to unecessary rebuilds if they
     // are included. Once all the places that write these files have been
     // tracked down and moved into assemble, these checks should be removable.
+    // We also remove files under .dart_tool, since these are intermediaries
+    // and don't need to be tracked by external systems.
     {
       buildInstance.inputFiles.removeWhere((String path, File file) {
-        return path.contains('pubspec.yaml') ||
-           path.contains('.flutter-plugins') ||
-           path.contains('xcconfig');
+        return path.contains('.flutter-plugins') ||
+                       path.contains('xcconfig') ||
+                     path.contains('.dart_tool');
       });
       buildInstance.outputFiles.removeWhere((String path, File file) {
-        return path.contains('pubspec.yaml') ||
-           path.contains('.flutter-plugins') ||
-           path.contains('xcconfig');
+        return path.contains('.flutter-plugins') ||
+                       path.contains('xcconfig') ||
+                     path.contains('.dart_tool');
       });
     }
     return BuildResult(
@@ -509,6 +511,7 @@
   }
 }
 
+
 /// An active instance of a build.
 class _BuildInstance {
   _BuildInstance(this.environment, this.fileCache, this.buildSystemConfig)
diff --git a/packages/flutter_tools/lib/src/build_system/targets/dart.dart b/packages/flutter_tools/lib/src/build_system/targets/dart.dart
index b2b66ff..76d2ab0 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/dart.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/dart.dart
@@ -64,6 +64,7 @@
 
   @override
   List<Source> get inputs => const <Source>[
+    Source.pattern('{PROJECT_DIR}/.packages'),
     Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/dart.dart'),
     Source.function(listDartSources), // <- every dart file under {PROJECT_DIR}/lib and in .packages
     Source.artifact(Artifact.platformKernelDill),
diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart
index 53c082d..bc31ad3 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart
@@ -2,21 +2,65 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:pool/pool.dart';
+
 import '../../artifacts.dart';
+import '../../asset.dart';
 import '../../base/file_system.dart';
 import '../../base/io.dart';
+import '../../base/process.dart';
 import '../../base/process_manager.dart';
 import '../../build_info.dart';
+import '../../devfs.dart';
 import '../../globals.dart';
-import '../../macos/cocoapods.dart';
+import '../../macos/xcode.dart';
 import '../../project.dart';
 import '../build_system.dart';
-import '../exceptions.dart';
-import 'assets.dart';
 import 'dart.dart';
 
 const String _kOutputPrefix = '{PROJECT_DIR}/macos/Flutter/ephemeral/FlutterMacOS.framework';
 
+/// The copying logic for flutter assets in macOS.
+// TODO(jonahwilliams): remove once build planning lands.
+class MacOSAssetBehavior extends SourceBehavior {
+  const MacOSAssetBehavior();
+
+  @override
+  List<File> inputs(Environment environment) {
+    final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
+    assetBundle.build(
+      manifestPath: environment.projectDir.childFile('pubspec.yaml').path,
+      packagesPath: environment.projectDir.childFile('.packages').path,
+    );
+    // Filter the file type to remove the files that are generated by this
+    // command as inputs.
+    final List<File> results = <File>[];
+    final Iterable<DevFSFileContent> files = assetBundle.entries.values.whereType<DevFSFileContent>();
+    for (DevFSFileContent devFsContent in files) {
+      results.add(fs.file(devFsContent.file.path));
+    }
+    return results;
+  }
+
+  @override
+  List<File> outputs(Environment environment) {
+    final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
+    assetBundle.build(
+      manifestPath: environment.projectDir.childFile('pubspec.yaml').path,
+      packagesPath: environment.projectDir.childFile('.packages').path,
+    );
+    final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
+    final String prefix = fs.path.join(flutterProject.macos.ephemeralDirectory.path,
+        'App.framework', 'flutter_assets');
+    final List<File> results = <File>[];
+    for (String key in assetBundle.entries.keys) {
+      final File file = fs.file(fs.path.join(prefix, key));
+      results.add(file);
+    }
+    return results;
+  }
+}
+
 /// Copy the macOS framework to the correct copy dir by invoking 'cp -R'.
 ///
 /// The shelling out is done to avoid complications with preserving special
@@ -41,18 +85,21 @@
   List<Source> get outputs => const <Source>[
     Source.pattern('$_kOutputPrefix/FlutterMacOS'),
     // Headers
-    Source.pattern('$_kOutputPrefix/Headers/FLEViewController.h'),
+    Source.pattern('$_kOutputPrefix/Headers/FlutterDartProject.h'),
+    Source.pattern('$_kOutputPrefix/Headers/FlutterEngine.h'),
+    Source.pattern('$_kOutputPrefix/Headers/FlutterViewController.h'),
     Source.pattern('$_kOutputPrefix/Headers/FlutterBinaryMessenger.h'),
     Source.pattern('$_kOutputPrefix/Headers/FlutterChannels.h'),
     Source.pattern('$_kOutputPrefix/Headers/FlutterCodecs.h'),
-    Source.pattern('$_kOutputPrefix/Headers/FlutterMacOS.h'),
+    Source.pattern('$_kOutputPrefix/Headers/FlutterMacros.h'),
     Source.pattern('$_kOutputPrefix/Headers/FlutterPluginMacOS.h'),
     Source.pattern('$_kOutputPrefix/Headers/FlutterPluginRegistrarMacOS.h'),
+    Source.pattern('$_kOutputPrefix/Headers/FlutterMacOS.h'),
     // Modules
     Source.pattern('$_kOutputPrefix/Modules/module.modulemap'),
     // Resources
     Source.pattern('$_kOutputPrefix/Resources/icudtl.dat'),
-    Source.pattern('$_kOutputPrefix/Resources/info.plist'),
+    Source.pattern('$_kOutputPrefix/Resources/Info.plist'),
     // Ignore Versions folder for now
   ];
 
@@ -62,11 +109,9 @@
   @override
   Future<void> build(List<File> inputFiles, Environment environment) async {
     final String basePath = artifacts.getArtifactPath(Artifact.flutterMacOSFramework);
-    final Directory targetDirectory = environment
-      .projectDir
-      .childDirectory('macos')
-      .childDirectory('Flutter')
-      .childDirectory('ephemeral')
+    final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
+    final Directory targetDirectory = flutterProject.macos
+      .ephemeralDirectory
       .childDirectory('FlutterMacOS.framework');
     if (targetDirectory.existsSync()) {
       targetDirectory.deleteSync(recursive: true);
@@ -83,111 +128,151 @@
   }
 }
 
-/// Tell cocoapods to re-fetch dependencies.
-class DebugMacOSPodInstall extends Target {
-  const DebugMacOSPodInstall();
+/// Create an App.framework for debug macOS targets.
+///
+/// This framework needs to exist for the Xcode project to link/bundle,
+/// but it isn't actually executed. To generate something valid, we compile a trivial
+/// constant.
+class DebugMacOSFramework extends Target {
+  const DebugMacOSFramework();
 
   @override
-  String get name => 'debug_macos_pod_install';
+  String get name => 'debug_macos_framework';
+
+  @override
+  Future<void> build(List<File> inputFiles, Environment environment) async {
+    final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
+    final File outputFile = fs.file(fs.path.join(
+        flutterProject.macos.ephemeralDirectory.path, 'App.framework', 'App'));
+    outputFile.createSync(recursive: true);
+    final File debugApp = environment.buildDir.childFile('debug_app.cc')
+        ..writeAsStringSync(r'''
+static const int Moo = 88;
+''');
+    final RunResult result = await xcode.clang(<String>[
+      '-x',
+      'c',
+      debugApp.path,
+      '-arch', 'x86_64',
+      '-dynamiclib',
+      '-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks',
+      '-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks',
+      '-install_name', '@rpath/App.framework/App',
+      '-o', 'macos/Flutter/ephemeral/App.framework/App',
+    ]);
+    if (result.exitCode != 0) {
+      throw Exception('Failed to compile debug App.framework');
+    }
+  }
+
+  @override
+  List<Target> get dependencies => const <Target>[];
 
   @override
   List<Source> get inputs => const <Source>[
-    Source.artifact(Artifact.flutterMacOSPodspec,
-      platform: TargetPlatform.darwin_x64,
-      mode: BuildMode.debug
-    ),
-    Source.pattern('{PROJECT_DIR}/macos/Podfile', optional: true),
-    Source.pattern('{PROJECT_DIR}/macos/Runner.xcodeproj/project.pbxproj'),
-    Source.pattern('{PROJECT_DIR}/macos/Flutter/ephemeral/Flutter-Generated.xcconfig'),
+    Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/macos.dart'),
   ];
 
   @override
   List<Source> get outputs => const <Source>[
-    // TODO(jonahwilliams): introduce configuration/planning phase to build.
-    // No outputs because Cocoapods is fully responsible for tracking. plus there
-    // is no concept of an optional output. Instead we will need a build config
-    // phase to conditionally add this rule so that it can be written properly.
+    Source.pattern('{PROJECT_DIR}/macos/Flutter/ephemeral/App.framework/App'),
   ];
+}
+
+/// Bundle the flutter assets, app.dill, and precompiled runtimes into the App.framework.
+class DebugBundleFlutterAssets extends Target {
+  const DebugBundleFlutterAssets();
 
   @override
-  List<Target> get dependencies => const <Target>[
-    UnpackMacOS(),
-    FlutterPlugins(),
-  ];
+  String get name => 'debug_bundle_flutter_assets';
 
   @override
   Future<void> build(List<File> inputFiles, Environment environment) async {
-    if (environment.defines[kBuildMode] == null) {
-      throw MissingDefineException(kBuildMode, 'debug_macos_pod_install');
+    final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
+    final Directory outputDirectory = flutterProject.macos
+        .ephemeralDirectory.childDirectory('App.framework');
+    if (!outputDirectory.existsSync()) {
+      throw Exception('App.framework must exist to bundle assets.');
     }
-    // If there is no podfile do not perform any pods actions.
-    if (!environment.projectDir.childDirectory('macos')
-        .childFile('Podfile').existsSync()) {
-      return;
+    // Copy assets into asset directory.
+    final Directory assetDirectory = outputDirectory.childDirectory('flutter_assets');
+    // We're not smart enough to only remove assets that are removed. If
+    // anything changes blow away the whole directory.
+    if (assetDirectory.existsSync()) {
+      assetDirectory.deleteSync(recursive: true);
     }
-    final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
-    final FlutterProject project = FlutterProject.fromDirectory(environment.projectDir);
-    final String enginePath = artifacts.getArtifactPath(Artifact.flutterMacOSPodspec,
-        mode: buildMode, platform: TargetPlatform.darwin_x64);
-
-    await cocoaPods.processPods(
-      xcodeProject: project.macos,
-      engineDir: enginePath,
-      isSwift: true,
-      dependenciesChanged: true,
+    assetDirectory.createSync();
+    final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
+    final int result = await assetBundle.build(
+      manifestPath: environment.projectDir.childFile('pubspec.yaml').path,
+      packagesPath: environment.projectDir.childFile('.packages').path,
     );
-  }
-}
-
-/// Build all of the artifacts for a debug macOS application.
-class DebugMacOSApplication extends Target {
-  const DebugMacOSApplication();
-
-  @override
-  Future<void> build(List<File> inputFiles, Environment environment) async {
-    final File sourceFile = environment.buildDir.childFile('app.dill');
-    final File destinationFile = environment.buildDir
-        .childDirectory('flutter_assets')
-        .childFile('kernel_blob.bin');
-    if (!destinationFile.parent.existsSync()) {
-      destinationFile.parent.createSync(recursive: true);
+    if (result != 0) {
+      throw Exception('Failed to create asset bundle: $result');
     }
-    sourceFile.copySync(destinationFile.path);
+    // Limit number of open files to avoid running out of file descriptors.
+    try {
+      final Pool pool = Pool(64);
+      await Future.wait<void>(
+        assetBundle.entries.entries.map<Future<void>>((MapEntry<String, DevFSContent> entry) async {
+          final PoolResource resource = await pool.request();
+          try {
+            final File file = fs.file(fs.path.join(assetDirectory.path, entry.key));
+            file.parent.createSync(recursive: true);
+            await file.writeAsBytes(await entry.value.contentsAsBytes());
+          } finally {
+            resource.release();
+          }
+        }));
+    } catch (err, st){
+      throw Exception('Failed to copy assets: $st');
+    }
+    // Copy dill file.
+    try {
+      final File sourceFile = environment.buildDir.childFile('app.dill');
+      sourceFile.copySync(assetDirectory.childFile('kernel_blob.bin').path);
+    } catch (err) {
+      throw Exception('Failed to copy app.dill: $err');
+    }
+    // Copy precompiled runtimes.
+    try {
+      final String vmSnapshotData = artifacts.getArtifactPath(Artifact.vmSnapshotData,
+          platform: TargetPlatform.darwin_x64, mode: BuildMode.debug);
+      final String isolateSnapshotData = artifacts.getArtifactPath(Artifact.isolateSnapshotData,
+          platform: TargetPlatform.darwin_x64, mode: BuildMode.debug);
+      fs.file(vmSnapshotData).copySync(
+          assetDirectory.childFile('vm_snapshot_data').path);
+      fs.file(isolateSnapshotData).copySync(
+          assetDirectory.childFile('isolate_snapshot_data').path);
+    } catch (err) {
+      throw Exception('Failed to copy precompiled runtimes: $err');
+    }
   }
 
   @override
   List<Target> get dependencies => const <Target>[
-    FlutterPlugins(),
-    UnpackMacOS(),
     KernelSnapshot(),
-    CopyAssets(),
-    DebugMacOSPodInstall(),
+    DebugMacOSFramework(),
+    UnpackMacOS(),
   ];
 
   @override
   List<Source> get inputs => const <Source>[
-    Source.pattern('{BUILD_DIR}/app.dill')
+    Source.pattern('{PROJECT_DIR}/pubspec.yaml'),
+    Source.behavior(MacOSAssetBehavior()),
+    Source.pattern('{BUILD_DIR}/app.dill'),
+    Source.artifact(Artifact.isolateSnapshotData, platform: TargetPlatform.darwin_x64, mode: BuildMode.debug),
+    Source.artifact(Artifact.vmSnapshotData, platform: TargetPlatform.darwin_x64, mode: BuildMode.debug),
   ];
 
   @override
-  String get name => 'debug_macos_application';
-
-  @override
   List<Source> get outputs => const <Source>[
-    Source.pattern('{BUILD_DIR}/flutter_assets/kernel_blob.bin'),
+    Source.behavior(MacOSAssetBehavior()),
+    Source.pattern('{PROJECT_DIR}/macos/Flutter/ephemeral/App.framework/flutter_assets/AssetManifest.json'),
+    Source.pattern('{PROJECT_DIR}/macos/Flutter/ephemeral/App.framework/flutter_assets/FontManifest.json'),
+    Source.pattern('{PROJECT_DIR}/macos/Flutter/ephemeral/App.framework/flutter_assets/LICENSE'),
+    Source.pattern('{PROJECT_DIR}/macos/Flutter/ephemeral/App.framework/flutter_assets/kernel_blob.bin'),
+    Source.pattern('{PROJECT_DIR}/macos/Flutter/ephemeral/App.framework/flutter_assets/vm_snapshot_data'),
+    Source.pattern('{PROJECT_DIR}/macos/Flutter/ephemeral/App.framework/flutter_assets/isolate_snapshot_data'),
   ];
 }
-
-// TODO(jonahwilliams): real AOT implementation.
-class ReleaseMacOSApplication extends DebugMacOSApplication {
-  const ReleaseMacOSApplication();
-
-  @override
-  String get name => 'release_macos_application';
-}
-class ProfileMacOSApplication extends DebugMacOSApplication {
-  const ProfileMacOSApplication();
-
-  @override
-  String get name => 'profile_macos_application';
-}
diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart
index 476d788..85c9efe 100644
--- a/packages/flutter_tools/lib/src/commands/assemble.dart
+++ b/packages/flutter_tools/lib/src/commands/assemble.dart
@@ -32,9 +32,8 @@
   AotElfRelease(),
   AotAssemblyProfile(),
   AotAssemblyRelease(),
-  DebugMacOSApplication(),
-  ProfileMacOSApplication(),
-  ReleaseMacOSApplication(),
+  DebugMacOSFramework(),
+  DebugBundleFlutterAssets(),
 ];
 
 /// Assemble provides a low level API to interact with the flutter tool build
diff --git a/packages/flutter_tools/lib/src/desktop.dart b/packages/flutter_tools/lib/src/desktop.dart
index 26773a2..69a9bf8 100644
--- a/packages/flutter_tools/lib/src/desktop.dart
+++ b/packages/flutter_tools/lib/src/desktop.dart
@@ -11,6 +11,9 @@
 
 /// Kills a process on linux or macOS.
 Future<bool> killProcess(String executable) async {
+  if (executable == null) {
+    return false;
+  }
   final RegExp whitespace = RegExp(r'\s+');
   bool succeeded = true;
   try {
diff --git a/packages/flutter_tools/lib/src/macos/application_package.dart b/packages/flutter_tools/lib/src/macos/application_package.dart
index cfa4abc..278122a 100644
--- a/packages/flutter_tools/lib/src/macos/application_package.dart
+++ b/packages/flutter_tools/lib/src/macos/application_package.dart
@@ -141,7 +141,7 @@
       return null;
     }
     final _ExecutableAndId executableAndId = MacOSApp._executableFromBundle(fs.directory(directory));
-    return executableAndId.executable;
+    return executableAndId?.executable;
   }
 }
 
diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart
index 95fa007..9fa7513 100644
--- a/packages/flutter_tools/lib/src/macos/build_macos.dart
+++ b/packages/flutter_tools/lib/src/macos/build_macos.dart
@@ -36,6 +36,13 @@
     setSymroot: false,
   );
   await processPodsIfNeeded(flutterProject.macos, getMacOSBuildDirectory(), buildInfo.mode);
+  // If the xcfilelists do not exist, create empty version.
+  if (!flutterProject.macos.inputFileList.existsSync()) {
+     flutterProject.macos.inputFileList.createSync(recursive: true);
+  }
+  if (!flutterProject.macos.outputFileList.existsSync()) {
+    flutterProject.macos.outputFileList.createSync(recursive: true);
+  }
 
   // Set debug or release mode.
   String config = 'Debug';
diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart
index 3ce30f1..27a67b6 100644
--- a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart
+++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart
@@ -2,22 +2,48 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:flutter_tools/src/base/build.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/io.dart';
 import 'package:flutter_tools/src/base/platform.dart';
 import 'package:flutter_tools/src/base/process_manager.dart';
 import 'package:flutter_tools/src/build_system/build_system.dart';
-import 'package:flutter_tools/src/build_system/exceptions.dart';
 import 'package:flutter_tools/src/build_system/targets/dart.dart';
 import 'package:flutter_tools/src/build_system/targets/macos.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/macos/cocoapods.dart';
+import 'package:flutter_tools/src/macos/xcode.dart';
 import 'package:mockito/mockito.dart';
 import 'package:process/process.dart';
 
 import '../../../src/common.dart';
 import '../../../src/testbed.dart';
 
+const String _kInputPrefix = 'bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework';
+const String _kOutputPrefix = 'macos/Flutter/ephemeral/FlutterMacOS.framework';
+
+final List<File> inputs = <File>[
+  fs.file('$_kInputPrefix/FlutterMacOS'),
+  // Headers
+  fs.file('$_kInputPrefix/Headers/FlutterDartProject.h'),
+  fs.file('$_kInputPrefix/Headers/FlutterEngine.h'),
+  fs.file('$_kInputPrefix/Headers/FlutterViewController.h'),
+  fs.file('$_kInputPrefix/Headers/FlutterBinaryMessenger.h'),
+  fs.file('$_kInputPrefix/Headers/FlutterChannels.h'),
+  fs.file('$_kInputPrefix/Headers/FlutterCodecs.h'),
+  fs.file('$_kInputPrefix/Headers/FlutterMacros.h'),
+  fs.file('$_kInputPrefix/Headers/FlutterPluginMacOS.h'),
+  fs.file('$_kInputPrefix/Headers/FlutterPluginRegistrarMacOS.h'),
+  fs.file('$_kInputPrefix/Headers/FlutterMacOS.h'),
+  // Modules
+  fs.file('$_kInputPrefix/Modules/module.modulemap'),
+  // Resources
+  fs.file('$_kInputPrefix/Resources/icudtl.dat'),
+  fs.file('$_kInputPrefix/Resources/Info.plist'),
+  // Ignore Versions folder for now
+  fs.file('packages/flutter_tools/lib/src/build_system/targets/macos.dart'),
+];
+
 void main() {
   Testbed testbed;
   Environment environment;
@@ -37,44 +63,11 @@
     testbed = Testbed(setup: () {
       environment = Environment(
         projectDir: fs.currentDirectory,
-      );
-      final List<File> inputs = <File>[
-        fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/FlutterMacOS'),
-        fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FLEOpenGLContextHandling.h'),
-        fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FLEReshapeListener.h'),
-        fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FLEView.h'),
-        fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FLEViewController.h'),
-        fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterBinaryMessenger.h'),
-        fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterChannels.h'),
-        fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterCodecs.h'),
-        fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterMacOS.h'),
-        fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterPluginMacOS.h'),
-        fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterPluginRegisrarMacOS.h'),
-        fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Modules/module.modulemap'),
-        fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Resources/icudtl.dat'),
-        fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Resources/info.plist'),
-        fs.file('packages/flutter_tools/lib/src/build_system/targets/macos.dart'),
-      ];
-      for (File input in inputs) {
-        input.createSync(recursive: true);
-      }
-      when(processManager.run(any)).thenAnswer((Invocation invocation) async {
-        final List<String> arguments = invocation.positionalArguments.first;
-        final Directory source = fs.directory(arguments[arguments.length - 2]);
-        final Directory target = fs.directory(arguments.last)
-          ..createSync(recursive: true);
-        for (FileSystemEntity entity in source.listSync(recursive: true)) {
-          if (entity is File) {
-            final String relative = fs.path.relative(entity.path, from: source.path);
-            final String destination = fs.path.join(target.path, relative);
-            if (!fs.file(destination).parent.existsSync()) {
-              fs.file(destination).parent.createSync();
-            }
-            entity.copySync(destination);
-          }
+        defines: <String, String>{
+          kBuildMode: 'debug',
+          kTargetPlatform: 'darwin-x64',
         }
-        return FakeProcessResult()..exitCode = 0;
-      });
+      );
     }, overrides: <Type, Generator>{
       ProcessManager: () => MockProcessManager(),
       Platform: () => mockPlatform,
@@ -82,115 +75,69 @@
   });
 
   test('Copies files to correct cache directory', () => testbed.run(() async {
+    for (File input in inputs) {
+      input.createSync(recursive: true);
+    }
+    when(processManager.run(any)).thenAnswer((Invocation invocation) async {
+      final List<String> arguments = invocation.positionalArguments.first;
+      final Directory source = fs.directory(arguments[arguments.length - 2]);
+      final Directory target = fs.directory(arguments.last)
+        ..createSync(recursive: true);
+      for (FileSystemEntity entity in source.listSync(recursive: true)) {
+        if (entity is File) {
+          final String relative = fs.path.relative(entity.path, from: source.path);
+          final String destination = fs.path.join(target.path, relative);
+          if (!fs.file(destination).parent.existsSync()) {
+            fs.file(destination).parent.createSync();
+          }
+          entity.copySync(destination);
+        }
+      }
+      return FakeProcessResult()..exitCode = 0;
+    });
     await const UnpackMacOS().build(<File>[], environment);
 
-    expect(fs.directory('macos/Flutter/ephemeral/FlutterMacOS.framework').existsSync(), true);
-    expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/FlutterMacOS').existsSync(), true);
-    expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FLEViewController.h').existsSync(), true);
-    expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterBinaryMessenger.h').existsSync(), true);
-    expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterChannels.h').existsSync(), true);
-    expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterCodecs.h').existsSync(), true);
-    expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterMacOS.h').existsSync(), true);
-    expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterPluginMacOS.h').existsSync(), true);
-    expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FlutterPluginRegisrarMacOS.h').existsSync(), true);
-    expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Modules/module.modulemap').existsSync(), true);
-    expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Resources/icudtl.dat').existsSync(), true);
-    expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Resources/info.plist').existsSync(), true);
+    expect(fs.directory('$_kOutputPrefix').existsSync(), true);
+    for (File file in inputs) {
+      expect(fs.file(file.path.replaceFirst(_kInputPrefix, _kOutputPrefix)).existsSync(), true);
+    }
+  }));
+
+  test('debug macOS application fails if App.framework missing', () => testbed.run(() async {
+    final String inputKernel = fs.path.join(environment.buildDir.path, 'app.dill');
+    fs.file(inputKernel)
+      ..createSync(recursive: true)
+      ..writeAsStringSync('testing');
+
+    expect(() async => await const DebugBundleFlutterAssets().build(<File>[], environment),
+        throwsA(isInstanceOf<Exception>()));
   }));
 
   test('debug macOS application copies kernel blob', () => testbed.run(() async {
+    fs.file(fs.path.join('bin', 'cache', 'artifacts', 'engine', 'darwin-x64',
+        'vm_isolate_snapshot.bin')).createSync(recursive: true);
+    fs.file(fs.path.join('bin', 'cache', 'artifacts', 'engine', 'darwin-x64',
+        'isolate_snapshot.bin')).createSync(recursive: true);
+    final String frameworkPath = fs.path.join(environment.projectDir.path,
+        'macos', 'Flutter', 'ephemeral', 'App.framework');
     final String inputKernel = fs.path.join(environment.buildDir.path, 'app.dill');
-    final String outputKernel = fs.path.join(environment.buildDir.path, 'flutter_assets', 'kernel_blob.bin');
+    fs.directory(frameworkPath).createSync(recursive: true);
+    final String outputKernel = fs.path.join(frameworkPath, 'flutter_assets', 'kernel_blob.bin');
     fs.file(inputKernel)
       ..createSync(recursive: true)
       ..writeAsStringSync('testing');
 
-    await const DebugMacOSApplication().build(<File>[], environment);
+    await const DebugBundleFlutterAssets().build(<File>[], environment);
 
     expect(fs.file(outputKernel).readAsStringSync(), 'testing');
   }));
-
-  test('profile macOS application copies kernel blob', () => testbed.run(() async {
-    final String inputKernel = fs.path.join(environment.buildDir.path, 'app.dill');
-    final String outputKernel = fs.path.join(environment.buildDir.path, 'flutter_assets', 'kernel_blob.bin');
-    fs.file(inputKernel)
-      ..createSync(recursive: true)
-      ..writeAsStringSync('testing');
-
-    await const ProfileMacOSApplication().build(<File>[], environment);
-
-    expect(fs.file(outputKernel).readAsStringSync(), 'testing');
-  }));
-
-  test('release macOS application copies kernel blob', () => testbed.run(() async {
-    final String inputKernel = fs.path.join(environment.buildDir.path, 'app.dill');
-    final String outputKernel = fs.path.join(environment.buildDir.path, 'flutter_assets', 'kernel_blob.bin');
-    fs.file(inputKernel)
-      ..createSync(recursive: true)
-      ..writeAsStringSync('testing');
-
-    await const ReleaseMacOSApplication().build(<File>[], environment);
-
-    expect(fs.file(outputKernel).readAsStringSync(), 'testing');
-  }));
-
-  // Changing target names will require a corresponding update in flutter_tools/bin/macos_build_flutter_assets.sh.
-  test('Target names match those expected by bin scripts', () => testbed.run(() async {
-    expect(const DebugMacOSApplication().name, 'debug_macos_application');
-    expect(const ProfileMacOSApplication().name, 'profile_macos_application');
-    expect(const ReleaseMacOSApplication().name, 'release_macos_application');
-  }));
-
-
-  test('DebugMacOSPodInstall throws if missing build mode', () => testbed.run(() async {
-    expect(() => const DebugMacOSPodInstall().build(<File>[], environment),
-        throwsA(isInstanceOf<MissingDefineException>()));
-  }));
-
-  test('DebugMacOSPodInstall skips if podfile does not exist', () => testbed.run(() async {
-    await const DebugMacOSPodInstall().build(<File>[], Environment(
-      projectDir: fs.currentDirectory,
-      defines: <String, String>{
-        kBuildMode: 'debug'
-      }
-    ));
-
-    verifyNever(cocoaPods.processPods(
-      xcodeProject: anyNamed('xcodeProject'),
-      engineDir: anyNamed('engineDir'),
-      isSwift: true,
-      dependenciesChanged: true));
-  }, overrides: <Type, Generator>{
-    CocoaPods: () => MockCocoaPods(),
-  }));
-
-  test('DebugMacOSPodInstall invokes processPods with podfile', () => testbed.run(() async {
-    fs.file(fs.path.join('macos', 'Podfile')).createSync(recursive: true);
-    await const DebugMacOSPodInstall().build(<File>[], Environment(
-        projectDir: fs.currentDirectory,
-        defines: <String, String>{
-          kBuildMode: 'debug'
-        }
-    ));
-
-    verify(cocoaPods.processPods(
-      xcodeProject: anyNamed('xcodeProject'),
-      engineDir: anyNamed('engineDir'),
-      isSwift: true,
-      dependenciesChanged: true)).called(1);
-  }, overrides: <Type, Generator>{
-    CocoaPods: () => MockCocoaPods(),
-  }));
-
-  test('b', () => testbed.run(() async {
-
-  }));
-
 }
 
 class MockPlatform extends Mock implements Platform {}
 class MockCocoaPods extends Mock implements CocoaPods {}
 class MockProcessManager extends Mock implements ProcessManager {}
+class MockGenSnapshot extends Mock implements GenSnapshot {}
+class MockXCode extends Mock implements Xcode {}
 class FakeProcessResult implements ProcessResult {
   @override
   int exitCode;
@@ -204,5 +151,3 @@
   @override
   String stdout = '';
 }
-
-
diff --git a/packages/flutter_tools/test/general.shard/commands/assemble_test.dart b/packages/flutter_tools/test/general.shard/commands/assemble_test.dart
index 638582b..afc5f7d 100644
--- a/packages/flutter_tools/test/general.shard/commands/assemble_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/assemble_test.dart
@@ -73,7 +73,7 @@
       return BuildResult(
         success: true,
         inputFiles: <File>[fs.file('foo'), fs.file('fizz')..createSync()],
-        outputFiles: <File>[fs.file('bar')]);
+        outputFiles: <File>[fs.file('bar'), fs.file(fs.path.join('.dart_tool', 'fizz2'))..createSync(recursive: true)]);
     });
     await commandRunner.run(<String>['assemble', '--build-outputs=outputs', '--build-inputs=inputs', 'unpack_macos']);