• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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