Add version, build mode to the snapshot checksums (#11787)

This change ensures that snapshot build checksums used to avoid
duplicate builds are invalidated by a change to framework revision
(in case gen_snapshot is updated), as well as by build mode.

Currently, only FLX snapshotting uses checksums to avoid duplicate
builds. FLX snapshotting is always done with BuildMode.debug, so didn't
include build mode in the checksum file.
This commit is contained in:
Chris Bracken 2017-08-25 14:32:01 -07:00 committed by GitHub
parent e960ba0b88
commit fd54bd4caf
3 changed files with 106 additions and 26 deletions

View file

@ -5,7 +5,10 @@
import 'dart:convert' show JSON; import 'dart:convert' show JSON;
import 'package:crypto/crypto.dart' show md5; import 'package:crypto/crypto.dart' show md5;
import 'package:quiver/core.dart' show hash2;
import '../build_info.dart';
import '../version.dart';
import 'file_system.dart'; import 'file_system.dart';
/// A collection of checksums for a set of input files. /// A collection of checksums for a set of input files.
@ -15,30 +18,59 @@ import 'file_system.dart';
/// build step. This assumes that build outputs are strictly a product of the /// build step. This assumes that build outputs are strictly a product of the
/// input files. /// input files.
class Checksum { class Checksum {
Checksum.fromFiles(Set<String> inputPaths) : _checksums = <String, String>{} { Checksum.fromFiles(BuildMode buildMode, Set<String> inputPaths) {
final Iterable<File> files = inputPaths.map(fs.file); final Iterable<File> files = inputPaths.map(fs.file);
final Iterable<File> missingInputs = files.where((File file) => !file.existsSync()); final Iterable<File> missingInputs = files.where((File file) => !file.existsSync());
if (missingInputs.isNotEmpty) if (missingInputs.isNotEmpty)
throw new ArgumentError('Missing input files:\n' + missingInputs.join('\n')); throw new ArgumentError('Missing input files:\n' + missingInputs.join('\n'));
_buildMode = buildMode.toString();
_checksums = <String, String>{};
for (File file in files) { for (File file in files) {
final List<int> bytes = file.readAsBytesSync(); final List<int> bytes = file.readAsBytesSync();
_checksums[file.path] = md5.convert(bytes).toString(); _checksums[file.path] = md5.convert(bytes).toString();
} }
} }
Checksum.fromJson(String json) : _checksums = JSON.decode(json); /// Creates a checksum from serialized JSON.
///
/// Throws [ArgumentError] in the following cases:
/// * Version mismatch between the serializing framework and this framework.
/// * BuildMode is unspecified.
/// * File checksum map is unspecified.
Checksum.fromJson(String json) {
final Map<String, dynamic> content = JSON.decode(json);
final Map<String, String> _checksums; final String version = content['version'];
if (version != FlutterVersion.instance.frameworkRevision)
throw new ArgumentError('Incompatible checksum version: $version');
String toJson() => JSON.encode(_checksums); _buildMode = content['buildMode'];
if (_buildMode == null || _buildMode.isEmpty)
throw new ArgumentError('BuildMode unspecified in checksum JSON');
_checksums = content['files'];
if (_checksums == null)
throw new ArgumentError('File checksums unspecified in checksum JSON');
}
String _buildMode;
Map<String, String> _checksums;
String toJson() => JSON.encode(<String, dynamic>{
'version': FlutterVersion.instance.frameworkRevision,
'buildMode': _buildMode,
'files': _checksums,
});
@override @override
bool operator==(dynamic other) { bool operator==(dynamic other) {
return other is Checksum && return other is Checksum &&
_buildMode == other._buildMode &&
_checksums.length == other._checksums.length && _checksums.length == other._checksums.length &&
_checksums.keys.every((String key) => _checksums[key] == other._checksums[key]); _checksums.keys.every((String key) => _checksums[key] == other._checksums[key]);
} }
@override @override
int get hashCode => _checksums.hashCode; int get hashCode => hash2(_buildMode, _checksums);
} }

View file

@ -40,7 +40,8 @@ Future<int> _createSnapshot({
assert(snapshotPath != null); assert(snapshotPath != null);
assert(depfilePath != null); assert(depfilePath != null);
assert(packages != null); assert(packages != null);
final String snapshotterPath = artifacts.getArtifactPath(Artifact.genSnapshot, null, BuildMode.debug); final BuildMode buildMode = BuildMode.debug;
final String snapshotterPath = artifacts.getArtifactPath(Artifact.genSnapshot, null, buildMode);
final String vmSnapshotData = artifacts.getArtifactPath(Artifact.vmSnapshotData); final String vmSnapshotData = artifacts.getArtifactPath(Artifact.vmSnapshotData);
final String isolateSnapshotData = artifacts.getArtifactPath(Artifact.isolateSnapshotData); final String isolateSnapshotData = artifacts.getArtifactPath(Artifact.isolateSnapshotData);
@ -68,7 +69,7 @@ Future<int> _createSnapshot({
final Set<String> inputPaths = await _readDepfile(depfilePath); final Set<String> inputPaths = await _readDepfile(depfilePath);
inputPaths.add(snapshotPath); inputPaths.add(snapshotPath);
inputPaths.add(mainPath); inputPaths.add(mainPath);
final Checksum newChecksum = new Checksum.fromFiles(inputPaths); final Checksum newChecksum = new Checksum.fromFiles(buildMode, inputPaths);
if (oldChecksum == newChecksum) { if (oldChecksum == newChecksum) {
printTrace('Skipping snapshot build. Checksums match.'); printTrace('Skipping snapshot build. Checksums match.');
return 0; return 0;
@ -89,7 +90,7 @@ Future<int> _createSnapshot({
final Set<String> inputPaths = await _readDepfile(depfilePath); final Set<String> inputPaths = await _readDepfile(depfilePath);
inputPaths.add(snapshotPath); inputPaths.add(snapshotPath);
inputPaths.add(mainPath); inputPaths.add(mainPath);
final Checksum checksum = new Checksum.fromFiles(inputPaths); final Checksum checksum = new Checksum.fromFiles(buildMode, inputPaths);
await checksumFile.writeAsString(checksum.toJson()); await checksumFile.writeAsString(checksum.toJson());
} catch (e, s) { } catch (e, s) {
// Log exception and continue, this step is a performance improvement only. // Log exception and continue, this step is a performance improvement only.

View file

@ -2,15 +2,30 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:convert';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/base/build.dart'; import 'package:flutter_tools/src/base/build.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../src/context.dart'; import '../src/context.dart';
class MockFlutterVersion extends Mock implements FlutterVersion {}
void main() { void main() {
group('Checksum', () { group('Checksum', () {
MockFlutterVersion mockVersion;
const String kVersion = '123456abcdef';
setUp(() {
mockVersion = new MockFlutterVersion();
when(mockVersion.frameworkRevision).thenReturn(kVersion);
});
group('fromFiles', () { group('fromFiles', () {
MemoryFileSystem fs; MemoryFileSystem fs;
@ -20,47 +35,79 @@ void main() {
testUsingContext('throws if any input file does not exist', () async { testUsingContext('throws if any input file does not exist', () async {
await fs.file('a.dart').create(); await fs.file('a.dart').create();
expect(() => new Checksum.fromFiles(<String>['a.dart', 'b.dart'].toSet()), throwsA(anything)); expect(() => new Checksum.fromFiles(BuildMode.debug, <String>['a.dart', 'b.dart'].toSet()), throwsA(anything));
}, overrides: <Type, Generator>{ FileSystem: () => fs}); }, overrides: <Type, Generator>{ FileSystem: () => fs});
testUsingContext('populates checksums for valid files', () async { testUsingContext('populates checksums for valid files', () async {
await fs.file('a.dart').writeAsString('This is a'); await fs.file('a.dart').writeAsString('This is a');
await fs.file('b.dart').writeAsString('This is b'); await fs.file('b.dart').writeAsString('This is b');
final Checksum checksum = new Checksum.fromFiles(<String>['a.dart', 'b.dart'].toSet()); final Checksum checksum = new Checksum.fromFiles(BuildMode.debug, <String>['a.dart', 'b.dart'].toSet());
final String json = checksum.toJson();
expect(json, '{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}'); final Map<String, dynamic> json = JSON.decode(checksum.toJson());
}, overrides: <Type, Generator>{ FileSystem: () => fs}); expect(json, hasLength(3));
expect(json['version'], mockVersion.frameworkRevision);
expect(json['buildMode'], BuildMode.debug.toString());
expect(json['files'], hasLength(2));
expect(json['files']['a.dart'], '8a21a15fad560b799f6731d436c1b698');
expect(json['files']['b.dart'], '6f144e08b58cd0925328610fad7ac07c');
}, overrides: <Type, Generator>{
FileSystem: () => fs,
FlutterVersion: () => mockVersion,
});
}); });
group('fromJson', () { group('fromJson', () {
test('throws if JSON is invalid', () async { testUsingContext('throws if JSON is invalid', () async {
expect(() => new Checksum.fromJson('<xml></xml>'), throwsA(anything)); expect(() => new Checksum.fromJson('<xml></xml>'), throwsA(anything));
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
}); });
test('populates checksums for valid JSON', () async { testUsingContext('populates checksums for valid JSON', () async {
final String json = '{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}'; final String json = '{"version":"$kVersion","buildMode":"BuildMode.release","files":{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}}';
final Checksum checksum = new Checksum.fromJson(json); final Checksum checksum = new Checksum.fromJson(json);
expect(checksum.toJson(), '{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}');
final Map<String, dynamic> content = JSON.decode(checksum.toJson());
expect(content, hasLength(3));
expect(content['version'], mockVersion.frameworkRevision);
expect(content['buildMode'], BuildMode.release.toString());
expect(content['files']['a.dart'], '8a21a15fad560b799f6731d436c1b698');
expect(content['files']['b.dart'], '6f144e08b58cd0925328610fad7ac07c');
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
testUsingContext('throws ArgumentError for unknown versions', () async {
final String json = '{"version":"bad","buildMode":"BuildMode.release","files":{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}}';
expect(() => new Checksum.fromJson(json), throwsArgumentError);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
}); });
}); });
group('operator ==', () { group('operator ==', () {
test('reports not equal if checksums do not match', () async { testUsingContext('reports not equal if checksums do not match', () async {
final Checksum a = new Checksum.fromJson('{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}'); final Checksum a = new Checksum.fromJson('{"version":"$kVersion","buildMode":"BuildMode.release","files":{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}}');
final Checksum b = new Checksum.fromJson('{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07d"}'); final Checksum b = new Checksum.fromJson('{"version":"$kVersion","buildMode":"BuildMode.release","files":{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07d"}}');
expect(a == b, isFalse); expect(a == b, isFalse);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
}); });
test('reports not equal if keys do not match', () async { testUsingContext('reports not equal if keys do not match', () async {
final Checksum a = new Checksum.fromJson('{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}'); final Checksum a = new Checksum.fromJson('{"version":"$kVersion","buildMode":"BuildMode.release","files":{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}}');
final Checksum b = new Checksum.fromJson('{"a.dart":"8a21a15fad560b799f6731d436c1b698","c.dart":"6f144e08b58cd0925328610fad7ac07c"}'); final Checksum b = new Checksum.fromJson('{"version":"$kVersion","buildMode":"BuildMode.release","files":{"a.dart":"8a21a15fad560b799f6731d436c1b698","c.dart":"6f144e08b58cd0925328610fad7ac07c"}}');
expect(a == b, isFalse); expect(a == b, isFalse);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
}); });
test('reports equal if all checksums match', () async { testUsingContext('reports equal if all checksums match', () async {
final Checksum a = new Checksum.fromJson('{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}'); final Checksum a = new Checksum.fromJson('{"version":"$kVersion","buildMode":"BuildMode.release","files":{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}}');
final Checksum b = new Checksum.fromJson('{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}'); final Checksum b = new Checksum.fromJson('{"version":"$kVersion","buildMode":"BuildMode.release","files":{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}}');
expect(a == b, isTrue); expect(a == b, isTrue);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
}); });
}); });
}); });