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 |