Flutter assemble for macos take 2! (#36987)

diff --git a/packages/flutter_tools/bin/macos_build_flutter_assets.sh b/packages/flutter_tools/bin/macos_build_flutter_assets.sh
index d576a87..1380fbf 100755
--- a/packages/flutter_tools/bin/macos_build_flutter_assets.sh
+++ b/packages/flutter_tools/bin/macos_build_flutter_assets.sh
@@ -3,8 +3,6 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-# TODO(jonahwilliams): refactor this and xcode_backend.sh into one script
-# once macOS supports the same configuration as iOS.
 RunCommand() {
   if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
     echo "♦ $*"
@@ -33,18 +31,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 +49,40 @@
     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:]")"
+case "$build_mode" in
+    debug)
+        build_target="debug_macos_application"
+        ;;
+    profile)
+        build_target="profile_macos_application"
+        ;;
+    release)
+        build_target="release_macos_application"
+        ;;
+    *)
+        EchoError "Unknown build mode ${build_mode}"
+        exit -1
+        ;;
+esac
 
+# The path where the input/output xcfilelists are stored. These are used by xcode
+# to conditionally skip this script phase if neither have changed.
+build_inputs_path="${SOURCE_ROOT}/Flutter/ephemeral/FlutterInputs.xcfilelist"
+build_outputs_path="${SOURCE_ROOT}/Flutter/ephemeral/FlutterOutputs.xcfilelist"
+
+# TODO(jonahwilliams): support flavors https://github.com/flutter/flutter/issues/32923
 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                                                                \
+    -dTargetFile="${target_path}"                                           \
+    -dTargetPlatform=darwin-x64                                             \
+    -dBuildMode="${build_mode}"                                             \
+    --build-inputs="${build_inputs_path}"                                   \
+    --build-outputs="${build_outputs_path}"                                 \
+    "${build_target}"
diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart
index a763fc4..711092e 100644
--- a/packages/flutter_tools/lib/src/artifacts.dart
+++ b/packages/flutter_tools/lib/src/artifacts.dart
@@ -54,6 +54,8 @@
   windowsDesktopPath,
   /// The root of the sky_engine package
   skyEnginePath,
+  /// The location of the macOS engine podspec file.
+  flutterMacOSPodspec,
 }
 
 String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMode mode ]) {
@@ -119,6 +121,8 @@
       return '';
     case Artifact.skyEnginePath:
       return 'sky_engine';
+    case Artifact.flutterMacOSPodspec:
+      return 'FlutterMacOS.podspec';
   }
   assert(false, 'Invalid artifact $artifact.');
   return null;
@@ -269,6 +273,7 @@
       case Artifact.flutterMacOSFramework:
       case Artifact.linuxDesktopPath:
       case Artifact.windowsDesktopPath:
+      case Artifact.flutterMacOSPodspec:
         final String engineArtifactsPath = cache.getArtifactDirectory('engine').path;
         final String platformDirName = getNameForTargetPlatform(platform);
         return fs.path.join(engineArtifactsPath, platformDirName, _artifactToFileName(artifact, platform, mode));
@@ -384,6 +389,8 @@
         return fs.path.join(_hostEngineOutPath, artifactFileName);
       case Artifact.skyEnginePath:
         return fs.path.join(_hostEngineOutPath, 'gen', 'dart-pkg', artifactFileName);
+      case Artifact.flutterMacOSPodspec:
+        return fs.path.join(_hostEngineOutPath, _artifactToFileName(artifact));
     }
     assert(false, 'Invalid artifact $artifact.');
     return null;
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 5d0a88b..c3cf8cd 100644
--- a/packages/flutter_tools/lib/src/build_system/build_system.dart
+++ b/packages/flutter_tools/lib/src/build_system/build_system.dart
@@ -171,7 +171,7 @@
       }
     }
 
-    // For each outut, first determine if we've already computed the hash
+    // For each output, first determine if we've already computed the hash
     // for it. Then collect it to be sent off for hashing as a group.
     for (String previousOutput in previousOutputs) {
       final File file = fs.file(previousOutput);
@@ -436,11 +436,19 @@
 
 /// The result information from the build system.
 class BuildResult {
-  BuildResult(this.success, this.exceptions, this.performance);
+  BuildResult({
+    @required this.success,
+    this.exceptions = const <String, ExceptionMeasurement>{},
+    this.performance = const <String, PerformanceMeasurement>{},
+    this.inputFiles = const <File>[],
+    this.outputFiles = const <File>[],
+  });
 
   final bool success;
   final Map<String, ExceptionMeasurement> exceptions;
   final Map<String, PerformanceMeasurement> performance;
+  final List<File> inputFiles;
+  final List<File> outputFiles;
 
   bool get hasException => exceptions.isNotEmpty;
 }
@@ -472,10 +480,31 @@
       // Always persist the file cache to disk.
       fileCache.persist();
     }
+    // TODO(jonahwilliams): this is a bit of a hack, due to various parts of
+    // the flutter tool writing these files unconditionally. Since Xcode uses
+    // 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.
+    {
+      buildInstance.inputFiles.removeWhere((String path, File file) {
+        return path.contains('pubspec.yaml') ||
+           path.contains('.flutter-plugins') ||
+           path.contains('xcconfig');
+      });
+      buildInstance.outputFiles.removeWhere((String path, File file) {
+        return path.contains('pubspec.yaml') ||
+           path.contains('.flutter-plugins') ||
+           path.contains('xcconfig');
+      });
+    }
     return BuildResult(
-      passed,
-      buildInstance.exceptionMeasurements,
-      buildInstance.stepTimings,
+      success: passed,
+      exceptions: buildInstance.exceptionMeasurements,
+      performance: buildInstance.stepTimings,
+      inputFiles: buildInstance.inputFiles.values.toList()
+          ..sort((File a, File b) => a.path.compareTo(b.path)),
+      outputFiles: buildInstance.outputFiles.values.toList()
+          ..sort((File a, File b) => a.path.compareTo(b.path)),
     );
   }
 }
@@ -490,6 +519,8 @@
   final Map<String, AsyncMemoizer<void>> pending = <String, AsyncMemoizer<void>>{};
   final Environment environment;
   final FileHashStore fileCache;
+  final Map<String, File> inputFiles = <String, File>{};
+  final Map<String, File> outputFiles = <String, File>{};
 
   // Timings collected during target invocation.
   final Map<String, PerformanceMeasurement> stepTimings = <String, PerformanceMeasurement>{};
