Reland: Switch to assemble API for dart2js (#41659)

* Stop using build_runner for dart2js

* fixes to yield when computing hashes and to imports

* add missing await

* Update filecache_test.dart

* Fix paths in filecache test

* use file uri for import

* add test cases and configurable override

* remove test dep

* fix filepaths for windows

* test no longer failing

* fix paths for test cases

* fix typo

* address comments

* make a constant

* make filehash async and use constant

* fix silly logic error
diff --git a/packages/flutter_tools/lib/src/build_runner/build_script.dart b/packages/flutter_tools/lib/src/build_runner/build_script.dart
index 0638b8f..69d0569 100644
--- a/packages/flutter_tools/lib/src/build_runner/build_script.dart
+++ b/packages/flutter_tools/lib/src/build_runner/build_script.dart
@@ -4,21 +4,17 @@
 
 // ignore_for_file: implementation_imports
 import 'dart:async';
-import 'dart:convert'; // ignore: dart_convert_import
-import 'dart:io'; // ignore: dart_io_import
 import 'dart:isolate';
 
 import 'package:analyzer/dart/analysis/results.dart';
 import 'package:analyzer/dart/analysis/utilities.dart';
 import 'package:analyzer/dart/ast/ast.dart';
-import 'package:archive/archive.dart';
 import 'package:build/build.dart';
 import 'package:build_config/build_config.dart';
 import 'package:build_modules/build_modules.dart';
 import 'package:build_modules/builders.dart';
 import 'package:build_modules/src/module_builder.dart';
 import 'package:build_modules/src/platform.dart';
-import 'package:build_modules/src/workers.dart';
 import 'package:build_runner/build_runner.dart' as build_runner;
 import 'package:build_runner_core/build_runner_core.dart' as core;
 import 'package:build_test/builder.dart';
@@ -26,10 +22,7 @@
 import 'package:build_web_compilers/build_web_compilers.dart';
 import 'package:build_web_compilers/builders.dart';
 import 'package:build_web_compilers/src/dev_compiler_bootstrap.dart';
-import 'package:crypto/crypto.dart';
-import 'package:glob/glob.dart';
 import 'package:path/path.dart' as path; // ignore: package_path_import
-import 'package:scratch_space/scratch_space.dart';
 import 'package:test_core/backend.dart';
 
 const String ddcBootstrapExtension = '.dart.bootstrap.js';
@@ -248,12 +241,8 @@
 
   @override
   Future<void> build(BuildStep buildStep) async {
-    if (release || profile) {
-      await bootstrapDart2Js(buildStep, flutterWebSdk, profile);
-    } else {
-      await bootstrapDdc(buildStep, platform: flutterWebPlatform,
-          skipPlatformCheckPackages: skipPlatformCheckPackages);
-    }
+    await bootstrapDdc(buildStep, platform: flutterWebPlatform,
+        skipPlatformCheckPackages: skipPlatformCheckPackages);
   }
 }
 
@@ -422,116 +411,6 @@
   };
 }
 
