fingerprint.dart 6.74 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8
// 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;

9
import '../convert.dart' show json;
10
import '../globals.dart' as globals;
11 12
import '../version.dart';
import 'file_system.dart';
13
import 'utils.dart';
14

15
typedef FingerprintPathFilter = bool Function(String path);
16

17 18 19 20 21 22 23 24 25 26 27 28
/// 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,
29
    Iterable<String> depfilePaths = const <String>[],
30
    FingerprintPathFilter pathFilter,
31
  }) : _paths = paths.toList(),
32
       _properties = Map<String, String>.from(properties),
33
       _depfilePaths = depfilePaths.toList(),
34
       _pathFilter = pathFilter,
35 36 37 38 39 40 41 42 43
       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;
44
  final FingerprintPathFilter _pathFilter;
45

46 47
  Fingerprint buildFingerprint() {
    final List<String> paths = _getPaths();
48
    return Fingerprint.fromBuildInputs(_properties, paths);
49 50
  }

51
  bool doesFingerprintMatch() {
52
    try {
53
      final File fingerprintFile = globals.fs.file(fingerprintPath);
54
      if (!fingerprintFile.existsSync()) {
55
        return false;
56
      }
57

58
      if (!_depfilePaths.every(globals.fs.isFileSync)) {
59
        return false;
60
      }
61

62
      final List<String> paths = _getPaths();
63
      if (!paths.every(globals.fs.isFileSync)) {
64
        return false;
65
      }
66

67 68
      final Fingerprint oldFingerprint = Fingerprint.fromJson(fingerprintFile.readAsStringSync());
      final Fingerprint newFingerprint = buildFingerprint();
69 70 71
      return oldFingerprint == newFingerprint;
    } catch (e) {
      // Log exception and continue, fingerprinting is only a performance improvement.
72
      globals.printTrace('Fingerprint check error: $e');
73 74 75 76
    }
    return false;
  }

77
  void writeFingerprint() {
78
    try {
79
      final Fingerprint fingerprint = buildFingerprint();
80
      globals.fs.file(fingerprintPath).writeAsStringSync(fingerprint.toJson());
81 82
    } catch (e) {
      // Log exception and continue, fingerprinting is only a performance improvement.
83
      globals.printTrace('Fingerprint write error: $e');
84 85 86
    }
  }

87
  List<String> _getPaths() {
88 89
    final Set<String> paths = <String>{
      ..._paths,
90
      for (final String depfilePath in _depfilePaths)
91
        ...readDepfile(depfilePath),
92
    };
93 94
    final FingerprintPathFilter filter = _pathFilter ?? (String path) => true;
    return paths.where(filter).toList()..sort();
95 96 97 98 99 100 101 102 103
  }
}

/// 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) {
104
    final Iterable<File> files = inputPaths.map<File>(globals.fs.file);
105
    final Iterable<File> missingInputs = files.where((File file) => !file.existsSync());
106
    if (missingInputs.isNotEmpty) {
107
      throw ArgumentError('Missing input files:\n' + missingInputs.join('\n'));
108
    }
109 110

    _checksums = <String, String>{};
111
    for (final File file in files) {
112 113 114
      final List<int> bytes = file.readAsBytesSync();
      _checksums[file.path] = md5.convert(bytes).toString();
    }
115
    _properties = <String, String>{...properties};
116 117 118 119 120 121 122
  }

  /// 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) {
123
    final Map<String, dynamic> content = castStringKeyedMap(json.decode(jsonData));
124

125
    final String version = content['version'] as String;
126
    if (version != FlutterVersion.instance.frameworkRevision) {
127
      throw ArgumentError('Incompatible fingerprint version: $version');
128
    }
129 130
    _checksums = castStringKeyedMap(content['files'])?.cast<String,String>() ?? <String, String>{};
    _properties = castStringKeyedMap(content['properties'])?.cast<String,String>() ?? <String, String>{};
131 132 133 134 135 136 137 138 139 140 141 142
  }

  Map<String, String> _checksums;
  Map<String, String> _properties;

  String toJson() => json.encode(<String, dynamic>{
    'version': FlutterVersion.instance.frameworkRevision,
    'properties': _properties,
    'files': _checksums,
  });

  @override
143
  bool operator==(Object other) {
144
    if (identical(other, this)) {
145
      return true;
146 147
    }
    if (other.runtimeType != runtimeType) {
148
      return false;
149
    }
150 151 152
    return other is Fingerprint
        && _equalMaps(other._checksums, _checksums)
        && _equalMaps(other._properties, _properties);
153 154 155 156 157 158 159 160 161 162 163
  }

  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);
164 165 166

  @override
  String toString() => '{checksums: $_checksums, properties: $_properties}';
167 168
}

169 170
final RegExp _separatorExpr = RegExp(r'([^\\]) ');
final RegExp _escapeExpr = RegExp(r'\\(.)');
171 172 173 174 175 176 177 178 179 180

/// 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'.
181
Set<String> readDepfile(String depfilePath) {
182 183
  // Depfile format:
  // outfile1 outfile2 : file1.dart file2.dart file3.dart
184
  final String contents = globals.fs.file(depfilePath).readAsStringSync();
185

186 187 188 189
  final String dependencies = contents.split(': ')[1];
  return dependencies
      .replaceAllMapped(_separatorExpr, (Match match) => '${match.group(1)}\n')
      .split('\n')
190
      .map<String>((String path) => path.replaceAllMapped(_escapeExpr, (Match match) => match.group(1)).trim())
191 192 193
      .where((String path) => path.isNotEmpty)
      .toSet();
}