Add initial implementation of flutter assemble (#32816)

diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart
index 42c1808..25b3904 100644
--- a/packages/flutter_tools/lib/executable.dart
+++ b/packages/flutter_tools/lib/executable.dart
@@ -14,6 +14,7 @@
 
 import 'src/codegen.dart';
 import 'src/commands/analyze.dart';
+import 'src/commands/assemble.dart';
 import 'src/commands/attach.dart';
 import 'src/commands/build.dart';
 import 'src/commands/channel.dart';
@@ -61,6 +62,7 @@
 
   await runner.run(args, <FlutterCommand>[
     AnalyzeCommand(verboseHelp: verboseHelp),
+    AssembleCommand(),
     AttachCommand(verboseHelp: verboseHelp),
     BuildCommand(verboseHelp: verboseHelp),
     ChannelCommand(verboseHelp: verboseHelp),
diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart
index 1c7c178..65a8b63 100644
--- a/packages/flutter_tools/lib/src/artifacts.dart
+++ b/packages/flutter_tools/lib/src/artifacts.dart
@@ -14,10 +14,13 @@
 import 'globals.dart';
 
 enum Artifact {
+  /// The tool which compiles a dart kernel file into native code.
   genSnapshot,
+  /// The flutter tester binary.
   flutterTester,
   snapshotDart,
   flutterFramework,
+  /// The framework directory of the macOS desktop.
   flutterMacOSFramework,
   vmSnapshotData,
   isolateSnapshotData,
@@ -25,12 +28,24 @@
   platformLibrariesJson,
   flutterPatchedSdkPath,
   frontendServerSnapshotForEngineDartSdk,
+  /// The root directory of the dartk SDK.
   engineDartSdkPath,
+  /// The dart binary used to execute any of the required snapshots.
   engineDartBinary,
+  /// The dart snapshot of the dart2js compiler.
   dart2jsSnapshot,
+  /// The dart snapshot of the dartdev compiler.
   dartdevcSnapshot,
+  /// The dart snpashot of the kernel worker compiler.
   kernelWorkerSnapshot,
+  /// The root of the web implementation of the dart SDK.
   flutterWebSdk,
+  /// The root of the Linux desktop sources.
+  linuxDesktopPath,
+  /// The root of the Windows desktop sources.
+  windowsDesktopPath,
+  /// The root of the sky_engine package
+  skyEnginePath,
 }
 
 String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMode mode ]) {
@@ -47,6 +62,10 @@
     case Artifact.flutterFramework:
       return 'Flutter.framework';
     case Artifact.flutterMacOSFramework:
+      if (platform != TargetPlatform.darwin_x64)  {
+        throw Exception('${getNameForTargetPlatform(platform)} does not support'
+            ' macOS desktop development');
+      }
       return 'FlutterMacOS.framework';
     case Artifact.vmSnapshotData:
       return 'vm_isolate_snapshot.bin';
@@ -74,6 +93,20 @@
       return 'dartdevc.dart.snapshot';
     case Artifact.kernelWorkerSnapshot:
       return 'kernel_worker.dart.snapshot';
+    case Artifact.linuxDesktopPath:
+      if (platform != TargetPlatform.linux_x64)  {
+        throw Exception('${getNameForTargetPlatform(platform)} does not support'
+            ' Linux desktop development');
+      }
+      return '';
+    case Artifact.windowsDesktopPath:
+      if (platform != TargetPlatform.windows_x64)  {
+        throw Exception('${getNameForTargetPlatform(platform)} does not support'
+            ' Windows desktop development');
+      }
+      return '';
+    case Artifact.skyEnginePath:
+      return 'sky_engine';
   }
   assert(false, 'Invalid artifact $artifact.');
   return null;
@@ -209,9 +242,14 @@
       case Artifact.kernelWorkerSnapshot:
         return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact));
       case Artifact.flutterMacOSFramework:
+      case Artifact.linuxDesktopPath:
+      case Artifact.windowsDesktopPath:
         final String engineArtifactsPath = cache.getArtifactDirectory('engine').path;
         final String platformDirName = getNameForTargetPlatform(platform);
         return fs.path.join(engineArtifactsPath, platformDirName, _artifactToFileName(artifact, platform, mode));
+      case Artifact.skyEnginePath:
+        final Directory dartPackageDirectory = cache.getCacheDir('pkg');
+        return fs.path.join(dartPackageDirectory.path,  _artifactToFileName(artifact));
       default:
         assert(false, 'Artifact $artifact not available for platform $platform.');
         return null;
@@ -302,6 +340,12 @@
         return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact));
       case Artifact.kernelWorkerSnapshot:
         return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', _artifactToFileName(artifact));
+      case Artifact.linuxDesktopPath:
+        return fs.path.join(_hostEngineOutPath, _artifactToFileName(artifact));
+      case Artifact.windowsDesktopPath:
+        return fs.path.join(_hostEngineOutPath, _artifactToFileName(artifact));
+      case Artifact.skyEnginePath:
+        return fs.path.join(_hostEngineOutPath, 'gen', 'dart-pkg', _artifactToFileName(artifact));
     }
     assert(false, 'Invalid artifact $artifact.');
     return null;
diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart
index 8d2338a..00264f3 100644
--- a/packages/flutter_tools/lib/src/base/build.dart
+++ b/packages/flutter_tools/lib/src/base/build.dart
@@ -9,7 +9,6 @@
 import '../artifacts.dart';
 import '../build_info.dart';
 import '../bundle.dart';
-import '../cache.dart';
 import '../compile.dart';
 import '../dart/package_map.dart';
 import '../globals.dart';
@@ -95,10 +94,6 @@
     IOSArch iosArch,
     List<String> extraGenSnapshotOptions = const <String>[],
   }) async {
-    FlutterProject flutterProject;
-    if (fs.file('pubspec.yaml').existsSync()) {
-      flutterProject = FlutterProject.current();
-    }
     if (!_isValidAotPlatform(platform, buildMode)) {
       printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.');
       return 1;
@@ -122,8 +117,6 @@
 
     final List<String> inputPaths = <String>[uiPath, vmServicePath, mainPath];
     final Set<String> outputPaths = <String>{};
-
-    final String depfilePath = fs.path.join(outputDir.path, 'snapshot.d');
     final List<String> genSnapshotArgs = <String>[
       '--deterministic',
     ];
@@ -165,26 +158,6 @@
       return 1;
     }
 
-    // If inputs and outputs have not changed since last run, skip the build.
-    final Fingerprinter fingerprinter = Fingerprinter(
-      fingerprintPath: '$depfilePath.fingerprint',
-      paths: <String>[mainPath, ...inputPaths, ...outputPaths],
-      properties: <String, String>{
-        'buildMode': buildMode.toString(),
-        'targetPlatform': platform.toString(),
-        'entryPoint': mainPath,
-        'extraGenSnapshotOptions': extraGenSnapshotOptions.join(' '),
-        'engineHash': Cache.instance.engineRevision,
-        'buildersUsed': '${flutterProject != null && flutterProject.hasBuilders}',
-      },
-      depfilePaths: <String>[],
-    );
-    // TODO(jonahwilliams): re-enable once this can be proved correct.
-    // if (await fingerprinter.doesFingerprintMatch()) {
-    //   printTrace('Skipping AOT snapshot build. Fingerprint match.');
-    //   return 0;
-    // }
-
     final SnapshotType snapshotType = SnapshotType(platform, buildMode);
     final int genSnapshotExitCode =
       await _timedStep('snapshot(CompileTime)', 'aot-snapshot',
@@ -210,9 +183,6 @@
       if (result.exitCode != 0)
         return result.exitCode;
     }
-
-    // Compute and record build fingerprint.
-    await fingerprinter.writeFingerprint();
     return 0;
   }
 
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index 8736135..ac0489a 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -115,6 +115,32 @@
   release,
 }
 
