OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 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. |
| 4 |
| 5 library pub.barback.build_environment; |
| 6 |
| 7 import 'dart:async'; |
| 8 |
| 9 import 'package:barback/barback.dart'; |
| 10 import 'package:path/path.dart' as path; |
| 11 import 'package:stack_trace/stack_trace.dart'; |
| 12 import 'package:watcher/watcher.dart'; |
| 13 |
| 14 import '../entrypoint.dart'; |
| 15 import '../io.dart'; |
| 16 import '../log.dart' as log; |
| 17 import '../package.dart'; |
| 18 import '../package_graph.dart'; |
| 19 import '../utils.dart'; |
| 20 import 'dart_forwarding_transformer.dart'; |
| 21 import 'dart2js_transformer.dart'; |
| 22 import 'load_all_transformers.dart'; |
| 23 import 'pub_package_provider.dart'; |
| 24 import 'server.dart'; |
| 25 |
| 26 /// The entire "visible" state of the assets of a package and all of its |
| 27 /// dependencies, taking into account the user's configuration when running pub. |
| 28 /// |
| 29 /// Where [PackageGraph] just describes the entrypoint's dependencies as |
| 30 /// specified by pubspecs, this includes "transient" information like the mode |
| 31 /// that the user is running pub in, or which directories they want to build. |
| 32 class BuildEnvironment { |
| 33 /// Creates a new build environment for working with the assets used by |
| 34 /// [entrypoint] and its dependencies. |
| 35 /// |
| 36 /// Spawns an HTTP server on [hostname] and [port]. Loads all used |
| 37 /// transformers using [mode] (including dart2js if [useDart2JS] is true). |
| 38 /// |
| 39 /// Includes [buildDirectories] in the root package, as well as "lib" and |
| 40 /// "asset". |
| 41 /// |
| 42 /// If [watcherType] is not [WatcherType.NONE], watches source assets for |
| 43 /// modification. |
| 44 /// |
| 45 /// Returns a [Future] that completes to the environment once the inputs, |
| 46 /// transformers, and server are loaded and ready. |
| 47 static Future<BuildEnvironment> create(Entrypoint entrypoint, |
| 48 String hostname, int port, BarbackMode mode, WatcherType watcherType, |
| 49 Set<String> buildDirectories, |
| 50 {bool useDart2JS: true}) { |
| 51 return entrypoint.loadPackageGraph().then((graph) { |
| 52 var barback = new Barback(new PubPackageProvider(graph)); |
| 53 barback.log.listen(_log); |
| 54 |
| 55 return BarbackServer.bind(hostname, port, barback, |
| 56 graph.entrypoint.root.name).then((server) { |
| 57 var environment = new BuildEnvironment._(graph, server, mode, |
| 58 watcherType, buildDirectories); |
| 59 |
| 60 // If the entrypoint package manually configures the dart2js |
| 61 // transformer, don't include it in the built-in transformer list. |
| 62 // |
| 63 // TODO(nweiz): if/when we support more built-in transformers, make |
| 64 // this more general. |
| 65 var containsDart2JS = graph.entrypoint.root.pubspec.transformers |
| 66 .any((transformers) => transformers |
| 67 .any((id) => id.package == '\$dart2js')); |
| 68 |
| 69 if (!containsDart2JS && useDart2JS) { |
| 70 environment._builtInTransformers.addAll([ |
| 71 new Dart2JSTransformer(environment, mode), |
| 72 new DartForwardingTransformer(mode) |
| 73 ]); |
| 74 } |
| 75 |
| 76 return environment._load(barback).then((_) => environment); |
| 77 }); |
| 78 }); |
| 79 } |
| 80 |
| 81 /// The server serving this environment's assets. |
| 82 final BarbackServer server; |
| 83 |
| 84 /// The [Barback] instance used to process assets in this environment. |
| 85 Barback get barback => server.barback; |
| 86 |
| 87 /// The root package being built. |
| 88 Package get rootPackage => graph.entrypoint.root; |
| 89 |
| 90 /// The underlying [PackageGraph] being built. |
| 91 final PackageGraph graph; |
| 92 |
| 93 /// The mode to run the transformers in. |
| 94 final BarbackMode mode; |
| 95 |
| 96 /// The [Transformer]s that should be appended by default to the root |
| 97 /// package's transformer cascade. Will be empty if there are none. |
| 98 final _builtInTransformers = <Transformer>[]; |
| 99 |
| 100 /// How source files should be watched. |
| 101 final WatcherType _watcherType; |
| 102 |
| 103 /// The set of top-level directories in the entrypoint package that should be |
| 104 /// built. |
| 105 final Set<String> _buildDirectories; |
| 106 |
| 107 BuildEnvironment._(this.graph, this.server, this.mode, this._watcherType, |
| 108 this._buildDirectories); |
| 109 |
| 110 /// Gets the built-in [Transformer]s that should be added to [package]. |
| 111 /// |
| 112 /// Returns `null` if there are none. |
| 113 Iterable<Transformer> getBuiltInTransformers(Package package) { |
| 114 // Built-in transformers only apply to the root package. |
| 115 if (package.name != rootPackage.name) return null; |
| 116 |
| 117 // The built-in transformers are for dart2js and forwarding assets around |
| 118 // dart2js. |
| 119 if (_builtInTransformers.isEmpty) return null; |
| 120 |
| 121 return _builtInTransformers; |
| 122 } |
| 123 |
| 124 /// Creates a [BarbackServer] for this environment. |
| 125 /// |
| 126 /// This transforms and serves all library and asset files in all packages in |
| 127 /// the environment's package graph. It loads any transformer plugins defined |
| 128 /// in packages in [graph] and re-runs them as necessary when any input files |
| 129 /// change. |
| 130 /// |
| 131 /// Returns a [Future] that completes once all inputs and transformers are |
| 132 /// loaded. |
| 133 Future _load(Barback barback) { |
| 134 return _provideSources(barback).then((_) { |
| 135 var completer = new Completer(); |
| 136 |
| 137 // If any errors get emitted either by barback or by the server, |
| 138 // including non-programmatic barback errors, they should take down the |
| 139 // whole program. |
| 140 var subscriptions = [ |
| 141 server.barback.errors.listen((error) { |
| 142 if (error is TransformerException) error = error.error; |
| 143 if (!completer.isCompleted) { |
| 144 completer.completeError(error, new Chain.current()); |
| 145 } |
| 146 }), |
| 147 server.barback.results.listen((_) {}, onError: (error, stackTrace) { |
| 148 if (completer.isCompleted) return; |
| 149 completer.completeError(error, stackTrace); |
| 150 }), |
| 151 server.results.listen((_) {}, onError: (error, stackTrace) { |
| 152 if (completer.isCompleted) return; |
| 153 completer.completeError(error, stackTrace); |
| 154 }) |
| 155 ]; |
| 156 |
| 157 loadAllTransformers(this).then((_) { |
| 158 if (!completer.isCompleted) completer.complete(); |
| 159 }).catchError((error, stackTrace) { |
| 160 if (!completer.isCompleted) { |
| 161 completer.completeError(error, stackTrace); |
| 162 } |
| 163 }); |
| 164 |
| 165 return completer.future.whenComplete(() { |
| 166 for (var subscription in subscriptions) { |
| 167 subscription.cancel(); |
| 168 } |
| 169 }); |
| 170 }); |
| 171 } |
| 172 |
| 173 /// Provides all of the source assets in the environment to barback. |
| 174 /// |
| 175 /// If [watcherType] is not [WatcherType.NONE], enables watching on them. |
| 176 Future _provideSources(Barback barback) { |
| 177 if (_watcherType != WatcherType.NONE) { |
| 178 return _watchSources(barback); |
| 179 } |
| 180 |
| 181 return syncFuture(() { |
| 182 _loadSources(barback); |
| 183 }); |
| 184 } |
| 185 |
| 186 /// Provides all of the source assets in the environment to barback. |
| 187 void _loadSources(Barback barback) { |
| 188 for (var package in graph.packages.values) { |
| 189 barback.updateSources(_listAssets(graph.entrypoint, package)); |
| 190 } |
| 191 } |
| 192 |
| 193 /// Adds all of the source assets in this environment to barback and then |
| 194 /// watches the public directories for changes. |
| 195 /// |
| 196 /// Returns a Future that completes when the sources are loaded and the |
| 197 /// watchers are active. |
| 198 Future _watchSources(Barback barback) { |
| 199 return Future.wait(graph.packages.values.map((package) { |
| 200 // If this package comes from a cached source, its contents won't change |
| 201 // so we don't need to monitor it. `packageId` will be null for the |
| 202 // application package, since that's not locked. |
| 203 var packageId = graph.lockFile.packages[package.name]; |
| 204 if (packageId != null && |
| 205 graph.entrypoint.cache.sources[packageId.source].shouldCache) { |
| 206 barback.updateSources(_listAssets(graph.entrypoint, package)); |
| 207 return new Future.value(); |
| 208 } |
| 209 |
| 210 // Watch the visible package directories for changes. |
| 211 return Future.wait(_getPublicDirectories(graph.entrypoint, package) |
| 212 .map((name) { |
| 213 var subdirectory = path.join(package.dir, name); |
| 214 if (!dirExists(subdirectory)) return new Future.value(); |
| 215 |
| 216 // TODO(nweiz): close these watchers when [barback] is closed. |
| 217 var watcher = _watcherType.create(subdirectory); |
| 218 watcher.events.listen((event) { |
| 219 // Don't watch files symlinked into these directories. |
| 220 // TODO(rnystrom): If pub gets rid of symlinks, remove this. |
| 221 var parts = path.split(event.path); |
| 222 if (parts.contains("packages") || parts.contains("assets")) return; |
| 223 |
| 224 // Skip ".js" files that were (most likely) compiled from nearby |
| 225 // ".dart" files. These are created by the Editor's "Run as |
| 226 // JavaScript" command and are written directly into the package's |
| 227 // directory. When pub's dart2js transformer then tries to create the |
| 228 // same file name, we get a build error. To avoid that, just don't |
| 229 // consider that file to be a source. |
| 230 // TODO(rnystrom): Remove this when the Editor no longer generates |
| 231 // .js files. See #15859. |
| 232 if (event.path.endsWith(".dart.js")) return; |
| 233 |
| 234 var id = new AssetId(package.name, |
| 235 path.relative(event.path, from: package.dir)); |
| 236 if (event.type == ChangeType.REMOVE) { |
| 237 barback.removeSources([id]); |
| 238 } else { |
| 239 barback.updateSources([id]); |
| 240 } |
| 241 }); |
| 242 return watcher.ready; |
| 243 })).then((_) { |
| 244 barback.updateSources(_listAssets(graph.entrypoint, package)); |
| 245 }); |
| 246 })); |
| 247 } |
| 248 |
| 249 /// Lists all of the visible files in [package]. |
| 250 /// |
| 251 /// This is the recursive contents of the "asset" and "lib" directories (if |
| 252 /// present). If [package] is the entrypoint package, it also includes the |
| 253 /// contents of "web". |
| 254 List<AssetId> _listAssets(Entrypoint entrypoint, Package package) { |
| 255 var files = <AssetId>[]; |
| 256 |
| 257 for (var dirPath in _getPublicDirectories(entrypoint, package)) { |
| 258 var dir = path.join(package.dir, dirPath); |
| 259 if (!dirExists(dir)) continue; |
| 260 for (var entry in listDir(dir, recursive: true)) { |
| 261 // Ignore "packages" symlinks if there. |
| 262 if (path.split(entry).contains("packages")) continue; |
| 263 |
| 264 // Skip directories. |
| 265 if (!fileExists(entry)) continue; |
| 266 |
| 267 // Skip ".js" files that were (most likely) compiled from nearby ".dart" |
| 268 // files. These are created by the Editor's "Run as JavaScript" command |
| 269 // and are written directly into the package's directory. When pub's |
| 270 // dart2js transformer then tries to create the same file name, we get |
| 271 // a build error. To avoid that, just don't consider that file to be a |
| 272 // source. |
| 273 // TODO(rnystrom): Remove this when the Editor no longer generates .js |
| 274 // files. See #15859. |
| 275 if (entry.endsWith(".dart.js")) continue; |
| 276 |
| 277 var id = new AssetId(package.name, |
| 278 path.relative(entry, from: package.dir)); |
| 279 files.add(id); |
| 280 } |
| 281 } |
| 282 |
| 283 return files; |
| 284 } |
| 285 |
| 286 /// Gets the names of the top-level directories in [package] whose contents |
| 287 /// should be provided as source assets. |
| 288 Iterable<String> _getPublicDirectories(Entrypoint entrypoint, |
| 289 Package package) { |
| 290 var directories = ["asset", "lib"]; |
| 291 |
| 292 if (package.name == entrypoint.root.name) { |
| 293 directories.addAll(_buildDirectories); |
| 294 } |
| 295 |
| 296 return directories; |
| 297 } |
| 298 } |
| 299 |
| 300 /// Log [entry] using Pub's logging infrastructure. |
| 301 /// |
| 302 /// Since both [LogEntry] objects and the message itself often redundantly |
| 303 /// show the same context like the file where an error occurred, this tries |
| 304 /// to avoid showing redundant data in the entry. |
| 305 void _log(LogEntry entry) { |
| 306 messageMentions(String text) { |
| 307 return entry.message.toLowerCase().contains(text.toLowerCase()); |
| 308 } |
| 309 |
| 310 var prefixParts = []; |
| 311 |
| 312 // Show the level (unless the message mentions it). |
| 313 if (!messageMentions(entry.level.name)) { |
| 314 prefixParts.add("${entry.level} from"); |
| 315 } |
| 316 |
| 317 // Show the transformer. |
| 318 prefixParts.add(entry.transform.transformer); |
| 319 |
| 320 // Mention the primary input of the transform unless the message seems to. |
| 321 if (!messageMentions(entry.transform.primaryId.path)) { |
| 322 prefixParts.add("on ${entry.transform.primaryId}"); |
| 323 } |
| 324 |
| 325 // If the relevant asset isn't the primary input, mention it unless the |
| 326 // message already does. |
| 327 if (entry.assetId != entry.transform.primaryId && |
| 328 !messageMentions(entry.assetId.path)) { |
| 329 prefixParts.add("with input ${entry.assetId}"); |
| 330 } |
| 331 |
| 332 var prefix = "[${prefixParts.join(' ')}]:"; |
| 333 var message = entry.message; |
| 334 if (entry.span != null) { |
| 335 message = entry.span.getLocationMessage(entry.message); |
| 336 } |
| 337 |
| 338 switch (entry.level) { |
| 339 case LogLevel.ERROR: |
| 340 log.error("${log.red(prefix)}\n$message"); |
| 341 break; |
| 342 |
| 343 case LogLevel.WARNING: |
| 344 log.warning("${log.yellow(prefix)}\n$message"); |
| 345 break; |
| 346 |
| 347 case LogLevel.INFO: |
| 348 log.message("${log.cyan(prefix)}\n$message"); |
| 349 break; |
| 350 } |
| 351 } |
| 352 |
| 353 /// An enum describing different modes of constructing a [DirectoryWatcher]. |
| 354 abstract class WatcherType { |
| 355 /// A watcher that automatically chooses its type based on the operating |
| 356 /// system. |
| 357 static const AUTO = const _AutoWatcherType(); |
| 358 |
| 359 /// A watcher that always polls the filesystem for changes. |
| 360 static const POLLING = const _PollingWatcherType(); |
| 361 |
| 362 /// No directory watcher at all. |
| 363 static const NONE = const _NoneWatcherType(); |
| 364 |
| 365 /// Creates a new DirectoryWatcher. |
| 366 DirectoryWatcher create(String directory); |
| 367 |
| 368 String toString(); |
| 369 } |
| 370 |
| 371 class _AutoWatcherType implements WatcherType { |
| 372 const _AutoWatcherType(); |
| 373 |
| 374 DirectoryWatcher create(String directory) => |
| 375 new DirectoryWatcher(directory); |
| 376 |
| 377 String toString() => "auto"; |
| 378 } |
| 379 |
| 380 class _PollingWatcherType implements WatcherType { |
| 381 const _PollingWatcherType(); |
| 382 |
| 383 DirectoryWatcher create(String directory) => |
| 384 new PollingDirectoryWatcher(directory); |
| 385 |
| 386 String toString() => "polling"; |
| 387 } |
| 388 |
| 389 class _NoneWatcherType implements WatcherType { |
| 390 const _NoneWatcherType(); |
| 391 |
| 392 DirectoryWatcher create(String directory) => null; |
| 393 |
| 394 String toString() => "none"; |
| 395 } |
OLD | NEW |