switch dart2js build to depfile, remove Source.function (#42977)

diff --git a/packages/flutter_tools/lib/src/build_system/depfile.dart b/packages/flutter_tools/lib/src/build_system/depfile.dart
index 1df5e7d..a9db0e6 100644
--- a/packages/flutter_tools/lib/src/build_system/depfile.dart
+++ b/packages/flutter_tools/lib/src/build_system/depfile.dart
@@ -26,6 +26,28 @@
     return Depfile(inputs, outputs);
   }
 
+  /// Parse the output of dart2js's used dependencies.
+  ///
+  /// The [file] contains a list of newline separated file URIs. The output
+  /// file must be manually specified.
+  factory Depfile.parseDart2js(File file, File output) {
+    final List<File> inputs = <File>[];
+    for (String rawUri in file.readAsLinesSync()) {
+      if (rawUri.trim().isEmpty) {
+        continue;
+      }
+      final Uri fileUri = Uri.tryParse(rawUri);
+      if (fileUri == null) {
+        continue;
+      }
+      if (fileUri.scheme != 'file') {
+        continue;
+      }
+      inputs.add(fs.file(fileUri));
+    }
+    return Depfile(inputs, <File>[output]);
+  }
+
   /// The input files for this depfile.
   final List<File> inputs;
 
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 afce327..ef4207f 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
@@ -139,7 +139,8 @@
     final List<File> dirty = <File>[];
     final Pool openFiles = Pool(kMaxOpenFiles);
     await Future.wait(<Future<void>>[
-      for (File file in files) _hashFile(file, dirty, openFiles)]);
+       for (File file in files) _hashFile(file, dirty, openFiles)
+    ]);
     return dirty;
   }
 
@@ -148,6 +149,13 @@
     try {
       final String absolutePath = file.path;
       final String previousHash = previousHashes[absolutePath];
+      // If the file is missing it is assumed to be dirty.
+      if (!file.existsSync()) {
+        currentHashes.remove(absolutePath);
+        previousHashes.remove(absolutePath);
+        dirty.add(file);
+        return;
+      }
       final Digest digest = md5.convert(await file.readAsBytes());
       final String currentHash = digest.toString();
       if (currentHash != previousHash) {
diff --git a/packages/flutter_tools/lib/src/build_system/source.dart b/packages/flutter_tools/lib/src/build_system/source.dart
index cbf048d..16a860a 100644
--- a/packages/flutter_tools/lib/src/build_system/source.dart
+++ b/packages/flutter_tools/lib/src/build_system/source.dart
@@ -9,10 +9,6 @@
 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);
-
 /// A set of source files.
 abstract class ResolvedFiles {
   /// Whether any of the sources we evaluated contained a missing depfile.
@@ -45,13 +41,6 @@
   bool get containsNewDepfile => _containsNewDepfile;
   bool _containsNewDepfile = false;
 
-  /// 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 depfile which contains both input and output files.
   ///
   /// If the file is missing, this visitor is marked as [containsNewDepfile].
@@ -206,9 +195,6 @@
   /// environment variables.
   const factory Source.pattern(String pattern, { bool optional }) = _PatternSource;
 
-  /// This source is produced by invoking the provided function.
-  const factory Source.function(InputFunction function) = _FunctionSource;
-
   /// This source is produced by the [SourceBehavior] class.
   const factory Source.behavior(SourceBehavior behavior) = _SourceBehavior;
 
@@ -264,18 +250,6 @@
   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, { this.optional = false });
 
diff --git a/packages/flutter_tools/lib/src/build_system/targets/web.dart b/packages/flutter_tools/lib/src/build_system/targets/web.dart
index 5a018d5..52281b8 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/web.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/web.dart
@@ -12,6 +12,7 @@
 import '../../globals.dart';
 import '../../project.dart';
 import '../build_system.dart';
+import '../depfile.dart';
 import 'assets.dart';
 import 'dart.dart';
 
@@ -108,18 +109,17 @@
 
   @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.pattern('{BUILD_DIR}/main.dart'),
     Source.pattern('{PROJECT_DIR}/.packages'),
-    Source.function(listDartSources), // <- every dart file under {PROJECT_DIR}/lib and in .packages
+    Source.depfile('dart2js.d'),
   ];
 
   @override
   List<Source> get outputs => const <Source>[
-    Source.pattern('{BUILD_DIR}/main.dart.js'),
+    Source.depfile('dart2js.d'),
   ];
 
   @override