@@ -514,18 +545,41 @@
     try {
       final List<File> inputs = target.resolveInputs(environment);
       final bool canSkip = await target.computeChanges(inputs, environment, fileCache);
+      for (File input in inputs) {
+        // The build system should produce a list of aggregate input and output
+        // files for the overall build. The goal is to provide this to a hosting
+        // build system, such as Xcode, to configure logic for when to skip the
+        // rule/phase which contains the flutter build. When looking at the
+        // inputs and outputs for the individual rules, we need to be careful to
+        // remove inputs that were actually output from previous build steps.
+        // This indicates that the file is actual an output or intermediary. If
+        // these files are included as both inputs and outputs then it isn't
+        // possible to construct a DAG describing the build.
+        final String resolvedPath = input.resolveSymbolicLinksSync();
+        if (outputFiles.containsKey(resolvedPath)) {
+          continue;
+        }
+        inputFiles[resolvedPath] = input;
+      }
       if (canSkip) {
         skipped = true;
         printStatus('Skipping target: ${target.name}');
+        final List<File> outputs = target.resolveOutputs(environment, implicit: true);
+        for (File output in outputs) {
+          outputFiles[output.resolveSymbolicLinksSync()] = output;
+        }
       } else {
         printStatus('${target.name}: Starting');
         await target.build(inputs, environment);
         printStatus('${target.name}: Complete');
 
-        final List<File> outputs = target.resolveOutputs(environment);
+        final List<File> outputs = target.resolveOutputs(environment, implicit: true);
         // Update hashes for output files.
         await fileCache.hashFiles(outputs);
         target._writeStamp(inputs, outputs, environment);
+        for (File output in outputs) {
+          outputFiles[output.resolveSymbolicLinksSync()] = output;
+        }
       }
     } catch (exception, stackTrace) {
       target.clearStamp(environment);
@@ -554,10 +608,10 @@
 
 /// Helper class to collect measurement data.
 class PerformanceMeasurement {
-  PerformanceMeasurement(this.target, this.elapsedMilliseconds, this.skiped, this.passed);
+  PerformanceMeasurement(this.target, this.elapsedMilliseconds, this.skipped, this.passed);
   final int elapsedMilliseconds;
   final String target;
-  final bool skiped;
+  final bool skipped;
   final bool passed;
 }
 
diff --git a/packages/flutter_tools/lib/src/build_system/source.dart b/packages/flutter_tools/lib/src/build_system/source.dart
index a9727e8..6aeda2c 100644
--- a/packages/flutter_tools/lib/src/build_system/source.dart
+++ b/packages/flutter_tools/lib/src/build_system/source.dart
@@ -38,8 +38,10 @@
 
   /// Visit a [Source] which contains a file uri.
   ///
-  /// The uri may that may include constants defined in an [Environment].
-  void visitPattern(String pattern) {
+  /// The uri may include constants defined in an [Environment]. If
+  /// [optional] is true, the file is not required to exist. In this case, it
+  /// is never resolved as an input.
+  void visitPattern(String pattern, bool optional) {
     // perform substitution of the environmental values and then
     // of the local values.
     final List<String> segments = <String>[];
@@ -74,38 +76,41 @@
     }
     rawParts.skip(1).forEach(segments.add);
     final String filePath = fs.path.joinAll(segments);
-    if (hasWildcard) {
-      // Perform a simple match by splitting the wildcard containing file one
-      // the `*`. For example, for `/*.dart`, we get [.dart]. We then check
-      // that part of the file matches. If there are values before and after
-      // the `*` we need to check that both match without overlapping. For
-      // example, `foo_*_.dart`. We want to match `foo_b_.dart` but not
-      // `foo_.dart`. To do so, we first subtract the first section from the
-      // string if the first segment matches.
-      final List<String> segments = wildcardFile.split('*');
-      if (segments.length > 2) {
-        throw InvalidPatternException(pattern);
+    if (!hasWildcard) {
+      if (optional && !fs.isFileSync(filePath)) {
+        return;
       }
-      if (!fs.directory(filePath).existsSync()) {
-        throw Exception('$filePath does not exist!');
-      }
-      for (FileSystemEntity entity in fs.directory(filePath).listSync()) {
-        final String filename = fs.path.basename(entity.path);
-        if (segments.isEmpty) {
-          sources.add(fs.file(entity.absolute));
-        } else if (segments.length == 1) {
-          if (filename.startsWith(segments[0]) ||
-              filename.endsWith(segments[0])) {
-            sources.add(entity.absolute);
-          }
-        } else if (filename.startsWith(segments[0])) {
-          if (filename.substring(segments[0].length).endsWith(segments[1])) {
-            sources.add(entity.absolute);
-          }
+      sources.add(fs.file(fs.path.normalize(filePath)));
+      return;
+    }
+    // Perform a simple match by splitting the wildcard containing file one
+    // the `*`. For example, for `/*.dart`, we get [.dart]. We then check
+    // that part of the file matches. If there are values before and after
+    // the `*` we need to check that both match without overlapping. For
+    // example, `foo_*_.dart`. We want to match `foo_b_.dart` but not
+    // `foo_.dart`. To do so, we first subtract the first section from the
+    // string if the first segment matches.
+    final List<String> wildcardSegments = wildcardFile.split('*');
+    if (wildcardSegments.length > 2) {
+      throw InvalidPatternException(pattern);
+    }
+    if (!fs.directory(filePath).existsSync()) {
+      throw Exception('$filePath does not exist!');
+    }
+    for (FileSystemEntity entity in fs.directory(filePath).listSync()) {
+      final String filename = fs.path.basename(entity.path);
+      if (wildcardSegments.isEmpty) {
+        sources.add(fs.file(entity.absolute));
+      } else if (wildcardSegments.length == 1) {
+        if (filename.startsWith(wildcardSegments[0]) ||
+            filename.endsWith(wildcardSegments[0])) {
+          sources.add(entity.absolute);
+        }
+      } else if (filename.startsWith(wildcardSegments[0])) {
+        if (filename.substring(wildcardSegments[0].length).endsWith(wildcardSegments[1])) {
+          sources.add(entity.absolute);
         }
       }
-    } else {
-      sources.add(fs.file(fs.path.normalize(filePath)));
     }
   }
 
@@ -139,7 +144,7 @@
 abstract class Source {
   /// This source is a file-uri which contains some references to magic
   /// environment variables.
-  const factory Source.pattern(String pattern) = _PatternSource;
+  const factory Source.pattern(String pattern, { bool optional }) = _PatternSource;
 
   /// This source is produced by invoking the provided function.
   const factory Source.function(InputFunction function) = _FunctionSource;
@@ -203,12 +208,13 @@
 }
 
 class _PatternSource implements Source {
-  const _PatternSource(this.value);
+  const _PatternSource(this.value, { this.optional = false });
 
   final String value;
+  final bool optional;
 
   @override
-  void accept(SourceVisitor visitor) => visitor.visitPattern(value);
+  void accept(SourceVisitor visitor) => visitor.visitPattern(value, optional);
 
   @override
   bool get implicit => value.contains('*');
diff --git a/packages/flutter_tools/lib/src/build_system/targets/assets.dart b/packages/flutter_tools/lib/src/build_system/targets/assets.dart
index 7b21df8..934fc0f 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/assets.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/assets.dart
@@ -7,6 +7,8 @@
 import '../../asset.dart';
 import '../../base/file_system.dart';
 import '../../devfs.dart';
+import '../../plugins.dart';
+import '../../project.dart';
 import '../build_system.dart';
 
 /// The copying logic for flutter assets.
@@ -100,3 +102,45 @@
       }));
   }
 }
+
+/// Rewrites the `.flutter-plugins` file of [project] based on the plugin
+/// dependencies declared in `pubspec.yaml`.
+// TODO(jonahwiliams): this should be per platform and located in build
+// outputs.
+class FlutterPlugins extends Target {
+  const FlutterPlugins();
+
+  @override
+  String get name => 'flutter_plugins';
+
+  @override
+  List<Target> get dependencies => const <Target>[];
+
+  @override
+  List<Source> get inputs => const <Source>[
+    Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/assets.dart'),
+    Source.pattern('{PROJECT_DIR}/pubspec.yaml'),
+  ];
+
+  @override
+  List<Source> get outputs => const <Source>[
+    Source.pattern('{PROJECT_DIR}/.flutter-plugins')
+  ];
+
+  @override
+  Future<void> build(List<File> inputFiles, Environment environment) async {
+    // The pubspec may change for reasons other than plugins changing, so we compare
+    // the manifest before writing. Some hosting build systems use timestamps
+    // so we need to be careful to avoid tricking them into doing more work than
+    // necessary.
+    final FlutterProject project = FlutterProject.fromDirectory(environment.projectDir);
+    final List<Plugin> plugins = findPlugins(project);
+    final String pluginManifest = plugins
+        .map<String>((Plugin p) => '${p.name}=${escapePath(p.path)}')
+        .join('\n');
+    final File flutterPluginsFile = environment.projectDir.childFile('.flutter-plugins');
+    if (!flutterPluginsFile.existsSync() || flutterPluginsFile.readAsStringSync() != pluginManifest) {
+      flutterPluginsFile.writeAsStringSync(pluginManifest);
+    }
+  }
+}
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 6b7be2e..2c2d4d7 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/dart.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/dart.dart
@@ -86,6 +86,9 @@
     }
     final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
     final String targetFile = environment.defines[kTargetFile] ?? fs.path.join('lib', 'main.dart');
