1// Copyright 2018 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6 7import 'package:crypto/crypto.dart' show md5; 8import 'package:meta/meta.dart'; 9import 'package:quiver/core.dart' show hash2; 10 11import '../convert.dart' show json; 12import '../globals.dart'; 13import '../version.dart'; 14import 'file_system.dart'; 15import 'platform.dart'; 16 17typedef FingerprintPathFilter = bool Function(String path); 18 19/// Whether to completely disable build caching. 20/// 21/// This is done by always returning false from fingerprinter invocations. This 22/// is safe to do generally, because fingerprinting is only a performance 23/// improvement. 24bool get _disableBuildCache => platform.environment['DISABLE_FLUTTER_BUILD_CACHE']?.toLowerCase() == 'true'; 25 26/// A tool that can be used to compute, compare, and write [Fingerprint]s for a 27/// set of input files and associated build settings. 28/// 29/// This class can be used during build actions to compute a fingerprint of the 30/// build action inputs and options, and if unchanged from the previous build, 31/// skip the build step. This assumes that build outputs are strictly a product 32/// of the fingerprint inputs. 33class Fingerprinter { 34 Fingerprinter({ 35 @required this.fingerprintPath, 36 @required Iterable<String> paths, 37 @required Map<String, String> properties, 38 Iterable<String> depfilePaths = const <String>[], 39 FingerprintPathFilter pathFilter, 40 }) : _paths = paths.toList(), 41 _properties = Map<String, String>.from(properties), 42 _depfilePaths = depfilePaths.toList(), 43 _pathFilter = pathFilter, 44 assert(fingerprintPath != null), 45 assert(paths != null && paths.every((String path) => path != null)), 46 assert(properties != null), 47 assert(depfilePaths != null && depfilePaths.every((String path) => path != null)); 48 49 final String fingerprintPath; 50 final List<String> _paths; 51 final Map<String, String> _properties; 52 final List<String> _depfilePaths; 53 final FingerprintPathFilter _pathFilter; 54 55 Future<Fingerprint> buildFingerprint() async { 56 final List<String> paths = await _getPaths(); 57 return Fingerprint.fromBuildInputs(_properties, paths); 58 } 59 60 Future<bool> doesFingerprintMatch() async { 61 if (_disableBuildCache) { 62 return false; 63 } 64 try { 65 final File fingerprintFile = fs.file(fingerprintPath); 66 if (!fingerprintFile.existsSync()) 67 return false; 68 69 if (!_depfilePaths.every(fs.isFileSync)) 70 return false; 71 72 final List<String> paths = await _getPaths(); 73 if (!paths.every(fs.isFileSync)) 74 return false; 75 76 final Fingerprint oldFingerprint = Fingerprint.fromJson(await fingerprintFile.readAsString()); 77 final Fingerprint newFingerprint = await buildFingerprint(); 78 return oldFingerprint == newFingerprint; 79 } catch (e) { 80 // Log exception and continue, fingerprinting is only a performance improvement. 81 printTrace('Fingerprint check error: $e'); 82 } 83 return false; 84 } 85 86 Future<void> writeFingerprint() async { 87 try { 88 final Fingerprint fingerprint = await buildFingerprint(); 89 fs.file(fingerprintPath).writeAsStringSync(fingerprint.toJson()); 90 } catch (e) { 91 // Log exception and continue, fingerprinting is only a performance improvement. 92 printTrace('Fingerprint write error: $e'); 93 } 94 } 95 96 Future<List<String>> _getPaths() async { 97 final Set<String> paths = <String>{ 98 ..._paths, 99 for (String depfilePath in _depfilePaths) 100 ...await readDepfile(depfilePath), 101 }; 102 final FingerprintPathFilter filter = _pathFilter ?? (String path) => true; 103 return paths.where(filter).toList()..sort(); 104 } 105} 106 107/// A fingerprint that uniquely identifies a set of build input files and 108/// properties. 109/// 110/// See [Fingerprinter]. 111class Fingerprint { 112 Fingerprint.fromBuildInputs(Map<String, String> properties, Iterable<String> inputPaths) { 113 final Iterable<File> files = inputPaths.map<File>(fs.file); 114 final Iterable<File> missingInputs = files.where((File file) => !file.existsSync()); 115 if (missingInputs.isNotEmpty) 116 throw ArgumentError('Missing input files:\n' + missingInputs.join('\n')); 117 118 _checksums = <String, String>{}; 119 for (File file in files) { 120 final List<int> bytes = file.readAsBytesSync(); 121 _checksums[file.path] = md5.convert(bytes).toString(); 122 } 123 _properties = <String, String>{...properties}; 124 } 125 126 /// Creates a Fingerprint from serialized JSON. 127 /// 128 /// Throws [ArgumentError], if there is a version mismatch between the 129 /// serializing framework and this framework. 130 Fingerprint.fromJson(String jsonData) { 131 final Map<String, dynamic> content = json.decode(jsonData); 132 133 final String version = content['version']; 134 if (version != FlutterVersion.instance.frameworkRevision) 135 throw ArgumentError('Incompatible fingerprint version: $version'); 136 _checksums = content['files']?.cast<String,String>() ?? <String, String>{}; 137 _properties = content['properties']?.cast<String,String>() ?? <String, String>{}; 138 } 139 140 Map<String, String> _checksums; 141 Map<String, String> _properties; 142 143 String toJson() => json.encode(<String, dynamic>{ 144 'version': FlutterVersion.instance.frameworkRevision, 145 'properties': _properties, 146 'files': _checksums, 147 }); 148 149 @override 150 bool operator==(dynamic other) { 151 if (identical(other, this)) 152 return true; 153 if (other.runtimeType != runtimeType) 154 return false; 155 final Fingerprint typedOther = other; 156 return _equalMaps(typedOther._checksums, _checksums) 157 && _equalMaps(typedOther._properties, _properties); 158 } 159 160 bool _equalMaps(Map<String, String> a, Map<String, String> b) { 161 return a.length == b.length 162 && a.keys.every((String key) => a[key] == b[key]); 163 } 164 165 @override 166 // Ignore map entries here to avoid becoming inconsistent with equals 167 // due to differences in map entry order. 168 int get hashCode => hash2(_properties.length, _checksums.length); 169 170 @override 171 String toString() => '{checksums: $_checksums, properties: $_properties}'; 172} 173 174final RegExp _separatorExpr = RegExp(r'([^\\]) '); 175final RegExp _escapeExpr = RegExp(r'\\(.)'); 176 177/// Parses a VM snapshot dependency file. 178/// 179/// Snapshot dependency files are a single line mapping the output snapshot to a 180/// space-separated list of input files used to generate that output. Spaces and 181/// backslashes are escaped with a backslash. e.g, 182/// 183/// outfile : file1.dart fil\\e2.dart fil\ e3.dart 184/// 185/// will return a set containing: 'file1.dart', 'fil\e2.dart', 'fil e3.dart'. 186Future<Set<String>> readDepfile(String depfilePath) async { 187 // Depfile format: 188 // outfile1 outfile2 : file1.dart file2.dart file3.dart 189 final String contents = await fs.file(depfilePath).readAsString(); 190 191 final String dependencies = contents.split(': ')[1]; 192 return dependencies 193 .replaceAllMapped(_separatorExpr, (Match match) => '${match.group(1)}\n') 194 .split('\n') 195 .map<String>((String path) => path.replaceAllMapped(_escapeExpr, (Match match) => match.group(1)).trim()) 196 .where((String path) => path.isNotEmpty) 197 .toSet(); 198} 199 200 201