+const List<String> _kBuildModes = <String>[
+  'debug',
+  'profile',
+  'release',
+  'dynamic-profile',
+  'dynamic-release',
+];
+
+/// Return the name for the build mode, or "any" if null.
+String getNameForBuildMode(BuildMode buildMode) {
+  return _kBuildModes[buildMode.index];
+}
+
+/// Returns the [BuildMode] for a particular `name`.
+BuildMode getBuildModeForName(String name) {
+  switch (name) {
+    case 'debug':
+      return BuildMode.debug;
+    case 'profile':
+      return BuildMode.profile;
+    case 'release':
+      return BuildMode.release;
+  }
+  return null;
+}
+
 String validatedBuildNumberForPlatform(TargetPlatform targetPlatform, String buildNumber) {
   if (buildNumber == null) {
     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
new file mode 100644
index 0000000..c0f9d22
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/build_system.dart
@@ -0,0 +1,686 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+import 'package:convert/convert.dart';
+import 'package:crypto/crypto.dart';
+import 'package:meta/meta.dart';
+import 'package:pool/pool.dart';
+
+import '../base/file_system.dart';
+import '../base/platform.dart';
+import '../cache.dart';
+import '../convert.dart';
+import '../globals.dart';
+import 'exceptions.dart';
+import 'file_hash_store.dart';
+import 'source.dart';
+import 'targets/assets.dart';
+import 'targets/dart.dart';
+import 'targets/ios.dart';
+import 'targets/linux.dart';
+import 'targets/macos.dart';
+import 'targets/windows.dart';
+
+export 'source.dart';
+
+/// The function signature of a build target which can be invoked to perform
+/// the underlying task.
+typedef BuildAction = FutureOr<void> Function(
+    Map<String, ChangeType> inputs, Environment environment);
+
+/// A description of the update to each input file.
+enum ChangeType {
+  /// The file was added.
+  Added,
+  /// The file was deleted.
+  Removed,
+  /// The file was modified.
+  Modified,
+}
+
+/// Configuration for the build system itself.
+class BuildSystemConfig {
+  /// Create a new [BuildSystemConfig].
+  const BuildSystemConfig({this.resourcePoolSize});
+
+  /// The maximum number of concurrent tasks the build system will run.
+  ///
+  /// If not provided, defaults to [platform.numberOfProcessors].
+  final int resourcePoolSize;
+}
+
+/// A Target describes a single step during a flutter build.
+///
+/// The target inputs are required to be files discoverable via a combination
+/// of at least one of the environment values and zero or more local values.
+///
+/// To determine if the action for a target needs to be executed, the
+/// [BuildSystem] performs a hash of the file contents for both inputs and
+/// outputs. This is tracked separately in the [FileHashStore].
+///
+/// A Target has both implicit and explicit inputs and outputs. Only the
+/// later are safe to evaluate before invoking the [buildAction]. For example,
+/// a wildcard output pattern requires the outputs to exist before it can
+/// glob files correctly.
+///
+/// - All listed inputs are considered explicit inputs.
+/// - Outputs which are provided as [Source.pattern].
+///   without wildcards are considered explicit.
+/// - The remaining outputs are considered implicit.
+///
+/// For each target, executing its action creates a corresponding stamp file
+/// which records both the input and output files. This file is read by
+/// subsequent builds to determine which file hashes need to be checked. If the
+/// stamp file is missing, the target's action is always rerun.
+///
+///  file: `example_target.stamp`
+///
+/// {
+///   "inputs": [
+///      "absolute/path/foo",
+///      "absolute/path/bar",
+///      ...
+///    ],
+///    "outputs": [
+///      "absolute/path/fizz"
+///    ]
+/// }
+///
+/// ## Code review
+///
+/// ### Targes should only depend on files that are provided as inputs
+///
+/// Example: gen_snapshot must be provided as an input to the aot_elf
+/// build steps, even though it isn't a source file. This ensures that changes
+/// to the gen_snapshot binary (during a local engine build) correctly
+/// trigger a corresponding build update.
+///
+/// Example: aot_elf has a dependency on the dill and packages file
+/// produced by the kernel_snapshot step.
+///
+/// ### Targest should declare all outputs produced
+///
+/// If a target produces an output it should be listed, even if it is not
+/// intended to be consumed by another target.
+///
+/// ## Unit testing
+///
+/// Most targets will invoke an external binary which makes unit testing
+/// trickier. It is recommend that for unit testing that a Fake is used and
+/// provided via the dependency injection system. a [Testbed] may be used to
+/// set up the environment before the test is run. Unit tests should fully
+/// exercise the rule, ensuring that the existing input and output verification
+/// logic can run, as well as verifying it correctly handles provided defines
+/// and meets any additional contracts present in the target.
+class Target {
+  const Target({
+    @required this.name,
+    @required this.inputs,
+    @required this.outputs,
+    @required this.buildAction,
+    this.dependencies = const <Target>[],
+  });
+
+  /// The user-readable name of the target.
+  ///
+  /// This information is surfaced in the assemble commands and used as an
+  /// argument to build a particular target.
+  final String name;
+
+  /// The dependencies of this target.
+  final List<Target> dependencies;
+
+  /// The input [Source]s which are diffed to determine if a target should run.
+  final List<Source> inputs;
+
+  /// The output [Source]s which we attempt to verify are correctly produced.
+  final List<Source> outputs;
+
+  /// The action which performs this build step.
+  final BuildAction buildAction;
+
+  /// Collect hashes for all inputs to determine if any have changed.
+  Future<Map<String, ChangeType>> computeChanges(
+    List<File> inputs,
+    Environment environment,
+    FileHashStore fileHashStore,
+  ) async {
+    final Map<String, ChangeType> updates = <String, ChangeType>{};
+    final File stamp = _findStampFile(environment);
+    final Set<String> previousInputs = <String>{};
+    final List<String> previousOutputs = <String>[];
+
+    // If the stamp file doesn't exist, we haven't run this step before and
+    // all inputs were added.
+    if (stamp.existsSync()) {
+      final String content = stamp.readAsStringSync();
+      // Something went wrong writing the stamp file.
+      if (content == null || content.isEmpty) {
+        stamp.deleteSync();
+      } else {
+        final Map<String, Object> values = json.decode(content);
+        final List<Object> inputs = values['inputs'];
+        final List<Object> outputs = values['outputs'];
+        inputs.cast<String>().forEach(previousInputs.add);
+        outputs.cast<String>().forEach(previousOutputs.add);
+      }
+    }
+
+    // For each input type, first determine if we've already computed the hash
+    // for it. If not and it is a directory we skip hashing and instead use a
+    // timestamp. If it is a file we collect it to be sent off for hashing as
+    // a group.
+    final List<File> sourcesToHash = <File>[];
+    final List<File> missingInputs = <File>[];
+    for (File file in inputs) {
+      if (!file.existsSync()) {
+        missingInputs.add(file);
+        continue;
+      }
+
+      final String absolutePath = file.resolveSymbolicLinksSync();
+      final String previousHash = fileHashStore.previousHashes[absolutePath];
+      if (fileHashStore.currentHashes.containsKey(absolutePath)) {
+        final String currentHash = fileHashStore.currentHashes[absolutePath];
+        if (currentHash != previousHash) {
+          updates[absolutePath] = previousInputs.contains(absolutePath)
+              ? ChangeType.Modified
+              : ChangeType.Added;
+        }
+      } else {
+        sourcesToHash.add(file);
+      }
+    }
+    // Check if any outputs were deleted or modified from the previous run.
+    for (String previousOutput in previousOutputs) {
+      final File file = fs.file(previousOutput);
+      if (!file.existsSync()) {
+        updates[previousOutput] = ChangeType.Removed;
+        continue;
+      }
+      final String absolutePath = file.resolveSymbolicLinksSync();
+      final String previousHash = fileHashStore.previousHashes[absolutePath];
+      if (fileHashStore.currentHashes.containsKey(absolutePath)) {
+        final String currentHash = fileHashStore.currentHashes[absolutePath];
+        if (currentHash != previousHash) {
+          updates[absolutePath] = previousInputs.contains(absolutePath)
+              ? ChangeType.Modified
+              : ChangeType.Added;
+        }
+      } else {
+        sourcesToHash.add(file);
+      }
+    }
+
+    if (missingInputs.isNotEmpty) {
+      throw MissingInputException(missingInputs, name);
+    }
+
+    // If we have files to hash, compute them asynchronously and then
+    // update the result.
+    if (sourcesToHash.isNotEmpty) {
+      final List<File> dirty = await fileHashStore.hashFiles(sourcesToHash);
+      for (File file in dirty) {
+        final String absolutePath = file.resolveSymbolicLinksSync();
+        updates[absolutePath] = previousInputs.contains(absolutePath)
+            ? ChangeType.Modified
+            : ChangeType.Added;
+      }
+    }
+
+    // Find which, if any, inputs have been deleted.
+    final Set<String> currentInputPaths = Set<String>.from(
+      inputs.map<String>((File entity) => entity.resolveSymbolicLinksSync())
+    );
+    for (String previousInput in previousInputs) {
+      if (!currentInputPaths.contains(previousInput)) {
+        updates[previousInput] = ChangeType.Removed;
+      }
+    }
+    return updates;
+  }
+
+  /// Invoke to remove the stamp file if the [buildAction] threw an exception;
+  void clearStamp(Environment environment) {
+    final File stamp = _findStampFile(environment);
+    if (stamp.existsSync()) {
+      stamp.deleteSync();
+    }
+  }
+
+  void _writeStamp(
+    List<File> inputs,
+    List<File> outputs,
+    Environment environment,
+  ) {
+    final File stamp = _findStampFile(environment);
+    final List<String> inputPaths = <String>[];
+    for (File input in inputs) {
+      inputPaths.add(input.resolveSymbolicLinksSync());
+    }
+    final List<String> outputPaths = <String>[];
+    for (File output in outputs) {
+      outputPaths.add(output.resolveSymbolicLinksSync());
+    }
+    final Map<String, Object> result = <String, Object>{
+      'inputs': inputPaths,
+      'outputs': outputPaths,
+    };
+    if (!stamp.existsSync()) {
+      stamp.createSync();
+    }
+    stamp.writeAsStringSync(json.encode(result));
+  }
+
+  /// Resolve the set of input patterns and functions into a concrete list of
+  /// files.
+  List<File> resolveInputs(
+    Environment environment,
+  ) {
+    return _resolveConfiguration(inputs, environment, implicit: true, inputs: true);
+  }
+
+  /// Find the current set of declared outputs, including wildcard directories.
+  ///
+  /// The [implicit] flag controls whether it is safe to evaluate [Source]s
+  /// which uses functions, behaviors, or patterns.
+  List<File> resolveOutputs(
+    Environment environment,
+    { bool implicit = true, }
+  ) {
+    final List<File> outputEntities = _resolveConfiguration(outputs, environment, implicit: implicit, inputs: false);
+    if (implicit) {
+      verifyOutputDirectories(outputEntities, environment, this);
+    }
+    return outputEntities;
+  }
+
+  /// Performs a fold across this target and its dependencies.
+  T fold<T>(T initialValue, T combine(T previousValue, Target target)) {
+    final T dependencyResult = dependencies.fold(
+        initialValue, (T prev, Target t) => t.fold(prev, combine));
+    return combine(dependencyResult, this);
+  }
+
+  /// Convert the target to a JSON structure appropriate for consumption by
+  /// external systems.
+  ///
+  /// This requires constants from the [Environment] to resolve the paths of
+  /// inputs and the output stamp.
+  Map<String, Object> toJson(Environment environment) {
+    return <String, Object>{
+      'name': name,
+      'dependencies': dependencies.map((Target target) => target.name).toList(),
+      'inputs': resolveInputs(environment)
+          .map((File file) => file.resolveSymbolicLinksSync())
+          .toList(),
+      'outputs': resolveOutputs(environment, implicit: false)
+          .map((File file) => file.path)
+          .toList(),
+      'stamp': _findStampFile(environment).absolute.path,
+    };
+  }
+
+  /// Locate the stamp file for a particular target name and environment.
+  File _findStampFile(Environment environment) {
+    final String fileName = '$name.stamp';
+    return environment.buildDir.childFile(fileName);
+  }
+
+  static List<File> _resolveConfiguration(
+      List<Source> config, Environment environment, { bool implicit = true, bool inputs = true }) {
+    final SourceVisitor collector = SourceVisitor(environment, inputs);
+    for (Source source in config) {
+      source.accept(collector);
+    }
+    return collector.sources;
+  }
+}
+
+/// The [Environment] defines several constants for use during the build.
+///
+/// The environment contains configuration and file paths that are safe to
+/// depend on and reference during the build.
+///
+/// Example (Good):
+///
+/// Use the environment to determine where to write an output file.
+///
+///    environment.buildDir.childFile('output')
+///      ..createSync()
+///      ..writeAsStringSync('output data');
+///
+/// Example (Bad):
+///
+/// Use a hard-coded path or directory relative to the current working
+/// directory to write an output file.
+///
+///   fs.file('build/linux/out')
+///     ..createSync()
+///     ..writeAsStringSync('output data');
+///
+/// Example (Good):
+///
+/// Using the build mode to produce different output. Note that the action
+/// is still responsible for outputting a different file, as defined by the
+/// corresponding output [Source].
+///
+///    final BuildMode buildMode = getBuildModeFromDefines(environment.defines);
+///    if (buildMode == BuildMode.debug) {
+///      environment.buildDir.childFile('debug.output')
+///        ..createSync()
+///        ..writeAsStringSync('debug');
+///    } else {
+///      environment.buildDir.childFile('non_debug.output')
+///        ..createSync()
+///        ..writeAsStringSync('non_debug');
+///    }
+class Environment {
+  /// Create a new [Environment] object.
+  ///
+  /// Only [projectDir] is required. The remaining environment locations have
+  /// defaults based on it.
+  factory Environment({
+    @required Directory projectDir,
+    Directory buildDir,
+    Map<String, String> defines = const <String, String>{},
+  }) {
+    // Compute a unique hash of this build's particular environment.
+    // Sort the keys by key so that the result is stable. We always
+    // include the engine and dart versions.
+    String buildPrefix;
+    final List<String> keys = defines.keys.toList()..sort();
+    final StringBuffer buffer = StringBuffer();
+    for (String key in keys) {
+      buffer.write(key);
+      buffer.write(defines[key]);
+    }
+    // in case there was no configuration, provide some value.
+    buffer.write('Flutter is awesome');
+    final String output = buffer.toString();
+    final Digest digest = md5.convert(utf8.encode(output));
+    buildPrefix = hex.encode(digest.bytes);
+
+    final Directory rootBuildDir = buildDir ?? projectDir.childDirectory('build');
+    final Directory buildDirectory = rootBuildDir.childDirectory(buildPrefix);
+    return Environment._(
+      projectDir: projectDir,
+      buildDir: buildDirectory,
+      rootBuildDir: rootBuildDir,
+      cacheDir: Cache.instance.getRoot(),
+      defines: defines,
+    );
+  }
+
+  Environment._({
+    @required this.projectDir,
+    @required this.buildDir,
+    @required this.rootBuildDir,
+    @required this.cacheDir,
+    @required this.defines,
+  });
+
+  /// The [Source] value which is substituted with the path to [projectDir].
+  static const String kProjectDirectory = '{PROJECT_DIR}';
+
+  /// The [Source] value which is substituted with the path to [buildDir].
+  static const String kBuildDirectory = '{BUILD_DIR}';
+
+  /// The [Source] value which is substituted with the path to [cacheDir].
+  static const String kCacheDirectory = '{CACHE_DIR}';
+
+  /// The [Source] value which is substituted with a path to the flutter root.
+  static const String kFlutterRootDirectory = '{FLUTTER_ROOT}';
+
+  /// The `PROJECT_DIR` environment variable.
+  ///
+  /// This should be root of the flutter project where a pubspec and dart files
+  /// can be located.
+  final Directory projectDir;
+
+  /// The `BUILD_DIR` environment variable.
+  ///
+  /// Defaults to `{PROJECT_ROOT}/build`. The root of the output directory where
+  /// build step intermediates and outputs are written.
+  final Directory buildDir;
+
+  /// The `CACHE_DIR` environment variable.
+  ///
+  /// Defaults to `{FLUTTER_ROOT}/bin/cache`. The root of the artifact cache for
+  /// the flutter tool.
+  final Directory cacheDir;
+
+  /// Additional configuration passed to the build targets.
+  ///
+  /// Setting values here forces a unique build directory to be chosen
+  /// which prevents the config from leaking into different builds.
+  final Map<String, String> defines;
+
+  /// The root build directory shared by all builds.
+  final Directory rootBuildDir;
+}
+
+/// The result information from the build system.
+class BuildResult {
+  BuildResult(this.success, this.exceptions, this.performance);
+
+  final bool success;
+  final Map<String, ExceptionMeasurement> exceptions;
+  final Map<String, PerformanceMeasurement> performance;
+
+  bool get hasException => exceptions.isNotEmpty;
+}
+
+/// The build system is responsible for invoking and ordering [Target]s.
+class BuildSystem {
+  BuildSystem([Map<String, Target> targets])
+    : targets = targets ?? _defaultTargets;
+
+  /// All currently registered targets.
+  static final Map<String, Target> _defaultTargets = <String, Target>{
+    unpackMacos.name: unpackMacos,
+    macosApplication.name: macosApplication,
+    macoReleaseApplication.name: macoReleaseApplication,
+    unpackLinux.name: unpackLinux,
+    unpackWindows.name: unpackWindows,
+    copyAssets.name: copyAssets,
+    kernelSnapshot.name: kernelSnapshot,
+    aotElfProfile.name: aotElfProfile,
+    aotElfRelease.name: aotElfRelease,
+    aotAssemblyProfile.name: aotAssemblyProfile,
+    aotAssemblyRelease.name: aotAssemblyRelease,
+    releaseIosApplication.name: releaseIosApplication,
+    profileIosApplication.name: profileIosApplication,
+    debugIosApplication.name: debugIosApplication,
+  };
+
+  final Map<String, Target> targets;
+
+  /// Build the target `name` and all of its dependencies.
+  Future<BuildResult> build(
+    String name,
+    Environment environment,
+    BuildSystemConfig buildSystemConfig,
+  ) async {
+    final Target target = _getNamedTarget(name);
+    environment.buildDir.createSync(recursive: true);
+
+    // Load file hash store from previous builds.
+    final FileHashStore fileCache = FileHashStore(environment)
+      ..initialize();
+
+    // Perform sanity checks on build.
+    checkCycles(target);
+
+    final _BuildInstance buildInstance = _BuildInstance(environment, fileCache, buildSystemConfig);
+    bool passed = true;
+    try {
+      passed = await buildInstance.invokeTarget(target);
+    } finally {
+      // Always persist the file cache to disk.
+      fileCache.persist();
+    }
+    return BuildResult(
+      passed,
+      buildInstance.exceptionMeasurements,
+      buildInstance.stepTimings,
+    );
+  }
+
+  /// Describe the target `name` and all of its dependencies.
+  List<Map<String, Object>> describe(
+    String name,
+    Environment environment,
+  ) {
+    final Target target = _getNamedTarget(name);
+    environment.buildDir.createSync(recursive: true);
+    checkCycles(target);
+    // Cheat a bit and re-use the same map.
+    Map<String, Map<String, Object>> fold(Map<String, Map<String, Object>> accumulation, Target current) {
+      accumulation[current.name] = current.toJson(environment);
+      return accumulation;
+    }
+
+    final Map<String, Map<String, Object>> result =
+        <String, Map<String, Object>>{};
+    final Map<String, Map<String, Object>> targets = target.fold(result, fold);
+    return targets.values.toList();
+  }
+
+  // Returns the corresponding target or throws.
+  Target _getNamedTarget(String name) {
+    final Target target = targets[name];
+    if (target == null) {
+      throw Exception('No registered target:$name.');
+    }
+    return target;
+  }
+}
+
+/// An active instance of a build.
+class _BuildInstance {
+  _BuildInstance(this.environment, this.fileCache, this.buildSystemConfig)
+    : resourcePool = Pool(buildSystemConfig.resourcePoolSize ?? platform?.numberOfProcessors ?? 1);
+
+  final BuildSystemConfig buildSystemConfig;
+  final Pool resourcePool;
+  final Map<String, AsyncMemoizer<void>> pending = <String, AsyncMemoizer<void>>{};
+  final Environment environment;
+  final FileHashStore fileCache;
+
+  // Timings collected during target invocation.
+  final Map<String, PerformanceMeasurement> stepTimings = <String, PerformanceMeasurement>{};
+
+  // Exceptions caught during the build process.
+  final Map<String, ExceptionMeasurement> exceptionMeasurements = <String, ExceptionMeasurement>{};
+
+  Future<bool> invokeTarget(Target target) async {
+    final List<bool> results = await Future.wait(target.dependencies.map(invokeTarget));
+    if (results.any((bool result) => !result)) {
+      return false;
+    }
+    final AsyncMemoizer<bool> memoizer = pending[target.name] ??= AsyncMemoizer<bool>();
+    return memoizer.runOnce(() => _invokeInternal(target));
+  }
+
+  Future<bool> _invokeInternal(Target target) async {
+    final PoolResource resource = await resourcePool.request();
+    final Stopwatch stopwatch = Stopwatch()..start();
+    bool passed = true;
+    bool skipped = false;
+    try {
+      final List<File> inputs = target.resolveInputs(environment);
+      final Map<String, ChangeType> updates = await target.computeChanges(inputs, environment, fileCache);
+      if (updates.isEmpty) {
+        skipped = true;
+        printStatus('Skipping target: ${target.name}');
+      } else {
+        printStatus('${target.name}: Starting');
+        // build actions may be null.
+        await target?.buildAction(updates, environment);
+        printStatus('${target.name}: Complete');
+
+        final List<File> outputs = target.resolveOutputs(environment);
+        // Update hashes for output files.
+        await fileCache.hashFiles(outputs);
+        target._writeStamp(inputs, outputs, environment);
+      }
+    } catch (exception, stackTrace) {
+      // TODO(jonahwilliams): test
+      target.clearStamp(environment);
+      passed = false;
+      skipped = false;
+      exceptionMeasurements[target.name] = ExceptionMeasurement(
+          target.name, exception, stackTrace);
+    } finally {
+      resource.release();
+      stopwatch.stop();
+      stepTimings[target.name] = PerformanceMeasurement(
+          target.name, stopwatch.elapsedMilliseconds, skipped, passed);
+    }
+    return passed;
+  }
+}
+
+/// Helper class to collect exceptions.
+class ExceptionMeasurement {
+  ExceptionMeasurement(this.target, this.exception, this.stackTrace);
+
+  final String target;
+  final dynamic exception;
+  final StackTrace stackTrace;
+}
+
+/// Helper class to collect measurement data.
+class PerformanceMeasurement {
+  PerformanceMeasurement(this.target, this.elapsedMilliseconds, this.skiped, this.passed);
+  final int elapsedMilliseconds;
+  final String target;
+  final bool skiped;
+  final bool passed;
+}
+
+/// Check if there are any dependency cycles in the target.
+///
+/// Throws a [CycleException] if one is encountered.
+void checkCycles(Target initial) {
+  void checkInternal(Target target, Set<Target> visited, Set<Target> stack) {
+    if (stack.contains(target)) {
+      throw CycleException(stack..add(target));
+    }
+    if (visited.contains(target)) {
+      return;
+    }
+    visited.add(target);
+    stack.add(target);
+    for (Target dependency in target.dependencies) {
+      checkInternal(dependency, visited, stack);
+    }
+    stack.remove(target);
+  }
+  checkInternal(initial, <Target>{}, <Target>{});
+}
+
+/// Verifies that all files exist and are in a subdirectory of [Environment.buildDir].
+void verifyOutputDirectories(List<File> outputs, Environment environment, Target target) {
+  final String buildDirectory = environment.buildDir.resolveSymbolicLinksSync();
+  final String projectDirectory = environment.projectDir.resolveSymbolicLinksSync();
+  final List<File> missingOutputs = <File>[];
+  for (File sourceFile in outputs) {
+    if (!sourceFile.existsSync()) {
+      missingOutputs.add(sourceFile);
+      continue;
+    }
+    final String path = sourceFile.resolveSymbolicLinksSync();
+    if (!path.startsWith(buildDirectory) && !path.startsWith(projectDirectory)) {
+      throw MisplacedOutputException(path, target.name);
+    }
+  }
+  if (missingOutputs.isNotEmpty) {
+    throw MissingOutputException(missingOutputs, target.name);
+  }
+}
diff --git a/packages/flutter_tools/lib/src/build_system/exceptions.dart b/packages/flutter_tools/lib/src/build_system/exceptions.dart
new file mode 100644
index 0000000..918c051
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/exceptions.dart
@@ -0,0 +1,95 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import '../base/file_system.dart';
+
+import 'build_system.dart';
+
+/// An exception thrown when a rule declares an input that does not exist on
+/// disk.
+class MissingInputException implements Exception {
+  const MissingInputException(this.missing, this.target);
+
+  /// The file or directory we expected to find.
+  final List<File> missing;
+
+  /// The name of the target this file should have been output from.
+  final String target;
+
+  @override
+  String toString() {
+    final String files = missing.map((File file) => file.path).join(', ');
+    return '$files were declared as an inputs, but did not exist. '
+        'Check the definition of target:$target for errors';
+  }
+}
+
+/// An exception thrown if we detect a cycle in the dependencies of a target.
+class CycleException implements Exception {
+  CycleException(this.targets);
+
+  final Set<Target> targets;
+
+  @override
+  String toString() => 'Dependency cycle detected in build: '
+      '${targets.map((Target target) => target.name).join(' -> ')}';
+}
+
+/// An exception thrown when a pattern is invalid.
+class InvalidPatternException implements Exception {
+  InvalidPatternException(this.pattern);
+
+  final String pattern;
+
+  @override
+  String toString() => 'The pattern "$pattern" is not valid';
+}
+
+/// An exception thrown when a rule declares an output that was not produced
+/// by the invocation.
+class MissingOutputException implements Exception {
+  const MissingOutputException(this.missing, this.target);
+
+  /// The files we expected to find.
+  final List<File> missing;
+
+  /// The name of the target this file should have been output from.
+  final String target;
+
+  @override
+  String toString() {
+    final String files = missing.map((File file) => file.path).join(', ');
+    return '$files were declared as outputs, but were not generated by '
+        'the action. Check the definition of target:$target for errors';
+  }
+}
+
+/// An exception thrown when in output is placed outside of
+/// [Environment.buildDir].
+class MisplacedOutputException implements Exception {
+  MisplacedOutputException(this.path, this.target);
+
+  final String path;
+  final String target;
+
+  @override
+  String toString() {
+    return 'Target $target produced an output at $path'
+        ' which is outside of the current build or project directory';
+  }
+}
+
+/// An exception thrown if a build action is missing a required define.
+class MissingDefineException implements Exception {
+  MissingDefineException(this.define, this.target);
+
+  final String define;
+  final String target;
+
+  @override
+  String toString() {
+    return 'Target $target required define $define '
+        'but it was not provided';
+  }
+}
diff --git a/packages/flutter_tools/lib/src/build_system/file_hash_store.dart b/packages/flutter_tools/lib/src/build_system/file_hash_store.dart
new file mode 100644
index 0000000..398f95f
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/file_hash_store.dart
@@ -0,0 +1,111 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:typed_data';
+
+import 'package:crypto/crypto.dart';
+
+import '../base/file_system.dart';
+import '../globals.dart';
+import 'build_system.dart';
+import 'filecache.pb.dart' as pb;
+
+/// A globally accessible cache of file hashes.
+///
+/// In cases where multiple targets read the same source files as inputs, we
+/// avoid recomputing or storing multiple copies of hashes by delegating
+/// through this class. All file hashes are held in memory during a build
+/// operation, and persisted to cache in the root build directory.
+///
+/// The format of the file store is subject to change and not part of its API.
+///
+/// To regenerate the protobuf entries used to construct the cache:
+///   1. If not already installed, https://developers.google.com/protocol-buffers/docs/downloads
+///   2. pub global active `protoc-gen-dart`
+///   3. protoc -I=lib/src/build_system/  --dart_out=lib/src/build_system/  lib/src/build_system/filecache.proto
+///   4. Add licenses headers to the newly generated file and check-in.
+///
+/// See also: https://developers.google.com/protocol-buffers/docs/darttutorial
+// TODO(jonahwilliams): find a better way to clear out old entries, perhaps
+// track the last access or modification date?
+class FileHashStore {
+  FileHashStore(this.environment);
+
+  final Environment environment;
+  final HashMap<String, String> previousHashes = HashMap<String, String>();
+  final HashMap<String, String> currentHashes = HashMap<String, String>();
+
+  // The name of the file which stores the file hashes.
+  static const String _kFileCache = '.filecache';
+
+  // The current version of the file cache storage format.
+  static const int _kVersion = 1;
+
+  /// Read file hashes from disk.
+  void initialize() {
+    printTrace('Initializing file store');
+    if (!_cacheFile.existsSync()) {
+      return;
+    }
+    final List<int> data = _cacheFile.readAsBytesSync();
+    final pb.FileStorage fileStorage = pb.FileStorage.fromBuffer(data);
+    if (fileStorage.version != _kVersion) {
+      _cacheFile.deleteSync();
+      return;
+    }
+    for (pb.FileHash fileHash in fileStorage.files) {
+      previousHashes[fileHash.path] = fileHash.hash;
+    }
+    printTrace('Done initializing file store');
+  }
+
+  /// Persist file hashes to disk.
+  void persist() {
+    printTrace('Persisting file store');
+    final pb.FileStorage fileStorage = pb.FileStorage();
+    fileStorage.version = _kVersion;
+    final File file = _cacheFile;
+    if (!file.existsSync()) {
+      file.createSync();
+    }
+    for (MapEntry<String, String> entry in currentHashes.entries) {
+      previousHashes[entry.key] = entry.value;
+    }
+    for (MapEntry<String, String> entry in previousHashes.entries) {
+      final pb.FileHash fileHash = pb.FileHash();
+      fileHash.path = entry.key;
+      fileHash.hash = entry.value;
+      fileStorage.files.add(fileHash);
+    }
+    final Uint8List buffer = fileStorage.writeToBuffer();
+    file.writeAsBytesSync(buffer);
+    printTrace('Done persisting file store');
+  }
+
+  /// Computes a hash of the provided files and returns a list of entities
+  /// that were dirty.
+  // TODO(jonahwilliams): compare hash performance with md5 tool on macOS and
+  // linux and certutil on Windows, as well as dividing up computation across
+  // isolates. This also related to the current performance issue with checking
+  // APKs before installing them on device.
+  Future<List<File>> hashFiles(List<File> files) async {
+    final List<File> dirty = <File>[];
+    for (File file in files) {
+      final String absolutePath = file.resolveSymbolicLinksSync();
+      final String previousHash = previousHashes[absolutePath];
+      final List<int> bytes = file.readAsBytesSync();
+      final String currentHash = md5.convert(bytes).toString();
+
+      if (currentHash != previousHash) {
+        dirty.add(file);
+      }
+      currentHashes[absolutePath] = currentHash;
+    }
+    return dirty;
+  }
+
+  File get _cacheFile => environment.rootBuildDir.childFile(_kFileCache);
+}
diff --git a/packages/flutter_tools/lib/src/build_system/filecache.pb.dart b/packages/flutter_tools/lib/src/build_system/filecache.pb.dart
new file mode 100644
index 0000000..4c095ac
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/filecache.pb.dart
@@ -0,0 +1,96 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+///
+//  Generated code. Do not modify.
+//  source: lib/src/build_system/filecache.proto
+///
+// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name, sort_constructors_first
+
+import 'dart:core' as $core show bool, Deprecated, double, int, List, Map, override, pragma, String, dynamic;
+
+import 'package:protobuf/protobuf.dart' as $pb;
+
+class FileHash extends $pb.GeneratedMessage {
+  factory FileHash() => create();
+
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo('FileHash', package: const $pb.PackageName('flutter_tools'))
+    ..aOS(1, 'path')
+    ..aOS(2, 'hash')
+    ..hasRequiredFields = false;
+
+  FileHash._() : super();
+
+  factory FileHash.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+
+  factory FileHash.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+
+  @$core.override
+  FileHash clone() => FileHash()..mergeFromMessage(this);
+
+  @$core.override
+  FileHash copyWith(void Function(FileHash) updates) => super.copyWith(($core.dynamic message) => updates(message as FileHash));
+
+  @$core.override
+  $pb.BuilderInfo get info_ => _i;
+
+  @$core.pragma('dart2js:noInline')
+  static FileHash create() => FileHash._();
+
+  @$core.override
+  FileHash createEmptyInstance() => create();
+  static $pb.PbList<FileHash> createRepeated() => $pb.PbList<FileHash>();
+  static FileHash getDefault() => _defaultInstance ??= create()..freeze();
+  static FileHash _defaultInstance;
+
+  $core.String get path => $_getS(0, '');
+  set path($core.String v) { $_setString(0, v); }
+  $core.bool hasPath() => $_has(0);
+  void clearPath() => clearField(1);
+
+  $core.String get hash => $_getS(1, '');
+  set hash($core.String v) { $_setString(1, v); }
+  $core.bool hasHash() => $_has(1);
+  void clearHash() => clearField(2);
+}
+
+class FileStorage extends $pb.GeneratedMessage {
+  factory FileStorage() => create();
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo('FileHashStore', package: const $pb.PackageName('flutter_tools'))
+    ..a<$core.int>(1, 'version', $pb.PbFieldType.O3)
+    ..pc<FileHash>(2, 'files', $pb.PbFieldType.PM,FileHash.create)
+    ..hasRequiredFields = false;
+
+  FileStorage._() : super();
+  factory FileStorage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory FileStorage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+
+  @$core.override
+  FileStorage clone() => FileStorage()..mergeFromMessage(this);
+
+  @$core.override
+  FileStorage copyWith(void Function(FileStorage) updates) => super.copyWith(($core.dynamic message) => updates(message as FileStorage));
+
+  @$core.override
+  $pb.BuilderInfo get info_ => _i;
+
+  @$core.pragma('dart2js:noInline')
+  static FileStorage create() => FileStorage._();
+
+  @$core.override
+  FileStorage createEmptyInstance() => create();
+
+  static $pb.PbList<FileStorage> createRepeated() => $pb.PbList<FileStorage>();
+
+  static FileStorage getDefault() => _defaultInstance ??= create()..freeze();
+
+  static FileStorage _defaultInstance;
+
+  $core.int get version => $_get(0, 0);
+  set version($core.int v) { $_setSignedInt32(0, v); }
+  $core.bool hasVersion() => $_has(0);
+  void clearVersion() => clearField(1);
+
+  $core.List<FileHash> get files => $_getList(1);
+}
diff --git a/packages/flutter_tools/lib/src/build_system/filecache.pbjson.dart b/packages/flutter_tools/lib/src/build_system/filecache.pbjson.dart
new file mode 100644
index 0000000..32f6f9e
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/filecache.pbjson.dart
@@ -0,0 +1,25 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+///
+//  Generated code. Do not modify.
+//  source: lib/src/build_system/filecache.proto
+///
+// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name
+
+const Map<String, Object> FileHash$json = <String, Object>{
+  '1': 'FileHash',
+  '2': <Map<String, Object>>[
+    <String, Object>{'1': 'path', '3': 1, '4': 1, '5': 9, '10': 'path'},
+    <String, Object>{'1': 'hash', '3': 2, '4': 1, '5': 9, '10': 'hash'},
+  ],
+};
+
+const Map<String, Object> FileStorage$json = <String, Object>{
+  '1': 'FileHashStore',
+  '2': <Map<String, Object>>[
+    <String, Object>{'1': 'version', '3': 1, '4': 1, '5': 5, '10': 'version'},
+    <String, Object>{'1': 'files', '3': 2, '4': 3, '5': 11, '6': '.flutter_tools.FileHash', '10': 'files'},
+  ],
+};
diff --git a/packages/flutter_tools/lib/src/build_system/filecache.proto b/packages/flutter_tools/lib/src/build_system/filecache.proto
new file mode 100644
index 0000000..63be878
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/filecache.proto
@@ -0,0 +1,18 @@
+syntax = "proto3";
+package flutter_tools;
+
+message FileHash {
+  // The absolute path to the file on disk.
+  string path = 1;
+
+  // The last computed file hash.
+  string hash = 2;
+}
+
+message FileStorage {
+  // The current version of the file store.
+  int32 version = 1;
+
+  // All currently stored files.
+  repeated FileHash files = 2;
+}
diff --git a/packages/flutter_tools/lib/src/build_system/source.dart b/packages/flutter_tools/lib/src/build_system/source.dart
new file mode 100644
index 0000000..e9f1e16
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/source.dart
@@ -0,0 +1,228 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import '../artifacts.dart';
+import '../base/file_system.dart';
+import '../build_info.dart';
+import '../globals.dart';
+import 'build_system.dart';
+import 'exceptions.dart';
+
+/// An input function produces a list of additional input files for an
+/// [Environment].
+typedef InputFunction = List<File> Function(Environment environment);
+
+/// Collects sources for a [Target] into a single list of [FileSystemEntities].
+class SourceVisitor {
+  /// Create a new [SourceVisitor] from an [Environment].
+  SourceVisitor(this.environment, [this.inputs = true]);
+
+  /// The current environment.
+  final Environment environment;
+
+  /// Whether we are visiting inputs or outputs.
+  ///
+  /// Defaults to `true`.
+  final bool inputs;
+
+  /// The entities are populated after visiting each source.
+  final List<File> sources = <File>[];
+
+  /// Visit a [Source] which contains a function.
+  ///
+  /// The function is expected to produce a list of [FileSystemEntities]s.
+  void visitFunction(InputFunction function) {
+    sources.addAll(function(environment));
+  }
+
+  /// Visit a [Source] which contains a file uri.
+  ///
+  /// The uri may that may include constants defined in an [Environment].
+  void visitPattern(String pattern) {
+    // perform substitution of the environmental values and then
+    // of the local values.
+    final List<String> segments = <String>[];
+    final List<String> rawParts = pattern.split('/');
+    final bool hasWildcard = rawParts.last.contains('*');
+    String wildcardFile;
+    if (hasWildcard) {
+      wildcardFile = rawParts.removeLast();
+    }
+    // If the pattern does not start with an env variable, then we have nothing
+    // to resolve it to, error out.
+    switch (rawParts.first) {
+      case Environment.kProjectDirectory:
+        segments.addAll(
+            fs.path.split(environment.projectDir.resolveSymbolicLinksSync()));
+        break;
+      case Environment.kBuildDirectory:
+        segments.addAll(fs.path.split(
+            environment.buildDir.resolveSymbolicLinksSync()));
+        break;
+      case Environment.kCacheDirectory:
+        segments.addAll(
+            fs.path.split(environment.cacheDir.resolveSymbolicLinksSync()));
+        break;
+      case Environment.kFlutterRootDirectory:
+        segments.addAll(
+            fs.path.split(environment.cacheDir.resolveSymbolicLinksSync()));
+        break;
+      default:
+        throw InvalidPatternException(pattern);
+    }
+    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 (!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);
+          }
+        }
+      }
+    } else {
+      sources.add(fs.file(fs.path.normalize(filePath)));
+    }
+  }
+
+  /// Visit a [Source] which contains a [SourceBehavior].
+  void visitBehavior(SourceBehavior sourceBehavior) {
+    if (inputs) {
+      sources.addAll(sourceBehavior.inputs(environment));
+    } else {
+      sources.addAll(sourceBehavior.outputs(environment));
+    }
+  }
+
+  /// Visit a [Source] which is defined by an [Artifact] from the flutter cache.
+  ///
+  /// If the [Artifact] points to a directory then all child files are included.
+  void visitArtifact(Artifact artifact, TargetPlatform platform, BuildMode mode) {
+    final String path = artifacts.getArtifactPath(artifact, platform: platform, mode: mode);
+    if (fs.isDirectorySync(path)) {
+      sources.addAll(<File>[
+        for (FileSystemEntity entity in fs.directory(path).listSync(recursive: true))
+          if (entity is File)
+            entity
+      ]);
+    } else {
+      sources.add(fs.file(path));
+    }
+  }
+}
+
+/// A description of an input or output of a [Target].
+abstract class Source {
+  /// This source is a file-uri which contains some references to magic
+  /// environment variables.
+  const factory Source.pattern(String pattern) = _PatternSource;
+
+  /// This source is produced by invoking the provided function.
+  const factory Source.function(InputFunction function) = _FunctionSource;
+
+  /// This source is produced by the [SourceBehavior] class.
+  const factory Source.behavior(SourceBehavior behavior) = _SourceBehavior;
+
+  /// The source is provided by an [Artifact].
+  ///
+  /// If [artifact] points to a directory then all child files are included.
+  const factory Source.artifact(Artifact artifact, {TargetPlatform platform,
+      BuildMode mode}) = _ArtifactSource;
+
+  /// Visit the particular source type.
+  void accept(SourceVisitor visitor);
+
+  /// Whether the output source provided can be known before executing the rule.
+  ///
+  /// This does not apply to inputs, which are always explicit and must be
+  /// evaluated before the build.
+  ///
+  /// For example, [Source.pattern] and [Source.version] are not implicit
+  /// provided they do not use any wildcards. [Source.behavior] and
+  /// [Source.function] are always implicit.
+  bool get implicit;
+}
+
+/// An interface for describing input and output copies together.
+abstract class SourceBehavior {
+  const SourceBehavior();
+
+  /// The inputs for a particular target.
+  List<File> inputs(Environment environment);
+
+  /// The outputs for a particular target.
+  List<File> outputs(Environment environment);
+}
+
+class _SourceBehavior implements Source {
+  const _SourceBehavior(this.value);
+
+  final SourceBehavior value;
+
+  @override
+  void accept(SourceVisitor visitor) => visitor.visitBehavior(value);
+
+  @override
+  bool get implicit => true;
+}
+
+class _FunctionSource implements Source {
+  const _FunctionSource(this.value);
+
+  final InputFunction value;
+
+  @override
+  void accept(SourceVisitor visitor) => visitor.visitFunction(value);
+
+  @override
+  bool get implicit => true;
+}
+
+class _PatternSource implements Source {
+  const _PatternSource(this.value);
+
+  final String value;
+
+  @override
+  void accept(SourceVisitor visitor) => visitor.visitPattern(value);
+
+  @override
+  bool get implicit => value.contains('*');
+}
+
+class _ArtifactSource implements Source {
+  const _ArtifactSource(this.artifact, { this.platform, this.mode });
+
+  final Artifact artifact;
+  final TargetPlatform platform;
+  final BuildMode mode;
+
+  @override
+  void accept(SourceVisitor visitor) => visitor.visitArtifact(artifact, platform, mode);
+
+  @override
+  bool get implicit => false;
+}
diff --git a/packages/flutter_tools/lib/src/build_system/targets/assets.dart b/packages/flutter_tools/lib/src/build_system/targets/assets.dart
new file mode 100644
index 0000000..e5545db
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/targets/assets.dart
@@ -0,0 +1,95 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// 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 '../../asset.dart';
+import '../../base/file_system.dart';
+import '../../devfs.dart';
+import '../build_system.dart';
+
+/// The copying logic for flutter assets.
+// TODO(jonahwilliams): combine the asset bundle logic with this rule so that
+// we can compute the key for deleted assets. This is required to remove assets
+// from build directories that are no longer part of the manifest and to unify
+// the update/diff logic.
+class AssetBehavior extends SourceBehavior {
+  const AssetBehavior();
+
+  @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,
+    );
+    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 List<File> results = <File>[];
+    for (MapEntry<String, DevFSContent> entry in assetBundle.entries.entries) {
+      final File file = fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', entry.key));
+      results.add(file);
+    }
+    return results;
+  }
+}
+
+/// Copies the asset files from the [copyAssets] rule into place.
+Future<void> copyAssetsInvocation(Map<String, ChangeType> updates, Environment environment) async {
+  final Directory output = environment
+    .buildDir
+    .childDirectory('flutter_assets');
+  if (output.existsSync()) {
+    output.deleteSync(recursive: true);
+  }
+  output.createSync(recursive: true);
+  final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
+  await assetBundle.build(
+    manifestPath: environment.projectDir.childFile('pubspec.yaml').path,
+    packagesPath: environment.projectDir.childFile('.packages').path,
+  );
+  // Limit number of open files to avoid running out of file descriptors.
+  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(output.path, entry.key));
+        file.parent.createSync(recursive: true);
+        await file.writeAsBytes(await entry.value.contentsAsBytes());
+      } finally {
+        resource.release();
+      }
+    }));
+}
+
+/// Copy the assets used in the application into a build directory.
+const Target copyAssets = Target(
+  name: 'copy_assets',
+  inputs: <Source>[
+    Source.pattern('{PROJECT_DIR}/pubspec.yaml'),
+    Source.behavior(AssetBehavior()),
+  ],
+  outputs: <Source>[
+    Source.pattern('{BUILD_DIR}/flutter_assets/AssetManifest.json'),
+    Source.pattern('{BUILD_DIR}/flutter_assets/FontManifest.json'),
+    Source.pattern('{BUILD_DIR}/flutter_assets/LICENSE'),
+    Source.behavior(AssetBehavior()), // <- everything in this subdirectory.
+  ],
+  dependencies: <Target>[],
+  buildAction: copyAssetsInvocation,
+);
diff --git a/packages/flutter_tools/lib/src/build_system/targets/dart.dart b/packages/flutter_tools/lib/src/build_system/targets/dart.dart
new file mode 100644
index 0000000..f7d17f6
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/targets/dart.dart
@@ -0,0 +1,283 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import '../../artifacts.dart';
+import '../../base/build.dart';
+import '../../base/file_system.dart';
+import '../../base/io.dart';
+import '../../base/platform.dart';
+import '../../base/process_manager.dart';
+import '../../build_info.dart';
+import '../../compile.dart';
+import '../../dart/package_map.dart';
+import '../../globals.dart';
+import '../../project.dart';
+import '../build_system.dart';
+import '../exceptions.dart';
+
+/// The define to pass a [BuildMode].
+const String kBuildMode= 'BuildMode';
+
+/// The define to pass whether we compile 64-bit android-arm code.
+const String kTargetPlatform = 'TargetPlatform';
+
+/// The define to control what target file is used.
+const String kTargetFile = 'TargetFile';
+
+/// The define to control what iOS architectures are built for.
+///
+/// This is expected to be a comma-separated list of architectures. If not
+/// provided, defaults to arm64.
+///
+/// The other supported value is armv7, the 32-bit iOS architecture.
+const String kIosArchs = 'IosArchs';
+
+/// Supports compiling dart source to kernel with a subset of flags.
+///
+/// This is a non-incremental compile so the specific [updates] are ignored.
+Future<void> compileKernel(Map<String, ChangeType> updates, Environment environment) async {
+  final KernelCompiler compiler = await kernelCompilerFactory.create(
+    FlutterProject.fromDirectory(environment.projectDir),
+  );
+  if (environment.defines[kBuildMode] == null) {
+    throw MissingDefineException(kBuildMode, 'kernel_snapshot');
+  }
+  final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
+  final String targetFile = environment.defines[kTargetFile] ?? fs.path.join('lib', 'main.dart');
+
+  final CompilerOutput output = await compiler.compile(
+    sdkRoot: artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath, mode: buildMode),
+    aot: buildMode != BuildMode.debug,
+    trackWidgetCreation: false,
+    targetModel: TargetModel.flutter,
+    targetProductVm: buildMode == BuildMode.release,
+    outputFilePath: environment
+      .buildDir
+      .childFile('main.app.dill')
+      .path,
+    depFilePath: null,
+    mainPath: targetFile,
+  );
+  if (output.errorCount != 0) {
+    throw Exception('Errors during snapshot creation: $output');
+  }
+}
+
+/// Supports compiling a dart kernel file to an ELF binary.
+Future<void> compileAotElf(Map<String, ChangeType> updates, Environment environment) async {
+  final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: false);
+  final String outputPath = environment.buildDir.path;
+  if (environment.defines[kBuildMode] == null) {
+    throw MissingDefineException(kBuildMode, 'aot_elf');
+  }
+  if (environment.defines[kTargetPlatform] == null) {
+    throw MissingDefineException(kTargetPlatform, 'aot_elf');
+  }
+  final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
+  final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]);
+  final int snapshotExitCode = await snapshotter.build(
+    platform: targetPlatform,
+    buildMode: buildMode,
+    mainPath: environment.buildDir.childFile('main.app.dill').path,
+    packagesPath: environment.projectDir.childFile('.packages').path,
+    outputPath: outputPath,
+  );
+  if (snapshotExitCode != 0) {
+    throw Exception('AOT snapshotter exited with code $snapshotExitCode');
+  }
+}
+
+/// Finds the locations of all dart files within the project.
+///
+/// This does not attempt to determine if a file is used or imported, so it
+/// may otherwise report more files than strictly necessary.
+List<File> listDartSources(Environment environment) {
+  final Map<String, Uri> packageMap = PackageMap(environment.projectDir.childFile('.packages').path).map;
+  final List<File> dartFiles = <File>[];
+  for (Uri uri in packageMap.values) {
+    final Directory libDirectory = fs.directory(uri.toFilePath(windows: platform.isWindows));
+    for (FileSystemEntity entity in libDirectory.listSync(recursive: true)) {
+      if (entity is File && entity.path.endsWith('.dart')) {
+        dartFiles.add(entity);
+      }
+    }
+  }
+  return dartFiles;
+}
+
+/// Supports compiling a dart kernel file to an assembly file.
+///
+/// If more than one iOS arch is provided, then this rule will
+/// produce a univeral binary.
+Future<void> compileAotAssembly(Map<String, ChangeType> updates, Environment environment) async {
+  final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: false);
+  final String outputPath = environment.buildDir.path;
+  if (environment.defines[kBuildMode] == null) {
+    throw MissingDefineException(kBuildMode, 'aot_assembly');
+  }
+  if (environment.defines[kTargetPlatform] == null) {
+    throw MissingDefineException(kTargetPlatform, 'aot_assembly');
+  }
+  final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
+  final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]);
+  final List<IOSArch> iosArchs = environment.defines[kIosArchs]?.split(',')?.map(getIOSArchForName)?.toList()
+      ?? <IOSArch>[IOSArch.arm64];
+  if (targetPlatform != TargetPlatform.ios) {
+    throw Exception('aot_assembly is only supported for iOS applications');
+  }
+
+  // If we're building for a single architecture (common), then skip the lipo.
+  if (iosArchs.length == 1) {
+    final int snapshotExitCode = await snapshotter.build(
+      platform: targetPlatform,
+      buildMode: buildMode,
+      mainPath: environment.buildDir.childFile('main.app.dill').path,
+      packagesPath: environment.projectDir.childFile('.packages').path,
+      outputPath: outputPath,
+      iosArch: iosArchs.single,
+    );
+    if (snapshotExitCode != 0) {
+      throw Exception('AOT snapshotter exited with code $snapshotExitCode');
+    }
+  } else {
+    // If we're building multiple iOS archs the binaries need to be lipo'd
+    // together.
+    final List<Future<int>> pending = <Future<int>>[];
+    for (IOSArch iosArch in iosArchs) {
+      pending.add(snapshotter.build(
+        platform: targetPlatform,
+        buildMode: buildMode,
+        mainPath: environment.buildDir.childFile('main.app.dill').path,
+        packagesPath: environment.projectDir.childFile('.packages').path,
+        outputPath: fs.path.join(outputPath, getNameForIOSArch(iosArch)),
+        iosArch: iosArch,
+      ));
+    }
+    final List<int> results = await Future.wait(pending);
+    if (results.any((int result) => result != 0)) {
+      throw Exception('AOT snapshotter exited with code ${results.join()}');
+    }
+    final ProcessResult result = await processManager.run(<String>[
+      'lipo',
+      ...iosArchs.map((IOSArch iosArch) =>
+          fs.path.join(outputPath, getNameForIOSArch(iosArch), 'App.framework', 'App')),
+      '-create',
+      '-output',
+      fs.path.join(outputPath, 'App.framework', 'App'),
+    ]);
+    if (result.exitCode != 0) {
+      throw Exception('lipo exited with code ${result.exitCode}');
+    }
+  }
+}
+
+/// Generate a snapshot of the dart code used in the program.
+const Target kernelSnapshot = Target(
+  name: 'kernel_snapshot',
+  inputs: <Source>[
+    Source.function(listDartSources), // <- every dart file under {PROJECT_DIR}/lib and in .packages
+    Source.artifact(Artifact.platformKernelDill),
+    Source.artifact(Artifact.engineDartBinary),
+    Source.artifact(Artifact.frontendServerSnapshotForEngineDartSdk),
+  ],
+  outputs: <Source>[
+    Source.pattern('{BUILD_DIR}/main.app.dill'),
+  ],
+  dependencies: <Target>[],
+  buildAction: compileKernel,
+);
+
+/// Generate an ELF binary from a dart kernel file in profile mode.
+const Target aotElfProfile = Target(
+  name: 'aot_elf_profile',
+  inputs: <Source>[
+    Source.pattern('{BUILD_DIR}/main.app.dill'),
+    Source.pattern('{PROJECT_DIR}/.packages'),
+    Source.artifact(Artifact.engineDartBinary),
+    Source.artifact(Artifact.skyEnginePath),
+    Source.artifact(Artifact.genSnapshot,
+      platform: TargetPlatform.android_arm,
+      mode: BuildMode.profile,
+    ),
+  ],
+  outputs: <Source>[
+    Source.pattern('{BUILD_DIR}/app.so'),
+  ],
+  dependencies: <Target>[
+    kernelSnapshot,
+  ],
+  buildAction: compileAotElf,
+);
+
+/// Generate an ELF binary from a dart kernel file in release mode.
+const Target aotElfRelease= Target(
+  name: 'aot_elf_release',
+  inputs: <Source>[
+    Source.pattern('{BUILD_DIR}/main.app.dill'),
+    Source.pattern('{PROJECT_DIR}/.packages'),
+    Source.artifact(Artifact.engineDartBinary),
+    Source.artifact(Artifact.skyEnginePath),
+    Source.artifact(Artifact.genSnapshot,
+      platform: TargetPlatform.android_arm,
+      mode: BuildMode.release,
+    ),
+  ],
+  outputs: <Source>[
+    Source.pattern('{BUILD_DIR}/app.so'),
+  ],
+  dependencies: <Target>[
+    kernelSnapshot,
+  ],
+  buildAction: compileAotElf,
+);
+
+/// Generate an assembly target from a dart kernel file in profile mode.
+const Target aotAssemblyProfile = Target(
+  name: 'aot_assembly_profile',
+  inputs: <Source>[
+    Source.pattern('{BUILD_DIR}/main.app.dill'),
+    Source.pattern('{PROJECT_DIR}/.packages'),
+    Source.artifact(Artifact.engineDartBinary),
+    Source.artifact(Artifact.skyEnginePath),
+    Source.artifact(Artifact.genSnapshot,
+      platform: TargetPlatform.ios,
+      mode: BuildMode.profile,
+    ),
+  ],
+  outputs: <Source>[
+    // TODO(jonahwilliams): are these used or just a side effect?
+    // Source.pattern('{BUILD_DIR}/snapshot_assembly.S'),
+    // Source.pattern('{BUILD_DIR}/snapshot_assembly.o'),
+    Source.pattern('{BUILD_DIR}/App.framework/App'),
+  ],
+  dependencies: <Target>[
+    kernelSnapshot,
+  ],
+  buildAction: compileAotAssembly,
+);
+
+/// Generate an assembly target from a dart kernel file in release mode.
+const Target aotAssemblyRelease = Target(
+  name: 'aot_assembly_release',
+  inputs: <Source>[
+    Source.pattern('{BUILD_DIR}/main.app.dill'),
+    Source.pattern('{PROJECT_DIR}/.packages'),
+    Source.artifact(Artifact.engineDartBinary),
+    Source.artifact(Artifact.skyEnginePath),
+    Source.artifact(Artifact.genSnapshot,
+      platform: TargetPlatform.ios,
+      mode: BuildMode.release,
+    ),
+  ],
+  outputs: <Source>[
+    // TODO(jonahwilliams): are these used or just a side effect?
+    // Source.pattern('{BUILD_DIR}/snapshot_assembly.S'),
+    // Source.pattern('{BUILD_DIR}/snapshot_assembly.o'),
+    Source.pattern('{BUILD_DIR}/App.framework/App'),
+  ],
+  dependencies: <Target>[
+    kernelSnapshot,
+  ],
+  buildAction: compileAotAssembly,
+);
diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart
new file mode 100644
index 0000000..19e38ea
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart
@@ -0,0 +1,43 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import '../build_system.dart';
+import 'assets.dart';
+import 'dart.dart';
+
+/// Create an iOS debug application.
+const Target debugIosApplication = Target(
+  name: 'debug_ios_application',
+  buildAction: null,
+  inputs: <Source>[],
+  outputs: <Source>[],
+  dependencies: <Target>[
+    copyAssets,
+    kernelSnapshot,
+  ]
+);
+
+/// Create an iOS profile application.
+const Target profileIosApplication = Target(
+  name: 'profile_ios_application',
+  buildAction: null,
+  inputs: <Source>[],
+  outputs: <Source>[],
+  dependencies: <Target>[
+    copyAssets,
+    aotAssemblyProfile,
+  ]
+);
+
+/// Create an iOS debug application.
+const Target releaseIosApplication = Target(
+  name: 'release_ios_application',
+  buildAction: null,
+  inputs: <Source>[],
+  outputs: <Source>[],
+  dependencies: <Target>[
+    copyAssets,
+    aotAssemblyRelease,
+  ]
+);
diff --git a/packages/flutter_tools/lib/src/build_system/targets/linux.dart b/packages/flutter_tools/lib/src/build_system/targets/linux.dart
new file mode 100644
index 0000000..c5ad64d
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/targets/linux.dart
@@ -0,0 +1,46 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import '../../artifacts.dart';
+import '../../base/file_system.dart';
+import '../../globals.dart';
+import '../build_system.dart';
+
+// Copies all of the input files to the correct copy dir.
+Future<void> copyLinuxAssets(Map<String, ChangeType> updates,
+    Environment environment) async {
+  final String basePath = artifacts.getArtifactPath(Artifact.linuxDesktopPath);
+  for (String input in updates.keys) {
+    final String outputPath = fs.path.join(
+      environment.projectDir.path,
+      'linux',
+      'flutter',
+      fs.path.relative(input, from: basePath),
+    );
+    final File destinationFile = fs.file(outputPath);
+    if (!destinationFile.parent.existsSync()) {
+      destinationFile.parent.createSync(recursive: true);
+    }
+    fs.file(input).copySync(destinationFile.path);
+  }
+}
+
+/// Copies the Linux desktop embedding files to the copy directory.
+const Target unpackLinux = Target(
+  name: 'unpack_linux',
+  inputs: <Source>[
+    Source.artifact(Artifact.linuxDesktopPath),
+  ],
+  outputs: <Source>[
+    Source.pattern('{PROJECT_DIR}/linux/flutter/libflutter_linux.so'),
+    Source.pattern('{PROJECT_DIR}/linux/flutter/flutter_export.h'),
+    Source.pattern('{PROJECT_DIR}/linux/flutter/flutter_messenger.h'),
+    Source.pattern('{PROJECT_DIR}/linux/flutter/flutter_plugin_registrar.h'),
+    Source.pattern('{PROJECT_DIR}/linux/flutter/flutter_glfw.h'),
+    Source.pattern('{PROJECT_DIR}/linux/flutter/icudtl.dat'),
+    Source.pattern('{PROJECT_DIR}/linux/flutter/cpp_client_wrapper/*'),
+  ],
+  dependencies: <Target>[],
+  buildAction: copyLinuxAssets,
+);
diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart
new file mode 100644
index 0000000..7bfb923
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart
@@ -0,0 +1,100 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import '../../artifacts.dart';
+import '../../base/file_system.dart';
+import '../../base/io.dart';
+import '../../base/process_manager.dart';
+import '../../globals.dart';
+import '../build_system.dart';
+import 'assets.dart';
+import 'dart.dart';
+
+/// Copy the macOS framework to the correct copy dir by invoking 'cp -R'.
+///
+/// The shelling out is done to avoid complications with preserving special
+/// files (e.g., symbolic links) in the framework structure.
+///
+/// Removes any previous version of the framework that already exists in the
+/// target directory.
+// TODO(jonahwilliams): remove shell out.
+Future<void> copyFramework(Map<String, ChangeType> updates,
+    Environment environment) async {
+  final String basePath = artifacts.getArtifactPath(Artifact.flutterMacOSFramework);
+  final Directory targetDirectory = environment
+    .projectDir
+    .childDirectory('macos')
+    .childDirectory('Flutter')
+    .childDirectory('FlutterMacOS.framework');
+  if (targetDirectory.existsSync()) {
+    targetDirectory.deleteSync(recursive: true);
+  }
+
+  final ProcessResult result = processManager
+      .runSync(<String>['cp', '-R', basePath, targetDirectory.path]);
+  if (result.exitCode != 0) {
+    throw Exception(
+      'Failed to copy framework (exit ${result.exitCode}:\n'
+      '${result.stdout}\n---\n${result.stderr}',
+    );
+  }
+}
+
+const String _kOutputPrefix = '{PROJECT_DIR}/macos/Flutter/FlutterMacOS.framework';
+
+/// Copies the macOS desktop framework to the copy directory.
+const Target unpackMacos = Target(
+  name: 'unpack_macos',
+  inputs: <Source>[
+    Source.artifact(Artifact.flutterMacOSFramework),
+  ],
+  outputs: <Source>[
+    Source.pattern('$_kOutputPrefix/FlutterMacOS'),
+    // Headers
+    Source.pattern('$_kOutputPrefix/Headers/FLEOpenGLContextHandling.h'),
+    Source.pattern('$_kOutputPrefix/Headers/FLEReshapeListener.h'),
+    Source.pattern('$_kOutputPrefix/Headers/FLEView.h'),
+    Source.pattern('$_kOutputPrefix/Headers/FLEViewController.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/FlutterPluginMacOS.h'),
+    Source.pattern('$_kOutputPrefix/Headers/FlutterPluginRegistrarMacOS.h'),
+    // Modules
+    Source.pattern('$_kOutputPrefix/Modules/module.modulemap'),
+    // Resources
+    Source.pattern('$_kOutputPrefix/Resources/icudtl.dat'),
+    Source.pattern('$_kOutputPrefix/Resources/info.plist'),
+    // Ignore Versions folder for now
+  ],
+  dependencies: <Target>[],
+  buildAction: copyFramework,
+);
+
+/// Build a macOS application.
+const Target macosApplication = Target(
+  name: 'debug_macos_application',
+  buildAction: null,
+  inputs: <Source>[],
+  outputs: <Source>[],
+  dependencies: <Target>[
+    unpackMacos,
+    kernelSnapshot,
+    copyAssets,
+  ]
+);
+
+/// Build a macOS release application.
+const Target macoReleaseApplication = Target(
+  name: 'release_macos_application',
+  buildAction: null,
+  inputs: <Source>[],
+  outputs: <Source>[],
+  dependencies: <Target>[
+    unpackMacos,
+    aotElfRelease,
+    copyAssets,
+  ]
+);
diff --git a/packages/flutter_tools/lib/src/build_system/targets/windows.dart b/packages/flutter_tools/lib/src/build_system/targets/windows.dart
new file mode 100644
index 0000000..2677cba
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/targets/windows.dart
@@ -0,0 +1,50 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import '../../artifacts.dart';
+import '../../base/file_system.dart';
+import '../../globals.dart';
+import '../build_system.dart';
+
+/// Copies all of the input files to the correct copy dir.
+Future<void> copyWindowsAssets(Map<String, ChangeType> updates,
+    Environment environment) async {
+  // This path needs to match the prefix in the rule below.
+  final String basePath = artifacts.getArtifactPath(Artifact.windowsDesktopPath);
+  for (String input in updates.keys) {
+    final String outputPath = fs.path.join(
+      environment.projectDir.path,
+      'windows',
+      'flutter',
+      fs.path.relative(input, from: basePath),
+    );
+    final File destinationFile = fs.file(outputPath);
+    if (!destinationFile.parent.existsSync()) {
+      destinationFile.parent.createSync(recursive: true);
+    }
+    fs.file(input).copySync(destinationFile.path);
+  }
+}
+
+/// Copies the Windows desktop embedding files to the copy directory.
+const Target unpackWindows = Target(
+  name: 'unpack_windows',
+  inputs: <Source>[
+    Source.artifact(Artifact.windowsDesktopPath),
+  ],
+  outputs: <Source>[
+    Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_windows.dll'),
+    Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_windows.dll.exp'),
+    Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_windows.dll.lib'),
+    Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_windows.dll.pdb'),
+    Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_export.h'),
+    Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_messenger.h'),
+    Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_plugin_registrar.h'),
+    Source.pattern('{PROJECT_DIR}/windows/flutter/flutter_glfw.h'),
+    Source.pattern('{PROJECT_DIR}/windows/flutter/icudtl.dat'),
+    Source.pattern('{PROJECT_DIR}/windows/flutter/cpp_client_wrapper/*'),
+  ],
+  dependencies: <Target>[],
+  buildAction: copyWindowsAssets,
+);
diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart
new file mode 100644
index 0000000..e5a6659
--- /dev/null
+++ b/packages/flutter_tools/lib/src/commands/assemble.dart
@@ -0,0 +1,221 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import '../base/common.dart';
+import '../base/context.dart';
+import '../base/file_system.dart';
+import '../build_info.dart';
+import '../build_system/build_system.dart';
+import '../convert.dart';
+import '../globals.dart';
+import '../project.dart';
+import '../runner/flutter_command.dart';
+
+/// The [BuildSystem] instance.
+BuildSystem get buildSystem => context.get<BuildSystem>();
+
+/// Assemble provides a low level API to interact with the flutter tool build
+/// system.
+class AssembleCommand extends FlutterCommand {
+  AssembleCommand() {
+    addSubcommand(AssembleRun());
+    addSubcommand(AssembleDescribe());
+    addSubcommand(AssembleListInputs());
+    addSubcommand(AssembleBuildDirectory());
+  }
+  @override
+  String get description => 'Assemble and build flutter resources.';
+
+  @override
+  String get name => 'assemble';
+
+  @override
+  bool get isExperimental => true;
+
+  @override
+  Future<FlutterCommandResult> runCommand() {
+    return null;
+  }
+}
+
+abstract class AssembleBase extends FlutterCommand {
+  AssembleBase() {
+    argParser.addMultiOption(
+      'define',
+      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(
+      'resource-pool-size',
+      help: 'The maximum number of concurrent tasks the build system will run.'
+    );
+  }
+
+  /// Returns the provided target platform.
+  ///
+  /// Throws a [ToolExit] if none is provided. This intentionally has no
+  /// default.
+  TargetPlatform get targetPlatform {
+    final String value = argResults['target-platform'] ?? 'darwin-x64';
+    if (value == null) {
+      throwToolExit('--target-platform is required for flutter assemble.');
+    }
+    return getTargetPlatformForName(value);
+  }
+
+  /// Returns the provided build mode.
+  ///
+  /// Throws a [ToolExit] if none is provided. This intentionally has no
+  /// default.
+  BuildMode get buildMode {
+    final String value = argResults['build-mode'] ?? 'debug';
+    if (value == null) {
+      throwToolExit('--build-mode is required for flutter assemble.');
+    }
+    return getBuildModeForName(value);
+  }
+
+  /// The name of the target we are describing or building.
+  String get targetName {
+    if (argResults.rest.isEmpty) {
+      throwToolExit('missing target name for flutter assemble.');
+    }
+    return argResults.rest.first;
+  }
+
+  /// The environmental configuration for a build invocation.
+  Environment get environment {
+    final FlutterProject flutterProject = FlutterProject.current();
+    final Environment result = Environment(
+      buildDir: fs.directory(getBuildDirectory()),
+      projectDir: flutterProject.directory,
+      defines: _parseDefines(argResults['define']),
+    );
+    return result;
+  }
+
+  static Map<String, String> _parseDefines(List<String> values) {
+    final Map<String, String> results = <String, String>{};
+    for (String chunk in values) {
+      final List<String> parts = chunk.split('=');
+      if (parts.length != 2) {
+        throwToolExit('Improperly formatted define flag: $chunk');
+      }
+      final String key = parts[0];
+      final String value = parts[1];
+      results[key] = value;
+    }
+    return results;
+  }
+}
+
+/// Execute a build starting from a target action.
+class AssembleRun extends AssembleBase {
+  @override
+  String get description => 'Execute the stages for a specified target.';
+
+  @override
+  String get name => 'run';
+
+  @override
+  bool get isExperimental => true;
+
+  @override
+  Future<FlutterCommandResult> runCommand() async {
+    final BuildResult result = await buildSystem.build(targetName, environment, BuildSystemConfig(
+      resourcePoolSize: argResults['resource-pool-size'],
+    ));
+    if (!result.success) {
+      for (MapEntry<String, ExceptionMeasurement> data in result.exceptions.entries) {
+        printError('Target ${data.key} failed: ${data.value.exception}');
+        printError('${data.value.exception}');
+      }
+      throwToolExit('build failed');
+    } else {
+      printStatus('build succeeded');
+    }
+    return null;
+  }
+}
+
+/// Fully describe a target and its dependencies.
+class AssembleDescribe extends AssembleBase {
+  @override
+  String get description => 'List the stages for a specified target.';
+
+  @override
+  String get name => 'describe';
+
+  @override
+  bool get isExperimental => true;
+
+  @override
+  Future<FlutterCommandResult> runCommand() {
+    try {
+      printStatus(
+        json.encode(buildSystem.describe(targetName, environment))
+      );
+    } on Exception catch (err, stackTrace) {
+      printTrace(stackTrace.toString());
+      throwToolExit(err.toString());
+    }
+    return null;
+  }
+}
+
+/// List input files for a target.
+class AssembleListInputs extends AssembleBase {
+  @override
+  String get description => 'List the inputs for a particular target.';
+
+  @override
+  String get name => 'inputs';
+
+  @override
+  bool get isExperimental => true;
+
+  @override
+  Future<FlutterCommandResult> runCommand() {
+    try {
+      final List<Map<String, Object>> results = buildSystem.describe(targetName, environment);
+      for (Map<String, Object> result in results) {
+        if (result['name'] == targetName) {
+          final List<String> inputs = result['inputs'];
+          inputs.forEach(printStatus);
+        }
+      }
+    } on Exception catch (err, stackTrace) {
+      printTrace(stackTrace.toString());
+      throwToolExit(err.toString());
+    }
+    return null;
+  }
+}
+
+/// Return the build directory for a configuiration.
+class AssembleBuildDirectory extends AssembleBase {
+  @override
+  String get description => 'List the inputs for a particular target.';
+
+  @override
+  String get name => 'build-dir';
+
+  @override
+  bool get isExperimental => true;
+
+  @override
+  Future<FlutterCommandResult> runCommand() {
+    printStatus(environment.buildDir.path);
+    return null;
+  }
+}
+
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index bc03cbe..533cfc6 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -21,6 +21,7 @@
 import 'base/time.dart';
 import 'base/user_messages.dart';
 import 'base/utils.dart';
+import 'build_system/build_system.dart';
 import 'cache.dart';
 import 'compile.dart';
 import 'devfs.dart';
@@ -67,6 +68,7 @@
       Artifacts: () => CachedArtifacts(),
       AssetBundleFactory: () => AssetBundleFactory.defaultInstance,
       BotDetector: () => const BotDetector(),
+      BuildSystem: () => BuildSystem(),
       Cache: () => Cache(),
       ChromeLauncher: () => const ChromeLauncher(),
       CocoaPods: () => CocoaPods(),