+    final String packagesPath = environment.projectDir.childFile('.packages').path;
+    final PackageUriMapper packageUriMapper = PackageUriMapper(targetFile,
+        packagesPath, null, null);
 
     final CompilerOutput output = await compiler.compile(
       sdkRoot: artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath, mode: buildMode),
@@ -95,7 +98,8 @@
       targetProductVm: buildMode == BuildMode.release,
       outputFilePath: environment.buildDir.childFile('app.dill').path,
       depFilePath: null,
-      mainPath: targetFile,
+      packagesPath: packagesPath,
+      mainPath: packageUriMapper.map(targetFile)?.toString() ?? targetFile,
     );
     if (output.errorCount != 0) {
       throw Exception('Errors during snapshot creation: $output');
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 3d14ca7..95d5edf 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart
@@ -6,10 +6,16 @@
 import '../../base/file_system.dart';
 import '../../base/io.dart';
 import '../../base/process_manager.dart';
+import '../../build_info.dart';
 import '../../globals.dart';
+import '../../macos/cocoapods.dart';
+import '../../project.dart';
 import '../build_system.dart';
+import '../exceptions.dart';
+import 'assets.dart';
+import 'dart.dart';
 
-const String _kOutputPrefix = '{PROJECT_DIR}/macos/Flutter/FlutterMacOS.framework';
+const String _kOutputPrefix = '{PROJECT_DIR}/macos/Flutter/ephemeral/FlutterMacOS.framework';
 
 /// Copy the macOS framework to the correct copy dir by invoking 'cp -R'.
 ///
@@ -63,6 +69,7 @@
       .projectDir
       .childDirectory('macos')
       .childDirectory('Flutter')
+      .childDirectory('ephemeral')
       .childDirectory('FlutterMacOS.framework');
     if (targetDirectory.existsSync()) {
       targetDirectory.deleteSync(recursive: true);
@@ -78,3 +85,112 @@
     }
   }
 }
+
+/// Tell cocoapods to re-fetch dependencies.
+class DebugMacOSPodInstall extends Target {
+  const DebugMacOSPodInstall();
+
+  @override
+  String get name => 'debug_macos_pod_install';
+
+  @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'),
+  ];
+
+  @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.
+  ];
+
+  @override
+  List<Target> get dependencies => const <Target>[
+    UnpackMacOS(),
+    FlutterPlugins(),
+  ];
+
+  @override
+  Future<void> build(List<File> inputFiles, Environment environment) async {
+    if (environment.defines[kBuildMode] == null) {
+      throw MissingDefineException(kBuildMode, 'debug_macos_pod_install');
+    }
+    // If there is no podfile do not perform any pods actions.
+    if (!environment.projectDir.childDirectory('macos')
+        .childFile('Podfile').existsSync()) {
+      return;
+    }
+    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,
+    );
+  }
+}
+
+/// 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);
+    }
+    sourceFile.copySync(destinationFile.path);
+  }
+
+  @override
+  List<Target> get dependencies => const <Target>[
+    FlutterPlugins(),
+    UnpackMacOS(),
+    KernelSnapshot(),
+    CopyAssets(),
+    DebugMacOSPodInstall(),
+  ];
+
+  @override
+  List<Source> get inputs => const <Source>[
+    Source.pattern('{BUILD_DIR}/app.dill')
+  ];
+
+  @override
+  String get name => 'debug_macos_application';
+
+  @override
+  List<Source> get outputs => const <Source>[
+    Source.pattern('{BUILD_DIR}/flutter_assets/kernel_blob.bin'),
+  ];
+}
+
+// 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 78cb1ee..476d788 100644
--- a/packages/flutter_tools/lib/src/commands/assemble.dart
+++ b/packages/flutter_tools/lib/src/commands/assemble.dart
@@ -2,8 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:meta/meta.dart';
+
 import '../base/common.dart';
 import '../base/context.dart';
+import '../base/file_system.dart';
 import '../build_system/build_system.dart';
 import '../build_system/targets/assets.dart';
 import '../build_system/targets/dart.dart';
@@ -29,6 +32,9 @@
   AotElfRelease(),
   AotAssemblyProfile(),
   AotAssemblyRelease(),
+  DebugMacOSApplication(),
+  ProfileMacOSApplication(),
+  ReleaseMacOSApplication(),
 ];
 
 /// Assemble provides a low level API to interact with the flutter tool build
@@ -40,14 +46,14 @@
       abbr: 'd',
       help: 'Allows passing configuration to a target with --define=target=key=value.'
     );
-    argParser.addOption(
-      'build-mode',
-      allowed: const <String>[
-        'debug',
-        'profile',
-        'release',
-      ],
-    );
+    argParser.addOption('build-inputs', help: 'A file path where a newline '
+        'separated file containing all inputs used will be written after a build.'
+        ' This file is not included as a build input or output. This file is not'
+        ' written if the build fails for any reason.');
+    argParser.addOption('build-outputs', help: 'A file path where a newline '
+        'separated file containing all outputs used will be written after a build.'
+        ' This file is not included as a build input or output. This file is not'
+        ' written if the build fails for any reason.');
     argParser.addOption(
       'resource-pool-size',
       help: 'The maximum number of concurrent tasks the build system will run.'
@@ -106,10 +112,33 @@
         printError('Target ${data.key} failed: ${data.value.exception}');
         printError('${data.value.exception}');
       }
-      throwToolExit('build failed');
-    } else {
-      printStatus('build succeeded');
+      throwToolExit('build failed.');
+    }
+    printStatus('build succeeded.');
+    if (argResults.wasParsed('build-inputs')) {
+      writeListIfChanged(result.inputFiles, argResults['build-inputs']);
+    }
+    if (argResults.wasParsed('build-outputs')) {
+      writeListIfChanged(result.outputFiles, argResults['build-outputs']);
     }
     return null;
   }
 }