-Future<void> bootstrapDart2Js(BuildStep buildStep, String flutterWebSdk, bool profile) async {
-  final AssetId dartEntrypointId = buildStep.inputId;
-  final AssetId moduleId = dartEntrypointId.changeExtension(moduleExtension(flutterWebPlatform));
-  final Module module = Module.fromJson(json.decode(await buildStep.readAsString(moduleId)));
-  final List<Module> allDeps = await module.computeTransitiveDependencies(
-    buildStep,
-    throwIfUnsupported: true,
-    skipPlatformCheckPackages: skipPlatformCheckPackages,
-  )..add(module);
-  final ScratchSpace scratchSpace = await buildStep.fetchResource(scratchSpaceResource);
-  final Iterable<AssetId> allSrcs = allDeps.expand((Module module) => module.sources);
-  await scratchSpace.ensureAssets(allSrcs, buildStep);
-
-  final String packageFile = _createPackageFile(allSrcs, buildStep, scratchSpace);
-  final String dartPath = dartEntrypointId.path.startsWith('lib/')
-      ? 'package:${dartEntrypointId.package}/'
-          '${dartEntrypointId.path.substring('lib/'.length)}'
-      : dartEntrypointId.path;
-  final String jsOutputPath =
-      '${path.withoutExtension(dartPath.replaceFirst('package:', 'packages/'))}'
-      '$jsEntrypointExtension';
-  final String flutterWebSdkPath = flutterWebSdk;
-  final String librariesPath = path.join(flutterWebSdkPath, 'libraries.json');
-  final List<String> args = <String>[
-    '--libraries-spec="$librariesPath"',
-    if (profile)
-      '-O1'
-    else
-      '-O4',
-    '-o',
-    '$jsOutputPath',
-    '--packages="$packageFile"',
-    if (profile)
-      '-Ddart.vm.profile=true'
-    else
-      '-Ddart.vm.product=true',
-    dartPath,
-  ];
-  final Dart2JsBatchWorkerPool dart2js = await buildStep.fetchResource(dart2JsWorkerResource);
-  final Dart2JsResult result = await dart2js.compile(args);
-  final AssetId jsOutputId = dartEntrypointId.changeExtension(jsEntrypointExtension);
-  final File jsOutputFile = scratchSpace.fileFor(jsOutputId);
-  if (result.succeeded && jsOutputFile.existsSync()) {
-    final String rootDir = path.dirname(jsOutputFile.path);
-    final String dartFile = path.basename(dartEntrypointId.path);
-    final Glob fileGlob = Glob('$dartFile.js*');
-    final Archive archive = Archive();
-    await for (FileSystemEntity jsFile in fileGlob.list(root: rootDir)) {
-      if (jsFile.path.endsWith(jsEntrypointExtension) ||
-          jsFile.path.endsWith(jsEntrypointSourceMapExtension)) {
-        // These are explicitly output, and are not part of the archive.
-        continue;
-      }
-      if (jsFile is File) {
-        final String fileName = path.relative(jsFile.path, from: rootDir);
-        final FileStat fileStats = jsFile.statSync();
-        archive.addFile(
-            ArchiveFile(fileName, fileStats.size, await jsFile.readAsBytes())
-              ..mode = fileStats.mode
-              ..lastModTime = fileStats.modified.millisecondsSinceEpoch);
-      }
-    }
-    if (archive.isNotEmpty) {
-      final AssetId archiveId = dartEntrypointId.changeExtension(jsEntrypointArchiveExtension);
-      await buildStep.writeAsBytes(archiveId, TarEncoder().encode(archive));
-    }
-
-    // Explicitly write out the original js file and sourcemap - we can't output
-    // these as part of the archive because they already have asset nodes.
-    await scratchSpace.copyOutput(jsOutputId, buildStep);
-    final AssetId jsSourceMapId =
-        dartEntrypointId.changeExtension(jsEntrypointSourceMapExtension);
-    await _copyIfExists(jsSourceMapId, scratchSpace, buildStep);
-  } else {
-    log.severe(result.output);
-  }
-}
-
-Future<void> _copyIfExists(
-    AssetId id, ScratchSpace scratchSpace, AssetWriter writer) async {
-  final File file = scratchSpace.fileFor(id);
-  if (file.existsSync()) {
-    await scratchSpace.copyOutput(id, writer);
-  }
-}
-
-/// Creates a `.packages` file unique to this entrypoint at the root of the
-/// scratch space and returns it's filename.
-///
-/// Since mulitple invocations of Dart2Js will share a scratch space and we only
-/// know the set of packages involved the current entrypoint we can't construct
-/// a `.packages` file that will work for all invocations of Dart2Js so a unique
-/// file is created for every entrypoint that is run.
-///
-/// The filename is based off the MD5 hash of the asset path so that files are
-/// unique regarless of situations like `web/foo/bar.dart` vs
-/// `web/foo-bar.dart`.
-String _createPackageFile(Iterable<AssetId> inputSources, BuildStep buildStep, ScratchSpace scratchSpace) {
-  final Uri inputUri = buildStep.inputId.uri;
-  final String packageFileName =
-      '.package-${md5.convert(inputUri.toString().codeUnits)}';
-  final File packagesFile =
-      scratchSpace.fileFor(AssetId(buildStep.inputId.package, packageFileName));
-  final Set<String> packageNames = inputSources.map((AssetId s) => s.package).toSet();
-  final String packagesFileContent =
-      packageNames.map((String name) => '$name:packages/$name/').join('\n');
-  packagesFile .writeAsStringSync('# Generated for $inputUri\n$packagesFileContent');
-  return packageFileName;
-}
-
 /// Returns whether or not [dartId] is an app entrypoint (basically, whether
 /// or not it has a `main` function).
 Future<bool> _isAppEntryPoint(AssetId dartId, AssetReader reader) async {
diff --git a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart
index 2ed586b..f4d1f94 100644
--- a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart
+++ b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart
@@ -6,6 +6,7 @@
 
 import 'package:meta/meta.dart';
 import 'package:vm_service/vm_service.dart' as vmservice;
+import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
 import '../application_package.dart';
 import '../base/common.dart';
@@ -20,6 +21,7 @@
 import '../project.dart';
 import '../reporting/reporting.dart';
 import '../resident_runner.dart';
+import '../web/chrome.dart';
 import '../web/web_device.dart';
 import '../web/web_runner.dart';
 import 'web_fs.dart';
@@ -170,7 +172,7 @@
         initializePlatform: debuggingOptions.initializePlatform,
         hostname: debuggingOptions.hostname,
         port: debuggingOptions.port,
-        skipDwds: device is WebServerDevice,
+        skipDwds: device is WebServerDevice || !debuggingOptions.buildInfo.isDebug,
       );
       // When connecting to a browser, update the message with a seemsSlow notification
       // to handle the case where we fail to connect.
