blob: 91557e7b06bfcc5bc9134142ddf5f5bdf8d1bf4c [file] [log] [blame]
// Copyright 2014 The Flutter 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:meta/meta.dart';
import 'package:mime/mime.dart' as mime;
import 'package:process/process.dart';
import '../../artifacts.dart';
import '../../base/common.dart';
import '../../base/file_system.dart';
import '../../base/io.dart';
import '../../base/logger.dart';
import '../../build_info.dart';
import '../../convert.dart';
import '../../devfs.dart';
import '../build_system.dart';
List<Map<String, Object?>> _getList(Object? object, String errorMessage) {
if (object is List<Object?>) {
return object.cast<Map<String, Object?>>();
throw IconTreeShakerException._(errorMessage);
/// A class that wraps the functionality of the const finder package and the
/// font subset utility to tree shake unused icons from fonts.
class IconTreeShaker {
/// Creates a wrapper for icon font subsetting.
/// The environment parameter must not be null.
/// If the `fontManifest` parameter is null, [enabled] will return false since
/// there are no fonts to shake.
/// The constructor will validate the environment and print a warning if
/// font subsetting has been requested in a debug build mode.
DevFSStringContent? fontManifest, {
required ProcessManager processManager,
required Logger logger,
required FileSystem fileSystem,
required Artifacts artifacts,
required TargetPlatform targetPlatform,
}) : _processManager = processManager,
_logger = logger,
_fs = fileSystem,
_artifacts = artifacts,
_fontManifest = fontManifest?.string,
_targetPlatform = targetPlatform {
if (_environment.defines[kIconTreeShakerFlag] == 'true' &&
_environment.defines[kBuildMode] == 'debug') {
logger.printError('Font subsetting is not supported in debug mode. The '
'--tree-shake-icons flag will be ignored.');
/// The MIME types for supported font sets.
static const Set<String> kTtfMimeTypes = <String>{
'font/ttf', // based on internet search
'application/x-font-ttf', // based on running locally.
/// The [Source] inputs that targets using this should depend on.
/// See [Target.inputs].
static const List<Source> inputs = <Source>[
final Environment _environment;
final String? _fontManifest;
Future<void>? _iconDataProcessing;
Map<String, _IconTreeShakerData>? _iconData;
final ProcessManager _processManager;
final Logger _logger;
final FileSystem _fs;
final Artifacts _artifacts;
final TargetPlatform _targetPlatform;
/// Whether font subsetting should be used for this [Environment].
bool get enabled => _fontManifest != null
&& _environment.defines[kIconTreeShakerFlag] == 'true'
&& _environment.defines[kBuildMode] != 'debug';
// Fills the [_iconData] map.
Future<void> _getIconData(Environment environment) async {
if (!enabled) {
final File appDill = environment.buildDir.childFile('app.dill');
if (!appDill.existsSync()) {
throw IconTreeShakerException._('Expected to find kernel file at ${appDill.path}, but no file found.');
final File constFinder = _fs.file(
final File dart = _fs.file(
final Map<String, List<int>> iconData = await _findConstants(
final Set<String> familyKeys = iconData.keys.toSet();
final Map<String, String> fonts = await _parseFontJson(
_fontManifest!, // Guarded by `enabled`.
if (fonts.length != iconData.length) {
'Expected to find fonts for ${iconData.keys}, 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 '
'the package that would include them, or are missing '
'"uses-material-design: true".',
final Map<String, _IconTreeShakerData> result = <String, _IconTreeShakerData>{};
const int kSpacePoint = 32;
for (final MapEntry<String, String> entry in fonts.entries) {
final List<int>? codePoints = iconData[entry.key];
if (codePoints == null) {
throw IconTreeShakerException._('Expected to font code points for ${entry.key}, but none were found.');
if (_targetPlatform == TargetPlatform.web_javascript) {
if (!codePoints.contains(kSpacePoint)) {
result[entry.value] = _IconTreeShakerData(
family: entry.key,
relativePath: entry.value,
codePoints: codePoints,
_iconData = result;
/// Calls font-subset, which transforms the [input] font file to a
/// subsetted version at [outputPath].
/// If [enabled] is false, or the relative path is not recognized as an icon
/// font used in the Flutter application, this returns false.
/// If the font-subset subprocess fails, it will [throwToolExit].
/// Otherwise, it will return true.
Future<bool> subsetFont({
required File input,
required String outputPath,
required String relativePath,
}) async {
if (!enabled) {
return false;
if (input.lengthSync() < 12) {
return false;
final String? mimeType = mime.lookupMimeType(
headerBytes: await input.openRead(0, 12).first,
if (!kTtfMimeTypes.contains(mimeType)) {
return false;
await (_iconDataProcessing ??= _getIconData(_environment));
assert(_iconData != null);
final _IconTreeShakerData? iconTreeShakerData = _iconData![relativePath];
if (iconTreeShakerData == null) {
return false;
final File fontSubset = _fs.file(
if (!fontSubset.existsSync()) {
throw IconTreeShakerException._('The font-subset utility is missing. Run "flutter doctor".');
final List<String> cmd = <String>[
final String codePoints = iconTreeShakerData.codePoints.join(' ');
_logger.printTrace('Running font-subset: ${cmd.join(' ')}, '
'using codepoints $codePoints');
final Process fontSubsetProcess = await _processManager.start(cmd);
try {
await fontSubsetProcess.stdin.flush();
await fontSubsetProcess.stdin.close();
} on Exception {
// handled by checking the exit code.
final int code = await fontSubsetProcess.exitCode;
if (code != 0) {
_logger.printTrace(await utf8.decodeStream(fontSubsetProcess.stdout));
_logger.printError(await utf8.decodeStream(fontSubsetProcess.stderr));
throw IconTreeShakerException._('Font subsetting failed with exit code $code.');
_logger.printStatus(getSubsetSummaryMessage(input, _fs.file(outputPath)));
return true;
String getSubsetSummaryMessage(File inputFont, File outputFont) {
final String fontName = inputFont.basename;
final double inputSize = inputFont.lengthSync().toDouble();
final double outputSize = outputFont.lengthSync().toDouble();
final double reductionBytes = inputSize - outputSize;
final String reductionPercentage = (reductionBytes / inputSize * 100).toStringAsFixed(1);
return 'Font asset "$fontName" was tree-shaken, reducing it from '
'${inputSize.ceil()} to ${outputSize.ceil()} bytes '
'($reductionPercentage% reduction). Tree-shaking can be disabled '
'by providing the --no-tree-shake-icons flag when building your app.';
/// Returns a map of { fontFamily: relativePath } pairs.
Future<Map<String, String>> _parseFontJson(
String fontManifestData,
Set<String> families,
) async {
final Map<String, String> result = <String, String>{};
final List<Map<String, Object?>> fontList = _getList(
'FontManifest.json invalid: expected top level to be a list of objects.',
for (final Map<String, Object?> map in fontList) {
final Object? familyKey = map['family'];
if (familyKey is! String) {
throw IconTreeShakerException._(
'FontManifest.json invalid: expected the family value to be a string, '
'got: ${map['family']}.');
if (!families.contains(familyKey)) {
final List<Map<String, Object?>> fonts = _getList(
'FontManifest.json invalid: expected "fonts" to be a list of objects.',
if (fonts.length != 1) {
throw IconTreeShakerException._(
'This tool cannot process icon fonts with multiple fonts in a '
'single family.');
final Object? asset = fonts.first['asset'];
if (asset is! String) {
throw IconTreeShakerException._(
'FontManifest.json invalid: expected "asset" value to be a string, '
'got: ${map['assets']}.');
result[familyKey] = asset;
return result;
Future<Map<String, List<int>>> _findConstants(
File dart,
File constFinder,
File appDill,
) async {
final List<String> cmd = <String>[
'--kernel-file', appDill.path,
'--class-library-uri', 'package:flutter/src/widgets/icon_data.dart',
'--class-name', 'IconData',
'--annotation-class-name', '_StaticIconProvider',
'--annotation-class-library-uri', 'package:flutter/src/widgets/icon_data.dart',
_logger.printTrace('Running command: ${cmd.join(' ')}');
final ProcessResult constFinderProcessResult = await;
if (constFinderProcessResult.exitCode != 0) {
throw IconTreeShakerException._('ConstFinder failure: ${constFinderProcessResult.stderr}');
final Object? constFinderMap = json.decode(constFinderProcessResult.stdout as String);
if (constFinderMap is! Map<String, Object?>) {
throw IconTreeShakerException._(
'Invalid ConstFinder output: expected a top level JSON object, '
'got $constFinderMap.');
final _ConstFinderResult constFinderResult = _ConstFinderResult(constFinderMap);
if (constFinderResult.hasNonConstantLocations) {
_logger.printError('This application cannot tree shake icons fonts. '
'It has non-constant instances of IconData at the '
'following locations:', emphasis: true);
for (final Map<String, Object?> location in constFinderResult.nonConstantLocations) {
'- ${location['file']}:${location['line']}:${location['column']}',
indent: 2,
hangingIndent: 4,
throwToolExit('Avoid non-constant invocations of IconData or try to '
'build again with --no-tree-shake-icons.');
return _parseConstFinderResult(constFinderResult);
Map<String, List<int>> _parseConstFinderResult(_ConstFinderResult constants) {
final Map<String, List<int>> result = <String, List<int>>{};
for (final Map<String, Object?> iconDataMap in constants.constantInstances) {
final Object? package = iconDataMap['fontPackage'];
final Object? fontFamily = iconDataMap['fontFamily'];
final Object? codePoint = iconDataMap['codePoint'];
if ((package ?? '') is! String || // Null is ok here.
fontFamily is! String ||
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, '
'got: $iconDataMap.');
final String family = fontFamily;
final String key = package == null
? family
: 'packages/$package/$family';
result[key] ??= <int>[];
return result;
class _ConstFinderResult {
final Map<String, Object?> result;
late final List<Map<String, Object?>> constantInstances = _getList(
'Invalid ConstFinder output: Expected "constInstances" to be a list of objects.',
late final List<Map<String, Object?>> nonConstantLocations = _getList(
'Invalid ConstFinder output: Expected "nonConstLocations" to be a list of objects',
bool get hasNonConstantLocations => nonConstantLocations.isNotEmpty;
/// The font family name, relative path to font file, and list of code points
/// the application is using.
class _IconTreeShakerData {
/// All parameters are required.
const _IconTreeShakerData({
required this.relativePath,
required this.codePoints,
/// The font family name, e.g. "MaterialIcons".
final String family;
/// The relative path to the font file.
final String relativePath;
/// The list of code points for the font.
final List<int> codePoints;
String toString() => 'FontSubsetData($family, $relativePath, $codePoints)';
class IconTreeShakerException implements Exception {
final String message;
String toString() => 'IconTreeShakerException: $message\n\n'
'To disable icon tree shaking, pass --no-tree-shake-icons to the requested '
'flutter build command';