+
+@visibleForTesting
+void writeListIfChanged(List<File> files, String path) {
+  final File file = fs.file(path);
+  final StringBuffer buffer = StringBuffer();
+  // These files are already sorted.
+  for (File file in files) {
+    buffer.writeln(file.resolveSymbolicLinksSync());
+  }
+  final String newContents = buffer.toString();
+  if (!file.existsSync()) {
+    file.writeAsStringSync(newContents);
+  }
+  final String currentContents = file.readAsStringSync();
+  if (currentContents != newContents) {
+    file.writeAsStringSync(newContents);
+  }
+}
diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart
index 32521d7..a9b1caf 100644
--- a/packages/flutter_tools/lib/src/commands/attach.dart
+++ b/packages/flutter_tools/lib/src/commands/attach.dart
@@ -257,7 +257,6 @@
         device,
         flutterProject: flutterProject,
         trackWidgetCreation: argResults['track-widget-creation'],
-        dillOutputPath: argResults['output-dill'],
         fileSystemRoots: argResults['filesystem-root'],
         fileSystemScheme: argResults['filesystem-scheme'],
         viewFilter: argResults['isolate-filter'],
diff --git a/packages/flutter_tools/lib/src/commands/build_macos.dart b/packages/flutter_tools/lib/src/commands/build_macos.dart
index f871186..dc27a17 100644
--- a/packages/flutter_tools/lib/src/commands/build_macos.dart
+++ b/packages/flutter_tools/lib/src/commands/build_macos.dart
@@ -14,7 +14,7 @@
 import '../runner/flutter_command.dart' show FlutterCommandResult;
 import 'build.dart';
 