@@ -291,6 +293,21 @@
         ).send();
       }
     }
+    // Allows browser refresh hot restart on non-debug builds.
+    if (device is ChromeDevice && debuggingOptions.browserLaunch) {
+      try {
+        final Chrome chrome = await ChromeLauncher.connectedInstance;
+        final ChromeTab chromeTab = await chrome.chromeConnection.getTab((ChromeTab chromeTab) {
+          return chromeTab.url.contains(debuggingOptions.hostname);
+        });
+        final WipConnection wipConnection = await chromeTab.connect();
+        await wipConnection.sendCommand('Page.reload');
+        status.stop();
+        return OperationResult.ok;
+      } catch (err) {
+        // Ignore error and continue with posted message;
+      }
+    }
     status.stop();
     printStatus('Recompile complete. Page requires refresh.');
     return OperationResult.ok;
diff --git a/packages/flutter_tools/lib/src/build_runner/web_fs.dart b/packages/flutter_tools/lib/src/build_runner/web_fs.dart
index 456cf07..c2cca4d 100644
--- a/packages/flutter_tools/lib/src/build_runner/web_fs.dart
+++ b/packages/flutter_tools/lib/src/build_runner/web_fs.dart
@@ -35,6 +35,7 @@
 import '../plugins.dart';
 import '../project.dart';
 import '../web/chrome.dart';
+import '../web/compile.dart';
 
 /// The name of the built web project.
 const String kBuildTargetName = 'web';
@@ -89,6 +90,11 @@
     this._dwds,
     this.uri,
     this._assetServer,
+    this._useBuildRunner,
+    this._flutterProject,
+    this._target,
+    this._buildInfo,
+    this._initializePlatform,
   );
 
   /// The server uri.
@@ -98,12 +104,17 @@
   final Dwds _dwds;
   final BuildDaemonClient _client;
   final AssetServer _assetServer;
