| // 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:crypto/crypto.dart' show md5; |
| import 'package:meta/meta.dart'; |
| |
| import '../convert.dart' show json; |
| import '../globals.dart' as globals; |
| import 'file_system.dart'; |
| import 'utils.dart'; |
| |
| typedef FingerprintPathFilter = bool Function(String path); |
| |
| /// A tool that can be used to compute, compare, and write [Fingerprint]s for a |
| /// set of input files and associated build settings. |
| /// |
| /// This class can be used during build actions to compute a fingerprint of the |
| /// build action inputs and options, and if unchanged from the previous build, |
| /// skip the build step. This assumes that build outputs are strictly a product |
| /// of the fingerprint inputs. |
| class Fingerprinter { |
| Fingerprinter({ |
| @required this.fingerprintPath, |
| @required Iterable<String> paths, |
| @required Map<String, String> properties, |
| Iterable<String> depfilePaths = const <String>[], |
| FingerprintPathFilter pathFilter, |
| }) : _paths = paths.toList(), |
| _properties = Map<String, String>.of(properties), |
| _depfilePaths = depfilePaths.toList(), |
| _pathFilter = pathFilter, |
| assert(fingerprintPath != null), |
| assert(paths != null && paths.every((String path) => path != null)), |
| assert(properties != null), |
| assert(depfilePaths != null && depfilePaths.every((String path) => path != null)); |
| |
| final String fingerprintPath; |
| final List<String> _paths; |
| final Map<String, String> _properties; |
| final List<String> _depfilePaths; |
| final FingerprintPathFilter _pathFilter; |
| |
| Fingerprint buildFingerprint() { |
| final List<String> paths = _getPaths(); |
| return Fingerprint.fromBuildInputs(_properties, paths); |
| } |
| |
| bool doesFingerprintMatch() { |
| try { |
| final File fingerprintFile = globals.fs.file(fingerprintPath); |
| if (!fingerprintFile.existsSync()) { |
| return false; |
| } |
| |
| if (!_depfilePaths.every(globals.fs.isFileSync)) { |
| return false; |
| } |
| |
| final List<String> paths = _getPaths(); |
| if (!paths.every(globals.fs.isFileSync)) { |
| return false; |
| } |
| |
| final Fingerprint oldFingerprint = Fingerprint.fromJson(fingerprintFile.readAsStringSync()); |
| final Fingerprint newFingerprint = buildFingerprint(); |
| return oldFingerprint == newFingerprint; |
| } on Exception catch (e) { |
| // Log exception and continue, fingerprinting is only a performance improvement. |
| globals.printTrace('Fingerprint check error: $e'); |
| } |
| return false; |
| } |
| |
| void writeFingerprint() { |
| try { |
| final Fingerprint fingerprint = buildFingerprint(); |
| globals.fs.file(fingerprintPath).writeAsStringSync(fingerprint.toJson()); |
| } on Exception catch (e) { |
| // Log exception and continue, fingerprinting is only a performance improvement. |
| globals.printTrace('Fingerprint write error: $e'); |
| } |
| } |
| |
| List<String> _getPaths() { |
| final Set<String> paths = <String>{ |
| ..._paths, |
| for (final String depfilePath in _depfilePaths) |
| ...readDepfile(depfilePath), |
| }; |
| final FingerprintPathFilter filter = _pathFilter ?? (String path) => true; |
| return paths.where(filter).toList()..sort(); |
| } |
| } |
| |
| /// A fingerprint that uniquely identifies a set of build input files and |
| /// properties. |
| /// |
| /// See [Fingerprinter]. |
| @immutable |
| class Fingerprint { |
| const Fingerprint._({ |
| Map<String, String> checksums, |
| Map<String, String> properties, |
| }) : _checksums = checksums, |
| _properties = properties; |
| |
| factory Fingerprint.fromBuildInputs(Map<String, String> properties, Iterable<String> inputPaths) { |
| final Iterable<File> files = inputPaths.map<File>(globals.fs.file); |
| final Iterable<File> missingInputs = files.where((File file) => !file.existsSync()); |
| if (missingInputs.isNotEmpty) { |
| throw Exception('Missing input files:\n' + missingInputs.join('\n')); |
| } |
| return Fingerprint._( |
| // ignore: prefer_const_literals_to_create_immutables, https://github.com/dart-lang/linter/issues/2025 |
| checksums: <String, String>{ |
| for (final File file in files) |
| file.path: md5.convert(file.readAsBytesSync()).toString(), |
| }, |
| properties: <String, String>{...properties}, |
| ); |
| } |
| |
| /// Creates a Fingerprint from serialized JSON. |
| /// |
| /// Throws [Exception], if there is a version mismatch between the |
| /// serializing framework and this framework. |
| factory Fingerprint.fromJson(String jsonData) { |
| final Map<String, dynamic> content = castStringKeyedMap(json.decode(jsonData)); |
| |
| final String version = content['version'] as String; |
| if (version != globals.flutterVersion.frameworkRevision) { |
| throw Exception('Incompatible fingerprint version: $version'); |
| } |
| return Fingerprint._( |
| checksums: castStringKeyedMap(content['files'])?.cast<String,String>() ?? <String, String>{}, |
| properties: castStringKeyedMap(content['properties'])?.cast<String,String>() ?? <String, String>{}, |
| ); |
| } |
| |
| final Map<String, String> _checksums; |
| final Map<String, String> _properties; |
| |
| String toJson() => json.encode(<String, dynamic>{ |
| 'version': globals.flutterVersion.frameworkRevision, |
| 'properties': _properties, |
| 'files': _checksums, |
| }); |
| |
| @override |
| bool operator==(Object other) { |
| if (identical(other, this)) { |
| return true; |
| } |
| if (other.runtimeType != runtimeType) { |
| return false; |
| } |
| return other is Fingerprint |
| && _equalMaps(other._checksums, _checksums) |
| && _equalMaps(other._properties, _properties); |
| } |
| |
| bool _equalMaps(Map<String, String> a, Map<String, String> b) { |
| return a.length == b.length |
| && a.keys.every((String key) => a[key] == b[key]); |
| } |
| |
| @override |
| // Ignore map entries here to avoid becoming inconsistent with equals |
| // due to differences in map entry order. This is a really bad hash |
| // function and should eventually be deprecated and removed. |
| int get hashCode => _properties.length + _checksums.length; |
| |
| @override |
| String toString() => '{checksums: $_checksums, properties: $_properties}'; |
| } |
| |
| final RegExp _separatorExpr = RegExp(r'([^\\]) '); |
| final RegExp _escapeExpr = RegExp(r'\\(.)'); |
| |
| /// Parses a VM snapshot dependency file. |
| /// |
| /// Snapshot dependency files are a single line mapping the output snapshot to a |
| /// space-separated list of input files used to generate that output. Spaces and |
| /// backslashes are escaped with a backslash. For example: |
| /// |
| /// outfile : file1.dart fil\\e2.dart fil\ e3.dart |
| /// |
| /// will return a set containing: 'file1.dart', 'fil\e2.dart', 'fil e3.dart'. |
| Set<String> readDepfile(String depfilePath) { |
| // Depfile format: |
| // outfile1 outfile2 : file1.dart file2.dart file3.dart |
| final String contents = globals.fs.file(depfilePath).readAsStringSync(); |
| |
| final List<String> dependencies = contents.split(': '); |
| if (dependencies.length < 2) { |
| throw Exception('malformed depfile'); |
| } |
| return dependencies[1] |
| .replaceAllMapped(_separatorExpr, (Match match) => '${match.group(1)}\n') |
| .split('\n') |
| .map<String>((String path) => path.replaceAllMapped(_escapeExpr, (Match match) => match.group(1)).trim()) |
| .where((String path) => path.isNotEmpty) |
| .toSet(); |
| } |