| // Copyright 2018 The Chromium 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 'dart:async'; |
| |
| 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'; |
| import '../version.dart'; |
| import 'file_system.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; |
| |
| Future<Fingerprint> buildFingerprint() async { |
| final List<String> paths = await _getPaths(); |
| return Fingerprint.fromBuildInputs(_properties, paths); |
| } |
| |
| Future<bool> doesFingerprintMatch() async { |
| try { |
| final File fingerprintFile = fs.file(fingerprintPath); |
| if (!fingerprintFile.existsSync()) |
| return false; |
| |
| if (!_depfilePaths.every(fs.isFileSync)) |
| return false; |
| |
| final List<String> paths = await _getPaths(); |
| if (!paths.every(fs.isFileSync)) |
| return false; |
| |
| final Fingerprint oldFingerprint = Fingerprint.fromJson(await fingerprintFile.readAsString()); |
| final Fingerprint newFingerprint = await buildFingerprint(); |
| return oldFingerprint == newFingerprint; |
| } catch (e) { |
| // Log exception and continue, fingerprinting is only a performance improvement. |
| printTrace('Fingerprint check error: $e'); |
| } |
| return false; |
| } |
| |
| Future<void> writeFingerprint() async { |
| try { |
| final Fingerprint fingerprint = await buildFingerprint(); |
| fs.file(fingerprintPath).writeAsStringSync(fingerprint.toJson()); |
| } catch (e) { |
| // Log exception and continue, fingerprinting is only a performance improvement. |
| printTrace('Fingerprint write error: $e'); |
| } |
| } |
| |
| Future<List<String>> _getPaths() async { |
| final Set<String> paths = _paths.toSet(); |
| for (String depfilePath in _depfilePaths) |
| paths.addAll(await 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>(fs.file); |
| final Iterable<File> missingInputs = files.where((File file) => !file.existsSync()); |
| if (missingInputs.isNotEmpty) |
| throw ArgumentError('Missing input files:\n' + missingInputs.join('\n')); |
| |
| _checksums = <String, String>{}; |
| for (File file in files) { |
| final List<int> bytes = file.readAsBytesSync(); |
| _checksums[file.path] = md5.convert(bytes).toString(); |
| } |
| _properties = <String, String>{}..addAll(properties); |
| } |
| |
| /// Creates a Fingerprint from serialized JSON. |
| /// |
| /// Throws [ArgumentError], if there is a version mismatch between the |
| /// serializing framework and this framework. |
| Fingerprint.fromJson(String jsonData) { |
| final Map<String, dynamic> content = json.decode(jsonData); |
| |
| final String version = content['version']; |
| if (version != FlutterVersion.instance.frameworkRevision) |
| throw ArgumentError('Incompatible fingerprint version: $version'); |
| _checksums = content['files']?.cast<String,String>() ?? <String, String>{}; |
| _properties = content['properties']?.cast<String,String>() ?? <String, String>{}; |
| } |
| |
| Map<String, String> _checksums; |
| Map<String, String> _properties; |
| |
| String toJson() => json.encode(<String, dynamic>{ |
| 'version': FlutterVersion.instance.frameworkRevision, |
| 'properties': _properties, |
| 'files': _checksums, |
| }); |
| |
| @override |
| bool operator==(dynamic other) { |
| if (identical(other, this)) |
| return true; |
| if (other.runtimeType != runtimeType) |
| return false; |
| final Fingerprint typedOther = other; |
| return _equalMaps(typedOther._checksums, _checksums) |
| && _equalMaps(typedOther._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'. |
| Future<Set<String>> readDepfile(String depfilePath) async { |
| // Depfile format: |
| // outfile1 outfile2 : file1.dart file2.dart file3.dart |
| final String contents = await fs.file(depfilePath).readAsString(); |
| |
| final String dependencies = contents.split(': ')[1]; |
| return dependencies |
| .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(); |
| } |
| |
| |