-/// A command to build a macos desktop target through a build shell script.
+/// A command to build a macOS desktop target through a build shell script.
 class BuildMacosCommand extends BuildSubCommand {
   BuildMacosCommand() {
     usesTargetOption();
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index a441ecf..e1bf5ec 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -406,7 +406,6 @@
       device,
       flutterProject: flutterProject,
       trackWidgetCreation: trackWidgetCreation,
-      dillOutputPath: dillOutputPath,
       viewFilter: isolateFilter,
       target: target,
       buildMode: options.buildInfo.mode,
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 298386c..6e9d869 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -397,7 +397,6 @@
         device,
         flutterProject: flutterProject,
         trackWidgetCreation: argResults['track-widget-creation'],
-        dillOutputPath: argResults['output-dill'],
         fileSystemRoots: argResults['filesystem-root'],
         fileSystemScheme: argResults['filesystem-scheme'],
         viewFilter: argResults['isolate-filter'],
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
index af96308..bd9b6d1 100644
--- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart
+++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -48,13 +48,15 @@
   String targetOverride,
   bool useMacOSConfig = false,
   bool setSymroot = true,
+  String buildDirOverride,
 }) async {
   final List<String> xcodeBuildSettings = _xcodeBuildSettingsLines(
     project: project,
     buildInfo: buildInfo,
     targetOverride: targetOverride,
     useMacOSConfig: useMacOSConfig,
-    setSymroot: setSymroot
+    setSymroot: setSymroot,
+    buildDirOverride: buildDirOverride
   );
 
   _updateGeneratedXcodePropertiesFile(
@@ -121,6 +123,7 @@
   String targetOverride,
   bool useMacOSConfig = false,
   bool setSymroot = true,
+  String buildDirOverride,
 }) {
   final List<String> xcodeBuildSettings = <String>[];
 
@@ -135,7 +138,7 @@
     xcodeBuildSettings.add('FLUTTER_TARGET=$targetOverride');
 
   // The build outputs directory, relative to FLUTTER_APPLICATION_PATH.
-  xcodeBuildSettings.add('FLUTTER_BUILD_DIR=${getBuildDirectory()}');
+  xcodeBuildSettings.add('FLUTTER_BUILD_DIR=${buildDirOverride ?? getBuildDirectory()}');
 
   if (setSymroot) {
     xcodeBuildSettings.add('SYMROOT=\${SOURCE_ROOT}/../${getIosBuildDirectory()}');
diff --git a/packages/flutter_tools/lib/src/macos/application_package.dart b/packages/flutter_tools/lib/src/macos/application_package.dart
index cfa4abc..000bb7f 100644
--- a/packages/flutter_tools/lib/src/macos/application_package.dart
+++ b/packages/flutter_tools/lib/src/macos/application_package.dart
@@ -6,7 +6,6 @@
 
 import '../application_package.dart';
 import '../base/file_system.dart';
-import '../build_info.dart';
 import '../globals.dart';
 import '../ios/plist_utils.dart' as plist;
 import '../project.dart';
@@ -31,18 +30,17 @@
   /// which is expected to start the application and send the observatory
   /// port over stdout.
   factory MacOSApp.fromPrebuiltApp(FileSystemEntity applicationBinary) {
-    final _ExecutableAndId executableAndId = _executableFromBundle(applicationBinary);
+    final ExecutableAndId executableAndId = executableFromBundle(applicationBinary);
     final Directory applicationBundle = fs.directory(applicationBinary);
     return PrebuiltMacOSApp(
       bundleDir: applicationBundle,
       bundleName: applicationBundle.path,
-      projectBundleId: executableAndId.id,
-      executable: executableAndId.executable,
+      executableAndId: executableAndId,
     );
   }
 
   /// Look up the executable name for a macOS application bundle.
-  static _ExecutableAndId _executableFromBundle(Directory applicationBundle) {
+  static ExecutableAndId executableFromBundle(Directory applicationBundle) {
     final FileSystemEntityType entityType = fs.typeSync(applicationBundle.path);
     if (entityType == FileSystemEntityType.notFound) {
       printError('File "${applicationBundle.path}" does not exist.');
@@ -75,40 +73,28 @@
     if (!fs.file(executable).existsSync()) {
       printError('Could not find macOS binary at $executable');
     }
-    return _ExecutableAndId(executable, id);
+    return ExecutableAndId(executable, id);
   }
 
   @override
   String get displayName => id;
-
-  String applicationBundle(BuildMode buildMode);
-
-  String executable(BuildMode buildMode);
 }
 
 class PrebuiltMacOSApp extends MacOSApp {
   PrebuiltMacOSApp({
     @required this.bundleDir,
     @required this.bundleName,
-    @required this.projectBundleId,
-    @required String executable,
-  }) : _executable = executable,
-       super(projectBundleId: projectBundleId);
+    @required this.executableAndId,
+  });
 
   final Directory bundleDir;
   final String bundleName;
-  final String projectBundleId;
-
-  final String _executable;
+  final ExecutableAndId executableAndId;
 
   @override
   String get name => bundleName;
 
-  @override
-  String applicationBundle(BuildMode buildMode) => bundleDir.path;
-
-  @override
-  String executable(BuildMode buildMode) => _executable;
+  String get executable => executableAndId.executable;
 }
 
 class BuildableMacOSApp extends MacOSApp {
@@ -118,35 +104,10 @@
 
   @override
   String get name => 'macOS';
-
-  @override
-  String applicationBundle(BuildMode buildMode) {
-    final File appBundleNameFile = project.nameFile;
-    if (!appBundleNameFile.existsSync()) {
-      printError('Unable to find app name. ${appBundleNameFile.path} does not exist');
-      return null;
-    }
-    return fs.path.join(
-        getMacOSBuildDirectory(),
-        'Build',
-        'Products',
-        buildMode == BuildMode.debug ? 'Debug' : 'Release',
-        appBundleNameFile.readAsStringSync().trim());
-  }
-
-  @override
-  String executable(BuildMode buildMode) {
-    final String directory = applicationBundle(buildMode);
-    if (directory == null) {
-      return null;
-    }
-    final _ExecutableAndId executableAndId = MacOSApp._executableFromBundle(fs.directory(directory));
-    return executableAndId.executable;
-  }
 }
 
-class _ExecutableAndId {
-  _ExecutableAndId(this.executable, this.id);
+class ExecutableAndId {
+  ExecutableAndId(this.executable, this.id);
 
   final String executable;
   final String id;
diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart
index 2b45525..a72a8b8 100644
--- a/packages/flutter_tools/lib/src/macos/build_macos.dart
+++ b/packages/flutter_tools/lib/src/macos/build_macos.dart
@@ -8,25 +8,34 @@
 import '../base/logger.dart';
 import '../base/process_manager.dart';
 import '../build_info.dart';
+import '../build_system/build_system.dart';
+import '../build_system/targets/dart.dart';
 import '../convert.dart';
 import '../globals.dart';
 import '../ios/xcodeproj.dart';
 import '../project.dart';
 import '../reporting/reporting.dart';
+import 'application_package.dart';
 
-import 'cocoapod_utils.dart';
-
-/// Builds the macOS project through xcodebuild.
-// TODO(jonahwilliams): refactor to share code with the existing iOS code.
-Future<void> buildMacOS({
+/// Builds the macOS project through xcodebuild and returns the app bundle.
+Future<PrebuiltMacOSApp> buildMacOS({
   FlutterProject flutterProject,
   BuildInfo buildInfo,
-  String targetOverride,
+  String targetOverride = 'lib/main.dart',
 }) async {
-  final Directory flutterBuildDir = fs.directory(getMacOSBuildDirectory());
-  if (!flutterBuildDir.existsSync()) {
-    flutterBuildDir.createSync(recursive: true);
-  }
+  // Create the environment used to process the build. This needs to match what
+  // is provided in bin/macos_build_flutter_assets.sh otherwise the directories
+  // will be different.
+  final Environment environment = Environment(
+    projectDir: flutterProject.directory,
+    buildDir: flutterProject.dartTool.childDirectory('flutter_build'),
+    defines: <String, String>{
+      kBuildMode: buildInfo.isDebug == true ? 'debug' : 'release',
+      kTargetPlatform: 'darwin-x64',
+      kTargetFile: fs.file(targetOverride).absolute.path
+    },
+  );
+
   // Write configuration to an xconfig file in a standard location.
   await updateGeneratedXcodeProperties(
     project: flutterProject,
@@ -34,27 +43,34 @@
     targetOverride: targetOverride,
     useMacOSConfig: true,
     setSymroot: false,
+    buildDirOverride: environment.buildDir.path,
   );
-  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';
-  if (buildInfo.isRelease) {
+  if (buildInfo.isRelease ?? false) {
     config = 'Release';
   }
-  // Run build script provided by application.
+  // Invoke Xcode with correct configuration.
   final Stopwatch sw = Stopwatch()..start();
-  final Process process = await processManager.start(<String>[
+  final List<String> command = <String>[
     '/usr/bin/env',
     'xcrun',
     'xcodebuild',
     '-workspace', flutterProject.macos.xcodeWorkspace.path,
-    '-configuration', '$config',
+    '-configuration', config,
     '-scheme', 'Runner',
-    '-derivedDataPath', flutterBuildDir.absolute.path,
-    'OBJROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Intermediates.noindex')}',
-    'SYMROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Products')}',
-  ], runInShell: true);
+    '-derivedDataPath', environment.buildDir.path,
+    'OBJROOT=${fs.path.join(environment.buildDir.path, 'Build', 'Intermediates.noindex')}',
+    'SYMROOT=${fs.path.join(environment.buildDir.path, 'Build', 'Products')}',
+  ];
+  final Process process = await processManager.start(command);
   final Status status = logger.startProgress(
     'Building macOS application...',
     timeout: null,
@@ -77,4 +93,13 @@
     throwToolExit('Build process failed');
   }
   flutterUsage.sendTiming('build', 'xcode-macos', Duration(milliseconds: sw.elapsedMilliseconds));
+  final File appBundleNameFile = flutterProject.macos.nameFile;
+  final Directory bundleDir = fs.directory(fs.path.join(
+    environment.buildDir.path,
+    'Build',
+    'Products',
+    buildInfo.mode == BuildMode.debug ? 'Debug' : 'Release',
+    appBundleNameFile.readAsStringSync().trim(),
+  ));
+  return MacOSApp.fromPrebuiltApp(bundleDir);
 }
diff --git a/packages/flutter_tools/lib/src/macos/macos_device.dart b/packages/flutter_tools/lib/src/macos/macos_device.dart
index 27ca0bb..fb37cb6 100644
--- a/packages/flutter_tools/lib/src/macos/macos_device.dart
+++ b/packages/flutter_tools/lib/src/macos/macos_device.dart
@@ -80,10 +80,13 @@
     bool usesTerminalUi = true,
     bool ipv6 = false,
   }) async {
+    Cache.releaseLockEarly();
     // Stop any running applications with the same executable.
-    if (!prebuiltApplication) {
-      Cache.releaseLockEarly();
-      await buildMacOS(
+    PrebuiltMacOSApp prebuiltMacOSApp;
+    if (prebuiltApplication) {
+      prebuiltMacOSApp = package;
+    } else {
+      prebuiltMacOSApp = await buildMacOS(
         flutterProject: FlutterProject.current(),
         buildInfo: debuggingOptions?.buildInfo,
         targetOverride: mainPath,
@@ -91,8 +94,7 @@
     }
 
     // Ensure that the executable is locatable.
-    final String executable = package.executable(debuggingOptions?.buildInfo?.mode);
-    if (executable == null) {
+    if (prebuiltMacOSApp == null) {
       printError('Unable to find executable to run');
       return LaunchResult.failed();
     }
@@ -100,7 +102,7 @@
     // Make sure to call stop app after we've built.
     await stopApp(package);
     final Process process = await processManager.start(<String>[
-      executable
+      prebuiltMacOSApp.executable,
     ]);
     if (debuggingOptions?.buildInfo?.isRelease == true) {
       return LaunchResult.succeeded();
@@ -111,7 +113,7 @@
       final Uri observatoryUri = await observatoryDiscovery.uri;
       // Bring app to foreground.
       await processManager.run(<String>[
-        'open', package.applicationBundle(debuggingOptions?.buildInfo?.mode),
+        'open', prebuiltMacOSApp.bundleName,
       ]);
       return LaunchResult.succeeded(observatoryUri: observatoryUri);
     } catch (error) {
@@ -125,9 +127,8 @@
   // TODO(jonahwilliams): implement using process manager.
   // currently we rely on killing the isolate taking down the application.
   @override
-  Future<bool> stopApp(covariant MacOSApp app) async {
-    // Assume debug for now.
-    return killProcess(app.executable(BuildMode.debug));
+  Future<bool> stopApp(covariant PrebuiltMacOSApp app) async {
+    return killProcess(app.executable);
   }
 
   @override
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index d9d7a3e..513dcee 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -664,6 +664,14 @@
   /// checked in should live here.
   Directory get ephemeralDirectory => managedDirectory.childDirectory('ephemeral');
 
+  /// The xcfilelist used to track the inputs for the Flutter script phase in
+  /// the Xcode build.
+  File get inputFileList => ephemeralDirectory.childFile('FlutterInputs.xcfilelist');
+
+  /// The xcfilelist used to track the outputs for the Flutter script phase in
+  /// the Xcode build.
+  File get outputFileList => ephemeralDirectory.childFile('FlutterOutputs.xcfilelist');
+
   @override
   File get generatedXcodePropertiesFile => ephemeralDirectory.childFile('Flutter-Generated.xcconfig');
 
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index da4cbe9..88ab5dc 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -31,7 +31,6 @@
   FlutterDevice(
     this.device, {
     @required this.trackWidgetCreation,
-    this.dillOutputPath,
     this.fileSystemRoots,
     this.fileSystemScheme,
     this.viewFilter,
@@ -56,7 +55,6 @@
     @required bool trackWidgetCreation,
     @required String target,
     @required BuildMode buildMode,
-    String dillOutputPath,
     List<String> fileSystemRoots,
     String fileSystemScheme,
     String viewFilter,
@@ -82,7 +80,6 @@
     return FlutterDevice(
       device,
       trackWidgetCreation: trackWidgetCreation,
-      dillOutputPath: dillOutputPath,
       fileSystemRoots: fileSystemRoots,
       fileSystemScheme:fileSystemScheme,
       viewFilter: viewFilter,
@@ -99,7 +96,6 @@
   List<VMService> vmServices;
   DevFS devFS;
   ApplicationPackage package;
-  String dillOutputPath;
   List<String> fileSystemRoots;
   String fileSystemScheme;
   StreamSubscription<String> _loggingSubscription;
@@ -476,6 +472,7 @@
     bool fullRestart = false,
     String projectRootPath,
     String pathToReload,
+    @required String dillOutputPath,
     @required List<Uri> invalidatedFiles,
   }) async {
     final Status devFSStatus = logger.startProgress(
@@ -527,12 +524,25 @@
     this.usesTerminalUi = true,
     this.stayResident = true,
     this.hotMode = true,
+    this.dillOutputPath,
   }) {
     _mainPath = findMainDartFile(target);
     _projectRootPath = projectRootPath ?? fs.currentDirectory.path;
     _packagesFilePath =
         packagesFilePath ?? fs.path.absolute(PackageMap.globalPackagesPath);
     _assetBundle = AssetBundleFactory.instance.createBundle();
+    // TODO(jonahwilliams): this is transitionary logic to allow us to support
+    // platforms that are not yet using flutter assemble. In the "new world",
+    // builds are isolated based on a number of factors. Thus, we cannot assume
+    // that a debug build will create the expected `build/app.dill` file. For
+    // now, I'm working around this by just creating it if it is missing here.
+    // In the future, once build & run are more strongly separated, the build
+    // environment will be plumbed through so that it all comes from a single
+    // source of truth, the [Environment].
+    final File dillOutput = fs.file(dillOutputPath ?? fs.path.join('build', 'app.dill'));
+    if (!dillOutput.existsSync()) {
+      dillOutput.createSync(recursive: true);
+    }
   }
 
   final List<FlutterDevice> flutterDevices;
@@ -542,6 +552,7 @@
   final bool stayResident;
   final bool ipv6;
   final Completer<int> _finished = Completer<int>();
+  final String dillOutputPath;
   bool _exited = false;
   bool hotMode ;
   String _packagesFilePath;
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index 08a4956..7a8105d 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -63,7 +63,7 @@
     this.hostIsIde = false,
     String projectRootPath,
     String packagesFilePath,
-    this.dillOutputPath,
+    String dillOutputPath,
     bool stayResident = true,
     bool ipv6 = false,
   }) : super(devices,
@@ -74,13 +74,13 @@
              packagesFilePath: packagesFilePath,
              stayResident: stayResident,
              hotMode: true,
+             dillOutputPath: dillOutputPath,
              ipv6: ipv6);
 
   final bool benchmarkMode;
   final File applicationBinary;
   final bool hostIsIde;
   bool _didAttach = false;
-  final String dillOutputPath;
 
   final Map<String, List<int>> benchmarkData = <String, List<int>>{};
   // The initial launch is from a snapshot.
@@ -304,6 +304,7 @@
         projectRootPath: projectRootPath,
         pathToReload: getReloadPath(fullRestart: fullRestart),
         invalidatedFiles: invalidatedFiles,
+        dillOutputPath: dillOutputPath,
       ));
     }
     return results;
diff --git a/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart b/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart
index ca49ef0..e806251 100644
--- a/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart
+++ b/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart
@@ -138,6 +138,14 @@
     expect(stampContents['inputs'], <Object>['/foo.dart']);
   }));
 