@@ -130,6 +130,7 @@
     final String packageFile = FlutterProject.fromDirectory(environment.projectDir).hasBuilders
       ? PackageMap.globalGeneratedPackagesPath
       : PackageMap.globalPackagesPath;
+    final File outputFile = environment.buildDir.childFile('main.dart.js');
     final ProcessResult result = await processManager.run(<String>[
       artifacts.getArtifactPath(Artifact.engineDartBinary),
       artifacts.getArtifactPath(Artifact.dart2jsSnapshot),
@@ -141,7 +142,7 @@
       else
         '-O4',
       '-o',
-      environment.buildDir.childFile('main.dart.js').path,
+      outputFile.path,
       '--packages=$packageFile',
       if (buildMode == BuildMode.profile)
         '-Ddart.vm.profile=true'
@@ -152,6 +153,18 @@
     if (result.exitCode != 0) {
       throw Exception(result.stdout + result.stderr);
     }
+    final File dart2jsDeps = environment.buildDir
+      .childFile('main.dart.js.deps');
+    if (!dart2jsDeps.existsSync()) {
+      printError('Warning: dart2js did not produced expected deps list at '
+        '${dart2jsDeps.path}');
+      return;
+    }
+    final Depfile depfile = Depfile.parseDart2js(
+      environment.buildDir.childFile('main.dart.js.deps'),
+      outputFile,
+    );
+    depfile.writeToFile(environment.buildDir.childFile('dart2js.d'));
   }
 }
 
@@ -170,7 +183,6 @@
   @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'),
   ];
diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart
index 7644f63..e567d91 100644
--- a/packages/flutter_tools/lib/src/web/compile.dart
+++ b/packages/flutter_tools/lib/src/web/compile.dart
@@ -30,27 +30,32 @@
   await injectPlugins(flutterProject, checkProjects: true);
   final Status status = logger.startProgress('Compiling $target for the Web...', timeout: null);
   final Stopwatch sw = Stopwatch()..start();
-  final BuildResult result = await 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());
+  try {
+    final BuildResult result = await 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());
+      }
+      throwToolExit('Failed to compile application for the Web.');
     }
-    throwToolExit('Failed to compile application for the Web.');
+  } catch (err) {
+    throwToolExit(err.toString());
+  } finally {
+    status.stop();
   }
-  status.stop();
   flutterUsage.sendTiming('build', 'dart2js', Duration(milliseconds: sw.elapsedMilliseconds));
 }
 
diff --git a/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart b/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart
index b782682..ed016fb 100644
--- a/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart
+++ b/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart
@@ -114,7 +114,7 @@
     expect(buildResult.hasException, false);
   }));
 
