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 'asset.dart'; | 9 import 'asset.dart'; |
10 import 'asset_id.dart'; | 10 import 'asset_id.dart'; |
11 import 'asset_node.dart'; | 11 import 'asset_node.dart'; |
12 import 'declaring_transform.dart'; | 12 import 'declaring_transform.dart'; |
| 13 import 'declaring_transformer.dart'; |
13 import 'errors.dart'; | 14 import 'errors.dart'; |
14 import 'lazy_transformer.dart'; | 15 import 'lazy_transformer.dart'; |
15 import 'log.dart'; | 16 import 'log.dart'; |
16 import 'phase.dart'; | 17 import 'phase.dart'; |
17 import 'stream_pool.dart'; | 18 import 'stream_pool.dart'; |
18 import 'transform.dart'; | 19 import 'transform.dart'; |
19 import 'transformer.dart'; | 20 import 'transformer.dart'; |
20 import 'utils.dart'; | 21 import 'utils.dart'; |
21 | 22 |
22 /// Describes a transform on a set of assets and its relationship to the build | 23 /// Describes a transform on a set of assets and its relationship to the build |
(...skipping 14 matching lines...) Expand all Loading... |
37 /// A string describing the location of [this] in the transformer graph. | 38 /// A string describing the location of [this] in the transformer graph. |
38 final String _location; | 39 final String _location; |
39 | 40 |
40 /// The subscription to [primary]'s [AssetNode.onStateChange] stream. | 41 /// The subscription to [primary]'s [AssetNode.onStateChange] stream. |
41 StreamSubscription _primarySubscription; | 42 StreamSubscription _primarySubscription; |
42 | 43 |
43 /// The subscription to [phase]'s [Phase.onAsset] stream. | 44 /// The subscription to [phase]'s [Phase.onAsset] stream. |
44 StreamSubscription<AssetNode> _phaseSubscription; | 45 StreamSubscription<AssetNode> _phaseSubscription; |
45 | 46 |
46 /// Whether [this] is dirty and still has more processing to do. | 47 /// Whether [this] is dirty and still has more processing to do. |
47 bool get isDirty => !_state.isDone; | 48 bool get isDirty => _state != _State.NOT_PRIMARY && _state != _State.APPLIED; |
48 | 49 |
49 /// Whether [transformer] is lazy and this transform has yet to be forced. | 50 /// Whether [transformer] is lazy and this transform has yet to be forced. |
50 bool _isLazy; | 51 bool _isLazy; |
51 | 52 |
52 /// The subscriptions to each input's [AssetNode.onStateChange] stream. | 53 /// The subscriptions to each input's [AssetNode.onStateChange] stream. |
53 final _inputSubscriptions = new Map<AssetId, StreamSubscription>(); | 54 final _inputSubscriptions = new Map<AssetId, StreamSubscription>(); |
54 | 55 |
55 /// The controllers for the asset nodes emitted by this node. | 56 /// The controllers for the asset nodes emitted by this node. |
56 final _outputControllers = new Map<AssetId, AssetNodeController>(); | 57 final _outputControllers = new Map<AssetId, AssetNodeController>(); |
57 | 58 |
| 59 /// The ids of inputs the transformer tried and failed to read last time it |
| 60 /// ran. |
58 final _missingInputs = new Set<AssetId>(); | 61 final _missingInputs = new Set<AssetId>(); |
59 | 62 |
60 /// The controller that's used to pass [primary] through [this] if it's not | 63 /// The controller that's used to pass [primary] through [this] if it's not |
61 /// consumed or overwritten. | 64 /// consumed or overwritten. |
62 /// | 65 /// |
63 /// This needs an intervening controller to ensure that the output can be | 66 /// This needs an intervening controller to ensure that the output can be |
64 /// marked dirty when determining whether [this] will consume or overwrite it, | 67 /// marked dirty when determining whether [this] will consume or overwrite it, |
65 /// and be marked removed if it does. [_passThroughController] will be null | 68 /// and be marked removed if it does. [_passThroughController] will be null |
66 /// if the asset is not being passed through. | 69 /// if the asset is not being passed through. |
67 AssetNodeController _passThroughController; | 70 AssetNodeController _passThroughController; |
(...skipping 14 matching lines...) Expand all Loading... |
82 new StreamController<AssetNode>.broadcast(sync: true); | 85 new StreamController<AssetNode>.broadcast(sync: true); |
83 | 86 |
84 /// A stream that emits an event whenever this transform logs an entry. | 87 /// A stream that emits an event whenever this transform logs an entry. |
85 /// | 88 /// |
86 /// This is synchronous because error logs can cause the transform to fail, so | 89 /// This is synchronous because error logs can cause the transform to fail, so |
87 /// we need to ensure that their processing isn't delayed until after the | 90 /// we need to ensure that their processing isn't delayed until after the |
88 /// transform or build has finished. | 91 /// transform or build has finished. |
89 Stream<LogEntry> get onLog => _onLogPool.stream; | 92 Stream<LogEntry> get onLog => _onLogPool.stream; |
90 final _onLogPool = new StreamPool<LogEntry>.broadcast(); | 93 final _onLogPool = new StreamPool<LogEntry>.broadcast(); |
91 | 94 |
| 95 /// A controller for log entries emitted by this node. |
| 96 final _onLogController = new StreamController<LogEntry>.broadcast(sync: true); |
| 97 |
92 /// The current state of [this]. | 98 /// The current state of [this]. |
93 var _state = _TransformNodeState.PROCESSING; | 99 var _state = _State.COMPUTING_IS_PRIMARY; |
94 | 100 |
95 /// Whether [this] has been marked as removed. | 101 /// Whether [this] has been marked as removed. |
96 bool get _isRemoved => _onAssetController.isClosed; | 102 bool get _isRemoved => _onAssetController.isClosed; |
97 | 103 |
98 /// Whether the most recent run of this transform has declared that it | 104 /// Whether the most recent run of this transform has declared that it |
99 /// consumes the primary input. | 105 /// consumes the primary input. |
100 /// | 106 /// |
101 /// Defaults to `false`. This is not meaningful unless [_state] is | 107 /// Defaults to `false`. This is not meaningful unless [_state] is |
102 /// [_TransformNodeState.APPLIED]. | 108 /// [_State.APPLIED]. |
103 bool _consumePrimary = false; | 109 bool _consumePrimary = false; |
104 | 110 |
| 111 /// The set of output ids that [transformer] declared it would emit. |
| 112 /// |
| 113 /// This is only non-null if [transformer] is a [DeclaringTransformer] and its |
| 114 /// [declareOutputs] has been run successfully. |
| 115 Set<AssetId> _declaredOutputs; |
| 116 |
105 TransformNode(this.phase, Transformer transformer, this.primary, | 117 TransformNode(this.phase, Transformer transformer, this.primary, |
106 this._location) | 118 this._location) |
107 : transformer = transformer, | 119 : transformer = transformer, |
108 _isLazy = transformer is LazyTransformer { | 120 _isLazy = transformer is LazyTransformer { |
| 121 _onLogPool.add(_onLogController.stream); |
| 122 |
109 _primarySubscription = primary.onStateChange.listen((state) { | 123 _primarySubscription = primary.onStateChange.listen((state) { |
110 if (state.isRemoved) { | 124 if (state.isRemoved) { |
111 remove(); | 125 remove(); |
112 } else { | 126 } else { |
113 _dirty(primaryChanged: true); | 127 _dirty(); |
114 } | 128 } |
115 }); | 129 }); |
116 | 130 |
117 _phaseSubscription = phase.previous.onAsset.listen((node) { | 131 _phaseSubscription = phase.previous.onAsset.listen((node) { |
118 if (_missingInputs.contains(node.id)) _dirty(primaryChanged: false); | 132 if (_missingInputs.contains(node.id)) _dirty(); |
119 }); | 133 }); |
120 | 134 |
121 _process(); | 135 _isPrimary(); |
122 } | 136 } |
123 | 137 |
124 /// The [TransformInfo] describing this node. | 138 /// The [TransformInfo] describing this node. |
125 /// | 139 /// |
126 /// [TransformInfo] is the publicly-visible representation of a transform | 140 /// [TransformInfo] is the publicly-visible representation of a transform |
127 /// node. | 141 /// node. |
128 TransformInfo get info => new TransformInfo(transformer, primary.id); | 142 TransformInfo get info => new TransformInfo(transformer, primary.id); |
129 | 143 |
130 /// Marks this transform as removed. | 144 /// Marks this transform as removed. |
131 /// | 145 /// |
132 /// This causes all of the transform's outputs to be marked as removed as | 146 /// This causes all of the transform's outputs to be marked as removed as |
133 /// well. Normally this will be automatically done internally based on events | 147 /// well. Normally this will be automatically done internally based on events |
134 /// from the primary input, but it's possible for a transform to no longer be | 148 /// from the primary input, but it's possible for a transform to no longer be |
135 /// valid even if its primary input still exists. | 149 /// valid even if its primary input still exists. |
136 void remove() { | 150 void remove() { |
| 151 _onLogController.close(); |
137 _onAssetController.close(); | 152 _onAssetController.close(); |
138 _onDoneController.close(); | 153 _onDoneController.close(); |
139 _primarySubscription.cancel(); | 154 _primarySubscription.cancel(); |
140 _phaseSubscription.cancel(); | 155 _phaseSubscription.cancel(); |
141 _clearInputSubscriptions(); | 156 _clearInputSubscriptions(); |
142 _clearOutputs(); | 157 _clearOutputs(); |
143 if (_passThroughController != null) { | 158 if (_passThroughController != null) { |
144 _passThroughController.setRemoved(); | 159 _passThroughController.setRemoved(); |
145 _passThroughController = null; | 160 _passThroughController = null; |
146 } | 161 } |
147 } | 162 } |
148 | 163 |
149 /// If [transformer] is lazy, ensures that its concrete outputs will be | 164 /// If [transformer] is lazy, ensures that its concrete outputs will be |
150 /// generated. | 165 /// generated. |
151 void force() { | 166 void force() { |
152 // TODO(nweiz): we might want to have a timeout after which, if the | 167 // TODO(nweiz): we might want to have a timeout after which, if the |
153 // transform's outputs have gone unused, we switch it back to lazy mode. | 168 // transform's outputs have gone unused, we switch it back to lazy mode. |
154 if (!_isLazy) return; | 169 if (!_isLazy) return; |
155 _isLazy = false; | 170 _isLazy = false; |
156 _dirty(primaryChanged: false); | 171 _dirty(); |
157 } | 172 } |
158 | 173 |
159 /// Marks this transform as dirty. | 174 /// Marks this transform as dirty. |
160 /// | 175 /// |
161 /// This causes all of the transform's outputs to be marked as dirty as well. | 176 /// This causes all of the transform's outputs to be marked as dirty as well. |
162 /// [primaryChanged] should be true if and only if [this] was set dirty | 177 void _dirty() { |
163 /// because [primary] changed. | 178 if (_state == _State.NOT_PRIMARY) { |
164 void _dirty({bool primaryChanged: false}) { | 179 _emitPassThrough(); |
165 if (!primaryChanged && _state.isNotPrimary) return; | 180 return; |
| 181 } |
| 182 if (_state == _State.COMPUTING_IS_PRIMARY || _isLazy) return; |
166 | 183 |
167 if (_passThroughController != null) _passThroughController.setDirty(); | 184 if (_passThroughController != null) _passThroughController.setDirty(); |
168 for (var controller in _outputControllers.values) { | 185 for (var controller in _outputControllers.values) { |
169 controller.setDirty(); | 186 controller.setDirty(); |
170 } | 187 } |
171 | 188 |
172 if (_state.isDone) { | 189 if (_state == _State.APPLIED) { |
173 if (primaryChanged) { | 190 _apply(); |
174 _process(); | 191 } else { |
175 } else { | 192 _state = _State.NEEDS_APPLY; |
176 _apply(); | |
177 } | |
178 } else if (primaryChanged) { | |
179 _state = _TransformNodeState.NEEDS_IS_PRIMARY; | |
180 } else if (!_state.needsIsPrimary) { | |
181 _state = _TransformNodeState.NEEDS_APPLY; | |
182 } | 193 } |
183 } | 194 } |
184 | 195 |
185 /// Determines whether [primary] is primary for [transformer], and if so runs | 196 /// Runs [transformer.isPrimary] and adjusts [this]'s state according to the |
186 /// [transformer.apply]. | 197 /// result. |
187 void _process() { | 198 /// |
188 // Clear all the old input subscriptions. If an input is re-used, we'll | 199 /// This will also run [_declareOutputs] and/or [_apply] as appropriate. |
189 // re-subscribe. | 200 void _isPrimary() { |
190 _clearInputSubscriptions(); | 201 syncFuture(() => transformer.isPrimary(primary.id)) |
191 _state = _TransformNodeState.PROCESSING; | 202 .catchError((error, stackTrace) { |
192 primary.whenAvailable((_) { | 203 if (_isRemoved) return false; |
193 _state = _TransformNodeState.PROCESSING; | |
194 return transformer.isPrimary(primary.asset); | |
195 }).catchError((error, stackTrace) { | |
196 // If the transform became dirty while processing, ignore any errors from | |
197 // it. | |
198 if (_state.needsIsPrimary || _isRemoved) return false; | |
199 | 204 |
200 // Catch all transformer errors and pipe them to the results stream. This | 205 // Catch all transformer errors and pipe them to the results stream. This |
201 // is so a broken transformer doesn't take down the whole graph. | 206 // is so a broken transformer doesn't take down the whole graph. |
202 phase.cascade.reportError(_wrapException(error, stackTrace)); | 207 phase.cascade.reportError(_wrapException(error, stackTrace)); |
203 | 208 |
204 return false; | 209 return false; |
205 }).then((isPrimary) { | 210 }).then((isPrimary) { |
| 211 if (_isRemoved) return null; |
| 212 if (isPrimary) { |
| 213 return _declareOutputs().then((_) { |
| 214 if (_isRemoved) return; |
| 215 if (_isLazy) { |
| 216 _state = _State.APPLIED; |
| 217 _onDoneController.add(null); |
| 218 } else { |
| 219 _apply(); |
| 220 } |
| 221 }); |
| 222 } |
| 223 |
| 224 _emitPassThrough(); |
| 225 _state = _State.NOT_PRIMARY; |
| 226 _onDoneController.add(null); |
| 227 }); |
| 228 } |
| 229 |
| 230 /// Runs [transform.declareOutputs] and emits the resulting assets as dirty |
| 231 /// assets. |
| 232 Future _declareOutputs() { |
| 233 if (transformer is! DeclaringTransformer) return new Future.value(); |
| 234 |
| 235 var controller = new DeclaringTransformController(this); |
| 236 return syncFuture(() { |
| 237 return (transformer as DeclaringTransformer) |
| 238 .declareOutputs(controller.transform); |
| 239 }).then((_) { |
206 if (_isRemoved) return; | 240 if (_isRemoved) return; |
207 if (_state.needsIsPrimary) { | 241 if (controller.loggedError) return; |
208 _process(); | 242 |
209 } else if (isPrimary) { | 243 _consumePrimary = controller.consumePrimary; |
210 _apply(); | 244 _declaredOutputs = controller.outputIds; |
211 } else { | 245 var invalidIds = _declaredOutputs |
212 _clearOutputs(); | 246 .where((id) => id.package != phase.cascade.package).toSet(); |
213 _emitPassThrough(); | 247 for (var id in invalidIds) { |
214 _state = _TransformNodeState.NOT_PRIMARY; | 248 _declaredOutputs.remove(id); |
215 _onDoneController.add(null); | 249 // TODO(nweiz): report this as a warning rather than a failing error. |
| 250 phase.cascade.reportError(new InvalidOutputException(info, id)); |
216 } | 251 } |
| 252 |
| 253 if (!_declaredOutputs.contains(primary.id)) _emitPassThrough(); |
| 254 |
| 255 for (var id in _declaredOutputs) { |
| 256 var controller = transformer is LazyTransformer |
| 257 ? new AssetNodeController.lazy(id, force, this) |
| 258 : new AssetNodeController(id, this); |
| 259 _outputControllers[id] = controller; |
| 260 _onAssetController.add(controller.node); |
| 261 } |
| 262 }).catchError((error, stackTrace) { |
| 263 if (_isRemoved) return; |
| 264 phase.cascade.reportError(_wrapException(error, stackTrace)); |
217 }); | 265 }); |
218 } | 266 } |
219 | 267 |
220 /// Applies this transform. | 268 /// Applies this transform. |
221 void _apply() { | 269 void _apply() { |
222 assert(!_onAssetController.isClosed); | 270 assert(!_isRemoved && !_isLazy); |
223 | 271 |
224 // Clear input subscriptions here as well as in [_process] because [_apply] | 272 // Clear input subscriptions here as well as in [_process] because [_apply] |
225 // may be restarted independently if only a secondary input changes. | 273 // may be restarted independently if only a secondary input changes. |
226 _clearInputSubscriptions(); | 274 _clearInputSubscriptions(); |
227 _state = _TransformNodeState.PROCESSING; | 275 _state = _State.APPLYING; |
228 primary.whenAvailable((_) { | 276 _runApply().then((hadError) { |
229 if (_state.needsIsPrimary) return null; | |
230 _state = _TransformNodeState.PROCESSING; | |
231 // TODO(nweiz): If [transformer] is a [DeclaringTransformer] but not a | |
232 // [LazyTransformer], we can get some mileage out of doing a declarative | |
233 // first so we know how to hook up the assets. | |
234 if (_isLazy) return _declareLazy(); | |
235 return _applyImmediate(); | |
236 }).catchError((error, stackTrace) { | |
237 // If the transform became dirty while processing, ignore any errors from | |
238 // it. | |
239 if (!_state.isProcessing || _isRemoved) return false; | |
240 | |
241 // Catch all transformer errors and pipe them to the results stream. This | |
242 // is so a broken transformer doesn't take down the whole graph. | |
243 phase.cascade.reportError(_wrapException(error, stackTrace)); | |
244 return true; | |
245 }).then((hadError) { | |
246 if (_isRemoved) return; | 277 if (_isRemoved) return; |
247 | 278 |
248 if (_state.needsIsPrimary) { | 279 if (_state == _State.NEEDS_APPLY) { |
249 _process(); | |
250 } else if (_state.needsApply) { | |
251 _apply(); | 280 _apply(); |
252 } else { | 281 return; |
253 assert(_state.isProcessing); | 282 } |
254 if (hadError) { | 283 |
255 _clearOutputs(); | 284 assert(_state == _State.APPLYING); |
| 285 if (hadError) { |
| 286 _clearOutputs(); |
| 287 // If the transformer threw an error, we don't want to emit the |
| 288 // pass-through asset in case it will be overwritten by the transformer. |
| 289 // However, if the transformer declared that it wouldn't overwrite or |
| 290 // consume the pass-through asset, we can safely emit it. |
| 291 if (_declaredOutputs != null && !_consumePrimary && |
| 292 !_declaredOutputs.contains(primary.id)) { |
| 293 _emitPassThrough(); |
| 294 } else { |
256 _dontEmitPassThrough(); | 295 _dontEmitPassThrough(); |
257 } | 296 } |
| 297 } |
258 | 298 |
259 _state = _TransformNodeState.APPLIED; | 299 _state = _State.APPLIED; |
260 _onDoneController.add(null); | 300 _onDoneController.add(null); |
261 } | |
262 }); | 301 }); |
263 } | 302 } |
264 | 303 |
265 /// Gets the asset for an input [id]. | 304 /// Gets the asset for an input [id]. |
266 /// | 305 /// |
267 /// If an input with [id] cannot be found, throws an [AssetNotFoundException]. | 306 /// If an input with [id] cannot be found, throws an [AssetNotFoundException]. |
268 Future<Asset> getInput(AssetId id) { | 307 Future<Asset> getInput(AssetId id) { |
269 return phase.previous.getOutput(id).then((node) { | 308 return phase.previous.getOutput(id).then((node) { |
270 // Throw if the input isn't found. This ensures the transformer's apply | 309 // Throw if the input isn't found. This ensures the transformer's apply |
271 // is exited. We'll then catch this and report it through the proper | 310 // is exited. We'll then catch this and report it through the proper |
272 // results stream. | 311 // results stream. |
273 if (node == null) { | 312 if (node == null) { |
274 _missingInputs.add(id); | 313 _missingInputs.add(id); |
275 throw new AssetNotFoundException(id); | 314 throw new AssetNotFoundException(id); |
276 } | 315 } |
277 | 316 |
278 _inputSubscriptions.putIfAbsent(node.id, () { | 317 _inputSubscriptions.putIfAbsent(node.id, () { |
279 return node.onStateChange.listen((_) => _dirty(primaryChanged: false)); | 318 return node.onStateChange.listen((_) => _dirty()); |
280 }); | 319 }); |
281 | 320 |
282 return node.asset; | 321 return node.asset; |
283 }); | 322 }); |
284 } | 323 } |
285 | 324 |
286 /// Applies the transform so that it produces concrete (as opposed to lazy) | 325 /// Run [Transformer.apply] as soon as [primary] is available. |
287 /// outputs. | |
288 /// | 326 /// |
289 /// Returns whether or not the transformer logged an error. | 327 /// Returns whether or not an error occurred while running the transformer. |
290 Future<bool> _applyImmediate() { | 328 Future<bool> _runApply() { |
291 var transformController = new TransformController(this); | 329 var transformController = new TransformController(this); |
292 _onLogPool.add(transformController.onLog); | 330 _onLogPool.add(transformController.onLog); |
293 | 331 |
294 return syncFuture(() { | 332 return primary.whenAvailable((_) { |
295 return transformer.apply(transformController.transform); | 333 if (_isRemoved) return null; |
| 334 _state = _State.APPLYING; |
| 335 return syncFuture(() => transformer.apply(transformController.transform)); |
296 }).then((_) { | 336 }).then((_) { |
297 if (!_state.isProcessing || _onAssetController.isClosed) return false; | 337 if (_state == _State.NEEDS_APPLY || _isRemoved) return false; |
298 if (transformController.loggedError) return true; | 338 if (transformController.loggedError) return true; |
| 339 _handleApplyResults(transformController); |
| 340 return false; |
| 341 }).catchError((error, stackTrace) { |
| 342 // If the transform became dirty while processing, ignore any errors from |
| 343 // it. |
| 344 if (_state == _State.NEEDS_APPLY || _isRemoved) return false; |
299 | 345 |
300 _consumePrimary = transformController.consumePrimary; | 346 // Catch all transformer errors and pipe them to the results stream. This |
301 | 347 // is so a broken transformer doesn't take down the whole graph. |
302 var newOutputs = transformController.outputs; | 348 phase.cascade.reportError(_wrapException(error, stackTrace)); |
303 // Any ids that are for a different package are invalid. | 349 return true; |
304 var invalidIds = newOutputs | |
305 .map((asset) => asset.id) | |
306 .where((id) => id.package != phase.cascade.package) | |
307 .toSet(); | |
308 for (var id in invalidIds) { | |
309 newOutputs.removeId(id); | |
310 // TODO(nweiz): report this as a warning rather than a failing error. | |
311 phase.cascade.reportError(new InvalidOutputException(info, id)); | |
312 } | |
313 | |
314 // Remove outputs that used to exist but don't anymore. | |
315 for (var id in _outputControllers.keys.toList()) { | |
316 if (newOutputs.containsId(id)) continue; | |
317 _outputControllers.remove(id).setRemoved(); | |
318 } | |
319 | |
320 // Emit or stop emitting the pass-through asset between removing and | |
321 // adding outputs to ensure there are no collisions. | |
322 if (!newOutputs.containsId(primary.id)) { | |
323 _emitPassThrough(); | |
324 } else { | |
325 _dontEmitPassThrough(); | |
326 } | |
327 | |
328 // Store any new outputs or new contents for existing outputs. | |
329 for (var asset in newOutputs) { | |
330 var controller = _outputControllers[asset.id]; | |
331 if (controller != null) { | |
332 controller.setAvailable(asset); | |
333 } else { | |
334 var controller = new AssetNodeController.available(asset, this); | |
335 _outputControllers[asset.id] = controller; | |
336 _onAssetController.add(controller.node); | |
337 } | |
338 } | |
339 | |
340 return false; | |
341 }); | 350 }); |
342 } | 351 } |
343 | 352 |
344 /// Applies the transform in declarative mode so that it produces lazy | 353 /// Handle the results of running [Transformer.apply]. |
345 /// outputs. | |
346 /// | 354 /// |
347 /// Returns whether or not the transformer logged an error. | 355 /// [transformController] should be the controller for the [Transform] passed |
348 Future<bool> _declareLazy() { | 356 /// to [Transformer.apply]. |
349 var transformController = new DeclaringTransformController(this); | 357 void _handleApplyResults(TransformController transformController) { |
| 358 _consumePrimary = transformController.consumePrimary; |
350 | 359 |
351 return syncFuture(() { | 360 var newOutputs = transformController.outputs; |
352 return (transformer as LazyTransformer) | 361 // Any ids that are for a different package are invalid. |
353 .declareOutputs(transformController.transform); | 362 var invalidIds = newOutputs |
354 }).then((_) { | 363 .map((asset) => asset.id) |
355 if (!_state.isProcessing || _onAssetController.isClosed) return false; | 364 .where((id) => id.package != phase.cascade.package) |
356 if (transformController.loggedError) return true; | 365 .toSet(); |
| 366 for (var id in invalidIds) { |
| 367 newOutputs.removeId(id); |
| 368 // TODO(nweiz): report this as a warning rather than a failing error. |
| 369 phase.cascade.reportError(new InvalidOutputException(info, id)); |
| 370 } |
357 | 371 |
358 _consumePrimary = transformController.consumePrimary; | 372 if (_declaredOutputs != null) { |
| 373 var missingOutputs = _declaredOutputs.difference( |
| 374 newOutputs.map((asset) => asset.id).toSet()); |
| 375 if (missingOutputs.isNotEmpty) { |
| 376 _warn("This transformer didn't emit declared " |
| 377 "${pluralize('output asset', missingOutputs.length)} " |
| 378 "${toSentence(missingOutputs)}."); |
| 379 } |
| 380 } |
359 | 381 |
360 var newIds = transformController.outputIds; | 382 // Remove outputs that used to exist but don't anymore. |
361 var invalidIds = | 383 for (var id in _outputControllers.keys.toList()) { |
362 newIds.where((id) => id.package != phase.cascade.package).toSet(); | 384 if (newOutputs.containsId(id)) continue; |
363 for (var id in invalidIds) { | 385 _outputControllers.remove(id).setRemoved(); |
364 newIds.remove(id); | 386 } |
365 // TODO(nweiz): report this as a warning rather than a failing error. | 387 |
366 phase.cascade.reportError(new InvalidOutputException(info, id)); | 388 // Emit or stop emitting the pass-through asset between removing and |
| 389 // adding outputs to ensure there are no collisions. |
| 390 if (!_consumePrimary && !newOutputs.containsId(primary.id)) { |
| 391 _emitPassThrough(); |
| 392 } else { |
| 393 _dontEmitPassThrough(); |
| 394 } |
| 395 |
| 396 // Store any new outputs or new contents for existing outputs. |
| 397 for (var asset in newOutputs) { |
| 398 var controller = _outputControllers[asset.id]; |
| 399 if (controller != null) { |
| 400 controller.setAvailable(asset); |
| 401 } else { |
| 402 var controller = new AssetNodeController.available(asset, this); |
| 403 _outputControllers[asset.id] = controller; |
| 404 _onAssetController.add(controller.node); |
367 } | 405 } |
368 | 406 } |
369 // Remove outputs that used to exist but don't anymore. | |
370 for (var id in _outputControllers.keys.toList()) { | |
371 if (newIds.contains(id)) continue; | |
372 _outputControllers.remove(id).setRemoved(); | |
373 } | |
374 | |
375 // Emit or stop emitting the pass-through asset between removing and | |
376 // adding outputs to ensure there are no collisions. | |
377 if (!newIds.contains(primary.id)) { | |
378 _emitPassThrough(); | |
379 } else { | |
380 _dontEmitPassThrough(); | |
381 } | |
382 | |
383 for (var id in newIds) { | |
384 var controller = _outputControllers[id]; | |
385 if (controller != null) { | |
386 controller.setLazy(force); | |
387 } else { | |
388 var controller = new AssetNodeController.lazy(id, force, this); | |
389 _outputControllers[id] = controller; | |
390 _onAssetController.add(controller.node); | |
391 } | |
392 } | |
393 | |
394 return false; | |
395 }); | |
396 } | 407 } |
397 | 408 |
398 /// Cancels all subscriptions to secondary input nodes. | 409 /// Cancels all subscriptions to secondary input nodes. |
399 void _clearInputSubscriptions() { | 410 void _clearInputSubscriptions() { |
400 _missingInputs.clear(); | 411 _missingInputs.clear(); |
401 for (var subscription in _inputSubscriptions.values) { | 412 for (var subscription in _inputSubscriptions.values) { |
402 subscription.cancel(); | 413 subscription.cancel(); |
403 } | 414 } |
404 _inputSubscriptions.clear(); | 415 _inputSubscriptions.clear(); |
405 } | 416 } |
406 | 417 |
407 /// Removes all output assets. | 418 /// Removes all output assets. |
408 void _clearOutputs() { | 419 void _clearOutputs() { |
409 // Remove all the previously-emitted assets. | 420 // Remove all the previously-emitted assets. |
410 for (var controller in _outputControllers.values) { | 421 for (var controller in _outputControllers.values) { |
411 controller.setRemoved(); | 422 controller.setRemoved(); |
412 } | 423 } |
413 _outputControllers.clear(); | 424 _outputControllers.clear(); |
414 } | 425 } |
415 | 426 |
416 /// Emit the pass-through asset if it's not being emitted already. | 427 /// Emit the pass-through asset if it's not being emitted already. |
417 void _emitPassThrough() { | 428 void _emitPassThrough() { |
418 assert(!_outputControllers.containsKey(primary.id)); | 429 assert(!_outputControllers.containsKey(primary.id)); |
419 | 430 |
420 if (_consumePrimary) return; | 431 if (_consumePrimary) return; |
421 if (_passThroughController == null) { | 432 if (_passThroughController == null) { |
422 _passThroughController = new AssetNodeController.from(primary); | 433 _passThroughController = new AssetNodeController.from(primary); |
423 _onAssetController.add(_passThroughController.node); | 434 _onAssetController.add(_passThroughController.node); |
424 } else { | 435 } else if (primary.state.isDirty) { |
| 436 _passThroughController.setDirty(); |
| 437 } else if (!_passThroughController.node.state.isAvailable) { |
425 _passThroughController.setAvailable(primary.asset); | 438 _passThroughController.setAvailable(primary.asset); |
426 } | 439 } |
427 } | 440 } |
428 | 441 |
429 /// Stop emitting the pass-through asset if it's being emitted already. | 442 /// Stop emitting the pass-through asset if it's being emitted already. |
430 void _dontEmitPassThrough() { | 443 void _dontEmitPassThrough() { |
431 if (_passThroughController == null) return; | 444 if (_passThroughController == null) return; |
432 _passThroughController.setRemoved(); | 445 _passThroughController.setRemoved(); |
433 _passThroughController = null; | 446 _passThroughController = null; |
434 } | 447 } |
435 | 448 |
436 BarbackException _wrapException(error, StackTrace stackTrace) { | 449 BarbackException _wrapException(error, StackTrace stackTrace) { |
437 if (error is! AssetNotFoundException) { | 450 if (error is! AssetNotFoundException) { |
438 return new TransformerException(info, error, stackTrace); | 451 return new TransformerException(info, error, stackTrace); |
439 } else { | 452 } else { |
440 return new MissingInputException(info, error.id); | 453 return new MissingInputException(info, error.id); |
441 } | 454 } |
442 } | 455 } |
443 | 456 |
| 457 /// Emit a warning about the transformer on [id]. |
| 458 void _warn(String message) { |
| 459 _onLogController.add( |
| 460 new LogEntry(info, primary.id, LogLevel.WARNING, message, null)); |
| 461 } |
| 462 |
444 String toString() => | 463 String toString() => |
445 "transform node in $_location for $transformer on $primary"; | 464 "transform node in $_location for $transformer on $primary"; |
446 } | 465 } |
447 | 466 |
448 /// The enum of states that [TransformNode] can be in. | 467 /// The enum of states that [TransformNode] can be in. |
449 class _TransformNodeState { | 468 class _State { |
450 /// The transform node is running [Transformer.isPrimary] or | 469 /// The transform is running [Transformer.isPrimary]. |
451 /// [Transformer.apply] and doesn't need to re-run them. | |
452 /// | 470 /// |
453 /// If there are no external changes by the time the processing finishes, this | 471 /// This is the initial state of the transformer. Once [Transformer.isPrimary] |
454 /// will transition to [APPLIED] or [NOT_PRIMARY] depending on the result of | 472 /// finishes running, this will transition to [APPLYING] if the input is |
455 /// [Transformer.isPrimary]. If the primary input changes, this will | 473 /// primary, or [NOT_PRIMARY] if it's not. |
456 /// transition to [NEEDS_IS_PRIMARY]. If a secondary input changes, this will | 474 static final COMPUTING_IS_PRIMARY = const _State._("computing isPrimary"); |
457 /// transition to [NEEDS_APPLY]. | |
458 static final PROCESSING = const _TransformNodeState._("processing"); | |
459 | 475 |
460 /// The transform is running [Transformer.isPrimary] or [Transformer.apply], | 476 /// The transform is running [Transformer.apply]. |
461 /// but since it started the primary input changed, so it will need to re-run | |
462 /// [Transformer.isPrimary]. | |
463 /// | 477 /// |
464 /// This will always transition to [Transformer.PROCESSING]. | 478 /// If an input changes while in this state, it will transition to |
465 static final NEEDS_IS_PRIMARY = | 479 /// [NEEDS_APPLY]. If the [TransformNode] is still in this state when |
466 const _TransformNodeState._("needs isPrimary"); | 480 /// [Transformer.apply] finishes running, it will transition to [APPLIED]. |
| 481 static final APPLYING = const _State._("applying"); |
467 | 482 |
468 /// The transform is running [Transformer.apply], but since it started a | 483 /// The transform is running [Transformer.apply], but an input changed after |
469 /// secondary input changed, so it will need to re-run [Transformer.apply]. | 484 /// it started, so it will need to re-run [Transformer.apply]. |
470 /// | 485 /// |
471 /// If there are no external changes by the time [Transformer.apply] finishes, | 486 /// This will transition to [APPLYING] once [Transformer.apply] finishes |
472 /// this will transition to [PROCESSING]. If the primary input changes, this | 487 /// running. |
473 /// will transition to [NEEDS_IS_PRIMARY]. | 488 static final NEEDS_APPLY = const _State._("needs apply"); |
474 static final NEEDS_APPLY = const _TransformNodeState._("needs apply"); | |
475 | 489 |
476 /// The transform has finished running [Transformer.apply], whether or not it | 490 /// The transform has finished running [Transformer.apply], whether or not it |
477 /// emitted an error. | 491 /// emitted an error. |
478 /// | 492 /// |
479 /// If the primary input or a secondary input changes, this will transition to | 493 /// If the transformer is lazy, the [TransformNode] can also be in this state |
480 /// [PROCESSING]. | 494 /// when [Transformer.declareOutputs] has been run but [Transformer.apply] has |
481 static final APPLIED = const _TransformNodeState._("applied"); | 495 /// not. |
| 496 /// |
| 497 /// If an input changes, this will transition to [APPLYING]. |
| 498 static final APPLIED = const _State._("applied"); |
482 | 499 |
483 /// The transform has finished running [Transformer.isPrimary], which returned | 500 /// The transform has finished running [Transformer.isPrimary], which returned |
484 /// `false`. | 501 /// `false`. |
485 /// | 502 /// |
486 /// If the primary input changes, this will transition to [PROCESSING]. | 503 /// This will never transition to another state. |
487 static final NOT_PRIMARY = const _TransformNodeState._("not primary"); | 504 static final NOT_PRIMARY = const _State._("not primary"); |
488 | |
489 /// Whether [this] is [PROCESSING]. | |
490 bool get isProcessing => this == _TransformNodeState.PROCESSING; | |
491 | |
492 /// Whether [this] is [NEEDS_IS_PRIMARY]. | |
493 bool get needsIsPrimary => this == _TransformNodeState.NEEDS_IS_PRIMARY; | |
494 | |
495 /// Whether [this] is [NEEDS_APPLY]. | |
496 bool get needsApply => this == _TransformNodeState.NEEDS_APPLY; | |
497 | |
498 /// Whether [this] is [APPLIED]. | |
499 bool get isApplied => this == _TransformNodeState.APPLIED; | |
500 | |
501 /// Whether [this] is [NOT_PRIMARY]. | |
502 bool get isNotPrimary => this == _TransformNodeState.NOT_PRIMARY; | |
503 | |
504 /// Whether the transform has finished running [Transformer.isPrimary] and | |
505 /// [Transformer.apply]. | |
506 /// | |
507 /// Specifically, whether [this] is [APPLIED] or [NOT_PRIMARY]. | |
508 bool get isDone => isApplied || isNotPrimary; | |
509 | 505 |
510 final String name; | 506 final String name; |
511 | 507 |
512 const _TransformNodeState._(this.name); | 508 const _State._(this.name); |
513 | 509 |
514 String toString() => name; | 510 String toString() => name; |
515 } | 511 } |
OLD | NEW |