[flutter_tools] Reland: supports tree-shake-icons for web builds (#52054)


diff --git a/.cirrus.yml b/.cirrus.yml
index 9b39129..5b86a63 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -184,7 +184,15 @@
         - bash <(curl -s https://codecov.io/bash) -c -f packages/flutter_tools/coverage/lcov.info -F flutter_tool
 
     - name: web_integration_tests
-      << : *WEB_SHARD_TEMPLATE
+      only_if: "changesInclude('.cirrus.yml', 'dev/**', 'packages/flutter/**', 'packages/flutter_test/**', 'packages/flutter_tools/**', 'packages/flutter_web_plugins/**', 'bin/internal/**') || $CIRRUS_PR == ''"
+      environment:
+        # As of October 2019, the Web shards needed more than 6G of RAM.
+        CPU: 2
+        MEMORY: 8G
+        CHROME_NO_SANDBOX: true
+        GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095]
+      script:
+        - dart --enable-asserts ./dev/bots/test.dart
 
     - name: web_tests-0-linux
       << : *WEB_SHARD_TEMPLATE
diff --git a/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart b/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart
index b401ec9..8e85481 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart
@@ -289,7 +289,7 @@
     for (final Map<String, dynamic> iconDataMap in consts.constantInstances) {
       if ((iconDataMap['fontPackage'] ?? '') is! String || // Null is ok here.
            iconDataMap['fontFamily'] is! String ||
-           iconDataMap['codePoint'] is! int) {
+           iconDataMap['codePoint'] is! num) {
         throw IconTreeShakerException._(
           'Invalid ConstFinder result. Expected "fontPackage" to be a String, '
           '"fontFamily" to be a String, and "codePoint" to be an int, '
@@ -301,7 +301,7 @@
         ? family
         : 'packages/$package/$family';
       result[key] ??= <int>[];
-      result[key].add(iconDataMap['codePoint'] as int);
+      result[key].add((iconDataMap['codePoint'] as num).round());
     }
     return result;
   }
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 79618de..01c1c31 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/web.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/web.dart
@@ -156,9 +156,26 @@
     final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
     final String specPath = globals.fs.path.join(globals.artifacts.getArtifactPath(Artifact.flutterWebSdk), 'libraries.json');
     final String packageFile = PackageMap.globalPackagesPath;
+    final File outputKernel = environment.buildDir.childFile('app.dill');
     final File outputFile = environment.buildDir.childFile('main.dart.js');
+    final List<String> dartDefines = parseDartDefines(environment);
 
-    final ProcessResult result = await globals.processManager.run(<String>[
+    // Run the dart2js compilation in two stages, so that icon tree shaking can
+    // parse the kernel file for web builds.
+    final ProcessResult kernelResult = await globals.processManager.run(<String>[
+      globals.artifacts.getArtifactPath(Artifact.engineDartBinary),
+      globals.artifacts.getArtifactPath(Artifact.dart2jsSnapshot),
+      '--libraries-spec=$specPath',
+      '-o',
+      outputKernel.path,
+      '--packages=$packageFile',
+      '--cfe-only',
+      environment.buildDir.childFile('main.dart').path,
+    ]);
+    if (kernelResult.exitCode != 0) {
+      throw Exception(kernelResult.stdout + kernelResult.stderr);
+    }
+    final ProcessResult javaScriptResult = await globals.processManager.run(<String>[
       globals.artifacts.getArtifactPath(Artifact.engineDartBinary),
       globals.artifacts.getArtifactPath(Artifact.dart2jsSnapshot),
       '--libraries-spec=$specPath',
@@ -167,25 +184,24 @@
       else
         '-O4',
       if (buildMode == BuildMode.profile)
-        '--no-minify',
-      '-o',
-      outputFile.path,
-      '--packages=$packageFile',
-      if (buildMode == BuildMode.profile)
         '-Ddart.vm.profile=true'
       else
         '-Ddart.vm.product=true',
+      for (final String dartDefine in dartDefines)
+        '-D$dartDefine',
+      if (buildMode == BuildMode.profile)
+        '--no-minify',
       if (csp)
         '--csp',
-      for (final String dartDefine in parseDartDefines(environment))
-        '-D$dartDefine',
-      environment.buildDir.childFile('main.dart').path,
+      '-o',
+      outputFile.path,
+      environment.buildDir.childFile('app.dill').path,
     ]);
-    if (result.exitCode != 0) {
-      throw Exception(result.stdout + result.stderr);
+    if (javaScriptResult.exitCode != 0) {
+      throw Exception(javaScriptResult.stdout + javaScriptResult.stderr);
     }
     final File dart2jsDeps = environment.buildDir
-      .childFile('main.dart.js.deps');
+      .childFile('app.dill.deps');
     if (!dart2jsDeps.existsSync()) {
       globals.printError('Warning: dart2js did not produced expected deps list at '
         '${dart2jsDeps.path}');
@@ -197,7 +213,7 @@
       platform: globals.platform,
     );
     final Depfile depfile = depfileService.parseDart2js(
-      environment.buildDir.childFile('main.dart.js.deps'),
+      environment.buildDir.childFile('app.dill.deps'),
       outputFile,
     );
     depfileService.writeToFile(
diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart
index 15a1f6e..6d8acc5 100644
--- a/packages/flutter_tools/lib/src/web/compile.dart
+++ b/packages/flutter_tools/lib/src/web/compile.dart
@@ -11,6 +11,7 @@
 import '../build_info.dart';
 import '../build_system/build_system.dart';
 import '../build_system/targets/dart.dart';
+import '../build_system/targets/icon_tree_shaker.dart';
 import '../build_system/targets/web.dart';
 import '../convert.dart';
 import '../globals.dart' as globals;
@@ -52,8 +53,7 @@
         kHasWebPlugins: hasWebPlugins.toString(),
         kDartDefines: jsonEncode(dartDefines),
         kCspMode: csp.toString(),
-        // TODO(dnfield): Enable font subset. We need to get a kernel file to do
-        // that. https://github.com/flutter/flutter/issues/49730
+        kIconTreeShakerFlag: buildInfo.treeShakeIcons.toString(),
       },
     ));
     if (!result.success) {
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 3e8002b7..f20b296 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
@@ -16,35 +16,38 @@
 import 'package:platform/platform.dart';
 
 import '../../../src/common.dart';
+import '../../../src/context.dart';
 import '../../../src/mocks.dart';
 import '../../../src/testbed.dart';
 
+const List<String> kDart2jsLinuxArgs = <String>[
+  'bin/cache/dart-sdk/bin/dart',
+  'bin/cache/dart-sdk/bin/snapshots/dart2js.dart.snapshot',
+  '--libraries-spec=bin/cache/flutter_web_sdk/libraries.json',
+];
+
 void main() {
   Testbed testbed;
   Environment environment;
-  MockPlatform mockPlatform;
-  MockPlatform  mockWindowsPlatform;
+  FakeProcessManager processManager;
+  final Platform linux = FakePlatform(
+    operatingSystem: 'linux',
+    environment: <String, String>{},
+  );
+  final Platform windows = FakePlatform(
+    operatingSystem: 'windows',
+    environment: <String, String>{},
+  );
   DepfileService depfileService;
 
   setUp(() {
-    mockPlatform = MockPlatform();
-    mockWindowsPlatform = MockPlatform();
-
-    when(mockPlatform.isWindows).thenReturn(false);
-    when(mockPlatform.isMacOS).thenReturn(true);
-    when(mockPlatform.isLinux).thenReturn(false);
-    when(mockPlatform.environment).thenReturn(const <String, String>{});
-
-    when(mockWindowsPlatform.isWindows).thenReturn(true);
-    when(mockWindowsPlatform.isMacOS).thenReturn(false);
-    when(mockWindowsPlatform.isLinux).thenReturn(false);
-
     testbed = Testbed(setup: () {
       final File packagesFile = globals.fs.file(globals.fs.path.join('foo', '.packages'))
         ..createSync(recursive: true)
         ..writeAsStringSync('foo:lib/\n');
       PackageMap.globalPackagesPath = packagesFile.path;
       globals.fs.currentDirectory.childDirectory('bar').createSync();
+      processManager = FakeProcessManager.list(<FakeCommand>[]);
 
       environment = Environment.test(
         globals.fs.currentDirectory,
@@ -61,7 +64,7 @@
     );
       environment.buildDir.createSync(recursive: true);
     }, overrides: <Type, Generator>{
-      Platform: () => mockPlatform,
+      Platform: () => linux,
     });
   });
 
@@ -156,7 +159,7 @@
     // Import.
     expect(generated, contains("import 'package:foo/main.dart' as entrypoint;"));
   }, overrides: <Type, Generator>{
-    Platform: () => mockWindowsPlatform,
+    Platform: () => windows,
   }));
 
   test('WebEntrypointTarget generates an entrypoint without plugins and init platform', () => testbed.run(() async {
@@ -216,111 +219,131 @@
   test('Dart2JSTarget calls dart2js with expected args with csp', () => testbed.run(() async {
     environment.defines[kBuildMode] = 'profile';
     environment.defines[kCspMode] = 'true';
-    when(globals.processManager.run(any)).thenAnswer((Invocation invocation) async {
-      return FakeProcessResult(exitCode: 0);
-    });
-    await const Dart2JSTarget().build(environment);
+    processManager.addCommand(FakeCommand(
+      command: <String>[
+        ...kDart2jsLinuxArgs,
+        '-o',
+        environment.buildDir.childFile('app.dill').absolute.path,
+         '--packages=${globals.fs.path.join('foo', '.packages')}',
+        '--cfe-only',
+        environment.buildDir.childFile('main.dart').absolute.path,
+      ]
+    ));
+    processManager.addCommand(FakeCommand(
+      command: <String>[
+        ...kDart2jsLinuxArgs,
+        '-O4',
+        '-Ddart.vm.profile=true',
+        '--no-minify',
+        '--csp',
+        '-o',
+        environment.buildDir.childFile('main.dart.js').absolute.path,
+        environment.buildDir.childFile('app.dill').absolute.path,
+      ]
+    ));
 
-    final List<String> expected = <String>[
-      globals.fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart'),
-      globals.fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'snapshots', 'dart2js.dart.snapshot'),
-      '--libraries-spec=' + globals.fs.path.join('bin', 'cache', 'flutter_web_sdk', 'libraries.json'),
-      '-O4', // highest optimizations
-      '--no-minify', // but uses unminified names for debugging
-      '-o',
-      environment.buildDir.childFile('main.dart.js').absolute.path,
-      '--packages=${globals.fs.path.join('foo', '.packages')}',
-      '-Ddart.vm.profile=true',
-      '--csp',
-      environment.buildDir.childFile('main.dart').absolute.path,
-    ];
-    verify(globals.processManager.run(expected)).called(1);
+    await const Dart2JSTarget().build(environment);
   }, overrides: <Type, Generator>{
-    ProcessManager: () => MockProcessManager(),
+    ProcessManager: () => processManager,
   }));
 
 
   test('Dart2JSTarget calls dart2js with expected args in profile mode', () => testbed.run(() async {
     environment.defines[kBuildMode] = 'profile';
-    when(globals.processManager.run(any)).thenAnswer((Invocation invocation) async {
-      return FakeProcessResult(exitCode: 0);
-    });
-    await const Dart2JSTarget().build(environment);
+    processManager.addCommand(FakeCommand(
+      command: <String>[
+        ...kDart2jsLinuxArgs,
+        '-o',
+        environment.buildDir.childFile('app.dill').absolute.path,
+         '--packages=${globals.fs.path.join('foo', '.packages')}',
+        '--cfe-only',
+        environment.buildDir.childFile('main.dart').absolute.path,
+      ]
+    ));
+    processManager.addCommand(FakeCommand(
+      command: <String>[
+        ...kDart2jsLinuxArgs,
+        '-O4',
+        '-Ddart.vm.profile=true',
+        '--no-minify',
+        '-o',
+        environment.buildDir.childFile('main.dart.js').absolute.path,
+        environment.buildDir.childFile('app.dill').absolute.path,
+      ]
+    ));
 
-    final List<String> expected = <String>[
-      globals.fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart'),
-      globals.fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'snapshots', 'dart2js.dart.snapshot'),
-      '--libraries-spec=' + globals.fs.path.join('bin', 'cache', 'flutter_web_sdk', 'libraries.json'),
-      '-O4', // highest optimizations
-      '--no-minify', // but uses unminified names for debugging
-      '-o',
-      environment.buildDir.childFile('main.dart.js').absolute.path,
-      '--packages=${globals.fs.path.join('foo', '.packages')}',
-      '-Ddart.vm.profile=true',
-      environment.buildDir.childFile('main.dart').absolute.path,
-    ];
-    verify(globals.processManager.run(expected)).called(1);
+    await const Dart2JSTarget().build(environment);
   }, overrides: <Type, Generator>{
-    ProcessManager: () => MockProcessManager(),
+    ProcessManager: () => processManager,
   }));
 
   test('Dart2JSTarget calls dart2js with expected args in release mode', () => testbed.run(() async {
     environment.defines[kBuildMode] = 'release';
-    when(globals.processManager.run(any)).thenAnswer((Invocation invocation) async {
-      return FakeProcessResult(exitCode: 0);
-    });
-    await const Dart2JSTarget().build(environment);
+    processManager.addCommand(FakeCommand(
+      command: <String>[
+        ...kDart2jsLinuxArgs,
+        '-o',
+        environment.buildDir.childFile('app.dill').absolute.path,
+         '--packages=${globals.fs.path.join('foo', '.packages')}',
+        '--cfe-only',
+        environment.buildDir.childFile('main.dart').absolute.path,
+      ]
+    ));
+    processManager.addCommand(FakeCommand(
+      command: <String>[
+        ...kDart2jsLinuxArgs,
+        '-O4',
+        '-Ddart.vm.product=true',
+        '-o',
+        environment.buildDir.childFile('main.dart.js').absolute.path,
+        environment.buildDir.childFile('app.dill').absolute.path,
+      ]
+    ));
 
-    final List<String> expected = <String>[
-      globals.fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart'),
-      globals.fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'snapshots', 'dart2js.dart.snapshot'),
-      '--libraries-spec=' + globals.fs.path.join('bin', 'cache', 'flutter_web_sdk', 'libraries.json'),
-      '-O4', // highest optimizations.
-      '-o',
-      environment.buildDir.childFile('main.dart.js').absolute.path,
-      '--packages=${globals.fs.path.join('foo', '.packages')}',
-      '-Ddart.vm.product=true',
-      environment.buildDir.childFile('main.dart').absolute.path,
-    ];
-    verify(globals.processManager.run(expected)).called(1);
+    await const Dart2JSTarget().build(environment);
   }, overrides: <Type, Generator>{
-    ProcessManager: () => MockProcessManager(),
+    ProcessManager: () => processManager,
   }));
 
   test('Dart2JSTarget calls dart2js with expected args in release with dart2js optimization override', () => testbed.run(() async {
     environment.defines[kBuildMode] = 'release';
     environment.defines[kDart2jsOptimization] = 'O3';
-    when(globals.processManager.run(any)).thenAnswer((Invocation invocation) async {
-      return FakeProcessResult(exitCode: 0);
-    });
-    await const Dart2JSTarget().build(environment);
+    processManager.addCommand(FakeCommand(
+      command: <String>[
+        ...kDart2jsLinuxArgs,
+        '-o',
+        environment.buildDir.childFile('app.dill').absolute.path,
+         '--packages=${globals.fs.path.join('foo', '.packages')}',
+        '--cfe-only',
+        environment.buildDir.childFile('main.dart').absolute.path,
+      ]
+    ));
+    processManager.addCommand(FakeCommand(
+      command: <String>[
+        ...kDart2jsLinuxArgs,
+        '-O3',
+        '-Ddart.vm.product=true',
+        '-o',
+        environment.buildDir.childFile('main.dart.js').absolute.path,
+        environment.buildDir.childFile('app.dill').absolute.path,
+      ]
+    ));
 
-    final List<String> expected = <String>[
-      globals.fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart'),
-      globals.fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'snapshots', 'dart2js.dart.snapshot'),
-      '--libraries-spec=' + globals.fs.path.join('bin', 'cache', 'flutter_web_sdk', 'libraries.json'),
-      '-O3', // configured optimizations.
-      '-o',
-      environment.buildDir.childFile('main.dart.js').absolute.path,
-      '--packages=${globals.fs.path.join('foo', '.packages')}',
-      '-Ddart.vm.product=true',
-      environment.buildDir.childFile('main.dart').absolute.path,
-    ];
-    verify(globals.processManager.run(expected)).called(1);
+    await const Dart2JSTarget().build(environment);
   }, overrides: <Type, Generator>{
-    ProcessManager: () => MockProcessManager(),
+    ProcessManager: () => processManager,
   }));
 
   test('Dart2JSTarget produces expected depfile', () => testbed.run(() async {
     environment.defines[kBuildMode] = 'release';
     when(globals.processManager.run(any)).thenAnswer((Invocation invocation) async {
-      environment.buildDir.childFile('main.dart.js.deps')
+      environment.buildDir.childFile('app.dill.deps')
         .writeAsStringSync('file:///a.dart');
       return FakeProcessResult(exitCode: 0);
     });
     await const Dart2JSTarget().build(environment);
 
-    expect(environment.buildDir.childFile('dart2js.d').existsSync(), true);
+    expect(environment.buildDir.childFile('dart2js.d'), exists);
     final Depfile depfile = depfileService.parse(environment.buildDir.childFile('dart2js.d'));
 
     expect(depfile.inputs.single.path, globals.fs.path.absolute('a.dart'));
@@ -333,54 +356,64 @@
   test('Dart2JSTarget calls dart2js with Dart defines in release mode', () => testbed.run(() async {
     environment.defines[kBuildMode] = 'release';
     environment.defines[kDartDefines] = '["FOO=bar","BAZ=qux"]';
-    when(globals.processManager.run(any)).thenAnswer((Invocation invocation) async {
-      return FakeProcessResult(exitCode: 0);
-    });
-    await const Dart2JSTarget().build(environment);
+    processManager.addCommand(FakeCommand(
+      command: <String>[
+        ...kDart2jsLinuxArgs,
+        '-o',
+        environment.buildDir.childFile('app.dill').absolute.path,
+         '--packages=${globals.fs.path.join('foo', '.packages')}',
+        '--cfe-only',
+        environment.buildDir.childFile('main.dart').absolute.path,
+      ]
+    ));
+    processManager.addCommand(FakeCommand(
+      command: <String>[
+        ...kDart2jsLinuxArgs,
+        '-O4',
+        '-Ddart.vm.product=true',
+        '-DFOO=bar',
+        '-DBAZ=qux',
+        '-o',
+        environment.buildDir.childFile('main.dart.js').absolute.path,
+        environment.buildDir.childFile('app.dill').absolute.path,
+      ]
+    ));
 
-    final List<String> expected = <String>[
-      globals.fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart'),
-      globals.fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'snapshots', 'dart2js.dart.snapshot'),
-      '--libraries-spec=' + globals.fs.path.join('bin', 'cache', 'flutter_web_sdk', 'libraries.json'),
-      '-O4',
-      '-o',
-      environment.buildDir.childFile('main.dart.js').absolute.path,
-      '--packages=${globals.fs.path.join('foo', '.packages')}',
-      '-Ddart.vm.product=true',
-      '-DFOO=bar',
-      '-DBAZ=qux',
-      environment.buildDir.childFile('main.dart').absolute.path,
-    ];
-    verify(globals.processManager.run(expected)).called(1);
+    await const Dart2JSTarget().build(environment);
   }, overrides: <Type, Generator>{
-    ProcessManager: () => MockProcessManager(),
+    ProcessManager: () => processManager,
   }));
 
   test('Dart2JSTarget calls dart2js with Dart defines in profile mode', () => testbed.run(() async {
     environment.defines[kBuildMode] = 'profile';
     environment.defines[kDartDefines] = '["FOO=bar","BAZ=qux"]';
-    when(globals.processManager.run(any)).thenAnswer((Invocation invocation) async {
-      return FakeProcessResult(exitCode: 0);
-    });
-    await const Dart2JSTarget().build(environment);
+    processManager.addCommand(FakeCommand(
+      command: <String>[
+        ...kDart2jsLinuxArgs,
+        '-o',
+        environment.buildDir.childFile('app.dill').absolute.path,
+         '--packages=${globals.fs.path.join('foo', '.packages')}',
+        '--cfe-only',
+        environment.buildDir.childFile('main.dart').absolute.path,
+      ]
+    ));
+    processManager.addCommand(FakeCommand(
+      command: <String>[
+        ...kDart2jsLinuxArgs,
+        '-O4',
+        '-Ddart.vm.profile=true',
+        '-DFOO=bar',
+        '-DBAZ=qux',
+        '--no-minify',
+        '-o',
+        environment.buildDir.childFile('main.dart.js').absolute.path,
+        environment.buildDir.childFile('app.dill').absolute.path,
+      ]
+    ));
 
-    final List<String> expected = <String>[
-      globals.fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart'),
-      globals.fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'snapshots', 'dart2js.dart.snapshot'),
-      '--libraries-spec=' + globals.fs.path.join('bin', 'cache', 'flutter_web_sdk', 'libraries.json'),
-      '-O4',
-      '--no-minify',
-      '-o',
-      environment.buildDir.childFile('main.dart.js').absolute.path,
-      '--packages=${globals.fs.path.join('foo', '.packages')}',
-      '-Ddart.vm.profile=true',
-      '-DFOO=bar',
-      '-DBAZ=qux',
-      environment.buildDir.childFile('main.dart').absolute.path,
-    ];
-    verify(globals.processManager.run(expected)).called(1);
+    await const Dart2JSTarget().build(environment);
   }, overrides: <Type, Generator>{
-    ProcessManager: () => MockProcessManager(),
+    ProcessManager: () => processManager,
   }));
 
   test('Dart2JSTarget throws developer-friendly exception on misformatted DartDefines', () => testbed.run(() async {
@@ -427,4 +460,3 @@
 }
 
 class MockProcessManager extends Mock implements ProcessManager {}
-class MockPlatform extends Mock implements Platform {}