+  final bool _useBuildRunner;
+  final FlutterProject _flutterProject;
+  final String _target;
+  final BuildInfo _buildInfo;
+  final bool _initializePlatform;
   StreamSubscription<void> _connectedApps;
 
   static const String _kHostName = 'localhost';
 
   Future<void> stop() async {
-    await _client.close();
+    await _client?.close();
     await _dwds?.stop();
     await _server.close(force: true);
     await _connectedApps?.cancel();
@@ -132,6 +143,10 @@
 
   /// Recompile the web application and return whether this was successful.
   Future<bool> recompile() async {
+    if (!_useBuildRunner) {
+      await buildWeb(_flutterProject, _target, _buildInfo, _initializePlatform);
+      return true;
+    }
     _client.startBuild();
     await for (BuildResults results in _client.buildResults) {
       final BuildResult result = results.results.firstWhere((BuildResult result) {
@@ -161,39 +176,7 @@
     if (!flutterProject.dartTool.existsSync()) {
       flutterProject.dartTool.createSync(recursive: true);
     }
-    final bool hasWebPlugins = findPlugins(flutterProject)
-        .any((Plugin p) => p.platforms.containsKey(WebPlugin.kConfigKey));
-    // Start the build daemon and run an initial build.
-    final Completer<bool> inititalBuild = Completer<bool>();
-    final BuildDaemonClient client = await buildDaemonCreator
-      .startBuildDaemon(fs.currentDirectory.path,
-          release: buildInfo.isRelease,
-          profile: buildInfo.isProfile,
-          hasPlugins: hasWebPlugins,
-          initializePlatform: initializePlatform,
-      );
-    client.startBuild();
-    // Only provide relevant build results
-    final Stream<BuildResult> filteredBuildResults = client.buildResults
-        .asyncMap<BuildResult>((BuildResults results) {
-          return results.results
-            .firstWhere((BuildResult result) => result.target == kBuildTargetName);
-        });
-    final StreamSubscription<void> firstBuild = client.buildResults.listen((BuildResults buildResults) {
-      if (inititalBuild.isCompleted) {
-        return;
-      }
-      final BuildResult result = buildResults.results.firstWhere((BuildResult result) {
-        return result.target == kBuildTargetName;
-      });
-      if (result.status == BuildStatus.failed) {
-        inititalBuild.complete(false);
-      }
-      if (result.status == BuildStatus.succeeded) {
-        inititalBuild.complete(true);
-      }
-    });
-    final int daemonAssetPort = buildDaemonCreator.assetServerPort(fs.currentDirectory);
+    final Completer<bool> firstBuildCompleter = Completer<bool>();
 
     // Initialize the asset bundle.
     final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
@@ -201,7 +184,7 @@
     await writeBundle(fs.directory(getAssetBuildDirectory()), assetBundle.entries);
 
     final String targetBaseName = fs.path
-        .withoutExtension(target).replaceFirst('lib${fs.path.separator}', '');
+      .withoutExtension(target).replaceFirst('lib${fs.path.separator}', '');
     final Map<String, String> mappedUrls = <String, String>{
       'main.dart.js': 'packages/${flutterProject.manifest.appName}/'
           '${targetBaseName}_web_entrypoint.dart.js',
@@ -236,29 +219,78 @@
         }
       };
     });
+
     Handler handler;
     Dwds dwds;
-    if (!skipDwds) {
-      dwds = await dwdsFactory(
-        hostname: hostname ?? _kHostName,
-        applicationPort: hostPort,
-        applicationTarget: kBuildTargetName,
-        assetServerPort: daemonAssetPort,
-        buildResults: filteredBuildResults,
-        chromeConnection: () async {
-          return (await ChromeLauncher.connectedInstance).chromeConnection;
-        },
-        reloadConfiguration: ReloadConfiguration.none,
-        serveDevTools: true,
-        verbose: false,
-        enableDebugExtension: true,
-        logWriter: (dynamic level, String message) => printTrace(message),
-      );
-      handler = pipeline.addHandler(dwds.handler);
+    BuildDaemonClient client;
+    StreamSubscription<void> firstBuild;
+    if (buildInfo.isDebug) {
+      final bool hasWebPlugins = findPlugins(flutterProject)
+          .any((Plugin p) => p.platforms.containsKey(WebPlugin.kConfigKey));
+      // Start the build daemon and run an initial build.
+      client = await buildDaemonCreator
+        .startBuildDaemon(fs.currentDirectory.path,
+            release: buildInfo.isRelease,
+            profile: buildInfo.isProfile,
+            hasPlugins: hasWebPlugins,
+            initializePlatform: initializePlatform,
+        );
+      client.startBuild();
+      // Only provide relevant build results
+      final Stream<BuildResult> filteredBuildResults = client.buildResults
+        .asyncMap<BuildResult>((BuildResults results) {
+          return results.results
+            .firstWhere((BuildResult result) => result.target == kBuildTargetName);
+        });
+      // Start the build daemon and run an initial build.
+      firstBuild = client.buildResults.listen((BuildResults buildResults) {
+        if (firstBuildCompleter.isCompleted) {
+          return;
+        }
+        final BuildResult result = buildResults.results.firstWhere((BuildResult result) {
+          return result.target == kBuildTargetName;
+        });
+        if (result.status == BuildStatus.failed) {
+          firstBuildCompleter.complete(false);
+        }
+        if (result.status == BuildStatus.succeeded) {
+          firstBuildCompleter.complete(true);
+        }
+      });
+      final int daemonAssetPort = buildDaemonCreator.assetServerPort(fs.currentDirectory);
+
+      // Initialize the asset bundle.
+      final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
+      await assetBundle.build();
+      await writeBundle(fs.directory(getAssetBuildDirectory()), assetBundle.entries);
+      if (!skipDwds) {
+        dwds = await dwdsFactory(
+          hostname: hostname ?? _kHostName,
+          applicationPort: hostPort,
+          applicationTarget: kBuildTargetName,
+          assetServerPort: daemonAssetPort,
+          buildResults: filteredBuildResults,
+          chromeConnection: () async {
+            return (await ChromeLauncher.connectedInstance).chromeConnection;
+          },
+          reloadConfiguration: ReloadConfiguration.none,
+          serveDevTools: true,
+          verbose: false,
+          enableDebugExtension: true,
+          logWriter: (dynamic level, String message) => printTrace(message),
+        );
+        handler = pipeline.addHandler(dwds.handler);
+      } else {
+        handler = pipeline.addHandler(proxyHandler('http://localhost:$daemonAssetPort/web/'));
+      }
     } else {
-      handler = pipeline.addHandler(proxyHandler('http://localhost:$daemonAssetPort/web/'));
+      await buildWeb(flutterProject, target, buildInfo, initializePlatform);
+      firstBuildCompleter.complete(true);
     }
-    final AssetServer assetServer = AssetServer(flutterProject, targetBaseName);
+
+    final AssetServer assetServer = buildInfo.isDebug
+      ? DebugAssetServer(flutterProject, targetBaseName)
+      : ReleaseAssetServer();
     Cascade cascade = Cascade();
     cascade = cascade.add(handler);
     cascade = cascade.add(assetServer.handle);
@@ -270,23 +302,65 @@
       dwds,
       'http://$_kHostName:$hostPort/',
       assetServer,
+      buildInfo.isDebug,
+      flutterProject,
+      target,
+      buildInfo,
+      initializePlatform,
     );
-    if (!await inititalBuild.future) {
+    if (!await firstBuildCompleter.future) {
       throw Exception('Failed to compile for the web.');
     }
-    await firstBuild.cancel();
+    await firstBuild?.cancel();
     return webFS;
   }
 }
 
-class AssetServer {
-  AssetServer(this.flutterProject, this.targetBaseName);
+abstract class AssetServer {
+  Future<Response> handle(Request request);
+
+  void dispose() {}
+}
+
+class ReleaseAssetServer extends AssetServer {
+  @override
+  Future<Response> handle(Request request) async {
+    final Uri artifactUri = fs.directory(getWebBuildDirectory()).uri.resolveUri(request.url);
+    final File file = fs.file(artifactUri);
+    if (file.existsSync()) {
+      return Response.ok(file.readAsBytesSync(), headers: <String, String>{
+        'Content-Type': _guessExtension(file),
+      });
+    }
+    if (request.url.path == '') {
+      final File file = fs.file(fs.path.join(getWebBuildDirectory(), 'index.html'));
+      return Response.ok(file.readAsBytesSync(), headers: <String, String>{
+        'Content-Type': _guessExtension(file),
+      });
+    }
+    return Response.notFound('');
+  }
+
+  String _guessExtension(File file) {
+    switch (fs.path.extension(file.path)) {
+      case '.js':
+        return 'text/javascript';
+      case '.html':
+        return 'text/html';
+    }
+    return 'text';
+  }
+}
+
+class DebugAssetServer extends AssetServer {
+  DebugAssetServer(this.flutterProject, this.targetBaseName);
 
   final FlutterProject flutterProject;
   final String targetBaseName;
   final PackageMap packageMap = PackageMap(PackageMap.globalPackagesPath);
   Directory partFiles;
 
+  @override
   Future<Response> handle(Request request) async {
     if (request.url.path.endsWith('.html')) {
       final Uri htmlUri = flutterProject.web.directory.uri.resolveUri(request.url);
@@ -411,6 +485,7 @@
     return Response.notFound('');
   }
 
+  @override
   void dispose() {
     partFiles?.deleteSync(recursive: true);
   }
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 b1efeff..e2bdbb8 100644
--- a/packages/flutter_tools/lib/src/build_system/build_system.dart
+++ b/packages/flutter_tools/lib/src/build_system/build_system.dart
@@ -25,6 +25,13 @@
 /// The [BuildSystem] instance.
 BuildSystem get buildSystem => context.get<BuildSystem>();
 
+/// A reasonable amount of files to open at the same time.
+///
+/// This number is somewhat arbitrary - it is difficult to detect whether
+/// or not we'll run out of file descriptiors when using async dart:io
+/// APIs.
+const int kMaxOpenFiles = 64;
+
 /// Configuration for the build system itself.
 class BuildSystemConfig {
   /// Create a new [BuildSystemConfig].
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
index b43c311..afce327 100644
--- a/packages/flutter_tools/lib/src/build_system/file_hash_store.dart
+++ b/packages/flutter_tools/lib/src/build_system/file_hash_store.dart
@@ -7,6 +7,7 @@
 import 'dart:typed_data';
 
 import 'package:crypto/crypto.dart';
+import 'package:pool/pool.dart';
 
 import '../base/file_system.dart';
 import '../convert.dart';
@@ -69,13 +70,6 @@
 ///
 /// 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 {
@@ -141,24 +135,28 @@
 
   /// 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();
+    final Pool openFiles = Pool(kMaxOpenFiles);
+    await Future.wait(<Future<void>>[
+      for (File file in files) _hashFile(file, dirty, openFiles)]);
+    return dirty;
+  }
 
+  Future<void> _hashFile(File file, List<File> dirty, Pool pool) async {
+    final PoolResource resource = await pool.request();
+    try {
+      final String absolutePath = file.path;
+      final String previousHash = previousHashes[absolutePath];
+      final Digest digest = md5.convert(await file.readAsBytes());
+      final String currentHash = digest.toString();
       if (currentHash != previousHash) {
         dirty.add(file);
       }
       currentHashes[absolutePath] = currentHash;
+    } finally {
+      resource.release();
     }
-    return dirty;
   }
 
   File get _cacheFile => environment.buildDir.childFile(_kFileCache);
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 127d432..f783237 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/assets.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/assets.dart
@@ -50,7 +50,9 @@
 
 /// A specific asset behavior for building bundles.
 class AssetOutputBehavior extends SourceBehavior {
-  const AssetOutputBehavior();
+  const AssetOutputBehavior([this._pathSuffix = '']);
+
+  final String _pathSuffix;
 
   @override
   List<File> inputs(Environment environment) {
@@ -64,7 +66,7 @@
     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));
+      results.add(fs.file(fs.path.join(_pathSuffix, devFsContent.file.path)));
     }
     return results;
   }
@@ -78,7 +80,7 @@
     );
     final List<File> results = <File>[];
     for (String key in assetBundle.entries.keys) {
-      final File file = fs.file(fs.path.join(environment.outputDir.path, key));
+      final File file = fs.file(fs.path.join(environment.outputDir.path, _pathSuffix, key));
       results.add(file);
     }
     return results;
@@ -125,7 +127,7 @@
       packagesPath: environment.projectDir.childFile('.packages').path,
     );
     // Limit number of open files to avoid running out of file descriptors.