+  test('Creates a BuildResult with inputs and outputs', () => testbed.run(() async {
+    final BuildResult result = await buildSystem.build(fooTarget, environment);
+
+    expect(result.inputFiles.single.path, fs.path.absolute('foo.dart'));
+    expect(result.outputFiles.single.path,
+        fs.path.absolute(fs.path.join(environment.buildDir.path, 'out')));
+  }));
+
   test('Does not re-invoke build if stamp is valid', () => testbed.run(() async {
     await buildSystem.build(fooTarget, environment);
     await buildSystem.build(fooTarget, environment);
diff --git a/packages/flutter_tools/test/general.shard/build_system/source_test.dart b/packages/flutter_tools/test/general.shard/build_system/source_test.dart
index d49b2e3..8d0c2a4 100644
--- a/packages/flutter_tools/test/general.shard/build_system/source_test.dart
+++ b/packages/flutter_tools/test/general.shard/build_system/source_test.dart
@@ -135,6 +135,14 @@
 
     expect(() => invalidBase.accept(visitor), throwsA(isInstanceOf<InvalidPatternException>()));
   }));
+
+  test('can substitute optional files', () => testbed.run(() {
+    const Source missingSource = Source.pattern('{PROJECT_DIR}/foo', optional: true);
+
+    expect(fs.file('foo').existsSync(), false);
+    missingSource.accept(visitor);
+    expect(visitor.sources, isEmpty);
+  }));
 }
 
 class TestBehavior extends SourceBehavior {
diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart
index 2541115..f4fbb50 100644
--- a/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart
+++ b/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart
@@ -65,4 +65,15 @@
     // See https://github.com/flutter/flutter/issues/35293
     expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'assets/foo/bar.png')).existsSync(), false);
   }));
+
+  test('FlutterPlugins updates required files as needed', () => testbed.run(() async {
+    fs.file('pubspec.yaml')
+      ..writeAsStringSync('name: foo\ndependencies:\n  foo: any\n');
+
+    await const FlutterPlugins().build(<File>[], Environment(
+      projectDir: fs.currentDirectory,
+    ));
+
+    expect(fs.file('.flutter-plugins').existsSync(), true);
+  }));
 }
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 554f147..b6e2f76 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
@@ -7,8 +7,11 @@
 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:mockito/mockito.dart';
 import 'package:process/process.dart';
 
