| OLD | NEW |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library barback.transform_node; | 5 library barback.transform_node; |
| 6 | 6 |
| 7 import 'dart:async'; | 7 import 'dart:async'; |
| 8 | 8 |
| 9 import 'package:source_maps/span.dart'; | 9 import 'package:source_maps/span.dart'; |
| 10 | 10 |
| 11 import 'asset.dart'; | 11 import 'asset.dart'; |
| 12 import 'asset_id.dart'; | 12 import 'asset_id.dart'; |
| 13 import 'asset_node.dart'; | 13 import 'asset_node.dart'; |
| 14 import 'asset_set.dart'; | 14 import 'asset_set.dart'; |
| 15 import 'declaring_transform.dart'; |
| 15 import 'errors.dart'; | 16 import 'errors.dart'; |
| 17 import 'lazy_transformer.dart'; |
| 16 import 'log.dart'; | 18 import 'log.dart'; |
| 17 import 'phase.dart'; | 19 import 'phase.dart'; |
| 18 import 'transform.dart'; | 20 import 'transform.dart'; |
| 19 import 'transformer.dart'; | 21 import 'transformer.dart'; |
| 22 import 'utils.dart'; |
| 20 | 23 |
| 21 /// Describes a transform on a set of assets and its relationship to the build | 24 /// Describes a transform on a set of assets and its relationship to the build |
| 22 /// dependency graph. | 25 /// dependency graph. |
| 23 /// | 26 /// |
| 24 /// Keeps track of whether it's dirty and needs to be run and which assets it | 27 /// Keeps track of whether it's dirty and needs to be run and which assets it |
| 25 /// depends on. | 28 /// depends on. |
| 26 class TransformNode { | 29 class TransformNode { |
| 27 /// The [Phase] that this transform runs in. | 30 /// The [Phase] that this transform runs in. |
| 28 final Phase phase; | 31 final Phase phase; |
| 29 | 32 |
| 30 /// The [Transformer] to apply to this node's inputs. | 33 /// The [Transformer] to apply to this node's inputs. |
| 31 final Transformer transformer; | 34 final Transformer transformer; |
| 32 | 35 |
| 33 /// The node for the primary asset this transform depends on. | 36 /// The node for the primary asset this transform depends on. |
| 34 final AssetNode primary; | 37 final AssetNode primary; |
| 35 | 38 |
| 36 /// A string describing the location of [this] in the transformer graph. | 39 /// A string describing the location of [this] in the transformer graph. |
| 37 final String _location; | 40 final String _location; |
| 38 | 41 |
| 39 /// The subscription to [primary]'s [AssetNode.onStateChange] stream. | 42 /// The subscription to [primary]'s [AssetNode.onStateChange] stream. |
| 40 StreamSubscription _primarySubscription; | 43 StreamSubscription _primarySubscription; |
| 41 | 44 |
| 42 /// True if an input has been modified since the last time this transform | 45 /// True if an input has been modified since the last time this transform |
| 43 /// began running. | 46 /// began running. |
| 44 bool get isDirty => _isDirty; | 47 bool get isDirty => _isDirty; |
| 45 var _isDirty = true; | 48 var _isDirty = true; |
| 46 | 49 |
| 50 /// Whether [transformer] is lazy and this transform has yet to be forced. |
| 51 bool _isLazy; |
| 52 |
| 47 /// The subscriptions to each input's [AssetNode.onStateChange] stream. | 53 /// The subscriptions to each input's [AssetNode.onStateChange] stream. |
| 48 var _inputSubscriptions = new Map<AssetId, StreamSubscription>(); | 54 var _inputSubscriptions = new Map<AssetId, StreamSubscription>(); |
| 49 | 55 |
| 50 /// The controllers for the asset nodes emitted by this node. | 56 /// The controllers for the asset nodes emitted by this node. |
| 51 var _outputControllers = new Map<AssetId, AssetNodeController>(); | 57 var _outputControllers = new Map<AssetId, AssetNodeController>(); |
| 52 | 58 |
| 53 /// A stream that emits an event whenever this transform becomes dirty and | 59 /// A stream that emits an event whenever this transform becomes dirty and |
| 54 /// needs to be re-run. | 60 /// needs to be re-run. |
| 55 /// | 61 /// |
| 56 /// This may emit events when the transform was already dirty or while | 62 /// This may emit events when the transform was already dirty or while |
| 57 /// processing transforms. Events are emitted synchronously to ensure that the | 63 /// processing transforms. Events are emitted synchronously to ensure that the |
| 58 /// dirty state is thoroughly propagated as soon as any assets are changed. | 64 /// dirty state is thoroughly propagated as soon as any assets are changed. |
| 59 Stream get onDirty => _onDirtyController.stream; | 65 Stream get onDirty => _onDirtyController.stream; |
| 60 final _onDirtyController = new StreamController.broadcast(sync: true); | 66 final _onDirtyController = new StreamController.broadcast(sync: true); |
| 61 | 67 |
| 62 /// A stream that emits an event whenever this transform logs an entry. | 68 /// A stream that emits an event whenever this transform logs an entry. |
| 63 /// | 69 /// |
| 64 /// This is synchronous because error logs can cause the transform to fail, so | 70 /// This is synchronous because error logs can cause the transform to fail, so |
| 65 /// we need to ensure that their processing isn't delayed until after the | 71 /// we need to ensure that their processing isn't delayed until after the |
| 66 /// transform or build has finished. | 72 /// transform or build has finished. |
| 67 Stream<LogEntry> get onLog => _onLogController.stream; | 73 Stream<LogEntry> get onLog => _onLogController.stream; |
| 68 final _onLogController = new StreamController<LogEntry>.broadcast(sync: true); | 74 final _onLogController = new StreamController<LogEntry>.broadcast(sync: true); |
| 69 | 75 |
| 70 TransformNode(this.phase, this.transformer, this.primary, this._location) { | 76 TransformNode(this.phase, Transformer transformer, this.primary, |
| 77 this._location) |
| 78 : transformer = transformer, |
| 79 _isLazy = transformer is LazyTransformer { |
| 71 _primarySubscription = primary.onStateChange.listen((state) { | 80 _primarySubscription = primary.onStateChange.listen((state) { |
| 72 if (state.isRemoved) { | 81 if (state.isRemoved) { |
| 73 remove(); | 82 remove(); |
| 74 } else { | 83 } else { |
| 75 _dirty(); | 84 _dirty(); |
| 76 } | 85 } |
| 77 }); | 86 }); |
| 78 } | 87 } |
| 79 | 88 |
| 80 /// The [TransformInfo] describing this node. | 89 /// The [TransformInfo] describing this node. |
| (...skipping 13 matching lines...) Expand all Loading... |
| 94 _onDirtyController.close(); | 103 _onDirtyController.close(); |
| 95 _primarySubscription.cancel(); | 104 _primarySubscription.cancel(); |
| 96 for (var subscription in _inputSubscriptions.values) { | 105 for (var subscription in _inputSubscriptions.values) { |
| 97 subscription.cancel(); | 106 subscription.cancel(); |
| 98 } | 107 } |
| 99 for (var controller in _outputControllers.values) { | 108 for (var controller in _outputControllers.values) { |
| 100 controller.setRemoved(); | 109 controller.setRemoved(); |
| 101 } | 110 } |
| 102 } | 111 } |
| 103 | 112 |
| 113 /// If [transformer] is lazy, ensures that its concrete outputs will be |
| 114 /// generated. |
| 115 void force() { |
| 116 // TODO(nweiz): we might want to have a timeout after which, if the |
| 117 // transform's outputs have gone unused, we switch it back to lazy mode. |
| 118 if (!_isLazy) return; |
| 119 _isLazy = false; |
| 120 _dirty(); |
| 121 } |
| 122 |
| 104 /// Marks this transform as dirty. | 123 /// Marks this transform as dirty. |
| 105 /// | 124 /// |
| 106 /// This causes all of the transform's outputs to be marked as dirty as well. | 125 /// This causes all of the transform's outputs to be marked as dirty as well. |
| 107 void _dirty() { | 126 void _dirty() { |
| 108 _isDirty = true; | 127 _isDirty = true; |
| 109 for (var controller in _outputControllers.values) { | 128 for (var controller in _outputControllers.values) { |
| 110 controller.setDirty(); | 129 controller.setDirty(); |
| 111 } | 130 } |
| 112 _onDirtyController.add(null); | 131 _onDirtyController.add(null); |
| 113 } | 132 } |
| 114 | 133 |
| 115 /// Applies this transform. | 134 /// Applies this transform. |
| 116 /// | 135 /// |
| 117 /// Returns a set of asset nodes representing the outputs from this transform | 136 /// Returns a set of asset nodes representing the outputs from this transform |
| 118 /// that weren't emitted last time it was run. | 137 /// that weren't emitted last time it was run. |
| 119 Future<Set<AssetNode>> apply() { | 138 Future<Set<AssetNode>> apply() { |
| 120 assert(!_onDirtyController.isClosed); | 139 assert(!_onDirtyController.isClosed); |
| 121 | 140 |
| 122 var newOutputs = new AssetSet(); | |
| 123 var transform = createTransform(this, newOutputs, _log); | |
| 124 | |
| 125 // Clear all the old input subscriptions. If an input is re-used, we'll | 141 // Clear all the old input subscriptions. If an input is re-used, we'll |
| 126 // re-subscribe. | 142 // re-subscribe. |
| 127 for (var subscription in _inputSubscriptions.values) { | 143 for (var subscription in _inputSubscriptions.values) { |
| 128 subscription.cancel(); | 144 subscription.cancel(); |
| 129 } | 145 } |
| 130 _inputSubscriptions.clear(); | 146 _inputSubscriptions.clear(); |
| 131 | 147 |
| 132 _isDirty = false; | 148 _isDirty = false; |
| 133 | 149 |
| 134 return transformer.apply(transform).catchError((error, stack) { | 150 return syncFuture(() { |
| 151 // TODO(nweiz): If [transformer] is a [DeclaringTransformer] but not a |
| 152 // [LazyTransformer], we can get some mileage out of doing a declarative |
| 153 // first so we know how to hook up the assets. |
| 154 if (_isLazy) return _declareLazy(); |
| 155 return _applyImmediate(); |
| 156 }).catchError((error, stackTrace) { |
| 135 // If the transform became dirty while processing, ignore any errors from | 157 // If the transform became dirty while processing, ignore any errors from |
| 136 // it. | 158 // it. |
| 137 if (_isDirty) return; | 159 if (_isDirty) return new Set(); |
| 138 | 160 |
| 139 if (error is! MissingInputException) { | 161 if (error is! MissingInputException) { |
| 140 error = new TransformerException(info, error, stack); | 162 error = new TransformerException(info, error, stackTrace); |
| 141 } | 163 } |
| 142 | 164 |
| 143 // Catch all transformer errors and pipe them to the results stream. | 165 // Catch all transformer errors and pipe them to the results stream. This |
| 144 // This is so a broken transformer doesn't take down the whole graph. | 166 // is so a broken transformer doesn't take down the whole graph. |
| 145 phase.cascade.reportError(error); | 167 phase.cascade.reportError(error); |
| 146 | 168 |
| 147 // Don't allow partial results from a failed transform. | 169 return new Set(); |
| 148 newOutputs.clear(); | |
| 149 }).then((_) { | |
| 150 if (_isDirty) return new Set(); | |
| 151 | |
| 152 return _adjustOutputs(newOutputs); | |
| 153 }); | 170 }); |
| 154 } | 171 } |
| 155 | 172 |
| 156 /// Gets the asset for an input [id]. | 173 /// Gets the asset for an input [id]. |
| 157 /// | 174 /// |
| 158 /// If an input with that ID cannot be found, throws an | 175 /// If an input with that ID cannot be found, throws an |
| 159 /// [AssetNotFoundException]. | 176 /// [AssetNotFoundException]. |
| 160 Future<Asset> getInput(AssetId id) { | 177 Future<Asset> getInput(AssetId id) { |
| 161 return phase.getInput(id).then((node) { | 178 return phase.getInput(id).then((node) { |
| 162 // Throw if the input isn't found. This ensures the transformer's apply | 179 // Throw if the input isn't found. This ensures the transformer's apply |
| (...skipping 10 matching lines...) Expand all Loading... |
| 173 return asset; | 190 return asset; |
| 174 }).catchError((error) { | 191 }).catchError((error) { |
| 175 if (error is! AssetNotFoundException || error.id != id) throw error; | 192 if (error is! AssetNotFoundException || error.id != id) throw error; |
| 176 // If the node was removed before it could be loaded, treat it as though | 193 // If the node was removed before it could be loaded, treat it as though |
| 177 // it never existed and throw a MissingInputException. | 194 // it never existed and throw a MissingInputException. |
| 178 throw new MissingInputException(info, id); | 195 throw new MissingInputException(info, id); |
| 179 }); | 196 }); |
| 180 }); | 197 }); |
| 181 } | 198 } |
| 182 | 199 |
| 183 /// Adjusts the outputs of the transform to reflect the outputs emitted on its | 200 /// Applies the transform so that it produces concrete (as opposed to lazy) |
| 184 /// most recent run. | 201 /// outputs. |
| 185 Set<AssetNode> _adjustOutputs(AssetSet newOutputs) { | 202 Future<Set<AssetNode>> _applyImmediate() { |
| 186 // Any ids that are for a different package are invalid. | 203 var newOutputs = new AssetSet(); |
| 187 var invalidIds = newOutputs | 204 var transform = new Transform(this, newOutputs, _log); |
| 188 .map((asset) => asset.id) | |
| 189 .where((id) => id.package != phase.cascade.package) | |
| 190 .toSet(); | |
| 191 for (var id in invalidIds) { | |
| 192 newOutputs.removeId(id); | |
| 193 // TODO(nweiz): report this as a warning rather than a failing error. | |
| 194 phase.cascade.reportError(new InvalidOutputException(info, id)); | |
| 195 } | |
| 196 | 205 |
| 197 // Remove outputs that used to exist but don't anymore. | 206 return syncFuture(() => transformer.apply(transform)).then((_) { |
| 198 for (var id in _outputControllers.keys.toList()) { | 207 if (_isDirty) return new Set(); |
| 199 if (newOutputs.containsId(id)) continue; | |
| 200 _outputControllers.remove(id).setRemoved(); | |
| 201 } | |
| 202 | 208 |
| 203 var brandNewOutputs = new Set<AssetNode>(); | 209 // Any ids that are for a different package are invalid. |
| 204 // Store any new outputs or new contents for existing outputs. | 210 var invalidIds = newOutputs |
| 205 for (var asset in newOutputs) { | 211 .map((asset) => asset.id) |
| 206 var controller = _outputControllers[asset.id]; | 212 .where((id) => id.package != phase.cascade.package) |
| 207 if (controller != null) { | 213 .toSet(); |
| 208 controller.setAvailable(asset); | 214 for (var id in invalidIds) { |
| 209 } else { | 215 newOutputs.removeId(id); |
| 210 var controller = new AssetNodeController.available(asset, this); | 216 // TODO(nweiz): report this as a warning rather than a failing error. |
| 211 _outputControllers[asset.id] = controller; | 217 phase.cascade.reportError(new InvalidOutputException(info, id)); |
| 212 brandNewOutputs.add(controller.node); | |
| 213 } | 218 } |
| 214 } | |
| 215 | 219 |
| 216 return brandNewOutputs; | 220 // Remove outputs that used to exist but don't anymore. |
| 221 for (var id in _outputControllers.keys.toList()) { |
| 222 if (newOutputs.containsId(id)) continue; |
| 223 _outputControllers.remove(id).setRemoved(); |
| 224 } |
| 225 |
| 226 var brandNewOutputs = new Set<AssetNode>(); |
| 227 // Store any new outputs or new contents for existing outputs. |
| 228 for (var asset in newOutputs) { |
| 229 var controller = _outputControllers[asset.id]; |
| 230 if (controller != null) { |
| 231 controller.setAvailable(asset); |
| 232 } else { |
| 233 var controller = new AssetNodeController.available(asset, this); |
| 234 _outputControllers[asset.id] = controller; |
| 235 brandNewOutputs.add(controller.node); |
| 236 } |
| 237 } |
| 238 |
| 239 return brandNewOutputs; |
| 240 }); |
| 241 } |
| 242 |
| 243 /// Applies the transform in declarative mode so that it produces lazy |
| 244 /// outputs. |
| 245 Future<Set<AssetNode>> _declareLazy() { |
| 246 var newIds = new Set(); |
| 247 var transform = new DeclaringTransform(this, newIds, _log); |
| 248 |
| 249 return syncFuture(() { |
| 250 return (transformer as LazyTransformer).declareOutputs(transform); |
| 251 }).then((_) { |
| 252 if (_isDirty) return new Set(); |
| 253 |
| 254 var invalidIds = |
| 255 newIds.where((id) => id.package != phase.cascade.package).toSet(); |
| 256 for (var id in invalidIds) { |
| 257 newIds.remove(id); |
| 258 // TODO(nweiz): report this as a warning rather than a failing error. |
| 259 phase.cascade.reportError(new InvalidOutputException(info, id)); |
| 260 } |
| 261 |
| 262 // Remove outputs that used to exist but don't anymore. |
| 263 for (var id in _outputControllers.keys.toList()) { |
| 264 if (newIds.contains(id)) continue; |
| 265 _outputControllers.remove(id).setRemoved(); |
| 266 } |
| 267 |
| 268 var brandNewOutputs = new Set<AssetNode>(); |
| 269 for (var id in newIds) { |
| 270 var controller = _outputControllers[id]; |
| 271 if (controller != null) { |
| 272 controller.setLazy(force); |
| 273 } else { |
| 274 var controller = new AssetNodeController.lazy(id, force, this); |
| 275 _outputControllers[id] = controller; |
| 276 brandNewOutputs.add(controller.node); |
| 277 } |
| 278 } |
| 279 |
| 280 return brandNewOutputs; |
| 281 }); |
| 217 } | 282 } |
| 218 | 283 |
| 219 void _log(AssetId asset, LogLevel level, String message, Span span) { | 284 void _log(AssetId asset, LogLevel level, String message, Span span) { |
| 220 // If the log isn't already associated with an asset, use the primary. | 285 // If the log isn't already associated with an asset, use the primary. |
| 221 if (asset == null) asset = primary.id; | 286 if (asset == null) asset = primary.id; |
| 222 var entry = new LogEntry(info, asset, level, message, span); | 287 var entry = new LogEntry(info, asset, level, message, span); |
| 223 _onLogController.add(entry); | 288 _onLogController.add(entry); |
| 224 } | 289 } |
| 225 | 290 |
| 226 String toString() => | 291 String toString() => |
| 227 "transform node in $_location for $transformer on $primary"; | 292 "transform node in $_location for $transformer on $primary"; |
| 228 } | 293 } |
| OLD | NEW |