-  test('Throws exception if it does not produce a specified output', () => testbed.run(() async {
+  test('Does not throw exception if it does not produce a specified output', () => testbed.run(() async {
     final Target badTarget = TestTarget((Environment environment) async {})
       ..inputs = const <Source>[
         Source.pattern('{PROJECT_DIR}/foo.dart'),
@@ -124,8 +124,7 @@
       ];
     final BuildResult result = await buildSystem.build(badTarget, environment);
 
-    expect(result.hasException, true);
-    expect(result.exceptions.values.single.exception, isInstanceOf<FileSystemException>());
+    expect(result.hasException, false);
   }));
 
   test('Saves a stamp file with inputs and outputs', () => testbed.run(() async {
diff --git a/packages/flutter_tools/test/general.shard/build_system/depfile_test.dart b/packages/flutter_tools/test/general.shard/build_system/depfile_test.dart
index aa0fd38..30524f5 100644
--- a/packages/flutter_tools/test/general.shard/build_system/depfile_test.dart
+++ b/packages/flutter_tools/test/general.shard/build_system/depfile_test.dart
@@ -26,7 +26,6 @@
   }));
 
   test('Can parse depfile with multiple inputs', () => testbed.run(() {
-    final FileSystem fs = MemoryFileSystem();
     final File depfileSource = fs.file('example.d')..writeAsStringSync('''
 a.txt: b.txt c.txt d.txt
 ''');
@@ -41,7 +40,6 @@
   }));
 
   test('Can parse depfile with multiple outputs', () => testbed.run(() {
-    final FileSystem fs = MemoryFileSystem();
     final File depfileSource = fs.file('example.d')..writeAsStringSync('''
 a.txt c.txt d.txt: b.txt
 ''');
@@ -56,7 +54,6 @@
   }));
 
   test('Can parse depfile with windows file paths', () => testbed.run(() {
-    final FileSystem fs = MemoryFileSystem();
     final File depfileSource = fs.file('example.d')..writeAsStringSync(r'''
 C:\\a.txt: C:\\b.txt
 ''');
@@ -69,7 +66,6 @@
   }));
 
   test('Resillient to weird whitespace', () => testbed.run(() {
-    final FileSystem fs = MemoryFileSystem();
     final File depfileSource = fs.file('example.d')..writeAsStringSync(r'''
 a.txt
   : b.txt    c.txt
@@ -83,7 +79,6 @@
   }));
 
   test('Resillient to duplicate files', () => testbed.run(() {
-    final FileSystem fs = MemoryFileSystem();
     final File depfileSource = fs.file('example.d')..writeAsStringSync(r'''
 a.txt: b.txt b.txt
 ''');
@@ -94,7 +89,6 @@
   }));
 
   test('Resillient to malformed file, missing :', () => testbed.run(() {
-    final FileSystem fs = MemoryFileSystem();
     final File depfileSource = fs.file('example.d')..writeAsStringSync(r'''
 a.text b.txt
 ''');
@@ -103,4 +97,42 @@
     expect(depfile.inputs, isEmpty);
     expect(depfile.outputs, isEmpty);
   }));
+
+  test('Can parse dart2js output format', () => testbed.run(() {
+    final File dart2jsDependencyFile = fs.file('main.dart.js.deps')..writeAsStringSync(r'''
+file:///Users/foo/collection.dart
+file:///Users/foo/algorithms.dart
+file:///Users/foo/canonicalized_map.dart
+''');
+
+    final Depfile depfile = Depfile.parseDart2js(dart2jsDependencyFile, fs.file('foo.dart.js'));
+
+    expect(depfile.inputs.map((File file) => file.path), <String>[
+      fs.path.absolute(fs.path.join('Users', 'foo', 'collection.dart')),
+      fs.path.absolute(fs.path.join('Users', 'foo', 'algorithms.dart')),
+      fs.path.absolute(fs.path.join('Users', 'foo', 'canonicalized_map.dart')),
+    ]);
+    expect(depfile.outputs.single.path, 'foo.dart.js');
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem(style: FileSystemStyle.posix)
+  }));
+
+  test('Can parse handle invalid uri', () => testbed.run(() {
+    final File dart2jsDependencyFile = fs.file('main.dart.js.deps')..writeAsStringSync('''
+file:///Users/foo/collection.dart
+abcdevf
+file:///Users/foo/canonicalized_map.dart
+''');
+
+    final Depfile depfile = Depfile.parseDart2js(dart2jsDependencyFile, fs.file('foo.dart.js'));
+
+    expect(depfile.inputs.map((File file) => file.path), <String>[
+      fs.path.absolute(fs.path.join('Users', 'foo', 'collection.dart')),
+      fs.path.absolute(fs.path.join('Users', 'foo', 'canonicalized_map.dart')),
+    ]);
+    expect(depfile.outputs.single.path, 'foo.dart.js');
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem(style: FileSystemStyle.posix)
+  }));
 }
+
diff --git a/packages/flutter_tools/test/general.shard/build_system/filecache_test.dart b/packages/flutter_tools/test/general.shard/build_system/filecache_test.dart
index ca53e66..92c338a 100644
--- a/packages/flutter_tools/test/general.shard/build_system/filecache_test.dart
+++ b/packages/flutter_tools/test/general.shard/build_system/filecache_test.dart
@@ -81,4 +81,15 @@
     // Does not throw.
     fileCache.persist();
   }));
+
+  test('handles hashing missing files', () => testbed.run(() async {
+    final FileHashStore fileCache = FileHashStore(environment);
+    fileCache.initialize();
+
+    final List<File> results = await fileCache.hashFiles(<File>[fs.file('hello.dart')]);
+
+    expect(results, hasLength(1));
+    expect(results.single.path, 'hello.dart');
+    expect(fileCache.currentHashes, isNot(contains(fs.path.absolute('hello.dart'))));
+  }));
 }
diff --git a/packages/flutter_tools/test/general.shard/build_system/source_test.dart b/packages/flutter_tools/test/general.shard/build_system/source_test.dart
index ebfe180..c4ce82e 100644
--- a/packages/flutter_tools/test/general.shard/build_system/source_test.dart
+++ b/packages/flutter_tools/test/general.shard/build_system/source_test.dart
@@ -41,7 +41,6 @@
   test('configures implicit vs explict correctly', () => testbed.run(() {
     expect(const Source.pattern('{PROJECT_DIR}/foo').implicit, false);
     expect(const Source.pattern('{PROJECT_DIR}/*foo').implicit, true);
-    expect(Source.function((Environment environment) => <File>[]).implicit, true);
     expect(Source.behavior(TestBehavior()).implicit, true);
   }));
 
diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart
index dac418d..a3a7fb9 100644
--- a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart
+++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart
@@ -6,6 +6,7 @@
 import 'package:flutter_tools/src/base/platform.dart';
 import 'package:flutter_tools/src/base/process_manager.dart';
 import 'package:flutter_tools/src/build_system/build_system.dart';
+import 'package:flutter_tools/src/build_system/depfile.dart';
 import 'package:flutter_tools/src/build_system/targets/dart.dart';
 import 'package:flutter_tools/src/build_system/targets/web.dart';
 import 'package:mockito/mockito.dart';
@@ -215,6 +216,25 @@
   }, overrides: <Type, Generator>{
     ProcessManager: () => MockProcessManager(),
   }));
+
+  test('Dart2JSTarget produces expected depfile', () => testbed.run(() async {
+    environment.defines[kBuildMode] = 'release';
+    when(processManager.run(any)).thenAnswer((Invocation invocation) async {
+      environment.buildDir.childFile('main.dart.js.deps')
+        ..writeAsStringSync('file:///a.dart');
+      return FakeProcessResult(exitCode: 0);
+    });
+    await const Dart2JSTarget().build(environment);
+
+    expect(environment.buildDir.childFile('dart2js.d').existsSync(), true);
+    final Depfile depfile = Depfile.parse(environment.buildDir.childFile('dart2js.d'));
+
+    expect(depfile.inputs.single.path, fs.path.absolute('a.dart'));
+    expect(depfile.outputs.single.path,
+      environment.buildDir.childFile('main.dart.js').absolute.path);
+  }, overrides: <Type, Generator>{
+    ProcessManager: () => MockProcessManager(),
+  }));
 }
 
 class MockProcessManager extends Mock implements ProcessManager {}