@@ -17,7 +20,6 @@
 
 void main() {
   Testbed testbed;
-  const BuildSystem buildSystem = BuildSystem();
   Environment environment;
   MockPlatform mockPlatform;
 
@@ -31,6 +33,7 @@
     when(mockPlatform.isWindows).thenReturn(false);
     when(mockPlatform.isMacOS).thenReturn(true);
     when(mockPlatform.isLinux).thenReturn(false);
+    when(mockPlatform.environment).thenReturn(const <String, String>{});
     testbed = Testbed(setup: () {
       environment = Environment(
         projectDir: fs.currentDirectory,
@@ -79,28 +82,117 @@
   });
 
   test('Copies files to correct cache directory', () => testbed.run(() async {
-    await buildSystem.build(const UnpackMacOS(), environment);
+    await const UnpackMacOS().build(<File>[], environment);
 
-    expect(fs.directory('macos/Flutter/FlutterMacOS.framework').existsSync(), true);
-    expect(fs.file('macos/Flutter/FlutterMacOS.framework/FlutterMacOS').existsSync(), true);
-    expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEOpenGLContextHandling.h').existsSync(), true);
-    expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEReshapeListener.h').existsSync(), true);
-    expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEView.h').existsSync(), true);
-    expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEViewController.h').existsSync(), true);
-    expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterBinaryMessenger.h').existsSync(), true);
-    expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterChannels.h').existsSync(), true);
-    expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterCodecs.h').existsSync(), true);
-    expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterMacOS.h').existsSync(), true);
-    expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterPluginMacOS.h').existsSync(), true);
-    expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterPluginRegisrarMacOS.h').existsSync(), true);
-    expect(fs.file('macos/Flutter/FlutterMacOS.framework/Modules/module.modulemap').existsSync(), true);
-    expect(fs.file('macos/Flutter/FlutterMacOS.framework/Resources/icudtl.dat').existsSync(), true);
-    expect(fs.file('macos/Flutter/FlutterMacOS.framework/Resources/info.plist').existsSync(), true);
+    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/FLEOpenGLContextHandling.h').existsSync(), true);
+    expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FLEReshapeListener.h').existsSync(), true);
+    expect(fs.file('macos/Flutter/ephemeral/FlutterMacOS.framework/Headers/FLEView.h').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);
   }));
+
+  test('debug 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 DebugMacOSApplication().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 FakeProcessResult implements ProcessResult {
   @override
@@ -115,3 +207,5 @@
   @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 61ea1a8..638582b 100644
--- a/packages/flutter_tools/test/general.shard/commands/assemble_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/assemble_test.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'package:args/command_runner.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/build_system/build_system.dart';
 import 'package:flutter_tools/src/cache.dart';
@@ -31,13 +32,54 @@
   test('Can run a build', () => testbed.run(() async {
     when(mockBuildSystem.build(any, any, buildSystemConfig: anyNamed('buildSystemConfig')))
         .thenAnswer((Invocation invocation) async {
-      return BuildResult(true, const <String, ExceptionMeasurement>{}, const <String, PerformanceMeasurement>{});
+      return BuildResult(success: true);
     });
     final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand());
     await commandRunner.run(<String>['assemble', 'unpack_macos']);
     final BufferLogger bufferLogger = logger;
 
-    expect(bufferLogger.statusText.trim(), 'build succeeded');
+    expect(bufferLogger.statusText.trim(), 'build succeeded.');
+  }));
+
+  test('Only writes input and output files when the values change', () => testbed.run(() async {
+    when(mockBuildSystem.build(any, any, buildSystemConfig: anyNamed('buildSystemConfig')))
+        .thenAnswer((Invocation invocation) async {
+      return BuildResult(
+        success: true,
+        inputFiles: <File>[fs.file('foo')..createSync()],
+        outputFiles: <File>[fs.file('bar')..createSync()],
+      );
+    });
+
+    final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand());
+    await commandRunner.run(<String>['assemble', '--build-outputs=outputs', '--build-inputs=inputs', 'unpack_macos']);
+
+    final File inputs = fs.file('inputs');
+    final File outputs = fs.file('outputs');
+    expect(inputs.readAsStringSync(), contains('foo'));
+    expect(outputs.readAsStringSync(), contains('bar'));
+
+    final DateTime theDistantPast = DateTime(1991, 8, 23);
+    inputs.setLastModifiedSync(theDistantPast);
+    outputs.setLastModifiedSync(theDistantPast);
+    await commandRunner.run(<String>['assemble', '--build-outputs=outputs', '--build-inputs=inputs', 'unpack_macos']);
+
+    expect(inputs.lastModifiedSync(), theDistantPast);
+    expect(outputs.lastModifiedSync(), theDistantPast);
+
+
+    when(mockBuildSystem.build(any, any, buildSystemConfig: anyNamed('buildSystemConfig')))
+        .thenAnswer((Invocation invocation) async {
+      return BuildResult(
+        success: true,
+        inputFiles: <File>[fs.file('foo'), fs.file('fizz')..createSync()],
+        outputFiles: <File>[fs.file('bar')]);
+    });
+    await commandRunner.run(<String>['assemble', '--build-outputs=outputs', '--build-inputs=inputs', 'unpack_macos']);
+
+    expect(inputs.readAsStringSync(), contains('foo'));
+    expect(inputs.readAsStringSync(), contains('fizz'));
+    expect(inputs.lastModifiedSync(), isNot(theDistantPast));
   }));
 }
 
diff --git a/packages/flutter_tools/test/general.shard/commands/attach_test.dart b/packages/flutter_tools/test/general.shard/commands/attach_test.dart
index c708f1e..bfe29a9 100644
--- a/packages/flutter_tools/test/general.shard/commands/attach_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/attach_test.dart
@@ -168,7 +168,6 @@
         // output dill, filesystem scheme, and filesystem root.
         final FlutterDevice flutterDevice = flutterDevices.first;
 
-        expect(flutterDevice.dillOutputPath, outputDill);
         expect(flutterDevice.fileSystemScheme, filesystemScheme);
         expect(flutterDevice.fileSystemRoots, const <String>[filesystemRoot]);
       }, overrides: <Type, Generator>{
diff --git a/packages/flutter_tools/test/general.shard/commands/build_macos_test.dart b/packages/flutter_tools/test/general.shard/commands/build_macos_test.dart
index 462e2d8..63f76c6 100644
--- a/packages/flutter_tools/test/general.shard/commands/build_macos_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/build_macos_test.dart
@@ -8,7 +8,8 @@
 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/build_info.dart';
+import 'package:flutter_tools/src/build_system/build_system.dart';
+import 'package:flutter_tools/src/build_system/targets/dart.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/build.dart';
 import 'package:flutter_tools/src/features.dart';
@@ -86,25 +87,35 @@
     fs.file('.packages').createSync();
     fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true);
     final FlutterProject flutterProject = FlutterProject.fromDirectory(fs.currentDirectory);
