[icon_tree_shaker] Tree-shake material and cupertino with 0 icons (#190905)
Bug:
* https://github.com/flutter/flutter/issues/190902
Ensures built-in fonts are tree-shaken if 0 icons are used.
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 3f7356a..4e66092 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
@@ -141,9 +141,12 @@
familyKeys,
);
- if (fonts.length != iconData.length) {
+ final Set<String> missingFonts = iconData.keys
+ .where((String key) => !fonts.containsKey(key))
+ .toSet();
+ if (missingFonts.isNotEmpty) {
environment.logger.printStatus(
- 'Expected to find fonts for ${iconData.keys}, but found '
+ 'Expected to find fonts for $missingFonts, but found '
'${fonts.keys}. This usually means you are referring to '
'font families in an IconData class but not including them '
'in the assets section of your pubspec.yaml, are missing '
@@ -155,7 +158,9 @@
final result = <String, _IconTreeShakerData>{};
const kSpacePoint = 32;
for (final MapEntry(:key, :value) in fonts.entries) {
- final List<int>? codePoints = iconData[key];
+ final int? fallbackCodePoint = _kKnownIconFontFallbackCodePoints[key];
+ final List<int>? codePoints =
+ iconData[key] ?? (fallbackCodePoint != null ? <int>[fallbackCodePoint] : null);
if (codePoints == null) {
throw IconTreeShakerException._(
'Expected to font code points for $key, but none were found.',
@@ -269,6 +274,14 @@
'by providing the --no-tree-shake-icons flag when building your app.';
}
+ /// Known icon font families that should be subsetted even if 0 icons are recorded.
+ /// Subsetting unused icon fonts to a single dummy icon ensures that unused fonts
+ /// are not bundled in their entirety.
+ static const Map<String, int> _kKnownIconFontFallbackCodePoints = <String, int>{
+ 'MaterialIcons': 57415, // 0xe047, Icons.add
+ 'packages/cupertino_icons/CupertinoIcons': 62418, // 0xf3d2, CupertinoIcons.chevron_left
+ };
+
/// Returns a map of { fontFamily: relativePath } pairs.
Future<Map<String, String>> _parseFontJson(String fontManifestData, Set<String> families) async {
final result = <String, String>{};
@@ -285,7 +298,8 @@
'got: ${map['family']}.',
);
}
- if (!families.contains(familyKey)) {
+ if (!families.contains(familyKey) &&
+ !_kKnownIconFontFallbackCodePoints.containsKey(familyKey)) {
continue;
}
final List<Map<String, Object?>> fonts = _getList(
diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/icon_tree_shaker_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/icon_tree_shaker_test.dart
index 6e66155..d8391d6 100644
--- a/packages/flutter_tools/test/general.shard/build_system/targets/icon_tree_shaker_test.dart
+++ b/packages/flutter_tools/test/general.shard/build_system/targets/icon_tree_shaker_test.dart
@@ -532,7 +532,12 @@
targetPlatform: TargetPlatform.android,
);
+ final stdinSink = CompleterIOSink();
writeRecordedUsesFile(appDill.path, content: emptyRecordedUsesResult);
+ resetFontSubsetInvocation(stdinSink: stdinSink);
+ fileSystem.file(outputPath)
+ ..createSync(recursive: true)
+ ..writeAsBytesSync(List<int>.filled(1200, 0));
// Does not throw
await iconTreeShaker.subsetFont(
input: fileSystem.file(inputPath),
@@ -540,6 +545,7 @@
relativePath: relativePath,
);
+ expect(stdinSink.getAndClear(), '57415\n');
expect(
logger.traceText,
contains(
@@ -913,6 +919,114 @@
expect(stdin, contains('59470'));
expect(processManager, hasNoRemainingExpectations);
});
+
+ testWithoutContext('Subsets unused CupertinoIcons font to fallback code point', () async {
+ final Environment environment = createEnvironment(<String, String>{
+ kIconTreeShakerFlag: 'true',
+ kBuildMode: 'release',
+ });
+ final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true);
+
+ const cupertinoFontPath = 'packages/cupertino_icons/assets/CupertinoIcons.ttf';
+ const cupertinoManifestJson =
+ '''
+[
+ {
+ "family": "packages/cupertino_icons/CupertinoIcons",
+ "fonts": [
+ {
+ "asset": "$cupertinoFontPath"
+ }
+ ]
+ }
+]
+''';
+ fontManifestContent = DevFSStringContent(cupertinoManifestJson);
+
+ final iconTreeShaker = IconTreeShaker(
+ environment,
+ fontManifestContent,
+ logger: logger,
+ processManager: processManager,
+ fileSystem: fileSystem,
+ artifacts: artifacts,
+ targetPlatform: TargetPlatform.android,
+ );
+
+ // Empty recordings (0 icons used)
+ writeRecordedUsesFile(appDill.path, content: emptyRecordedUsesResult);
+
+ final stdinSink = CompleterIOSink();
+ fontSubsetArgs = <String>[fontSubsetPath, outputPath, inputPath];
+ resetFontSubsetInvocation(stdinSink: stdinSink);
+
+ final File inputFont = fileSystem.file(inputPath)..writeAsBytesSync(List<int>.filled(2500, 0));
+ fileSystem.file(outputPath)
+ ..createSync(recursive: true)
+ ..writeAsBytesSync(List<int>.filled(1200, 0));
+
+ expect(
+ await iconTreeShaker.subsetFont(
+ input: inputFont,
+ outputPath: outputPath,
+ relativePath: cupertinoFontPath,
+ ),
+ true,
+ );
+
+ expect(stdinSink.getAndClear(), '62418\n');
+ expect(processManager, hasNoRemainingExpectations);
+ });
+
+ testWithoutContext('Does not subset unused non-icon font', () async {
+ final Environment environment = createEnvironment(<String, String>{
+ kIconTreeShakerFlag: 'true',
+ kBuildMode: 'release',
+ });
+ final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true);
+
+ const customFontPath = 'fonts/Roboto-Regular.ttf';
+ const customManifestJson =
+ '''
+[
+ {
+ "family": "Roboto",
+ "fonts": [
+ {
+ "asset": "$customFontPath"
+ }
+ ]
+ }
+]
+''';
+ fontManifestContent = DevFSStringContent(customManifestJson);
+
+ final iconTreeShaker = IconTreeShaker(
+ environment,
+ fontManifestContent,
+ logger: logger,
+ processManager: processManager,
+ fileSystem: fileSystem,
+ artifacts: artifacts,
+ targetPlatform: TargetPlatform.android,
+ );
+
+ // Empty recordings (0 icons used)
+ writeRecordedUsesFile(appDill.path, content: emptyRecordedUsesResult);
+
+ final File inputFont = fileSystem.file(inputPath)..writeAsBytesSync(List<int>.filled(2500, 0));
+
+ expect(
+ await iconTreeShaker.subsetFont(
+ input: inputFont,
+ outputPath: outputPath,
+ relativePath: customFontPath,
+ ),
+ false,
+ );
+
+ expect(processManager, hasNoRemainingExpectations);
+ });
}
const Library iconDataLibrary = Library('package:flutter/src/widgets/icon_data.dart');