diff --git a/lib/src/asset_graph/exceptions.dart b/lib/src/asset_graph/exceptions.dart new file mode 100644 index 000000000..3d477a59b --- /dev/null +++ b/lib/src/asset_graph/exceptions.dart @@ -0,0 +1,13 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'node.dart'; + +class DuplicateAssetNodeException implements Exception { + final AssetNode assetNode; + + DuplicateAssetNodeException(this.assetNode); + + @override + String toString() => 'DuplicateAssetNodeError: $assetNode'; +} diff --git a/lib/src/asset_graph/graph.dart b/lib/src/asset_graph/graph.dart new file mode 100644 index 000000000..7843c2182 --- /dev/null +++ b/lib/src/asset_graph/graph.dart @@ -0,0 +1,41 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import '../asset/id.dart'; +import 'exceptions.dart'; +import 'node.dart'; + +/// Represents all the [Asset]s in the system, and all of their dependencies. +class AssetGraph { + /// All the [AssetNode]s in the system, indexed by [AssetId]. + final _nodesById = {}; + + AssetGraph(); + + /// Checks if [id] exists in the graph. + bool contains(AssetId id) => _nodesById.containsKey(id); + + /// Gets the [AssetNode] for [id], if one exists. + AssetNode get(AssetId id) => _nodesById[id]; + + /// Adds [node] to the graph. + void add(AssetNode node) { + if (_nodesById.containsKey(node.id)) { + throw new DuplicateAssetNodeException(node); + } + _nodesById[node.id] = node; + } + + /// Adds the node returned by [ifAbsent] to the graph, if [id] doesn't + /// already exist. + /// + /// Returns either the existing value or the one just added. + AssetNode addIfAbsent(AssetId id, AssetNode ifAbsent()) => + _nodesById.putIfAbsent(id, ifAbsent); + + /// Removes [node] from the graph. + AssetNode remove(AssetId id) => _nodesById.remove(id); + + @override + toString() => _nodesById.values.toList().toString(); +} diff --git a/lib/src/asset_graph/node.dart b/lib/src/asset_graph/node.dart new file mode 100644 index 000000000..2c0c8e294 --- /dev/null +++ b/lib/src/asset_graph/node.dart @@ -0,0 +1,44 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import '../asset/id.dart'; +import '../builder/builder.dart'; + +/// A node in the asset graph. +/// +/// This class specifically represents normal (ie: non-generated) assets. +class AssetNode { + /// The asset this node represents. + final AssetId id; + + /// The [AssetId]s of all generated assets which depend on this node. + final outputs = new Set(); + + AssetNode(this.id); + + @override + String toString() => 'AssetNode: $id'; +} + +/// A generated node in the asset graph. +class GeneratedAssetNode extends AssetNode { + /// The builder which generated this node. + final Builder builder; + + /// The primary input which generated this node. + final AssetId primaryInput; + + /// The phase group number which generates this asset. + final int generatingPhaseGroup; + + /// Whether or not this asset needs to be updated. + bool needsUpdate; + + GeneratedAssetNode(this.builder, this.primaryInput, this.generatingPhaseGroup, + this.needsUpdate, AssetId id) + : super(id); + + @override + toString() => 'GeneratedAssetNode: $id generated for input $primaryInput to ' + '$builder in phase group $generatingPhaseGroup.'; +} diff --git a/lib/src/generate/build.dart b/lib/src/generate/build.dart index 8b8cdf46f..1946c84a7 100644 --- a/lib/src/generate/build.dart +++ b/lib/src/generate/build.dart @@ -15,6 +15,8 @@ import '../asset/file_based.dart'; import '../asset/id.dart'; import '../asset/reader.dart'; import '../asset/writer.dart'; +import '../asset_graph/graph.dart'; +import '../asset_graph/node.dart'; import '../builder/builder.dart'; import '../builder/build_step_impl.dart'; import '../package_graph/package_graph.dart'; @@ -45,7 +47,6 @@ Future build(List> phaseGroups, Logger.root.level = logLevel; onLog ??= print; var logListener = Logger.root.onRecord.listen(onLog); - // No need to create a package graph if we were supplied a reader/writer. packageGraph ??= new PackageGraph.forThisPackage(); var cache = new AssetCache(); reader ??= @@ -75,6 +76,7 @@ Future build(List> phaseGroups, exception: e, stackTrace: s); } }, zoneValues: { + _assetGraphKey: new AssetGraph(), _assetReaderKey: reader, _assetWriterKey: writer, _packageGraphKey: packageGraph, @@ -86,11 +88,13 @@ Future build(List> phaseGroups, } /// Keys for reading zone local values. +Symbol _assetGraphKey = #buildAssetGraph; Symbol _assetReaderKey = #buildAssetReader; Symbol _assetWriterKey = #buildAssetWriter; Symbol _packageGraphKey = #buildPackageGraph; /// Getters for zone local values. +AssetGraph get _assetGraph => Zone.current[_assetGraphKey]; AssetReader get _reader => Zone.current[_assetReaderKey]; AssetWriter get _writer => Zone.current[_assetWriterKey]; PackageGraph get _packageGraph => Zone.current[_packageGraphKey]; @@ -123,6 +127,7 @@ Future _deletePreviousOutputs(List> phaseGroups) async { for (var input in inputs) { for (var builder in phase.builders) { var outputs = builder.declareOutputs(input); + groupOutputIds.addAll(outputs); for (var output in outputs) { if (allInputs[output.package]?.contains(output) == true) { @@ -179,6 +184,7 @@ Future _deletePreviousOutputs(List> phaseGroups) async { Future _runPhases(List> phaseGroups) async { final allInputs = await _allInputs(phaseGroups); final outputs = []; + int phaseGroupNum = 0; for (var group in phaseGroups) { final groupOutputs = []; for (var phase in group) { @@ -186,7 +192,8 @@ Future _runPhases(List> phaseGroups) async { for (var builder in phase.builders) { // TODO(jakemac): Optimize, we can run all the builders in a phase // at the same time instead of sequentially. - await for (var output in _runBuilder(builder, inputs, allInputs)) { + await for (var output + in _runBuilder(builder, inputs, allInputs, phaseGroupNum)) { groupOutputs.add(output); outputs.add(output); } @@ -199,6 +206,7 @@ Future _runPhases(List> phaseGroups) async { allInputs.putIfAbsent(output.id.package, () => new Set()); allInputs[output.id.package].add(output.id); } + phaseGroupNum++; } return new BuildResult(BuildStatus.Success, BuildType.Full, outputs); } @@ -254,9 +262,10 @@ bool _isValidInput(AssetId input) { /// Runs [builder] with [inputs] as inputs. Stream _runBuilder(Builder builder, Iterable primaryInputs, - Map> allInputs) async* { + Map> allInputs, int phaseGroupNum) async* { for (var input in primaryInputs) { var expectedOutputs = builder.declareOutputs(input); + /// Validate [expectedOutputs]. for (var output in expectedOutputs) { if (output.package != _packageGraph.root.name) { @@ -267,11 +276,37 @@ Stream _runBuilder(Builder builder, Iterable primaryInputs, } } + /// Add nodes to the [AssetGraph] for [expectedOutputs] and [input]. + var inputNode = _assetGraph.addIfAbsent(input, () => new AssetNode(input)); + for (var output in expectedOutputs) { + inputNode.outputs.add(output); + _assetGraph.addIfAbsent( + output, + () => new GeneratedAssetNode( + builder, input, phaseGroupNum, true, output)); + } + + /// Skip the build step if none of the outputs need updating. + var skipBuild = !expectedOutputs.any((output) => + (_assetGraph.get(output) as GeneratedAssetNode).needsUpdate); + if (skipBuild) continue; + var inputAsset = new Asset(input, await _reader.readAsString(input)); - var buildStep = new BuildStepImpl( - inputAsset, expectedOutputs, _reader, _writer, _packageGraph.root.name); + var buildStep = new BuildStepImpl(inputAsset, expectedOutputs, _reader, + _writer, _packageGraph.root.name); await builder.build(buildStep); await buildStep.complete(); + + /// Update the asset graph based on the dependencies discovered. + for (var dependency in buildStep.dependencies) { + var dependencyNode = + _assetGraph.addIfAbsent(dependency, () => new AssetNode(dependency)); + + /// We care about all [expectedOutputs], not just real outputs. + dependencyNode.outputs.addAll(expectedOutputs); + } + + /// Yield the outputs. for (var output in buildStep.outputs) { yield output; } diff --git a/test/asset_graph/graph_test.dart b/test/asset_graph/graph_test.dart new file mode 100644 index 000000000..f12f73bc7 --- /dev/null +++ b/test/asset_graph/graph_test.dart @@ -0,0 +1,80 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. 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:test/test.dart'; + +import 'package:build/src/asset_graph/graph.dart'; +import 'package:build/src/asset_graph/node.dart'; + +import '../common/common.dart'; + +main() { + group('AssetGraph', () { + AssetGraph graph; + + setUp(() { + graph = new AssetGraph(); + }); + + void expectNodeDoesNotExist(AssetNode node) { + expect(graph.contains(node.id), isFalse); + expect(graph.get(node.id), isNull); + } + + void expectNodeExists(AssetNode node) { + expect(graph.contains(node.id), isTrue); + expect(graph.get(node.id), node); + } + + AssetNode testAddNode() { + var node = makeAssetNode(); + expectNodeDoesNotExist(node); + graph.add(node); + expectNodeExists(node); + return node; + } + + test('add, contains, get', () { + for (int i = 0; i < 5; i++) { + testAddNode(); + } + }); + + test('addIfAbsent', () { + var node = makeAssetNode(); + expect(graph.addIfAbsent(node.id, () => node), same(node)); + expect(graph.contains(node.id), isTrue); + + var otherNode = new AssetNode(node.id); + expect(graph.addIfAbsent(otherNode.id, () => otherNode), same(node)); + expect(graph.contains(otherNode.id), isTrue); + }); + + test('duplicate adds throw DuplicateAssetNodeException', () { + var node = testAddNode(); + expect(() => graph.add(node), throwsA(duplicateAssetNodeException)); + }); + + test('remove', () { + var nodes = []; + for (int i = 0; i < 5; i++) { + nodes.add(testAddNode()); + } + graph.remove(nodes[1].id); + graph.remove(nodes[4].id); + + expectNodeExists(nodes[0]); + expectNodeDoesNotExist(nodes[1]); + expectNodeExists(nodes[2]); + expectNodeDoesNotExist(nodes[4]); + expectNodeExists(nodes[3]); + + // Doesn't throw. + graph.remove(nodes[1].id); + + // Can be added back + graph.add(nodes[1]); + expectNodeExists(nodes[1]); + }); + }); +} diff --git a/test/common/assets.dart b/test/common/assets.dart index 498f4415b..583f1a192 100644 --- a/test/common/assets.dart +++ b/test/common/assets.dart @@ -2,6 +2,7 @@ // for details. 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:build/build.dart'; +import 'package:build/src/asset_graph/node.dart'; import 'in_memory_writer.dart'; @@ -33,3 +34,6 @@ void addAssets(Iterable assets, InMemoryAssetWriter writer) { writer.assets[asset.id] = asset.stringContents; } } + +AssetNode makeAssetNode([String assetIdString]) => + new AssetNode(makeAssetId(assetIdString)); diff --git a/test/common/matchers.dart b/test/common/matchers.dart index 33555523c..6fa5f0005 100644 --- a/test/common/matchers.dart +++ b/test/common/matchers.dart @@ -4,8 +4,11 @@ import 'package:test/test.dart'; import 'package:build/build.dart'; +import 'package:build/src/asset_graph/exceptions.dart'; final assetNotFoundException = new isInstanceOf(); +final duplicateAssetNodeException = + new isInstanceOf(); final invalidInputException = new isInstanceOf(); final invalidOutputException = new isInstanceOf(); final packageNotFoundException = new isInstanceOf();