-    final Pool pool = Pool(64);
+    final Pool pool = Pool(kMaxOpenFiles);
     await Future.wait<void>(
       assetBundle.entries.entries.map<Future<void>>((MapEntry<String, DevFSContent> entry) async {
         final PoolResource resource = await pool.request();
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 92474cd..02a4021 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/dart.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/dart.dart
@@ -115,23 +115,7 @@
 
     final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
     await assetBundle.build();
-    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(environment.outputDir.path, entry.key));
-          file.parent.createSync(recursive: true);
-          final DevFSContent content = entry.value;
-          if (content is DevFSFileContent && content.file is File) {
-            await (content.file as File).copy(file.path);
-          } else {
-            await file.writeAsBytes(await entry.value.contentsAsBytes());
-          }
-        } finally {
-          resource.release();
-        }
-      }));
+    await copyAssets(assetBundle, environment);
   }
 
   @override
@@ -140,6 +124,28 @@
   ];
 }
 
+/// A helper function to copy an [assetBundle] into an [environment]'s output directory,
+/// plus an optional [pathSuffix]
+Future<void> copyAssets(AssetBundle assetBundle, Environment environment, [String pathSuffix = '']) async {
+  final Pool pool = Pool(kMaxOpenFiles);
+  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(environment.outputDir.path, pathSuffix, entry.key));
+        file.parent.createSync(recursive: true);
+        final DevFSContent content = entry.value;
+        if (content is DevFSFileContent && content.file is File) {
+          await (content.file as File).copy(file.path);
+        } else {
+          await file.writeAsBytes(await entry.value.contentsAsBytes());
+        }
+      } finally {
+        resource.release();
+      }
+  }));
+}
+
 /// Copies the prebuilt flutter bundle for release mode.
 class ReleaseCopyFlutterBundle extends CopyFlutterBundle {
   const ReleaseCopyFlutterBundle();
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 b05b3b8..a448b5b 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart
@@ -336,7 +336,7 @@
     }
     // Limit number of open files to avoid running out of file descriptors.
     try {
-      final Pool pool = Pool(64);
+      final Pool pool = Pool(kMaxOpenFiles);
       await Future.wait<void>(
         assetBundle.entries.entries.map<Future<void>>((MapEntry<String, DevFSContent> entry) async {
           final PoolResource resource = await pool.request();
diff --git a/packages/flutter_tools/lib/src/build_system/targets/web.dart b/packages/flutter_tools/lib/src/build_system/targets/web.dart
new file mode 100644
index 0000000..d097dcb
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_system/targets/web.dart
@@ -0,0 +1,203 @@
+// 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 '../../asset.dart';
+import '../../base/file_system.dart';
+import '../../base/io.dart';
+import '../../base/process_manager.dart';
+import '../../build_info.dart';
+import '../../dart/package_map.dart';
+import '../../globals.dart';
+import '../../project.dart';
+import '../build_system.dart';
+import 'assets.dart';
+import 'dart.dart';
+
+/// Whether web builds should call the platform initialization logic.
+const String kInitializePlatform = 'InitializePlatform';
+
+/// Whether the application has web plugins.
+const String kHasWebPlugins = 'HasWebPlugins';
+
+/// An override for the dart2js build mode.
+///
+/// Valid values are O1 (lowest, profile default) to O4 (highest, release default).
+const String kDart2jsOptimization = 'Dart2jsOptimization';
+
+/// Generates an entrypoint for a web target.
+class WebEntrypointTarget extends Target {
+  const WebEntrypointTarget();
+
+  @override
+  String get name => 'web_entrypoint';
+
+  @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/web.dart'),
+  ];
+
+  @override
+  List<Source> get outputs => const <Source>[
+    Source.pattern('{BUILD_DIR}/main.dart'),
+  ];
+
+  @override
+  Future<void> build(Environment environment) async {
+    final String targetFile = environment.defines[kTargetFile];
+    final bool shouldInitializePlatform = environment.defines[kInitializePlatform] == 'true';
+    final bool hasPlugins = environment.defines[kHasWebPlugins] == 'true';
+    final String import = fs.file(fs.path.absolute(targetFile)).uri.toString();
+
+    String contents;
+    if (hasPlugins) {
+      contents = '''
+import 'dart:ui' as ui;
+
+import 'package:flutter_web_plugins/flutter_web_plugins.dart';
+
+import 'generated_plugin_registrant.dart';
+import "$import" as entrypoint;
+
+Future<void> main() async {
+  registerPlugins(webPluginRegistry);
+  if ($shouldInitializePlatform) {
+    await ui.webOnlyInitializePlatform();
+  }
+  entrypoint.main();
+}
+''';
+    } else {
+      contents = '''
+import 'dart:ui' as ui;
+
+import "$import" as entrypoint;
+
+Future<void> main() async {
+  if ($shouldInitializePlatform) {
+    await ui.webOnlyInitializePlatform();
+  }
+  entrypoint.main();
+}
+''';
+    }
+    environment.buildDir.childFile('main.dart')
+      ..writeAsStringSync(contents);
+  }
+}
+
+/// Compiles a web entrypoint with dart2js.
+class Dart2JSTarget extends Target {
+  const Dart2JSTarget();
+
+  @override
+  String get name => 'dart2js';
+
+  @override
+  List<Target> get dependencies => const <Target>[
+    WebEntrypointTarget()
+  ];
+
+  @override
+  List<Source> get inputs => const <Source>[
+    Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/web.dart'),
+    Source.artifact(Artifact.flutterWebSdk),
+    Source.artifact(Artifact.dart2jsSnapshot),
+    Source.artifact(Artifact.engineDartBinary),
+    Source.artifact(Artifact.engineDartSdkPath),
+    Source.pattern('{BUILD_DIR}/main.dart'),
+    Source.pattern('{PROJECT_DIR}/.packages'),
+    Source.function(listDartSources), // <- every dart file under {PROJECT_DIR}/lib and in .packages
+  ];
+
+  @override
+  List<Source> get outputs => const <Source>[
+    Source.pattern('{BUILD_DIR}/main.dart.js'),
+  ];
+
+  @override
+  Future<void> build(Environment environment) async {
+    final String dart2jsOptimization = environment.defines[kDart2jsOptimization];
+    final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
+    final String specPath = fs.path.join(artifacts.getArtifactPath(Artifact.flutterWebSdk), 'libraries.json');
+    final String packageFile = FlutterProject.fromDirectory(environment.projectDir).hasBuilders
+      ? PackageMap.globalGeneratedPackagesPath
+      : PackageMap.globalPackagesPath;
+    final ProcessResult result = await processManager.run(<String>[
+      artifacts.getArtifactPath(Artifact.engineDartBinary),
+      artifacts.getArtifactPath(Artifact.dart2jsSnapshot),
+      '--libraries-spec=$specPath',
+      if (dart2jsOptimization != null)
+        '-$dart2jsOptimization'
+      else if (buildMode == BuildMode.profile)
+        '-O1'
+      else
+        '-O4',
+      '-o',
+      environment.buildDir.childFile('main.dart.js').path,
+      '--packages=$packageFile',
+      if (buildMode == BuildMode.profile)
+        '-Ddart.vm.profile=true'
+      else
+        '-Ddart.vm.product=true',
+      environment.buildDir.childFile('main.dart').path,
+    ]);
+    if (result.exitCode != 0) {
+      throw Exception(result.stdout + result.stderr);
+    }
+  }
+}
+
+/// Unpacks the dart2js compilation to a given output directory
+class WebReleaseBundle extends Target {
+  const WebReleaseBundle();
+
+  @override
+  String get name => 'web_release_bundle';
+
+  @override
+  List<Target> get dependencies => const <Target>[
+    Dart2JSTarget(),
+  ];
+
+  @override
+  List<Source> get inputs => const <Source>[
+    Source.pattern('{BUILD_DIR}/main.dart.js'),
+    Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/web.dart'),
+    Source.behavior(AssetOutputBehavior('assets')),
+    Source.pattern('{PROJECT_DIR}/web/index.html'),
+  ];
+
+  @override
+  List<Source> get outputs => const <Source>[
+    Source.pattern('{OUTPUT_DIR}/main.dart.js'),
+    Source.pattern('{OUTPUT_DIR}/assets/AssetManifest.json'),
+    Source.pattern('{OUTPUT_DIR}/assets/FontManifest.json'),
+    Source.pattern('{OUTPUT_DIR}/assets/LICENSE'),
+    Source.pattern('{OUTPUT_DIR}/index.html'),
+    Source.behavior(AssetOutputBehavior('assets'))
+  ];
+
+  @override
+  Future<void> build(Environment environment) async {
+    for (File outputFile in environment.buildDir.listSync(recursive: true).whereType<File>()) {
+      if (!fs.path.basename(outputFile.path).contains('main.dart.js')) {
+        continue;
+      }
+      outputFile.copySync(
+        environment.outputDir.childFile(fs.path.basename(outputFile.path)).path
+      );
+    }
+    environment.projectDir
+      .childDirectory('web')
+      .childFile('index.html')
+      .copySync(fs.path.join(environment.outputDir.path, 'index.html'));
+    final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
+    await assetBundle.build();
+    await copyAssets(assetBundle, environment, 'assets');
+  }
+}
diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart
index 8fdb708..c8048cb 100644
--- a/packages/flutter_tools/lib/src/commands/assemble.dart
+++ b/packages/flutter_tools/lib/src/commands/assemble.dart
@@ -12,6 +12,7 @@
 import '../build_system/targets/ios.dart';
 import '../build_system/targets/linux.dart';
 import '../build_system/targets/macos.dart';
+import '../build_system/targets/web.dart';
 import '../build_system/targets/windows.dart';
 import '../globals.dart';
 import '../project.dart';
@@ -31,6 +32,7 @@
   DebugMacOSBundleFlutterAssets(),
   ProfileMacOSBundleFlutterAssets(),
   ReleaseMacOSBundleFlutterAssets(),
+  WebReleaseBundle(),
 ];
 
 /// Assemble provides a low level API to interact with the flutter tool build
diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart
index a682b6b..c924c3e 100644
--- a/packages/flutter_tools/lib/src/web/compile.dart
+++ b/packages/flutter_tools/lib/src/web/compile.dart
@@ -2,17 +2,19 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:archive/archive.dart';
 import 'package:meta/meta.dart';
 
-import '../asset.dart';
 import '../base/common.dart';
 import '../base/context.dart';
 import '../base/file_system.dart';
 import '../base/logger.dart';
 import '../build_info.dart';
-import '../bundle.dart';
+import '../build_system/build_system.dart';
+import '../build_system/targets/dart.dart';
+import '../build_system/targets/web.dart';
 import '../globals.dart';
+import '../platform_plugins.dart';
+import '../plugins.dart';
 import '../project.dart';
 import '../reporting/reporting.dart';
 
@@ -23,64 +25,32 @@
   if (!flutterProject.web.existsSync()) {
     throwToolExit('Missing index.html.');
   }
+  final bool hasWebPlugins = findPlugins(flutterProject)
+    .any((Plugin p) => p.platforms.containsKey(WebPlugin.kConfigKey));
   final Status status = logger.startProgress('Compiling $target for the Web...', timeout: null);
   final Stopwatch sw = Stopwatch()..start();
-  final Directory outputDir = fs.directory(getWebBuildDirectory())
-    ..createSync(recursive: true);
-  bool result;
-  try {
-    result = await webCompilationProxy.initialize(
-      projectDirectory: FlutterProject.current().directory,
-      mode: buildInfo.mode,
-      projectName: flutterProject.manifest.appName,
-      initializePlatform: initializePlatform,
-    );
-    if (result) {
-      // Places assets adjacent to the web stuff.
-      final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
-      await assetBundle.build();
-      await writeBundle(fs.directory(fs.path.join(outputDir.path, 'assets')), assetBundle.entries);
-
-      // Copy results to output directory.
-      final String outputPath = fs.path.join(
-        flutterProject.dartTool.path,
-        'build',
-        'flutter_web',
-        flutterProject.manifest.appName,
-        '${fs.path.withoutExtension(target)}_web_entrypoint.dart.js',
-      );
-      // Check for deferred import outputs.
-      final File dart2jsArchive = fs.file(fs.path.join(
-        flutterProject.dartTool.path,
-        'build',
-        'flutter_web',
-        '${flutterProject.manifest.appName}',
-        '${fs.path.withoutExtension(target)}_web_entrypoint.dart.js.tar.gz'),
-      );
-      fs.file(outputPath).copySync(fs.path.join(outputDir.path, 'main.dart.js'));
-      fs.file('$outputPath.map').copySync(fs.path.join(outputDir.path, 'main.dart.js.map'));
-      flutterProject.web.indexFile.copySync(fs.path.join(outputDir.path, 'index.html'));
-      if (dart2jsArchive.existsSync()) {
-        final Archive archive = TarDecoder().decodeBytes(dart2jsArchive.readAsBytesSync());
-        for (ArchiveFile file in archive.files) {
-          outputDir.childFile(file.name).writeAsBytesSync(file.content);
-        }
-      }
+  final BuildResult result = await const BuildSystem().build(const WebReleaseBundle(), Environment(
+    outputDir: fs.directory(getWebBuildDirectory()),
+    projectDir: fs.currentDirectory,
+    buildDir: flutterProject.directory
+      .childDirectory('.dart_tool')
+      .childDirectory('flutter_build'),
+    defines: <String, String>{
+      kBuildMode: getNameForBuildMode(buildInfo.mode),
+      kTargetFile: target,
+      kInitializePlatform: initializePlatform.toString(),
+      kHasWebPlugins: hasWebPlugins.toString(),
+    },
+  ));
+  if (!result.success) {
+    for (ExceptionMeasurement measurement in result.exceptions.values) {
+      printError(measurement.stackTrace.toString());
+      printError(measurement.exception.toString());
     }
-  } catch (err) {
-    printError(err.toString());
-    result = false;
-  } finally {
-    status.stop();
+    throwToolExit('Failed to compile application for the Web.');
   }
-  if (result == false) {
-    throwToolExit('Failed to compile $target for the Web.');
-  }
-  String buildName = 'ddc';
-  if (buildInfo.isRelease) {
-    buildName = 'dart2js';
-  }
-  flutterUsage.sendTiming('build', buildName, Duration(milliseconds: sw.elapsedMilliseconds));
+  status.stop();
+  flutterUsage.sendTiming('build', 'dart2js', Duration(milliseconds: sw.elapsedMilliseconds));
 }
 
 /// An indirection on web compilation.