blob: b401ec969d3a208dfd3704aadefeb604169ef707 [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:process/process.dart';
import '../../artifacts.dart';
import '../../base/common.dart';
import '../../base/file_system.dart';
import '../../base/io.dart';
import '../../base/logger.dart';
import '../../convert.dart';
import '../../devfs.dart';
import '../build_system.dart';
import 'dart.dart';
/// The build define controlling whether icon fonts should be stripped down to
/// only the glyphs used by the application.
const String kIconTreeShakerFlag = 'TreeShakeIcons';
/// Whether icon font subsetting is enabled by default.
const bool kIconTreeShakerEnabledDefault = false;
List<Map<String, dynamic>> _getList(dynamic object, String errorMessage) {
try {
return (object as List<dynamic>).cast<Map<String, dynamic>>();
} on CastError catch (_) {
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,
}) : assert(_environment != null),
assert(processManager != null),
assert(logger != null),
assert(fileSystem != null),
assert(artifacts != null),
_processManager = processManager,
_logger = logger,
_fs = fileSystem,
_artifacts = artifacts,
_fontManifest = fontManifest?.string {
if (_environment.defines[kIconTreeShakerFlag] == 'true' &&
_environment.defines[kBuildMode] == 'debug') {
logger.printError('Font subetting is not supported in debug mode. The '
'--tree-shake-icons flag will be ignored.');
/// 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;
Map<String, _IconTreeShakerData> _iconData;
final ProcessManager _processManager;
final Logger _logger;
final FileSystem _fs;
final Artifacts _artifacts;
/// 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<Map<String, _IconTreeShakerData>> _getIconData(Environment environment) async {
if (!enabled) {
return null;
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(
if (fonts.length != iconData.length) {
throwToolExit('Expected to find fonts for ${iconData.keys}, but found '
'${fonts.keys}. This usually means you are refering 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>{};
for (final MapEntry<String, String> entry in fonts.entries) {
result[entry.value] = _IconTreeShakerData(
family: entry.key,
relativePath: entry.value,
codePoints: iconData[entry.key],
return result;
/// Calls font-subset, which transforms the `inputPath` font file to a
/// subsetted version at `outputPath`.
/// The `relativePath` parameter
/// All parameters are required.
/// 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 String inputPath,
@required String outputPath,
@required String relativePath,
}) async {
if (!enabled) {
return false;
_iconData ??= await _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 catch (_) {
// handled by checking the exit code.
} on OSError catch (_) {
// 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.');
return true;
/// Returns a map of { fontFamly: relativePath } pairs.
Future<Map<String, String>> _parseFontJson(
String fontManifestData,
Set<String> families,
) async {
final Map<String, String> result = <String, String>{};
final List<Map<String, dynamic>> fontList = _getList(
'FontManifest.json invalid: expected top level to be a list of objects.',
for (final Map<String, dynamic> map in fontList) {
if (map['family'] is! String) {
throw IconTreeShakerException._(
'FontManifest.json invalid: expected the family value to be a string, '
'got: ${map['family']}.');
final String familyKey = map['family'] as String;
if (!families.contains(familyKey)) {
final List<Map<String, dynamic>> 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.');
if (fonts.first['asset'] is! String) {
throw IconTreeShakerException._(
'FontManifest.json invalid: expected "asset" value to be a string, '
'got: ${map['assets']}.');
result[familyKey] = fonts.first['asset'] as String;
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',
_logger.printTrace('Running command: ${cmd.join(' ')}');
final ProcessResult constFinderProcessResult = await;
if (constFinderProcessResult.exitCode != 0) {
throw IconTreeShakerException._('ConstFinder failure: ${constFinderProcessResult.stderr}');
final dynamic jsonDecode = json.decode(constFinderProcessResult.stdout as String);
if (jsonDecode is! Map<String, dynamic>) {
throw IconTreeShakerException._(
'Invalid ConstFinder output: expected a top level JSON object, '
'got $jsonDecode.');
final Map<String, dynamic> constFinderMap = jsonDecode as Map<String, dynamic>;
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, dynamic> 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 consts) {
final Map<String, List<int>> result = <String, List<int>>{};
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) {
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 package = iconDataMap['fontPackage'] as String;
final String family = iconDataMap['fontFamily'] as String;
final String key = package == null
? family
: 'packages/$package/$family';
result[key] ??= <int>[];
result[key].add(iconDataMap['codePoint'] as int);
return result;
class _ConstFinderResult {
final Map<String, dynamic> result;
List<Map<String, dynamic>> _constantInstances;
List<Map<String, dynamic>> get constantInstances {
_constantInstances ??= _getList(
'Invalid ConstFinder output: Expected "constInstances" to be a list of objects.',
return _constantInstances;
List<Map<String, dynamic>> _nonConstantLocations;
List<Map<String, dynamic>> get nonConstantLocations {
_nonConstantLocations ??= _getList(
'Invalid ConstFinder output: Expected "nonConstLocations" to be a list ofobjects',
return _nonConstantLocations;
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,
}) : assert(family != null),
assert(relativePath != null),
assert(codePoints != null);
/// 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() => 'FontSubset error: $message';