| // Copyright (c) 2014, 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. |
| |
| library pub.barback.build_environment; |
| |
| import 'dart:async'; |
| |
| import 'package:barback/barback.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:stack_trace/stack_trace.dart'; |
| import 'package:watcher/watcher.dart'; |
| |
| import '../entrypoint.dart'; |
| import '../io.dart'; |
| import '../log.dart' as log; |
| import '../package.dart'; |
| import '../package_graph.dart'; |
| import '../utils.dart'; |
| import 'dart_forwarding_transformer.dart'; |
| import 'dart2js_transformer.dart'; |
| import 'load_all_transformers.dart'; |
| import 'pub_package_provider.dart'; |
| import 'server.dart'; |
| |
| /// The entire "visible" state of the assets of a package and all of its |
| /// dependencies, taking into account the user's configuration when running pub. |
| /// |
| /// Where [PackageGraph] just describes the entrypoint's dependencies as |
| /// specified by pubspecs, this includes "transient" information like the mode |
| /// that the user is running pub in, or which directories they want to build. |
| class BuildEnvironment { |
| /// Creates a new build environment for working with the assets used by |
| /// [entrypoint] and its dependencies. |
| /// |
| /// Spawns an HTTP server on [hostname] and [port]. Loads all used |
| /// transformers using [mode] (including dart2js if [useDart2JS] is true). |
| /// |
| /// Includes [buildDirectories] in the root package, as well as "lib" and |
| /// "asset". |
| /// |
| /// If [watcherType] is not [WatcherType.NONE], watches source assets for |
| /// modification. |
| /// |
| /// Returns a [Future] that completes to the environment once the inputs, |
| /// transformers, and server are loaded and ready. |
| static Future<BuildEnvironment> create(Entrypoint entrypoint, |
| String hostname, int port, BarbackMode mode, WatcherType watcherType, |
| Set<String> buildDirectories, |
| {bool useDart2JS: true}) { |
| return entrypoint.loadPackageGraph().then((graph) { |
| var barback = new Barback(new PubPackageProvider(graph)); |
| barback.log.listen(_log); |
| |
| return BarbackServer.bind(hostname, port, barback, |
| graph.entrypoint.root.name).then((server) { |
| var environment = new BuildEnvironment._(graph, server, mode, |
| watcherType, buildDirectories); |
| |
| // If the entrypoint package manually configures the dart2js |
| // transformer, don't include it in the built-in transformer list. |
| // |
| // TODO(nweiz): if/when we support more built-in transformers, make |
| // this more general. |
| var containsDart2JS = graph.entrypoint.root.pubspec.transformers |
| .any((transformers) => transformers |
| .any((id) => id.package == '\$dart2js')); |
| |
| if (!containsDart2JS && useDart2JS) { |
| environment._builtInTransformers.addAll([ |
| new Dart2JSTransformer(environment, mode), |
| new DartForwardingTransformer(mode) |
| ]); |
| } |
| |
| return environment._load(barback).then((_) => environment); |
| }); |
| }); |
| } |
| |
| /// The server serving this environment's assets. |
| final BarbackServer server; |
| |
| /// The [Barback] instance used to process assets in this environment. |
| Barback get barback => server.barback; |
| |
| /// The root package being built. |
| Package get rootPackage => graph.entrypoint.root; |
| |
| /// The underlying [PackageGraph] being built. |
| final PackageGraph graph; |
| |
| /// The mode to run the transformers in. |
| final BarbackMode mode; |
| |
| /// The [Transformer]s that should be appended by default to the root |
| /// package's transformer cascade. Will be empty if there are none. |
| final _builtInTransformers = <Transformer>[]; |
| |
| /// How source files should be watched. |
| final WatcherType _watcherType; |
| |
| /// The set of top-level directories in the entrypoint package that should be |
| /// built. |
| final Set<String> _buildDirectories; |
| |
| BuildEnvironment._(this.graph, this.server, this.mode, this._watcherType, |
| this._buildDirectories); |
| |
| /// Gets the built-in [Transformer]s that should be added to [package]. |
| /// |
| /// Returns `null` if there are none. |
| Iterable<Transformer> getBuiltInTransformers(Package package) { |
| // Built-in transformers only apply to the root package. |
| if (package.name != rootPackage.name) return null; |
| |
| // The built-in transformers are for dart2js and forwarding assets around |
| // dart2js. |
| if (_builtInTransformers.isEmpty) return null; |
| |
| return _builtInTransformers; |
| } |
| |
| /// Creates a [BarbackServer] for this environment. |
| /// |
| /// This transforms and serves all library and asset files in all packages in |
| /// the environment's package graph. It loads any transformer plugins defined |
| /// in packages in [graph] and re-runs them as necessary when any input files |
| /// change. |
| /// |
| /// Returns a [Future] that completes once all inputs and transformers are |
| /// loaded. |
| Future _load(Barback barback) { |
| return _provideSources(barback).then((_) { |
| var completer = new Completer(); |
| |
| // If any errors get emitted either by barback or by the server, |
| // including non-programmatic barback errors, they should take down the |
| // whole program. |
| var subscriptions = [ |
| server.barback.errors.listen((error) { |
| if (error is TransformerException) error = error.error; |
| if (!completer.isCompleted) { |
| completer.completeError(error, new Chain.current()); |
| } |
| }), |
| server.barback.results.listen((_) {}, onError: (error, stackTrace) { |
| if (completer.isCompleted) return; |
| completer.completeError(error, stackTrace); |
| }), |
| server.results.listen((_) {}, onError: (error, stackTrace) { |
| if (completer.isCompleted) return; |
| completer.completeError(error, stackTrace); |
| }) |
| ]; |
| |
| loadAllTransformers(this).then((_) { |
| if (!completer.isCompleted) completer.complete(); |
| }).catchError((error, stackTrace) { |
| if (!completer.isCompleted) { |
| completer.completeError(error, stackTrace); |
| } |
| }); |
| |
| return completer.future.whenComplete(() { |
| for (var subscription in subscriptions) { |
| subscription.cancel(); |
| } |
| }); |
| }); |
| } |
| |
| /// Provides all of the source assets in the environment to barback. |
| /// |
| /// If [watcherType] is not [WatcherType.NONE], enables watching on them. |
| Future _provideSources(Barback barback) { |
| if (_watcherType != WatcherType.NONE) { |
| return _watchSources(barback); |
| } |
| |
| return syncFuture(() { |
| _loadSources(barback); |
| }); |
| } |
| |
| /// Provides all of the source assets in the environment to barback. |
| void _loadSources(Barback barback) { |
| for (var package in graph.packages.values) { |
| barback.updateSources(_listAssets(graph.entrypoint, package)); |
| } |
| } |
| |
| /// Adds all of the source assets in this environment to barback and then |
| /// watches the public directories for changes. |
| /// |
| /// Returns a Future that completes when the sources are loaded and the |
| /// watchers are active. |
| Future _watchSources(Barback barback) { |
| return Future.wait(graph.packages.values.map((package) { |
| // If this package comes from a cached source, its contents won't change |
| // so we don't need to monitor it. `packageId` will be null for the |
| // application package, since that's not locked. |
| var packageId = graph.lockFile.packages[package.name]; |
| if (packageId != null && |
| graph.entrypoint.cache.sources[packageId.source].shouldCache) { |
| barback.updateSources(_listAssets(graph.entrypoint, package)); |
| return new Future.value(); |
| } |
| |
| // Watch the visible package directories for changes. |
| return Future.wait(_getPublicDirectories(graph.entrypoint, package) |
| .map((name) { |
| var subdirectory = path.join(package.dir, name); |
| if (!dirExists(subdirectory)) return new Future.value(); |
| |
| // TODO(nweiz): close these watchers when [barback] is closed. |
| var watcher = _watcherType.create(subdirectory); |
| watcher.events.listen((event) { |
| // Don't watch files symlinked into these directories. |
| // TODO(rnystrom): If pub gets rid of symlinks, remove this. |
| var parts = path.split(event.path); |
| if (parts.contains("packages") || parts.contains("assets")) return; |
| |
| // Skip ".js" files that were (most likely) compiled from nearby |
| // ".dart" files. These are created by the Editor's "Run as |
| // JavaScript" command and are written directly into the package's |
| // directory. When pub's dart2js transformer then tries to create the |
| // same file name, we get a build error. To avoid that, just don't |
| // consider that file to be a source. |
| // TODO(rnystrom): Remove this when the Editor no longer generates |
| // .js files. See #15859. |
| if (event.path.endsWith(".dart.js")) return; |
| |
| var id = new AssetId(package.name, |
| path.relative(event.path, from: package.dir)); |
| if (event.type == ChangeType.REMOVE) { |
| barback.removeSources([id]); |
| } else { |
| barback.updateSources([id]); |
| } |
| }); |
| return watcher.ready; |
| })).then((_) { |
| barback.updateSources(_listAssets(graph.entrypoint, package)); |
| }); |
| })); |
| } |
| |
| /// Lists all of the visible files in [package]. |
| /// |
| /// This is the recursive contents of the "asset" and "lib" directories (if |
| /// present). If [package] is the entrypoint package, it also includes the |
| /// contents of "web". |
| List<AssetId> _listAssets(Entrypoint entrypoint, Package package) { |
| var files = <AssetId>[]; |
| |
| for (var dirPath in _getPublicDirectories(entrypoint, package)) { |
| var dir = path.join(package.dir, dirPath); |
| if (!dirExists(dir)) continue; |
| for (var entry in listDir(dir, recursive: true)) { |
| // Ignore "packages" symlinks if there. |
| if (path.split(entry).contains("packages")) continue; |
| |
| // Skip directories. |
| if (!fileExists(entry)) continue; |
| |
| // Skip ".js" files that were (most likely) compiled from nearby ".dart" |
| // files. These are created by the Editor's "Run as JavaScript" command |
| // and are written directly into the package's directory. When pub's |
| // dart2js transformer then tries to create the same file name, we get |
| // a build error. To avoid that, just don't consider that file to be a |
| // source. |
| // TODO(rnystrom): Remove this when the Editor no longer generates .js |
| // files. See #15859. |
| if (entry.endsWith(".dart.js")) continue; |
| |
| var id = new AssetId(package.name, |
| path.relative(entry, from: package.dir)); |
| files.add(id); |
| } |
| } |
| |
| return files; |
| } |
| |
| /// Gets the names of the top-level directories in [package] whose contents |
| /// should be provided as source assets. |
| Iterable<String> _getPublicDirectories(Entrypoint entrypoint, |
| Package package) { |
| var directories = ["asset", "lib"]; |
| |
| if (package.name == entrypoint.root.name) { |
| directories.addAll(_buildDirectories); |
| } |
| |
| return directories; |
| } |
| } |
| |
| /// Log [entry] using Pub's logging infrastructure. |
| /// |
| /// Since both [LogEntry] objects and the message itself often redundantly |
| /// show the same context like the file where an error occurred, this tries |
| /// to avoid showing redundant data in the entry. |
| void _log(LogEntry entry) { |
| messageMentions(String text) { |
| return entry.message.toLowerCase().contains(text.toLowerCase()); |
| } |
| |
| var prefixParts = []; |
| |
| // Show the level (unless the message mentions it). |
| if (!messageMentions(entry.level.name)) { |
| prefixParts.add("${entry.level} from"); |
| } |
| |
| // Show the transformer. |
| prefixParts.add(entry.transform.transformer); |
| |
| // Mention the primary input of the transform unless the message seems to. |
| if (!messageMentions(entry.transform.primaryId.path)) { |
| prefixParts.add("on ${entry.transform.primaryId}"); |
| } |
| |
| // If the relevant asset isn't the primary input, mention it unless the |
| // message already does. |
| if (entry.assetId != entry.transform.primaryId && |
| !messageMentions(entry.assetId.path)) { |
| prefixParts.add("with input ${entry.assetId}"); |
| } |
| |
| var prefix = "[${prefixParts.join(' ')}]:"; |
| var message = entry.message; |
| if (entry.span != null) { |
| message = entry.span.getLocationMessage(entry.message); |
| } |
| |
| switch (entry.level) { |
| case LogLevel.ERROR: |
| log.error("${log.red(prefix)}\n$message"); |
| break; |
| |
| case LogLevel.WARNING: |
| log.warning("${log.yellow(prefix)}\n$message"); |
| break; |
| |
| case LogLevel.INFO: |
| log.message("${log.cyan(prefix)}\n$message"); |
| break; |
| } |
| } |
| |
| /// An enum describing different modes of constructing a [DirectoryWatcher]. |
| abstract class WatcherType { |
| /// A watcher that automatically chooses its type based on the operating |
| /// system. |
| static const AUTO = const _AutoWatcherType(); |
| |
| /// A watcher that always polls the filesystem for changes. |
| static const POLLING = const _PollingWatcherType(); |
| |
| /// No directory watcher at all. |
| static const NONE = const _NoneWatcherType(); |
| |
| /// Creates a new DirectoryWatcher. |
| DirectoryWatcher create(String directory); |
| |
| String toString(); |
| } |
| |
| class _AutoWatcherType implements WatcherType { |
| const _AutoWatcherType(); |
| |
| DirectoryWatcher create(String directory) => |
| new DirectoryWatcher(directory); |
| |
| String toString() => "auto"; |
| } |
| |
| class _PollingWatcherType implements WatcherType { |
| const _PollingWatcherType(); |
| |
| DirectoryWatcher create(String directory) => |
| new PollingDirectoryWatcher(directory); |
| |
| String toString() => "polling"; |
| } |
| |
| class _NoneWatcherType implements WatcherType { |
| const _NoneWatcherType(); |
| |
| DirectoryWatcher create(String directory) => null; |
| |
| String toString() => "none"; |
| } |