// 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 'package:quiver/core.dart' show hash2; 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>.from(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]. class Fingerprint { 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')); } _checksums = <String, String>{}; for (final File file in files) { final List<int> bytes = file.readAsBytesSync(); _checksums[file.path] = md5.convert(bytes).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. 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'); } _checksums = castStringKeyedMap(content['files'])?.cast<String,String>() ?? <String, String>{}; _properties = castStringKeyedMap(content['properties'])?.cast<String,String>() ?? <String, String>{}; } Map<String, String> _checksums; 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. int get hashCode => hash2(_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. e.g, /// /// 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(); }