-    final Directory flutterBuildDir = fs.directory(getMacOSBuildDirectory());
-
+    final Environment environment = Environment(
+      projectDir: flutterProject.directory,
+      buildDir: flutterProject.dartTool.childDirectory('flutter_build'),
+      defines: <String, String>{
+        kBuildMode: 'release',
+        kTargetFile: fs.path.absolute(fs.path.join('lib', 'main.dart')),
+        kTargetPlatform: 'darwin-x64',
+      }
+    );
     when(mockProcessManager.start(<String>[
       '/usr/bin/env',
       'xcrun',
       'xcodebuild',
       '-workspace', flutterProject.macos.xcodeWorkspace.path,
-      '-configuration', 'Debug',
+      '-configuration', 'Release',
       '-scheme', 'Runner',
-      '-derivedDataPath', flutterBuildDir.absolute.path,
-      'OBJROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Intermediates.noindex')}',
-      'SYMROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Products')}',
-    ], runInShell: true)).thenAnswer((Invocation invocation) async {
+      '-derivedDataPath', environment.buildDir.path,
+      'OBJROOT=${fs.path.join(environment.buildDir.path, 'Build', 'Intermediates.noindex')}',
+      'SYMROOT=${fs.path.join(environment.buildDir.path, 'Build', 'Products')}',
+    ])).thenAnswer((Invocation invocation) async {
+      fs.file(fs.path.join('macos', 'Flutter', 'ephemeral', '.app_filename'))
+        ..createSync(recursive: true)
+        ..writeAsStringSync('example.app');
       return mockProcess;
     });
 
-    await createTestCommandRunner(command).run(
-      const <String>['build', 'macos']
-    );
+    expect(createTestCommandRunner(command).run(
+      const <String>['build', 'macos', '--release']
+    ), throwsA(isInstanceOf<AssertionError>()));
   }, overrides: <Type, Generator>{
     FileSystem: () => memoryFilesystem,
     ProcessManager: () => mockProcessManager,
diff --git a/packages/flutter_tools/test/general.shard/macos/macos_device_test.dart b/packages/flutter_tools/test/general.shard/macos/macos_device_test.dart
index 2dcb59ed..d0d7c3b 100644
--- a/packages/flutter_tools/test/general.shard/macos/macos_device_test.dart
+++ b/packages/flutter_tools/test/general.shard/macos/macos_device_test.dart
@@ -36,7 +36,6 @@
 
     testUsingContext('defaults', () async {
       final MockMacOSApp mockMacOSApp = MockMacOSApp();
-      when(mockMacOSApp.executable(any)).thenReturn('foo');
       expect(await device.targetPlatform, TargetPlatform.darwin_x64);
       expect(device.name, 'macOS');
       expect(await device.installApp(mockMacOSApp), true);
@@ -54,7 +53,7 @@
 tester    17193   0.0  0.2  4791128  37820   ??  S     2:27PM   0:00.09 /Applications/foo
 ''';
       final MockMacOSApp mockMacOSApp = MockMacOSApp();
-      when(mockMacOSApp.executable(any)).thenReturn('/Applications/foo');
+      when(mockMacOSApp.executable).thenReturn('tester');
       when(mockProcessManager.run(<String>['ps', 'aux'])).thenAnswer((Invocation invocation) async {
         return ProcessResult(1, 0, psOut, '');
       });
@@ -68,27 +67,35 @@
     });
 
     group('startApp', () {
-      final MockMacOSApp macOSApp = MockMacOSApp();
-      final MockFileSystem mockFileSystem = MockFileSystem();
-      final MockProcessManager mockProcessManager = MockProcessManager();
-      final MockFile mockFile = MockFile();
-      when(macOSApp.executable(any)).thenReturn('test');
-      when(mockFileSystem.file('test')).thenReturn(mockFile);
-      when(mockFile.existsSync()).thenReturn(true);
-      when(mockProcessManager.start(<String>['test'])).thenAnswer((Invocation invocation) async {
-        return FakeProcess(
-          exitCode: Completer<int>().future,
-          stdout: Stream<List<int>>.fromIterable(<List<int>>[
-            utf8.encode('Observatory listening on http://127.0.0.1/0\n'),
-          ]),
-          stderr: const Stream<List<int>>.empty(),
-        );
-      });
-      when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async {
-        return ProcessResult(0, 1, '', '');
+      MockMacOSApp macOSApp;
+      MockFileSystem mockFileSystem;
+      MockProcessManager mockProcessManager;
+      MockFile mockFile;
+
+
+      setUp(() {
+        macOSApp = MockMacOSApp();
+        mockFileSystem = MockFileSystem();
+        mockProcessManager = MockProcessManager();
+        mockFile = MockFile();
+        when(mockFileSystem.file('test')).thenReturn(mockFile);
+        when(mockFile.existsSync()).thenReturn(true);
+        when(macOSApp.executable).thenReturn('test');
+        when(mockProcessManager.start(<String>['test'])).thenAnswer((Invocation invocation) async {
+          return FakeProcess(
+            exitCode: Completer<int>().future,
+            stdout: Stream<List<int>>.fromIterable(<List<int>>[
+              utf8.encode('Observatory listening on http://127.0.0.1/0\n'),
+            ]),
+            stderr: const Stream<List<int>>.empty(),
+          );
+        });
+        when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async {
+          return ProcessResult(0, 1, '', '');
+        });
       });
 
-      testUsingContext('Can run from prebuilt application', () async {
+      testUsingContext('can run from prebuilt application', () async {
         final LaunchResult result = await device.startApp(macOSApp, prebuiltApplication: true);
         expect(result.started, true);
         expect(result.observatoryUri, Uri.parse('http://127.0.0.1/0'));
@@ -137,7 +144,7 @@
 
 class MockPlatform extends Mock implements Platform {}
 
-class MockMacOSApp extends Mock implements MacOSApp {}
+class MockMacOSApp extends Mock implements PrebuiltMacOSApp {}
 
 class MockFileSystem extends Mock implements FileSystem {}
 
diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
index 644f828..9a1a7f7 100644
--- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
@@ -68,6 +68,7 @@
       fullRestart: anyNamed('fullRestart'),
       projectRootPath: anyNamed('projectRootPath'),
       pathToReload: anyNamed('pathToReload'),
+      dillOutputPath: anyNamed('dillOutputPath'),
     )).thenAnswer((Invocation invocation) async {
       return UpdateFSReport(
         success: true,
@@ -175,6 +176,7 @@
       projectRootPath: anyNamed('projectRootPath'),
       pathToReload: anyNamed('pathToReload'),
       invalidatedFiles: anyNamed('invalidatedFiles'),
+      dillOutputPath: anyNamed('dillOutputPath'),
     )).thenThrow(RpcException(666, 'something bad happened'));
 
     final OperationResult result = await residentRunner.restart(fullRestart: false);
@@ -279,6 +281,7 @@
       projectRootPath: anyNamed('projectRootPath'),
       pathToReload: anyNamed('pathToReload'),
       invalidatedFiles: anyNamed('invalidatedFiles'),
+      dillOutputPath: anyNamed('dillOutputPath'),
     )).thenThrow(RpcException(666, 'something bad happened'));
 
     final OperationResult result = await residentRunner.restart(fullRestart: true);