From 79db57870806527e0ceb745785f80ce0bded53b9 Mon Sep 17 00:00:00 2001
From: "rnystrom@google.com" <rnystrom@google.com>
Date: Wed, 29 Jan 2014 19:03:57 +0000
Subject: [PATCH] Support directories other than "web" in pub build.

BUG=https://code.google.com/p/dart/issues/detail?id=14673
R=nweiz@google.com

Review URL: https://codereview.chromium.org//141113011

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge@32131 260f80e4-7a28-3924-810f-c04153c831b5
---
 bin/pub.dart                                  |   3 +-
 lib/src/barback.dart                          | 178 ++------
 lib/src/barback/build_environment.dart        | 395 ++++++++++++++++++
 lib/src/barback/dart2js_transformer.dart      |  46 +-
 .../barback/dart_forwarding_transformer.dart  |   8 +-
 lib/src/barback/load_all_transformers.dart    |  62 ++-
 lib/src/barback/load_transformers.dart        |  14 +-
 lib/src/barback/server.dart                   |   8 +-
 lib/src/barback/sources.dart                  | 172 --------
 lib/src/command.dart                          |   2 +-
 lib/src/command/build.dart                    | 220 +++++++---
 lib/src/command/serve.dart                    |  33 +-
 lib/src/exit_codes.dart                       |   3 +
 lib/src/io.dart                               |   9 +-
 ...cludes_all_buildable_directories_test.dart |  59 +++
 ...ll_with_no_buildable_directories_test.dart |  21 +
 test/build/allows_arbitrary_modes_test.dart   |   7 +-
 .../allows_multiple_dir_name_args_test.dart   |  40 ++
 .../cleans_entire_build_directory_test.dart   |  39 ++
 ...ompiles_entrypoints_in_benchmark_test.dart |  50 +++
 .../compiles_entrypoints_in_example_test.dart |  50 +++
 .../compiles_entrypoints_in_test_test.dart    |  50 +++
 ... => compiles_entrypoints_in_web_test.dart} |  31 +-
 ...s_browser_js_next_to_entrypoints_test.dart |  56 ++-
 .../copies_non_dart_files_to_build_test.dart  |  13 +-
 ...js_finds_imports_across_packages_test.dart |  11 +-
 test/build/defaults_to_release_mode_test.dart |   7 +-
 .../does_not_allow_args_with_all_test.dart    |  21 +
 test/build/handles_long_paths_test.dart       |  13 +-
 .../ignores_entrypoints_in_asset_test.dart    |  34 ++
 .../ignores_entrypoints_in_bin_test.dart      |  34 ++
 .../ignores_entrypoints_in_lib_test.dart      |  34 ++
 ...nores_existing_compiled_js_files_test.dart |  11 +-
 ...gnores_non_entrypoint_dart_files_test.dart |  12 +-
 ...ncludes_assets_from_dependencies_test.dart |  35 +-
 ...ncludes_dart_files_in_debug_mode_test.dart |  21 +-
 .../build/missing_build_directories_test.dart |  27 ++
 ...t.dart => missing_web_directory_test.dart} |  10 +-
 ...e_arg_builds_only_that_directory_test.dart |  43 ++
 .../build/reports_dart_parse_errors_test.dart |   4 +-
 .../unsupported_build_directories_test.dart   |  21 +
 test/build/warns_on_assets_paths_test.dart    |  15 +-
 .../archives_and_uploads_a_package_test.dart  |   3 +-
 ..._not_publish_if_there_are_errors_test.dart |   3 +-
 ..._tests_are_no_warnings_or_errors_test.dart |   3 +-
 ..._publishes_if_there_are_warnings_test.dart |   3 +-
 ...ckage_creation_provides_an_error_test.dart |   1 +
 ...tion_has_a_warning_and_continues_test.dart |   3 +-
 ...on_has_a_warning_and_is_canceled_test.dart |   3 +-
 .../package_validation_has_an_error_test.dart |   3 +-
 ...package_validation_has_a_warning_test.dart |   3 +-
 ...ckage_validation_has_no_warnings_test.dart |   3 +-
 test/pub_uploader_test.dart                   |  13 +-
 test/real_version_test.dart                   |   7 +-
 test/test_pub.dart                            |   3 +-
 ...ify_configuration_overrides_mode_test.dart |   2 -
 ..._can_be_consumed_by_successive_phases.dart |   2 -
 ...upports_configuration_with_build_test.dart |  91 ++++
 .../mode_defaults_to_debug_in_serve_test.dart |   2 -
 ...ode_defaults_to_release_in_build_test.dart |   7 +-
 60 files changed, 1456 insertions(+), 621 deletions(-)
 create mode 100644 lib/src/barback/build_environment.dart
 delete mode 100644 lib/src/barback/sources.dart
 create mode 100644 test/build/all_includes_all_buildable_directories_test.dart
 create mode 100644 test/build/all_with_no_buildable_directories_test.dart
 create mode 100644 test/build/allows_multiple_dir_name_args_test.dart
 create mode 100644 test/build/cleans_entire_build_directory_test.dart
 create mode 100644 test/build/compiles_entrypoints_in_benchmark_test.dart
 create mode 100644 test/build/compiles_entrypoints_in_example_test.dart
 create mode 100644 test/build/compiles_entrypoints_in_test_test.dart
 rename test/build/{compiles_dart_entrypoints_to_js_test.dart => compiles_entrypoints_in_web_test.dart} (53%)
 create mode 100644 test/build/does_not_allow_args_with_all_test.dart
 create mode 100644 test/build/ignores_entrypoints_in_asset_test.dart
 create mode 100644 test/build/ignores_entrypoints_in_bin_test.dart
 create mode 100644 test/build/ignores_entrypoints_in_lib_test.dart
 create mode 100644 test/build/missing_build_directories_test.dart
 rename test/build/{with_no_web_directory_test.dart => missing_web_directory_test.dart} (51%)
 create mode 100644 test/build/name_arg_builds_only_that_directory_test.dart
 create mode 100644 test/build/unsupported_build_directories_test.dart
 create mode 100644 test/transformer/dart2js/supports_configuration_with_build_test.dart

diff --git a/bin/pub.dart b/bin/pub.dart
index 777105ba..a589dcc8 100644
--- a/bin/pub.dart
+++ b/bin/pub.dart
@@ -19,7 +19,8 @@ void main(List<String> arguments) {
   ArgResults options;
 
   try {
-    options = PubCommand.pubArgParser.parse(arguments);
+    options = PubCommand.pubArgParser.parse(arguments,
+        allowTrailingOptions: true);
   } on FormatException catch (e) {
     log.error(e.message);
     log.error('Run "pub help" to see available options.');
diff --git a/lib/src/barback.dart b/lib/src/barback.dart
index 785d32f1..ae69197f 100644
--- a/lib/src/barback.dart
+++ b/lib/src/barback.dart
@@ -8,20 +8,10 @@ import 'dart:async';
 
 import 'package:barback/barback.dart';
 import 'package:path/path.dart' as path;
-import 'package:stack_trace/stack_trace.dart';
-
-import 'barback/dart2js_transformer.dart';
-import 'barback/load_all_transformers.dart';
-import 'barback/pub_package_provider.dart';
-import 'barback/server.dart';
-import 'barback/sources.dart';
-import 'log.dart' as log;
-import 'package_graph.dart';
+
 import 'utils.dart';
 import 'version.dart';
 
-export 'barback/sources.dart' show WatcherType;
-
 /// The currently supported version of the Barback package that this version of
 /// pub works with.
 ///
@@ -124,85 +114,6 @@ class TransformerId {
   }
 }
 
-/// Creates a [BarbackServer] serving on [host] and [port].
-///
-/// This transforms and serves all library and asset files in all packages in
-/// [graph]. It loads any transformer plugins defined in packages in [graph] and
-/// re-runs them as necessary when any input files change.
-///
-/// If [builtInTransformers] is provided, then a phase is added to the end of
-/// each package's cascade including those transformers.
-///
-/// If [watchForUpdates] is true (the default), the server will continually
-/// monitor the app and its dependencies for any updates. Otherwise the state of
-/// the app when the server is started will be maintained.
-Future<BarbackServer> createServer(String host, int port, PackageGraph graph,
-    BarbackMode mode, {Iterable<Transformer> builtInTransformers,
-    WatcherType watcher: WatcherType.AUTO}) {
-
-  // If the entrypoint package manually configures the dart2js transformer,
-  // remove it from 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) {
-    builtInTransformers = builtInTransformers.where(
-        (transformer) => transformer is! Dart2JSTransformer);
-  }
-
-  var provider = new PubPackageProvider(graph);
-  var barback = new Barback(provider);
-
-  barback.log.listen(_log);
-
-  return BarbackServer.bind(host, port, barback, graph.entrypoint.root.name)
-      .then((server) {
-    return syncFuture(() {
-      if (watcher != WatcherType.NONE) {
-        return watchSources(graph, barback, watcher);
-      }
-
-      loadSources(graph, 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(server, graph, mode, builtInTransformers).then((_) {
-        if (!completer.isCompleted) completer.complete(server);
-      }).catchError((error, stackTrace) {
-        if (!completer.isCompleted) completer.completeError(error, stackTrace);
-      });
-
-      return completer.future.whenComplete(() {
-        for (var subscription in subscriptions) {
-          subscription.cancel();
-        }
-      });
-    });
-  });
-}
-
 /// Converts [id] to a "package:" URI.
 ///
 /// This will throw an [ArgumentError] if [id] doesn't represent a library in
@@ -268,7 +179,9 @@ AssetId specialUrlToId(Uri url) {
 ///     foo|web/
 ///
 /// Throws a [FormatException] if [id] is not a valid public asset.
-String idtoUrlPath(String entrypoint, AssetId id) {
+// TODO(rnystrom): Get rid of [useWebAsRoot] once pub serve also serves out of
+// the package root directory.
+String idtoUrlPath(String entrypoint, AssetId id, {bool useWebAsRoot: true}) {
   var parts = path.url.split(id.path);
 
   if (parts.length < 2) {
@@ -276,78 +189,37 @@ String idtoUrlPath(String entrypoint, AssetId id) {
         "Can not serve assets from top-level directory.");
   }
 
-  // Each top-level directory gets handled differently.
+  // Map "asset" and "lib" to their shared directories.
   var dir = parts[0];
-  parts = parts.skip(1);
-
-  switch (dir) {
-    case "asset":
-      return path.url.join("/", "assets", id.package, path.url.joinAll(parts));
-
-    case "lib":
-      return path.url.join("/", "packages", id.package, path.url.joinAll(parts));
+  var rest = parts.skip(1);
 
-    case "web":
-      if (id.package != entrypoint) {
-        throw new FormatException(
-            'Cannot access "web" directory of non-root packages.');
-      }
-      return path.url.join("/", path.url.joinAll(parts));
-
-    default:
-      throw new FormatException('Cannot access assets from "$dir".');
+  if (dir == "asset") {
+    return path.url.join("/", "assets", id.package, path.url.joinAll(rest));
   }
-}
 
-/// 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");
+  if (dir == "lib") {
+    return path.url.join("/", "packages", id.package, path.url.joinAll(rest));
   }
 
-  // Show the transformer.
-  prefixParts.add(entry.transform.transformer);
+  if (useWebAsRoot) {
+    if (dir != "web") {
+      throw new FormatException('Cannot access assets from "$dir".');
+    }
 
-  // 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 (id.package != entrypoint) {
+      throw new FormatException(
+          'Cannot access "web" directory of non-root packages.');
+    }
 
-  // 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}");
+    return path.url.join("/", path.url.joinAll(rest));
   }
 
-  var prefix = "[${prefixParts.join(' ')}]:";
-  var message = entry.message;
-  if (entry.span != null) {
-    message = entry.span.getLocationMessage(entry.message);
+  if (id.package != entrypoint) {
+    throw new FormatException(
+        'Can only access "lib" and "asset" directories of non-entrypoint '
+    'packages.');
   }
 
-  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;
-  }
+  // Allow any path in the entrypoint package.
+  return path.url.join("/", path.url.joinAll(parts));
 }
diff --git a/lib/src/barback/build_environment.dart b/lib/src/barback/build_environment.dart
new file mode 100644
index 00000000..9e1467b0
--- /dev/null
+++ b/lib/src/barback/build_environment.dart
@@ -0,0 +1,395 @@
+// 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";
+}
diff --git a/lib/src/barback/dart2js_transformer.dart b/lib/src/barback/dart2js_transformer.dart
index aa09244e..f6f64c5e 100644
--- a/lib/src/barback/dart2js_transformer.dart
+++ b/lib/src/barback/dart2js_transformer.dart
@@ -23,6 +23,7 @@ import '../io.dart';
 import '../package.dart';
 import '../package_graph.dart';
 import '../utils.dart';
+import 'build_environment.dart';
 
 /// The set of all valid configuration options for this transformer.
 final _validOptions = new Set<String>.from([
@@ -33,14 +34,9 @@ final _validOptions = new Set<String>.from([
 /// A [Transformer] that uses dart2js's library API to transform Dart
 /// entrypoints in "web" to JavaScript.
 class Dart2JSTransformer extends Transformer {
-  final PackageGraph _graph;
+  final BuildEnvironment _environment;
   final BarbackSettings _settings;
 
-  /// The [AssetId]s the transformer has discovered so far. Used by pub build
-  /// to determine where to copy the JS bootstrap files.
-  // TODO(rnystrom): Do something cleaner for this, or eliminate those files.
-  final entrypoints = new Set<AssetId>();
-
   /// If this is non-null, then the transformer is currently being applied, so
   /// subsequent calls to [apply] will wait for this to finish before
   /// proceeding.
@@ -51,7 +47,7 @@ class Dart2JSTransformer extends Transformer {
   /// is here: https://code.google.com/p/dart/issues/detail?id=14730.
   Future _running;
 
-  Dart2JSTransformer.withSettings(this._graph, this._settings) {
+  Dart2JSTransformer.withSettings(this._environment, this._settings) {
     var invalidOptions = _settings.configuration.keys.toSet()
         .difference(_validOptions);
     if (invalidOptions.isEmpty) return;
@@ -61,14 +57,18 @@ class Dart2JSTransformer extends Transformer {
         "${toSentence(invalidOptions.map((option) => '"$option"'))}.");
   }
 
-  Dart2JSTransformer(PackageGraph graph, BarbackMode mode)
-      : this.withSettings(graph, new BarbackSettings({}, mode));
+  Dart2JSTransformer(BuildEnvironment environment, BarbackMode mode)
+      : this.withSettings(environment, new BarbackSettings({}, mode));
 
-  /// Only ".dart" files within "web/" are processed.
+  /// Only ".dart" files within a buildable directory are processed.
   Future<bool> isPrimary(Asset asset) {
-    return new Future.value(
-        asset.id.extension == ".dart" &&
-        asset.id.path.startsWith("web/"));
+    if (asset.id.extension != ".dart") return new Future.value(false);
+
+    for (var dir in ["benchmark", "example", "test", "web"]) {
+      if (asset.id.path.startsWith("$dir/")) return new Future.value(true);
+    }
+
+    return new Future.value(false);
   }
 
   Future apply(Transform transform) {
@@ -91,7 +91,7 @@ class Dart2JSTransformer extends Transformer {
       try {
         var id = transform.primaryInput.id;
         var name = id.path;
-        if (id.package != _graph.entrypoint.root.name) {
+        if (id.package != _environment.rootPackage.name) {
           name += " in ${id.package}";
         }
 
@@ -102,16 +102,15 @@ class Dart2JSTransformer extends Transformer {
         return null;
       }
 
-      var provider = new _BarbackCompilerProvider(_graph, transform);
+      var provider = new _BarbackCompilerProvider(_environment, transform);
 
       // Create a "path" to the entrypoint script. The entrypoint may not
       // actually be on disk, but this gives dart2js a root to resolve
       // relative paths against.
       var id = transform.primaryInput.id;
 
-      entrypoints.add(id);
-
-      var entrypoint = path.join(_graph.packages[id.package].dir, id.path);
+      var entrypoint = path.join(_environment.graph.packages[id.package].dir,
+          id.path);
 
       // TODO(rnystrom): Should have more sophisticated error-handling here.
       // Need to report compile errors to the user in an easily visible way.
@@ -125,7 +124,8 @@ class Dart2JSTransformer extends Transformer {
               'minify', defaultsTo: _settings.mode == BarbackMode.RELEASE),
           verbose: _configBool('verbose'),
           environment: _configEnvironment,
-          packageRoot: path.join(_graph.entrypoint.root.dir, "packages"),
+          packageRoot: path.join(_environment.rootPackage.dir,
+                                 "packages"),
           analyzeAll: _configBool('analyzeAll'),
           suppressWarnings: _configBool('suppressWarnings'),
           suppressHints: _configBool('suppressHints'),
@@ -187,7 +187,7 @@ class Dart2JSTransformer extends Transformer {
 /// difference is that it uses barback's logging code and, more importantly, it
 /// handles missing source files more gracefully.
 class _BarbackCompilerProvider implements dart.CompilerProvider {
-  final PackageGraph _graph;
+  final BuildEnvironment _environment;
   final Transform _transform;
 
   /// The map of previously loaded files.
@@ -221,7 +221,7 @@ class _BarbackCompilerProvider implements dart.CompilerProvider {
       compiler.Diagnostic.INFO.ordinal |
       compiler.Diagnostic.VERBOSE_INFO.ordinal;
 
-  _BarbackCompilerProvider(this._graph, this._transform);
+  _BarbackCompilerProvider(this._environment, this._transform);
 
   /// A [CompilerInputProvider] for dart2js.
   Future<String> provideInput(Uri resourceUri) {
@@ -344,14 +344,14 @@ class _BarbackCompilerProvider implements dart.CompilerProvider {
     // See if it's a path to a "public" asset within the root package. All
     // other files in the root package are not visible to transformers, so
     // should be loaded directly from disk.
-    var rootDir = _graph.entrypoint.root.dir;
+    var rootDir = _environment.rootPackage.dir;
     var sourcePath = path.fromUri(url);
     if (isBeneath(sourcePath, path.join(rootDir, "lib")) ||
         isBeneath(sourcePath, path.join(rootDir, "asset")) ||
         isBeneath(sourcePath, path.join(rootDir, "web"))) {
       var relative = path.relative(sourcePath, from: rootDir);
 
-      return new AssetId(_graph.entrypoint.root.name, relative);
+      return new AssetId(_environment.rootPackage.name, relative);
     }
 
     return null;
diff --git a/lib/src/barback/dart_forwarding_transformer.dart b/lib/src/barback/dart_forwarding_transformer.dart
index ed99c84c..d1129248 100644
--- a/lib/src/barback/dart_forwarding_transformer.dart
+++ b/lib/src/barback/dart_forwarding_transformer.dart
@@ -15,9 +15,7 @@ import '../utils.dart';
 ///
 /// Since the [Dart2JSTransformer] consumes its inputs, this is used in
 /// parallel to make sure the original Dart file is still available for use by
-/// Dartium. In release mode, it's used to the do the exact opposite: it
-/// ensures that no Dart files are emitted, since all that is deployed is the
-/// compiled JavaScript.
+/// Dartium.
 class DartForwardingTransformer extends Transformer {
   /// The mode that the transformer is running in.
   final BarbackMode _mode;
@@ -28,9 +26,7 @@ class DartForwardingTransformer extends Transformer {
 
   Future apply(Transform transform) {
     return newFuture(() {
-      if (_mode != BarbackMode.RELEASE) {
-        transform.addOutput(transform.primaryInput);
-      }
+      transform.addOutput(transform.primaryInput);
     });
   }
 }
diff --git a/lib/src/barback/load_all_transformers.dart b/lib/src/barback/load_all_transformers.dart
index 93c523cd..e1ac4634 100644
--- a/lib/src/barback/load_all_transformers.dart
+++ b/lib/src/barback/load_all_transformers.dart
@@ -8,23 +8,22 @@ import 'dart:async';
 
 import 'package:barback/barback.dart';
 
+import 'build_environment.dart';
 import 'dart2js_transformer.dart';
 import 'load_transformers.dart';
 import 'rewrite_import_transformer.dart';
-import 'server.dart';
 import '../barback.dart';
 import '../package_graph.dart';
 import '../utils.dart';
 
-/// Loads all transformers depended on by packages in [graph].
+/// Loads all transformers depended on by packages in [environment].
 ///
-/// This uses [server] to serve the Dart files from which transformers are
-/// loaded, then adds the transformers to `server.barback`.
+/// This uses [environment]'s server to serve the Dart files from which
+/// transformers are loaded, then adds the transformers to `server.barback`.
 ///
-/// Any [builtInTransformers] that are provided will automatically be added to
-/// the end of the root package's cascade.
-Future loadAllTransformers(BarbackServer server, PackageGraph graph,
-    BarbackMode mode, Iterable<Transformer> builtInTransformers) {
+/// Any built-in transformers that are provided by the environment will
+/// automatically be added to the end of the root package's cascade.
+Future loadAllTransformers(BuildEnvironment environment) {
   // In order to determine in what order we should load transformers, we need to
   // know which transformers depend on which others. This is different than
   // normal package dependencies. Let's begin with some terminology:
@@ -56,21 +55,21 @@ Future loadAllTransformers(BarbackServer server, PackageGraph graph,
   // Add a rewrite transformer for each package, so that we can resolve
   // "package:" imports while loading transformers.
   var rewrite = new RewriteImportTransformer();
-  for (var package in graph.packages.values) {
-    server.barback.updateTransformers(package.name, [[rewrite]]);
+  for (var package in environment.graph.packages.values) {
+    environment.barback.updateTransformers(package.name, [[rewrite]]);
   }
 
-  var orderingDeps = _computeOrderingDeps(graph);
-  var packageTransformers = _computePackageTransformers(graph);
+  var orderingDeps = _computeOrderingDeps(environment.graph);
+  var packageTransformers = _computePackageTransformers(environment.graph);
 
-  var loader = new _TransformerLoader(server, mode, graph);
+  var loader = new _TransformerLoader(environment);
 
   // The packages on which no packages have ordering dependencies -- that is,
   // the packages that don't need to be loaded before any other packages. These
   // packages will be loaded last, since all of their ordering dependencies need
   // to be loaded before they're loaded. However, they'll be traversed by
   // [loadPackage] first.
-  var rootPackages = graph.packages.keys.toSet()
+  var rootPackages = environment.graph.packages.keys.toSet()
       .difference(unionAll(orderingDeps.values));
 
   // The Futures for packages that have been loaded or are being actively loaded
@@ -89,13 +88,13 @@ Future loadAllTransformers(BarbackServer server, PackageGraph graph,
       // phase uses a transformer defined in [package] itself, that transform
       // should be loaded after running all previous phases.
       var transformers = [[rewrite]];
-      return Future.forEach(graph.packages[package].pubspec.transformers,
-          (phase) {
+      return Future.forEach(
+          environment.graph.packages[package].pubspec.transformers, (phase) {
         return Future.wait(phase.where((id) => id.package == package)
             .map(loader.load)).then((_) {
           transformers.add(unionAll(phase.map(
               (id) => loader.transformersFor(id))));
-          server.barback.updateTransformers(package, transformers);
+          environment.barback.updateTransformers(package, transformers);
         });
       }).then((_) {
         // Now that we've applied all the transformers used by [package] via
@@ -111,20 +110,15 @@ Future loadAllTransformers(BarbackServer server, PackageGraph graph,
   return Future.wait(rootPackages.map(loadPackage)).then((_) {
     /// Reset the transformers for each package to get rid of [rewrite], which
     /// is no longer needed.
-    for (var package in graph.packages.values) {
+    for (var package in environment.graph.packages.values) {
       var phases = package.pubspec.transformers.map((phase) {
         return unionAll(phase.map((id) => loader.transformersFor(id)));
       }).toList();
 
-      // The built-in transformers are for dart2js and forwarding assets around
-      // dart2js, and those only apply to the entrypoints in the root package.
-      if (package.name == graph.entrypoint.root.name &&
-          builtInTransformers != null &&
-          builtInTransformers.isNotEmpty) {
-        phases.add(builtInTransformers);
-      }
+      var transformers = environment.getBuiltInTransformers(package);
+      if (transformers != null) phases.add(transformers);
 
-      server.barback.updateTransformers(package.name, phases);
+      environment.barback.updateTransformers(package.name, phases);
     }
   });
 }
@@ -211,11 +205,7 @@ Map<String, Set<TransformerId>> _computePackageTransformers(
 
 /// A class that loads transformers defined in specific files.
 class _TransformerLoader {
-  final PackageGraph _graph;
-  final BarbackServer _server;
-
-  /// The mode that pub is running barback in.
-  final BarbackMode _mode;
+  final BuildEnvironment _environment;
 
   /// The loaded transformers defined in the library identified by each
   /// transformer id.
@@ -226,8 +216,8 @@ class _TransformerLoader {
   /// Used for error reporting.
   final _transformerUsers = new Map<Pair<String, String>, Set<String>>();
 
-  _TransformerLoader(this._server, this._mode, this._graph) {
-    for (var package in _graph.packages.values) {
+  _TransformerLoader(this._environment) {
+    for (var package in _environment.graph.packages.values) {
       for (var id in unionAll(package.pubspec.transformers)) {
         _transformerUsers.putIfAbsent(
             new Pair(id.package, id.path), () => new Set<String>())
@@ -246,7 +236,7 @@ class _TransformerLoader {
 
     // TODO(nweiz): load multiple instances of the same transformer from the
     // same isolate rather than spinning up a separate isolate for each one.
-    return loadTransformers(_server, _mode, id).then((transformers) {
+    return loadTransformers(_environment, id).then((transformers) {
       if (!transformers.isEmpty) {
         _transformers[id] = transformers;
         return;
@@ -282,8 +272,8 @@ class _TransformerLoader {
     assert(id.package == '\$dart2js');
     var transformer;
     try {
-      transformer = new Dart2JSTransformer.withSettings(
-          _graph, new BarbackSettings(id.configuration, _mode));
+      transformer = new Dart2JSTransformer.withSettings(_environment,
+          new BarbackSettings(id.configuration, _environment.mode));
     } on FormatException catch (error, stackTrace) {
       fail(error.message, error, stackTrace);
     }
diff --git a/lib/src/barback/load_transformers.dart b/lib/src/barback/load_transformers.dart
index 36790001..30aefdee 100644
--- a/lib/src/barback/load_transformers.dart
+++ b/lib/src/barback/load_transformers.dart
@@ -18,7 +18,7 @@ import '../barback.dart';
 import '../dart.dart' as dart;
 import '../log.dart' as log;
 import '../utils.dart';
-import 'server.dart';
+import 'build_environment.dart';
 
 /// A Dart script to run in an isolate.
 ///
@@ -362,14 +362,12 @@ Stream callbackStream(Stream callback()) {
 
 /// Load and return all transformers and groups from the library identified by
 /// [id].
-///
-/// [server] is used to serve any Dart files needed to load the transformers.
-Future<Set> loadTransformers(BarbackServer server, BarbackMode mode,
-    TransformerId id) {
-  return id.getAssetId(server.barback).then((assetId) {
+Future<Set> loadTransformers(BuildEnvironment environment, TransformerId id) {
+  return id.getAssetId(environment.barback).then((assetId) {
     var path = assetId.path.replaceFirst('lib/', '');
     // TODO(nweiz): load from a "package:" URI when issue 12474 is fixed.
-    var baseUrl = baseUrlForAddress(server.address, server.port);
+    var baseUrl = baseUrlForAddress(environment.server.address,
+                                    environment.server.port);
     var uri = '$baseUrl/packages/${id.package}/$path';
     var code = 'import "$uri";\n' +
         _TRANSFORMER_ISOLATE.replaceAll('<<URL_BASE>>', baseUrl);
@@ -381,7 +379,7 @@ Future<Set> loadTransformers(BarbackServer server, BarbackMode mode,
         .then((sendPort) {
       return _call(sendPort, {
         'library': uri,
-        'mode': mode.name,
+        'mode': environment.mode.name,
         // TODO(nweiz): support non-JSON-encodable configuration maps.
         'configuration': JSON.encode(id.configuration)
       }).then((transformers) {
diff --git a/lib/src/barback/server.dart b/lib/src/barback/server.dart
index 65e9f7d8..d285cfad 100644
--- a/lib/src/barback/server.dart
+++ b/lib/src/barback/server.dart
@@ -57,10 +57,10 @@ class BarbackServer {
 
   /// Creates a new server and binds it to [port] of [host].
   ///
-  /// This server will serve assets from [barback], and use [rootPackage] as the
-  /// root package.
-  static Future<BarbackServer> bind(String host, int port, Barback barback,
-      String rootPackage) {
+  /// This server will serve assets from [barback], and use [rootPackage] as
+  /// the root package.
+  static Future<BarbackServer> bind(String host, int port,
+      Barback barback, String rootPackage) {
     return Chain.track(HttpServer.bind(host, port))
         .then((server) => new BarbackServer._(server, barback, rootPackage));
   }
diff --git a/lib/src/barback/sources.dart b/lib/src/barback/sources.dart
deleted file mode 100644
index 672450a8..00000000
--- a/lib/src/barback/sources.dart
+++ /dev/null
@@ -1,172 +0,0 @@
-// Copyright (c) 2013, 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.sources;
-
-import 'dart:async';
-
-import 'package:barback/barback.dart';
-import 'package:path/path.dart' as path;
-import 'package:watcher/watcher.dart';
-
-import '../entrypoint.dart';
-import '../io.dart';
-import '../package.dart';
-import '../package_graph.dart';
-
-/// Adds all of the source assets in the provided packages to barback and
-/// then watches the public directories for changes.
-///
-/// [watcherFactory] should return a [DirectoryWatcher] watching the given
-/// directory for changes.
-///
-/// Returns a Future that completes when the sources are loaded and the watchers
-/// are active.
-Future watchSources(PackageGraph graph, Barback barback,
-    WatcherType watcherType) {
-  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));
-    });
-  }));
-}
-
-/// Adds all of the source assets in the provided packages to barback.
-void loadSources(PackageGraph graph, Barback barback) {
-  for (var package in graph.packages.values) {
-    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.add("web");
-  return directories;
-}
-
-/// 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";
-}
diff --git a/lib/src/command.dart b/lib/src/command.dart
index a6823384..e3dcab5e 100644
--- a/lib/src/command.dart
+++ b/lib/src/command.dart
@@ -186,7 +186,7 @@ and include the results in a bug report on http://dartbug.com/new.
         .then((_) {
       // Explicitly exit on success to ensure that any dangling dart:io handles
       // don't cause the process to never terminate.
-      return flushThenExit(0);
+      return flushThenExit(exit_codes.SUCCESS);
     });
   }
 
diff --git a/lib/src/command/build.dart b/lib/src/command/build.dart
index b13e4d8b..b470f1bf 100644
--- a/lib/src/command/build.dart
+++ b/lib/src/command/build.dart
@@ -9,8 +9,7 @@ import 'dart:async';
 import 'package:barback/barback.dart';
 import 'package:path/path.dart' as path;
 
-import '../barback/dart2js_transformer.dart';
-import '../barback/dart_forwarding_transformer.dart';
+import '../barback/build_environment.dart';
 import '../barback.dart' as barback;
 import '../command.dart';
 import '../exit_codes.dart' as exit_codes;
@@ -20,87 +19,71 @@ import '../utils.dart';
 
 final _arrow = getSpecial('\u2192', '=>');
 
+/// The set of top level directories in the entrypoint package that can be
+/// built.
+final _allowedBuildDirectories = new Set<String>.from([
+  "benchmark", "bin", "example", "test", "web"
+]);
+
 /// Handles the `build` pub command.
 class BuildCommand extends PubCommand {
   String get description =>
       "Copy and compile all Dart entrypoints in the 'web' directory.";
   String get usage => "pub build [options]";
   List<String> get aliases => const ["deploy", "settle-up"];
+  bool get takesArguments => true;
 
-  // TODO(nweiz): make these configurable.
-  /// The path to the source directory of the application.
-  String get source => path.join(entrypoint.root.dir, 'web');
-
+  // TODO(nweiz): make this configurable.
   /// The path to the application's build output directory.
   String get target => path.join(entrypoint.root.dir, 'build');
 
   /// The build mode.
   BarbackMode get mode => new BarbackMode(commandOptions['mode']);
 
+  /// The number of files that have been built and written to disc so far.
+  int builtFiles = 0;
+
+  /// The names of the top-level build directories that will be built.
+  final buildDirectories = new Set<String>();
+
   BuildCommand() {
     commandParser.addOption('mode', defaultsTo: BarbackMode.RELEASE.toString(),
         help: 'Mode to run transformers in.');
+
+    commandParser.addFlag('all', help: "Build all buildable directories.",
+        defaultsTo: false, negatable: false);
   }
 
   Future onRun() {
-    if (!dirExists(source)) {
-      throw new ApplicationException('There is no "$source" directory.');
-    }
+    var exitCode = _parseBuildDirectories();
+    if (exitCode != exit_codes.SUCCESS) return flushThenExit(exitCode);
 
     cleanDir(target);
 
-    var dart2jsTransformer;
-    var builtFiles = 0;
-
-    return entrypoint.loadPackageGraph().then((graph) {
-      dart2jsTransformer = new Dart2JSTransformer(graph, mode);
-      var builtInTransformers = [
-        dart2jsTransformer,
-        new DartForwardingTransformer(mode)
-      ];
-
-      // Since this server will only be hit by the transformer loader and isn't
-      // user-facing, just use an IPv4 address to avoid a weird bug on the
-      // OS X buildbots.
-      // TODO(rnystrom): Allow specifying mode.
-      return barback.createServer("127.0.0.1", 0, graph, mode,
-          builtInTransformers: builtInTransformers,
-          watcher: barback.WatcherType.NONE);
-    }).then((server) {
+    // Since this server will only be hit by the transformer loader and isn't
+    // user-facing, just use an IPv4 address to avoid a weird bug on the
+    // OS X buildbots.
+    return BuildEnvironment.create(entrypoint, "127.0.0.1", 0, mode,
+        WatcherType.NONE, buildDirectories, useDart2JS: true)
+          .then((environment) {
+
       // Show in-progress errors, but not results. Those get handled implicitly
       // by getAllAssets().
-      server.barback.errors.listen((error) {
+      environment.server.barback.errors.listen((error) {
         log.error(log.red("Build error:\n$error"));
       });
 
       return log.progress("Building ${entrypoint.root.name}",
-          () => server.barback.getAllAssets());
-    }).then((assets) {
-      return Future.wait(assets.map((asset) {
-        // In release mode, strip out .dart files since all relevant ones have
-        // been compiled to JavaScript already.
-        if (mode == BarbackMode.RELEASE && asset.id.extension == ".dart") {
-          return new Future.value();
-        }
-
-        builtFiles++;
-
-        // Figure out the output directory for the asset, which is the same
-        // as the path pub serve would use to serve it.
-        var relativeUrl = barback.idtoUrlPath(entrypoint.root.name, asset.id);
-
-        // Remove the leading "/".
-        relativeUrl = relativeUrl.substring(1);
-
-        var relativePath = path.fromUri(new Uri(path: relativeUrl));
-        var destPath = path.join(target, relativePath);
-
-        ensureDir(path.dirname(destPath));
-        // TODO(rnystrom): Should we display this to the user?
-        return createFileFromStream(asset.read(), destPath);
-      })).then((_) {
-        builtFiles += _copyBrowserJsFiles(dart2jsTransformer.entrypoints);
-        log.message("Built $builtFiles ${pluralize('file', builtFiles)}!");
+          () => environment.server.barback.getAllAssets()).then((assets) {
+        // Find all of the JS entrypoints we built.
+        var dart2JSEntrypoints = assets
+            .where((asset) => asset.id.path.endsWith(".dart.js"))
+            .map((asset) => asset.id);
+
+        return Future.wait(assets.map(_writeAsset)).then((_) {
+          builtFiles += _copyBrowserJsFiles(dart2JSEntrypoints);
+          log.message("Built $builtFiles ${pluralize('file', builtFiles)}!");
+        });
       });
     }).catchError((error) {
       // If [getAllAssets()] throws a BarbackException, the error has already
@@ -112,6 +95,128 @@ class BuildCommand extends PubCommand {
     });
   }
 
+  /// Parses the command-line arguments to determine the set of top-level
+  /// directories to build.
+  ///
+  /// If there are no arguments to `pub build`, this will just be "web".
+  ///
+  /// If the `--all` flag is set, then it will be all buildable directories
+  /// that exist.
+  ///
+  /// Otherwise, all arguments should be the names of directories to include.
+  ///
+  /// Returns the exit code of an error, or zero if it parsed correctly.
+  int _parseBuildDirectories() {
+    if (commandOptions["all"]) {
+      if (commandOptions.rest.isNotEmpty) {
+        log.error(
+            'Build directory names are not allowed if "--all" is passed.');
+        return exit_codes.USAGE;
+      }
+
+      // Include every build directory that exists in the package.
+      var allowed = _allowedBuildDirectories.where(
+          (d) => dirExists(path.join(entrypoint.root.dir, d)));
+
+      if (allowed.isEmpty) {
+        var buildDirs = toSentence(ordered(_allowedBuildDirectories.map(
+            (name) => '"$name"')));
+        log.error('There are no buildable directories.\n'
+                  'The supported directories are $buildDirs.');
+        return exit_codes.DATA;
+      }
+
+      buildDirectories.addAll(allowed);
+      return exit_codes.SUCCESS;
+    }
+
+    buildDirectories.addAll(commandOptions.rest);
+
+    // If no directory were specified, default to "web".
+    if (buildDirectories.isEmpty) {
+      buildDirectories.add("web");
+    }
+
+    // Make sure the arguments are known directories.
+    var disallowed = buildDirectories.where(
+        (dir) => !_allowedBuildDirectories.contains(dir));
+    if (disallowed.isNotEmpty) {
+      var dirs = pluralize("directory", disallowed.length,
+          plural: "directories");
+      var names = toSentence(ordered(disallowed).map((name) => '"$name"'));
+      var allowed = toSentence(ordered(_allowedBuildDirectories.map(
+          (name) => '"$name"')));
+      log.error('Unsupported build $dirs $names.\n'
+                'The allowed directories are $allowed.');
+      return exit_codes.USAGE;
+    }
+
+    // Make sure all of the build directories exist.
+    var missing = buildDirectories.where(
+        (dir) => !dirExists(path.join(entrypoint.root.dir, dir)));
+
+    if (missing.length == 1) {
+      log.error('Directory "${missing.single}" does not exist.');
+      return exit_codes.DATA;
+    } else if (missing.isNotEmpty) {
+      var names = toSentence(ordered(missing).map((name) => '"$name"'));
+      log.error('Directories $names do not exist.');
+      return exit_codes.DATA;
+    }
+
+    return exit_codes.SUCCESS;
+  }
+
+  /// Writes [asset] to the appropriate build directory.
+  ///
+  /// If [asset] is in the special "assets" directory, writes it to every
+  /// build directory.
+  Future _writeAsset(Asset asset) {
+    // In release mode, strip out .dart files since all relevant ones have been
+    // compiled to JavaScript already.
+    if (mode == BarbackMode.RELEASE && asset.id.extension == ".dart") {
+      return new Future.value();
+    }
+
+    // If the asset is from a package's "lib" directory, we make it available
+    // as an input for transformers, but don't want it in the final output.
+    // (Any Dart code in there should be imported and compiled to JS, anything
+    // else we want to omit.)
+    if (asset.id.path.startsWith("lib/")) {
+      return new Future.value();
+    }
+
+    // Figure out the output directory for the asset, which is the same as the
+    // path pub serve would use to serve it.
+    var relativeUrl = barback.idtoUrlPath(entrypoint.root.name, asset.id,
+        useWebAsRoot: false);
+
+    // Remove the leading "/".
+    relativeUrl = relativeUrl.substring(1);
+
+    // If the asset is from the shared "assets" directory, copy it into all of
+    // the top-level build directories.
+    if (relativeUrl.startsWith("assets/")) {
+      builtFiles += buildDirectories.length;
+      return Future.wait(buildDirectories.map(
+          (buildDir) => _writeOutputFile(asset,
+              path.url.join(buildDir, relativeUrl))));
+    }
+
+    builtFiles++;
+    return _writeOutputFile(asset, relativeUrl);
+  }
+
+  /// Writes the contents of [asset] to [relativeUrl] within the build
+  /// directory.
+  Future _writeOutputFile(Asset asset, String relativeUrl) {
+    var relativePath = path.fromUri(new Uri(path: relativeUrl));
+    var destPath = path.join(target, relativePath);
+
+    ensureDir(path.dirname(destPath));
+    return createFileFromStream(asset.read(), destPath);
+  }
+
   /// If this package depends directly on the `browser` package, this ensures
   /// that the JavaScript bootstrap files are copied into `packages/browser/`
   /// directories next to each entrypoint in [entrypoints].
@@ -127,7 +232,6 @@ class BuildCommand extends PubCommand {
     // Get all of the directories that contain Dart entrypoints.
     var entrypointDirs = entrypoints
         .map((id) => path.url.split(id.path))
-        .map((parts) => parts.skip(1)) // Remove "web/".
         .map((relative) => path.dirname(path.joinAll(relative)))
         .toSet();
 
@@ -149,6 +253,10 @@ class BuildCommand extends PubCommand {
     var jsPath = path.join(
         target, directory, 'packages', 'browser', '$name.js');
     ensureDir(path.dirname(jsPath));
+
+    // TODO(rnystrom): This won't work if we get rid of symlinks and the top
+    // level "packages" directory. Will need to copy from the browser
+    // directory.
     copyFile(path.join(entrypoint.packagesDir, 'browser', '$name.js'), jsPath);
   }
 }
diff --git a/lib/src/command/serve.dart b/lib/src/command/serve.dart
index f99df217..a03bdd99 100644
--- a/lib/src/command/serve.dart
+++ b/lib/src/command/serve.dart
@@ -8,12 +8,9 @@ import 'dart:async';
 
 import 'package:barback/barback.dart';
 
-import '../barback/dart_forwarding_transformer.dart';
-import '../barback/dart2js_transformer.dart';
+import '../barback/build_environment.dart';
 import '../barback/pub_package_provider.dart';
-import '../barback.dart' as barback;
 import '../command.dart';
-import '../entrypoint.dart';
 import '../exit_codes.dart' as exit_codes;
 import '../io.dart';
 import '../log.dart' as log;
@@ -65,34 +62,28 @@ class ServeCommand extends PubCommand {
       return flushThenExit(exit_codes.USAGE);
     }
 
-    return entrypoint.loadPackageGraph().then((graph) {
-      var builtInTransformers = [new DartForwardingTransformer(mode)];
-      if (useDart2JS) {
-        builtInTransformers.add(new Dart2JSTransformer(graph, mode));
-        // TODO(rnystrom): Add support for dart2dart transformer here.
-      }
+    var watcherType = commandOptions['force-poll'] ?
+        WatcherType.POLLING : WatcherType.AUTO;
+
+    return BuildEnvironment.create(entrypoint, hostname, port, mode,
+        watcherType, ["web"].toSet(),
+        useDart2JS: useDart2JS).then((environment) {
 
-      var watcherType = commandOptions['force-poll'] ?
-          barback.WatcherType.POLLING : barback.WatcherType.AUTO;
-      return barback.createServer(hostname, port, graph, mode,
-          builtInTransformers: builtInTransformers,
-          watcher: watcherType);
-    }).then((server) {
       // In release mode, strip out .dart files since all relevant ones have
       // been compiled to JavaScript already.
       if (mode == BarbackMode.RELEASE) {
-        server.allowAsset = (url) => !url.path.endsWith(".dart");
+        environment.server.allowAsset = (url) => !url.path.endsWith(".dart");
       }
 
       /// This completer is used to keep pub running (by not completing) and
       /// to pipe fatal errors to pub's top-level error-handling machinery.
       var completer = new Completer();
 
-      server.barback.errors.listen((error) {
+      environment.server.barback.errors.listen((error) {
         log.error(log.red("Build error:\n$error"));
       });
 
-      server.barback.results.listen((result) {
+      environment.server.barback.results.listen((result) {
         if (result.succeeded) {
           // TODO(rnystrom): Report using growl/inotify-send where available.
           log.message("Build completed ${log.green('successfully')}");
@@ -104,7 +95,7 @@ class ServeCommand extends PubCommand {
         if (!completer.isCompleted) completer.completeError(error, stackTrace);
       });
 
-      server.results.listen((result) {
+      environment.server.results.listen((result) {
         if (result.isSuccess) {
           log.message("${log.green('GET')} ${result.url.path} $_arrow "
               "${result.id}");
@@ -123,7 +114,7 @@ class ServeCommand extends PubCommand {
       });
 
       log.message("Serving ${entrypoint.root.name} "
-          "on http://$hostname:${server.port}");
+          "on http://$hostname:${environment.server.port}");
 
       return completer.future;
     });
diff --git a/lib/src/exit_codes.dart b/lib/src/exit_codes.dart
index 156e49e4..a1683cd9 100644
--- a/lib/src/exit_codes.dart
+++ b/lib/src/exit_codes.dart
@@ -9,6 +9,9 @@
 /// [manpage]: http://www.freebsd.org/cgi/man.cgi?query=sysexits
 library pub.exit_codes;
 
+/// The command completely successfully.
+const SUCCESS = 0;
+
 /// The command was used incorrectly.
 const USAGE = 64;
 
diff --git a/lib/src/io.dart b/lib/src/io.dart
index 804267c5..3f17a023 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -14,6 +14,7 @@ import 'package:path/path.dart' as path;
 import 'package:http/http.dart' show ByteStream;
 import 'package:stack_trace/stack_trace.dart';
 
+import 'exit_codes.dart' as exit_codes;
 import 'error_group.dart';
 import 'log.dart' as log;
 import 'pool.dart';
@@ -727,7 +728,7 @@ Future<bool> extractTarGz(Stream<List<int>> stream, String destination) {
     ]);
   }).then((results) {
     var exitCode = results[1];
-    if (exitCode != 0) {
+    if (exitCode != exit_codes.SUCCESS) {
       throw new Exception("Failed to extract .tar.gz stream to $destination "
           "(exit code $exitCode).");
     }
@@ -759,7 +760,7 @@ Future<bool> _extractTarGzWindows(Stream<List<int>> stream,
       // path because 7zip says "A full path is not allowed here."
       return runProcess(pathTo7zip, ['e', 'data.tar.gz'], workingDir: tempDir);
     }).then((result) {
-      if (result.exitCode != 0) {
+      if (result.exitCode != exit_codes.SUCCESS) {
         throw new Exception('Could not un-gzip (exit code ${result.exitCode}). '
                 'Error:\n'
             '${result.stdout.join("\n")}\n'
@@ -776,7 +777,7 @@ Future<bool> _extractTarGzWindows(Stream<List<int>> stream,
       // Untar the archive into the destination directory.
       return runProcess(pathTo7zip, ['x', tarFile], workingDir: destination);
     }).then((result) {
-      if (result.exitCode != 0) {
+      if (result.exitCode != exit_codes.SUCCESS) {
         throw new Exception('Could not un-tar (exit code ${result.exitCode}). '
                 'Error:\n'
             '${result.stdout.join("\n")}\n'
@@ -855,7 +856,7 @@ class PubProcessResult {
 
   const PubProcessResult(this.stdout, this.stderr, this.exitCode);
 
-  bool get success => exitCode == 0;
+  bool get success => exitCode == exit_codes.SUCCESS;
 }
 
 /// Gets a [Uri] for [uri], which can either already be one, or be a [String].
diff --git a/test/build/all_includes_all_buildable_directories_test.dart b/test/build/all_includes_all_buildable_directories_test.dart
new file mode 100644
index 00000000..1a9e4645
--- /dev/null
+++ b/test/build/all_includes_all_buildable_directories_test.dart
@@ -0,0 +1,59 @@
+// 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.
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+
+  integration("build --all finds assets in all buildable directories", () {
+    d.dir(appPath, [
+      d.appPubspec(),
+      d.dir('benchmark', [
+        d.file('file.txt', 'benchmark')
+      ]),
+      d.dir('bin', [
+        d.file('file.txt', 'bin')
+      ]),
+      d.dir('example', [
+        d.file('file.txt', 'example')
+      ]),
+      d.dir('test', [
+        d.file('file.txt', 'test')
+      ]),
+      d.dir('web', [
+        d.file('file.txt', 'web')
+      ]),
+      d.dir('unknown', [
+        d.file('file.txt', 'unknown')
+      ])
+    ]).create();
+
+    schedulePub(args: ["build", "--all"],
+        output: new RegExp(r"Built 5 files!"));
+
+    d.dir(appPath, [
+      d.dir('build', [
+        d.dir('benchmark', [
+          d.file('file.txt', 'benchmark')
+        ]),
+        d.dir('bin', [
+          d.file('file.txt', 'bin')
+        ]),
+        d.dir('example', [
+          d.file('file.txt', 'example')
+        ]),
+        d.dir('test', [
+          d.file('file.txt', 'test')
+        ]),
+        d.dir('web', [
+          d.file('file.txt', 'web')
+        ]),
+        // Only includes known buildable directories.
+        d.nothing('unknown')
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/build/all_with_no_buildable_directories_test.dart b/test/build/all_with_no_buildable_directories_test.dart
new file mode 100644
index 00000000..c83d94c8
--- /dev/null
+++ b/test/build/all_with_no_buildable_directories_test.dart
@@ -0,0 +1,21 @@
+// Copyright (c) 2013, 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.
+
+import '../../lib/src/exit_codes.dart' as exit_codes;
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+
+  integration("build --all with no buildable directories", () {
+    d.appDir().create();
+
+    schedulePub(args: ["build", "--all"],
+        error: 'There are no buildable directories.\n'
+               'The supported directories are "benchmark", "bin", "example", '
+               '"test" and "web".',
+        exitCode: exit_codes.DATA);
+  });
+}
diff --git a/test/build/allows_arbitrary_modes_test.dart b/test/build/allows_arbitrary_modes_test.dart
index 8a5f6bf7..5138236b 100644
--- a/test/build/allows_arbitrary_modes_test.dart
+++ b/test/build/allows_arbitrary_modes_test.dart
@@ -46,12 +46,13 @@ main() {
 
     createLockFile('myapp', pkg: ['barback']);
 
-    schedulePub(args: ["build", "--mode", "depeche"],
-        exitCode: 0);
+    schedulePub(args: ["build", "--mode", "depeche"]);
 
     d.dir(appPath, [
       d.dir('build', [
-        d.file('foo.out', 'depeche')
+        d.dir('web', [
+          d.file('foo.out', 'depeche')
+        ])
       ])
     ]).validate();
   });
diff --git a/test/build/allows_multiple_dir_name_args_test.dart b/test/build/allows_multiple_dir_name_args_test.dart
new file mode 100644
index 00000000..d662147b
--- /dev/null
+++ b/test/build/allows_multiple_dir_name_args_test.dart
@@ -0,0 +1,40 @@
+// 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.
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+
+  integration("allows multiple directory name arguments", () {
+    d.dir(appPath, [
+      d.appPubspec(),
+      d.dir('example', [
+        d.file('file.txt', 'example')
+      ]),
+      d.dir('test', [
+        d.file('file.txt', 'test')
+      ]),
+      d.dir('web', [
+        d.file('file.txt', 'web')
+      ])
+    ]).create();
+
+    schedulePub(args: ["build", "example", "test"],
+        output: new RegExp(r"Built 2 files!"));
+
+    d.dir(appPath, [
+      d.dir('build', [
+        d.dir('example', [
+          d.file('file.txt', 'example')
+        ]),
+        d.dir('test', [
+          d.file('file.txt', 'test')
+        ]),
+        d.nothing('web')
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/build/cleans_entire_build_directory_test.dart b/test/build/cleans_entire_build_directory_test.dart
new file mode 100644
index 00000000..142914aa
--- /dev/null
+++ b/test/build/cleans_entire_build_directory_test.dart
@@ -0,0 +1,39 @@
+// 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.
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+
+  integration("cleans entire build directory before a build", () {
+    d.dir(appPath, [
+      d.appPubspec(),
+      d.dir('example', [
+        d.file('file.txt', 'example')
+      ]),
+      d.dir('test', [
+        d.file('file.txt', 'test')
+      ])
+    ]).create();
+
+    // Make a build directory containing "example".
+    schedulePub(args: ["build", "example"],
+        output: new RegExp(r"Built 1 file!"));
+
+    // Now build again with just "test". Should wipe out "example".
+    schedulePub(args: ["build", "test"],
+        output: new RegExp(r"Built 1 file!"));
+
+    d.dir(appPath, [
+      d.dir('build', [
+        d.nothing('example'),
+        d.dir('test', [
+          d.file('file.txt', 'test')
+        ]),
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/build/compiles_entrypoints_in_benchmark_test.dart b/test/build/compiles_entrypoints_in_benchmark_test.dart
new file mode 100644
index 00000000..68edc389
--- /dev/null
+++ b/test/build/compiles_entrypoints_in_benchmark_test.dart
@@ -0,0 +1,50 @@
+// 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.
+
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+
+  integration("compiles Dart entrypoints in benchmark/ to JS", () {
+    // Dart2js can take a long time to compile dart code, so we increase the
+    // timeout to cope with that.
+    currentSchedule.timeout *= 3;
+
+    d.dir(appPath, [
+      d.appPubspec(),
+      d.dir('benchmark', [
+        d.file('file.dart', 'void main() => print("hello");'),
+        d.file('lib.dart', 'void foo() => print("hello");'),
+        d.dir('subdir', [
+          d.file('subfile.dart', 'void main() => print("ping");')
+        ])
+      ])
+    ]).create();
+
+    schedulePub(args: ["build", "benchmark"],
+        output: new RegExp(r"Built 6 files!"));
+
+    d.dir(appPath, [
+      d.dir('build', [
+        d.dir('benchmark', [
+          d.matcherFile('file.dart.js', isNot(isEmpty)),
+          d.matcherFile('file.dart.precompiled.js', isNot(isEmpty)),
+          d.matcherFile('file.dart.js.map', isNot(isEmpty)),
+          d.nothing('file.dart'),
+          d.nothing('lib.dart'),
+          d.dir('subdir', [
+            d.matcherFile('subfile.dart.js', isNot(isEmpty)),
+            d.matcherFile('subfile.dart.precompiled.js', isNot(isEmpty)),
+            d.matcherFile('subfile.dart.js.map', isNot(isEmpty)),
+            d.nothing('subfile.dart')
+          ])
+        ])
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/build/compiles_entrypoints_in_example_test.dart b/test/build/compiles_entrypoints_in_example_test.dart
new file mode 100644
index 00000000..5b54d3a8
--- /dev/null
+++ b/test/build/compiles_entrypoints_in_example_test.dart
@@ -0,0 +1,50 @@
+// 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.
+
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+
+  integration("compiles Dart entrypoints in example/ to JS", () {
+    // Dart2js can take a long time to compile dart code, so we increase the
+    // timeout to cope with that.
+    currentSchedule.timeout *= 3;
+
+    d.dir(appPath, [
+      d.appPubspec(),
+      d.dir('example', [
+        d.file('file.dart', 'void main() => print("hello");'),
+        d.file('lib.dart', 'void foo() => print("hello");'),
+        d.dir('subdir', [
+          d.file('subfile.dart', 'void main() => print("ping");')
+        ])
+      ])
+    ]).create();
+
+    schedulePub(args: ["build", "example"],
+        output: new RegExp(r"Built 6 files!"));
+
+    d.dir(appPath, [
+      d.dir('build', [
+        d.dir('example', [
+          d.matcherFile('file.dart.js', isNot(isEmpty)),
+          d.matcherFile('file.dart.precompiled.js', isNot(isEmpty)),
+          d.matcherFile('file.dart.js.map', isNot(isEmpty)),
+          d.nothing('file.dart'),
+          d.nothing('lib.dart'),
+          d.dir('subdir', [
+            d.matcherFile('subfile.dart.js', isNot(isEmpty)),
+            d.matcherFile('subfile.dart.precompiled.js', isNot(isEmpty)),
+            d.matcherFile('subfile.dart.js.map', isNot(isEmpty)),
+            d.nothing('subfile.dart')
+          ])
+        ])
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/build/compiles_entrypoints_in_test_test.dart b/test/build/compiles_entrypoints_in_test_test.dart
new file mode 100644
index 00000000..2b585d47
--- /dev/null
+++ b/test/build/compiles_entrypoints_in_test_test.dart
@@ -0,0 +1,50 @@
+// 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.
+
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+
+  integration("compiles Dart entrypoints in test/ to JS", () {
+    // Dart2js can take a long time to compile dart code, so we increase the
+    // timeout to cope with that.
+    currentSchedule.timeout *= 3;
+
+    d.dir(appPath, [
+      d.appPubspec(),
+      d.dir('test', [
+        d.file('file.dart', 'void main() => print("hello");'),
+        d.file('lib.dart', 'void foo() => print("hello");'),
+        d.dir('subdir', [
+          d.file('subfile.dart', 'void main() => print("ping");')
+        ])
+      ])
+    ]).create();
+
+    schedulePub(args: ["build", "test"],
+        output: new RegExp(r"Built 6 files!"));
+
+    d.dir(appPath, [
+      d.dir('build', [
+        d.dir('test', [
+          d.matcherFile('file.dart.js', isNot(isEmpty)),
+          d.matcherFile('file.dart.precompiled.js', isNot(isEmpty)),
+          d.matcherFile('file.dart.js.map', isNot(isEmpty)),
+          d.nothing('file.dart'),
+          d.nothing('lib.dart'),
+          d.dir('subdir', [
+            d.matcherFile('subfile.dart.js', isNot(isEmpty)),
+            d.matcherFile('subfile.dart.precompiled.js', isNot(isEmpty)),
+            d.matcherFile('subfile.dart.js.map', isNot(isEmpty)),
+            d.nothing('subfile.dart')
+          ])
+        ])
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/build/compiles_dart_entrypoints_to_js_test.dart b/test/build/compiles_entrypoints_in_web_test.dart
similarity index 53%
rename from test/build/compiles_dart_entrypoints_to_js_test.dart
rename to test/build/compiles_entrypoints_in_web_test.dart
index bc8319fe..ec942b66 100644
--- a/test/build/compiles_dart_entrypoints_to_js_test.dart
+++ b/test/build/compiles_entrypoints_in_web_test.dart
@@ -1,4 +1,4 @@
-// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// 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.
 
@@ -10,7 +10,7 @@ import '../test_pub.dart';
 main() {
   initConfig();
 
-  integration("compiles Dart entrypoints to JS", () {
+  integration("compiles Dart entrypoints in web/ to JS", () {
     // Dart2js can take a long time to compile dart code, so we increase the
     // timeout to cope with that.
     currentSchedule.timeout *= 3;
@@ -26,24 +26,23 @@ main() {
       ])
     ]).create();
 
-    // TODO(rnystrom): If we flesh out the command-line output, validate that
-    // here.
     schedulePub(args: ["build"],
-        output: new RegExp(r"Built 6 files!"),
-        exitCode: 0);
+        output: new RegExp(r"Built 6 files!"));
 
     d.dir(appPath, [
       d.dir('build', [
-        d.matcherFile('file.dart.js', isNot(isEmpty)),
-        d.matcherFile('file.dart.precompiled.js', isNot(isEmpty)),
-        d.matcherFile('file.dart.js.map', isNot(isEmpty)),
-        d.nothing('file.dart'),
-        d.nothing('lib.dart'),
-        d.dir('subdir', [
-          d.matcherFile('subfile.dart.js', isNot(isEmpty)),
-          d.matcherFile('subfile.dart.precompiled.js', isNot(isEmpty)),
-          d.matcherFile('subfile.dart.js.map', isNot(isEmpty)),
-          d.nothing('subfile.dart')
+        d.dir('web', [
+          d.matcherFile('file.dart.js', isNot(isEmpty)),
+          d.matcherFile('file.dart.precompiled.js', isNot(isEmpty)),
+          d.matcherFile('file.dart.js.map', isNot(isEmpty)),
+          d.nothing('file.dart'),
+          d.nothing('lib.dart'),
+          d.dir('subdir', [
+            d.matcherFile('subfile.dart.js', isNot(isEmpty)),
+            d.matcherFile('subfile.dart.precompiled.js', isNot(isEmpty)),
+            d.matcherFile('subfile.dart.js.map', isNot(isEmpty)),
+            d.nothing('subfile.dart')
+          ])
         ])
       ])
     ]).validate();
diff --git a/test/build/copies_browser_js_next_to_entrypoints_test.dart b/test/build/copies_browser_js_next_to_entrypoints_test.dart
index 9bd4ff41..f8bf83d3 100644
--- a/test/build/copies_browser_js_next_to_entrypoints_test.dart
+++ b/test/build/copies_browser_js_next_to_entrypoints_test.dart
@@ -4,7 +4,6 @@
 
 import 'dart:convert';
 
-import 'package:path/path.dart' as path;
 import 'package:scheduled_test/scheduled_test.dart';
 
 import '../descriptor.dart' as d;
@@ -51,37 +50,62 @@ main() {
 
     d.dir(appPath, [
       d.appPubspec({"browser": "1.0.0"}),
-      d.dir('web', [
+      d.dir('example', [
         d.file('file.dart', 'void main() => print("hello");'),
         d.dir('subdir', [
           d.file('subfile.dart', 'void main() => print("subhello");')
         ])
+      ]),
+      d.dir('web', [
+        d.file('file.dart', 'void main() => print("hello");'),
+        d.dir('subweb', [
+          d.file('subfile.dart', 'void main() => print("subhello");')
+        ])
       ])
     ]).create();
 
     pubGet();
 
-    schedulePub(args: ["build"],
-        output: new RegExp(r"Built 12 files!"),
-        exitCode: 0);
+    schedulePub(args: ["build", "--all"],
+        output: new RegExp(r"Built 20 files!"));
 
     d.dir(appPath, [
       d.dir('build', [
-        d.matcherFile('file.dart.js', isNot(isEmpty)),
-        d.matcherFile('file.dart.precompiled.js', isNot(isEmpty)),
-        d.matcherFile('file.dart.js.map', isNot(isEmpty)),
-        d.dir('packages', [d.dir('browser', [
-          d.file('dart.js', 'contents of dart.js'),
-          d.file('interop.js', 'contents of interop.js')
-        ])]),
-        d.dir('subdir', [
+        d.dir('example', [
+          d.matcherFile('file.dart.js', isNot(isEmpty)),
+          d.matcherFile('file.dart.precompiled.js', isNot(isEmpty)),
+          d.matcherFile('file.dart.js.map', isNot(isEmpty)),
           d.dir('packages', [d.dir('browser', [
             d.file('dart.js', 'contents of dart.js'),
             d.file('interop.js', 'contents of interop.js')
           ])]),
-          d.matcherFile('subfile.dart.js', isNot(isEmpty)),
-          d.matcherFile('subfile.dart.precompiled.js', isNot(isEmpty)),
-          d.matcherFile('subfile.dart.js.map', isNot(isEmpty))
+          d.dir('subdir', [
+            d.dir('packages', [d.dir('browser', [
+              d.file('dart.js', 'contents of dart.js'),
+              d.file('interop.js', 'contents of interop.js')
+            ])]),
+            d.matcherFile('subfile.dart.js', isNot(isEmpty)),
+            d.matcherFile('subfile.dart.precompiled.js', isNot(isEmpty)),
+            d.matcherFile('subfile.dart.js.map', isNot(isEmpty))
+          ])
+        ]),
+        d.dir('web', [
+          d.matcherFile('file.dart.js', isNot(isEmpty)),
+          d.matcherFile('file.dart.precompiled.js', isNot(isEmpty)),
+          d.matcherFile('file.dart.js.map', isNot(isEmpty)),
+          d.dir('packages', [d.dir('browser', [
+            d.file('dart.js', 'contents of dart.js'),
+            d.file('interop.js', 'contents of interop.js')
+          ])]),
+          d.dir('subweb', [
+            d.dir('packages', [d.dir('browser', [
+              d.file('dart.js', 'contents of dart.js'),
+              d.file('interop.js', 'contents of interop.js')
+            ])]),
+            d.matcherFile('subfile.dart.js', isNot(isEmpty)),
+            d.matcherFile('subfile.dart.precompiled.js', isNot(isEmpty)),
+            d.matcherFile('subfile.dart.js.map', isNot(isEmpty))
+          ])
         ])
       ])
     ]).validate();
diff --git a/test/build/copies_non_dart_files_to_build_test.dart b/test/build/copies_non_dart_files_to_build_test.dart
index 00b0757e..76abba29 100644
--- a/test/build/copies_non_dart_files_to_build_test.dart
+++ b/test/build/copies_non_dart_files_to_build_test.dart
@@ -24,15 +24,16 @@ main() {
     ]).create();
 
     schedulePub(args: ["build"],
-        output: new RegExp(r"Built \d+ files!"),
-        exitCode: 0);
+        output: new RegExp(r"Built 2 files!"));
 
     d.dir(appPath, [
       d.dir('build', [
-        d.nothing('packages'),
-        d.file('file.txt', 'contents'),
-        d.dir('subdir', [
-          d.file('subfile.txt', 'subcontents')
+        d.dir('web', [
+          d.nothing('packages'),
+          d.file('file.txt', 'contents'),
+          d.dir('subdir', [
+            d.file('subfile.txt', 'subcontents')
+          ])
         ])
       ])
     ]).validate();
diff --git a/test/build/dart2js_finds_imports_across_packages_test.dart b/test/build/dart2js_finds_imports_across_packages_test.dart
index fd8a7ac9..d5d9b88c 100644
--- a/test/build/dart2js_finds_imports_across_packages_test.dart
+++ b/test/build/dart2js_finds_imports_across_packages_test.dart
@@ -47,14 +47,15 @@ main() {
     ]).create();
 
     schedulePub(args: ["build"],
-        output: new RegExp(r"Built 3 files!"),
-        exitCode: 0);
+        output: new RegExp(r"Built 3 files!"));
 
     d.dir(appPath, [
       d.dir('build', [
-        d.matcherFile('main.dart.js', isNot(isEmpty)),
-        d.matcherFile('main.dart.precompiled.js', isNot(isEmpty)),
-        d.matcherFile('main.dart.js.map', isNot(isEmpty))
+        d.dir('web', [
+          d.matcherFile('main.dart.js', isNot(isEmpty)),
+          d.matcherFile('main.dart.precompiled.js', isNot(isEmpty)),
+          d.matcherFile('main.dart.js.map', isNot(isEmpty))
+        ])
       ])
     ]).validate();
   });
diff --git a/test/build/defaults_to_release_mode_test.dart b/test/build/defaults_to_release_mode_test.dart
index 7d0d4825..a7196919 100644
--- a/test/build/defaults_to_release_mode_test.dart
+++ b/test/build/defaults_to_release_mode_test.dart
@@ -46,12 +46,13 @@ main() {
 
     createLockFile('myapp', pkg: ['barback']);
 
-    schedulePub(args: ["build"],
-        exitCode: 0);
+    schedulePub(args: ["build"]);
 
     d.dir(appPath, [
       d.dir('build', [
-        d.file('foo.out', 'release')
+        d.dir('web', [
+          d.file('foo.out', 'release')
+        ])
       ])
     ]).validate();
   });
diff --git a/test/build/does_not_allow_args_with_all_test.dart b/test/build/does_not_allow_args_with_all_test.dart
new file mode 100644
index 00000000..5cd9dcf9
--- /dev/null
+++ b/test/build/does_not_allow_args_with_all_test.dart
@@ -0,0 +1,21 @@
+// 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.
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+import '../../lib/src/exit_codes.dart' as exit_codes;
+
+main() {
+  initConfig();
+
+  integration("does not allow directory names with --all", () {
+    d.dir(appPath, [
+      d.appPubspec()
+    ]).create();
+
+    schedulePub(args: ["build", "example", "--all"],
+        error: 'Build directory names are not allowed if "--all" is passed.',
+        exitCode: exit_codes.USAGE);
+  });
+}
diff --git a/test/build/handles_long_paths_test.dart b/test/build/handles_long_paths_test.dart
index ab99924f..78ddcf7b 100644
--- a/test/build/handles_long_paths_test.dart
+++ b/test/build/handles_long_paths_test.dart
@@ -55,15 +55,16 @@ main() {
     ]).create();
 
     schedulePub(args: ["build"],
-        output: new RegExp(r"Built 2 files!"),
-        exitCode: 0);
+        output: new RegExp(r"Built 2 files!"));
 
     d.dir(appPath, [
       d.dir('build', [
-        d.file("index.html", "html"),
-        d.dir('assets', [
-          d.dir('foo', [
-            d.file('foo.txt', 'foo')
+        d.dir('web', [
+          d.file("index.html", "html"),
+          d.dir('assets', [
+            d.dir('foo', [
+              d.file('foo.txt', 'foo')
+            ])
           ])
         ])
       ])
diff --git a/test/build/ignores_entrypoints_in_asset_test.dart b/test/build/ignores_entrypoints_in_asset_test.dart
new file mode 100644
index 00000000..27fd0821
--- /dev/null
+++ b/test/build/ignores_entrypoints_in_asset_test.dart
@@ -0,0 +1,34 @@
+// 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.
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+
+  integration("ignores entrypoint Dart files in asset/", () {
+    d.dir(appPath, [
+      d.appPubspec(),
+      d.dir('asset', [
+        d.file('file.dart', 'void main() => print("hello");'),
+      ]),
+      d.dir('web', [
+        d.file('index.html', 'html'),
+      ])
+    ]).create();
+
+    schedulePub(args: ["build", "--all"],
+        output: new RegExp(r"Built 1 file!"));
+
+    d.dir(appPath, [
+      d.dir('build', [
+        d.nothing('asset'),
+        d.dir('web', [
+          d.file('index.html', 'html')
+        ])
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/build/ignores_entrypoints_in_bin_test.dart b/test/build/ignores_entrypoints_in_bin_test.dart
new file mode 100644
index 00000000..148d5dbb
--- /dev/null
+++ b/test/build/ignores_entrypoints_in_bin_test.dart
@@ -0,0 +1,34 @@
+// 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.
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+
+  integration("ignores entrypoint Dart files in bin/", () {
+    d.dir(appPath, [
+      d.appPubspec(),
+      d.dir('bin', [
+        d.file('file.dart', 'void main() => print("hello");'),
+      ]),
+      d.dir('web', [
+        d.file('index.html', 'html'),
+      ])
+    ]).create();
+
+    schedulePub(args: ["build", "--all"],
+        output: new RegExp(r"Built 1 file!"));
+
+    d.dir(appPath, [
+      d.dir('build', [
+        d.nothing('bin'),
+        d.dir('web', [
+          d.file('index.html', 'html')
+        ])
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/build/ignores_entrypoints_in_lib_test.dart b/test/build/ignores_entrypoints_in_lib_test.dart
new file mode 100644
index 00000000..e42d762e
--- /dev/null
+++ b/test/build/ignores_entrypoints_in_lib_test.dart
@@ -0,0 +1,34 @@
+// 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.
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+
+  integration("ignores entrypoint Dart files in lib/", () {
+    d.dir(appPath, [
+      d.appPubspec(),
+      d.dir('lib', [
+        d.file('file.dart', 'void main() => print("hello");'),
+      ]),
+      d.dir('web', [
+        d.file('index.html', 'html'),
+      ])
+    ]).create();
+
+    schedulePub(args: ["build", "--all"],
+        output: new RegExp(r"Built 1 file!"));
+
+    d.dir(appPath, [
+      d.dir('build', [
+        d.nothing('lib'),
+        d.dir('web', [
+          d.file('index.html', 'html')
+        ])
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/build/ignores_existing_compiled_js_files_test.dart b/test/build/ignores_existing_compiled_js_files_test.dart
index 7aecefef..962e8586 100644
--- a/test/build/ignores_existing_compiled_js_files_test.dart
+++ b/test/build/ignores_existing_compiled_js_files_test.dart
@@ -28,14 +28,15 @@ main() {
     ]).create();
 
     schedulePub(args: ["build"],
-        output: new RegExp(r"Built 6 files!"),
-        exitCode: 0);
+        output: new RegExp(r"Built 6 files!"));
 
     d.dir(appPath, [
       d.dir('build', [
-        d.matcherFile('file.dart.js', isNot(equals('some js code'))),
-        d.dir('subdir', [
-          d.matcherFile('subfile.dart.js', isNot(equals('some js code')))
+        d.dir('web', [
+          d.matcherFile('file.dart.js', isNot(equals('some js code'))),
+          d.dir('subdir', [
+            d.matcherFile('subfile.dart.js', isNot(equals('some js code')))
+          ])
         ])
       ])
     ]).validate();
diff --git a/test/build/ignores_non_entrypoint_dart_files_test.dart b/test/build/ignores_non_entrypoint_dart_files_test.dart
index 39de56e5..d4f3f642 100644
--- a/test/build/ignores_non_entrypoint_dart_files_test.dart
+++ b/test/build/ignores_non_entrypoint_dart_files_test.dart
@@ -20,19 +20,11 @@ main() {
     ]).create();
 
     schedulePub(args: ["build"],
-        output: new RegExp(r"Built 0 files!"),
-        exitCode: 0);
+        output: new RegExp(r"Built 0 files!"));
 
     d.dir(appPath, [
       d.dir('build', [
-        d.nothing('file1.dart.js'),
-        d.nothing('file1.dart'),
-        d.nothing('file2.dart.js'),
-        d.nothing('file2.dart'),
-        d.nothing('file3.dart.js'),
-        d.nothing('file3.dart'),
-        d.nothing('file4.dart.js'),
-        d.nothing('file4.dart')
+        d.nothing('web')
       ])
     ]).validate();
   });
diff --git a/test/build/includes_assets_from_dependencies_test.dart b/test/build/includes_assets_from_dependencies_test.dart
index 1455808d..a5098b63 100644
--- a/test/build/includes_assets_from_dependencies_test.dart
+++ b/test/build/includes_assets_from_dependencies_test.dart
@@ -29,24 +29,39 @@ main() {
       d.appPubspec({
         "foo": {"path": "../foo"}
       }),
+      d.dir("example", [
+        d.file("index.html", "html"),
+      ]),
       d.dir("web", [
         d.file("index.html", "html"),
       ])
     ]).create();
 
-    schedulePub(args: ["build"],
-        output: new RegExp(r"Built 3 files!"),
-        exitCode: 0);
+    schedulePub(args: ["build", "--all"],
+        output: new RegExp(r"Built 6 files!"));
 
     d.dir(appPath, [
       d.dir('build', [
-        d.file("index.html", "html"),
-        d.dir('assets', [
-          d.dir('foo', [
-            d.file('foo.txt', 'foo'),
-            d.dir('sub', [
-              d.file('bar.txt', 'bar'),
-            ]),
+        d.dir('example', [
+          d.file("index.html", "html"),
+          d.dir('assets', [
+            d.dir('foo', [
+              d.file('foo.txt', 'foo'),
+              d.dir('sub', [
+                d.file('bar.txt', 'bar'),
+              ]),
+            ])
+          ])
+        ]),
+        d.dir('web', [
+          d.file("index.html", "html"),
+          d.dir('assets', [
+            d.dir('foo', [
+              d.file('foo.txt', 'foo'),
+              d.dir('sub', [
+                d.file('bar.txt', 'bar'),
+              ]),
+            ])
           ])
         ])
       ])
diff --git a/test/build/includes_dart_files_in_debug_mode_test.dart b/test/build/includes_dart_files_in_debug_mode_test.dart
index 3d3281c0..54130274 100644
--- a/test/build/includes_dart_files_in_debug_mode_test.dart
+++ b/test/build/includes_dart_files_in_debug_mode_test.dart
@@ -22,19 +22,20 @@ main() {
     ]).create();
 
     schedulePub(args: ["build", "--mode", "debug"],
-        output: new RegExp(r"Built 4 files!"),
-        exitCode: 0);
+        output: new RegExp(r"Built 4 files!"));
 
     d.dir(appPath, [
       d.dir('build', [
-        d.nothing('file1.dart.js'),
-        d.matcherFile('file1.dart', isNot(isEmpty)),
-        d.nothing('file2.dart.js'),
-        d.matcherFile('file2.dart', isNot(isEmpty)),
-        d.nothing('file3.dart.js'),
-        d.matcherFile('file3.dart', isNot(isEmpty)),
-        d.nothing('file4.dart.js'),
-        d.matcherFile('file4.dart', isNot(isEmpty))
+        d.dir('web', [
+          d.nothing('file1.dart.js'),
+          d.matcherFile('file1.dart', isNot(isEmpty)),
+          d.nothing('file2.dart.js'),
+          d.matcherFile('file2.dart', isNot(isEmpty)),
+          d.nothing('file3.dart.js'),
+          d.matcherFile('file3.dart', isNot(isEmpty)),
+          d.nothing('file4.dart.js'),
+          d.matcherFile('file4.dart', isNot(isEmpty))
+        ])
       ])
     ]).validate();
   });
diff --git a/test/build/missing_build_directories_test.dart b/test/build/missing_build_directories_test.dart
new file mode 100644
index 00000000..dc6ec8db
--- /dev/null
+++ b/test/build/missing_build_directories_test.dart
@@ -0,0 +1,27 @@
+// 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.
+
+import '../../lib/src/exit_codes.dart' as exit_codes;
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+
+  integration("fails if any specified build directories don't exist", () {
+    d.dir(appPath, [
+      d.appPubspec(),
+      d.dir('example', [
+        d.file('file.txt', 'example')
+      ]),
+      d.dir('web', [
+        d.file('file.txt', 'test')
+      ])
+    ]).create();
+
+    schedulePub(args: ["build", "benchmark", "example", "test", "web"],
+        error: 'Directories "benchmark" and "test" do not exist.',
+        exitCode: exit_codes.DATA);
+  });
+}
diff --git a/test/build/with_no_web_directory_test.dart b/test/build/missing_web_directory_test.dart
similarity index 51%
rename from test/build/with_no_web_directory_test.dart
rename to test/build/missing_web_directory_test.dart
index d25df725..ccf4264d 100644
--- a/test/build/with_no_web_directory_test.dart
+++ b/test/build/missing_web_directory_test.dart
@@ -1,19 +1,19 @@
-// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// 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.
 
+import '../../lib/src/exit_codes.dart' as exit_codes;
 import '../descriptor.dart' as d;
 import '../test_pub.dart';
 
 main() {
   initConfig();
 
-  integration("with no web directory", () {
+  integration("fails if 'web' doesn't exist and no directory is specified", () {
     d.appDir().create();
 
     schedulePub(args: ["build"],
-        error: new RegExp(r'^There is no "[^"]+[/\\]web" directory.$',
-            multiLine: true),
-        exitCode: 1);
+        error: 'Directory "web" does not exist.',
+        exitCode: exit_codes.DATA);
   });
 }
diff --git a/test/build/name_arg_builds_only_that_directory_test.dart b/test/build/name_arg_builds_only_that_directory_test.dart
new file mode 100644
index 00000000..699cc734
--- /dev/null
+++ b/test/build/name_arg_builds_only_that_directory_test.dart
@@ -0,0 +1,43 @@
+// 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.
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+
+  integration("if a dir name is given, only includes that dir", () {
+    d.dir(appPath, [
+      d.appPubspec(),
+      d.dir('asset', [
+        d.file('file.txt', 'asset')
+      ]),
+      d.dir('example', [
+        d.file('file.txt', 'example')
+      ]),
+      d.dir('web', [
+        d.file('file.txt', 'test')
+      ])
+    ]).create();
+
+    schedulePub(args: ["build", "example"],
+        output: new RegExp(r"Built 2 files!"));
+
+    d.dir(appPath, [
+      d.dir('build', [
+        d.dir('example', [
+          d.file('file.txt', 'example'),
+          d.dir('assets', [
+            d.dir('myapp', [
+              d.file('file.txt', 'asset')
+            ])
+          ])
+        ]),
+        // Only example should be built.
+        d.nothing('web')
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/build/reports_dart_parse_errors_test.dart b/test/build/reports_dart_parse_errors_test.dart
index 20f636ff..c05eab5c 100644
--- a/test/build/reports_dart_parse_errors_test.dart
+++ b/test/build/reports_dart_parse_errors_test.dart
@@ -41,9 +41,7 @@ main() {
     // Doesn't output anything if an error occurred.
     d.dir(appPath, [
       d.dir('build', [
-        d.nothing('file.dart.js'),
-        d.nothing('file.dart'),
-        d.nothing('subdir')
+        d.nothing('web')
       ])
     ]).validate();
   });
diff --git a/test/build/unsupported_build_directories_test.dart b/test/build/unsupported_build_directories_test.dart
new file mode 100644
index 00000000..7e3c2033
--- /dev/null
+++ b/test/build/unsupported_build_directories_test.dart
@@ -0,0 +1,21 @@
+// 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.
+
+import '../../lib/src/exit_codes.dart' as exit_codes;
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+
+  integration("fails if given directories are not buildable", () {
+    d.appDir().create();
+
+    schedulePub(args: ["build", "foo", "bar"],
+        error: 'Unsupported build directories "bar" and "foo".\n'
+               'The allowed directories are "benchmark", "bin", "example", '
+                   '"test" and "web".',
+        exitCode: exit_codes.USAGE);
+  });
+}
diff --git a/test/build/warns_on_assets_paths_test.dart b/test/build/warns_on_assets_paths_test.dart
index c9a19c24..963ebf13 100644
--- a/test/build/warns_on_assets_paths_test.dart
+++ b/test/build/warns_on_assets_paths_test.dart
@@ -32,8 +32,7 @@ main() {
 
     var assetsPath = path.join('web', 'assets');
     schedulePub(args: ['build'],
-        error: getWarningRegExp(assetsPath),
-        exitCode: 0);
+        error: getWarningRegExp(assetsPath));
   });
 
   integration('warns user about assets dir nested anywhere in "web"', () {
@@ -49,8 +48,7 @@ main() {
 
     var assetsPath = path.join('web', 'foo', 'assets');
     schedulePub(args: ['build'],
-        error: getWarningRegExp(assetsPath),
-        exitCode: 0);
+        error: getWarningRegExp(assetsPath));
   });
 
   integration('warns user about assets file in the root of "web"', () {
@@ -64,8 +62,7 @@ main() {
 
     var assetsPath = path.join('web', 'assets');
     schedulePub(args: ['build'],
-        error: getWarningRegExp(assetsPath),
-        exitCode: 0);
+        error: getWarningRegExp(assetsPath));
   });
 
   integration('warns user about assets file nested anywhere in "web"', () {
@@ -81,8 +78,7 @@ main() {
 
     var assetsPath = path.join('web', 'foo', 'assets');
     schedulePub(args: ['build'],
-        error: getWarningRegExp(assetsPath),
-        exitCode: 0);
+        error: getWarningRegExp(assetsPath));
   });
 
   integration('does not warn if no assets dir or file anywhere in "web"', () {
@@ -96,7 +92,6 @@ main() {
 
     schedulePub(args: ['build'],
         error: new RegExp(
-            r'^(?!Warning: Pub reserves paths containing "assets").*$'),
-        exitCode: 0);
+            r'^(?!Warning: Pub reserves paths containing "assets").*$'));
   });
 }
diff --git a/test/lish/archives_and_uploads_a_package_test.dart b/test/lish/archives_and_uploads_a_package_test.dart
index 1ef75ce7..def2c85a 100644
--- a/test/lish/archives_and_uploads_a_package_test.dart
+++ b/test/lish/archives_and_uploads_a_package_test.dart
@@ -7,6 +7,7 @@ import 'dart:convert';
 import 'package:scheduled_test/scheduled_test.dart';
 import 'package:scheduled_test/scheduled_server.dart';
 
+import '../../lib/src/exit_codes.dart' as exit_codes;
 import '../descriptor.dart' as d;
 import '../test_pub.dart';
 import 'utils.dart';
@@ -35,7 +36,7 @@ main() {
 
     expect(pub.nextLine(),
         completion(equals('Package test_pkg 1.0.0 uploaded!')));
-    pub.shouldExit(0);
+    pub.shouldExit(exit_codes.SUCCESS);
   });
 
   // TODO(nweiz): Once a multipart/form-data parser in Dart exists, we should
diff --git a/test/lish/force_does_not_publish_if_there_are_errors_test.dart b/test/lish/force_does_not_publish_if_there_are_errors_test.dart
index 4e85a6c2..ab7ef6e1 100644
--- a/test/lish/force_does_not_publish_if_there_are_errors_test.dart
+++ b/test/lish/force_does_not_publish_if_there_are_errors_test.dart
@@ -5,6 +5,7 @@
 import 'package:scheduled_test/scheduled_test.dart';
 import 'package:scheduled_test/scheduled_server.dart';
 
+import '../../lib/src/exit_codes.dart' as exit_codes;
 import '../descriptor.dart' as d;
 import '../test_pub.dart';
 
@@ -20,7 +21,7 @@ main() {
     var server = new ScheduledServer();
     var pub = startPublish(server, args: ['--force']);
 
-    pub.shouldExit(0);
+    pub.shouldExit(exit_codes.SUCCESS);
     expect(pub.remainingStderr(), completion(contains(
         "Sorry, your package is missing a requirement and can't be "
         "published yet.")));
diff --git a/test/lish/force_publishes_if_tests_are_no_warnings_or_errors_test.dart b/test/lish/force_publishes_if_tests_are_no_warnings_or_errors_test.dart
index 40b2aeec..0d8d8426 100644
--- a/test/lish/force_publishes_if_tests_are_no_warnings_or_errors_test.dart
+++ b/test/lish/force_publishes_if_tests_are_no_warnings_or_errors_test.dart
@@ -7,6 +7,7 @@ import 'dart:convert';
 import 'package:scheduled_test/scheduled_test.dart';
 import 'package:scheduled_test/scheduled_server.dart';
 
+import '../../lib/src/exit_codes.dart' as exit_codes;
 import '../descriptor.dart' as d;
 import '../test_pub.dart';
 import 'utils.dart';
@@ -30,7 +31,7 @@ main() {
       request.response.close();
     });
 
-    pub.shouldExit(0);
+    pub.shouldExit(exit_codes.SUCCESS);
     expect(pub.remainingStdout(), completion(contains(
         'Package test_pkg 1.0.0 uploaded!')));
   });
diff --git a/test/lish/force_publishes_if_there_are_warnings_test.dart b/test/lish/force_publishes_if_there_are_warnings_test.dart
index f89e6c29..d097e72a 100644
--- a/test/lish/force_publishes_if_there_are_warnings_test.dart
+++ b/test/lish/force_publishes_if_there_are_warnings_test.dart
@@ -7,6 +7,7 @@ import 'dart:convert';
 import 'package:scheduled_test/scheduled_test.dart';
 import 'package:scheduled_test/scheduled_server.dart';
 
+import '../../lib/src/exit_codes.dart' as exit_codes;
 import '../descriptor.dart' as d;
 import '../test_pub.dart';
 import 'utils.dart';
@@ -34,7 +35,7 @@ main() {
       request.response.close();
     });
 
-    pub.shouldExit(0);
+    pub.shouldExit(exit_codes.SUCCESS);
     expect(pub.remainingStderr(), completion(contains(
         'Suggestions:\n* Author "Nathan Weizenbaum" in pubspec.yaml'
         ' should have an email address\n'
diff --git a/test/lish/package_creation_provides_an_error_test.dart b/test/lish/package_creation_provides_an_error_test.dart
index a1c134d1..d747b3be 100644
--- a/test/lish/package_creation_provides_an_error_test.dart
+++ b/test/lish/package_creation_provides_an_error_test.dart
@@ -7,6 +7,7 @@ import 'dart:convert';
 import 'package:scheduled_test/scheduled_test.dart';
 import 'package:scheduled_test/scheduled_server.dart';
 
+import '../../lib/src/exit_codes.dart' as exit_codes;
 import '../descriptor.dart' as d;
 import '../test_pub.dart';
 import 'utils.dart';
diff --git a/test/lish/package_validation_has_a_warning_and_continues_test.dart b/test/lish/package_validation_has_a_warning_and_continues_test.dart
index 01916ef9..f068b67d 100644
--- a/test/lish/package_validation_has_a_warning_and_continues_test.dart
+++ b/test/lish/package_validation_has_a_warning_and_continues_test.dart
@@ -7,6 +7,7 @@ import 'dart:convert';
 import 'package:scheduled_test/scheduled_test.dart';
 import 'package:scheduled_test/scheduled_server.dart';
 
+import '../../lib/src/exit_codes.dart' as exit_codes;
 import '../descriptor.dart' as d;
 import '../test_pub.dart';
 import 'utils.dart';
@@ -34,7 +35,7 @@ main() {
       request.response.close();
     });
 
-    pub.shouldExit(0);
+    pub.shouldExit(exit_codes.SUCCESS);
     expect(pub.remainingStdout(),
         completion(contains('Package test_pkg 1.0.0 uploaded!')));
   });
diff --git a/test/lish/package_validation_has_a_warning_and_is_canceled_test.dart b/test/lish/package_validation_has_a_warning_and_is_canceled_test.dart
index c9c75fb9..9c115bcd 100644
--- a/test/lish/package_validation_has_a_warning_and_is_canceled_test.dart
+++ b/test/lish/package_validation_has_a_warning_and_is_canceled_test.dart
@@ -5,6 +5,7 @@
 import 'package:scheduled_test/scheduled_test.dart';
 import 'package:scheduled_test/scheduled_server.dart';
 
+import '../../lib/src/exit_codes.dart' as exit_codes;
 import '../descriptor.dart' as d;
 import '../test_pub.dart';
 
@@ -21,7 +22,7 @@ main() {
     var pub = startPublish(server);
 
     pub.writeLine("n");
-    pub.shouldExit(0);
+    pub.shouldExit(exit_codes.SUCCESS);
     expect(pub.remainingStderr(),
         completion(contains("Package upload canceled.")));
   });
diff --git a/test/lish/package_validation_has_an_error_test.dart b/test/lish/package_validation_has_an_error_test.dart
index b39694e4..50722131 100644
--- a/test/lish/package_validation_has_an_error_test.dart
+++ b/test/lish/package_validation_has_an_error_test.dart
@@ -5,6 +5,7 @@
 import 'package:scheduled_test/scheduled_test.dart';
 import 'package:scheduled_test/scheduled_server.dart';
 
+import '../../lib/src/exit_codes.dart' as exit_codes;
 import '../descriptor.dart' as d;
 import '../test_pub.dart';
 
@@ -20,7 +21,7 @@ main() {
     var server = new ScheduledServer();
     var pub = startPublish(server);
 
-    pub.shouldExit(0);
+    pub.shouldExit(exit_codes.SUCCESS);
     expect(pub.remainingStderr(), completion(contains(
         "Sorry, your package is missing a requirement and can't be published "
         "yet.")));
diff --git a/test/lish/preview_package_validation_has_a_warning_test.dart b/test/lish/preview_package_validation_has_a_warning_test.dart
index 4509f120..8bf1251d 100644
--- a/test/lish/preview_package_validation_has_a_warning_test.dart
+++ b/test/lish/preview_package_validation_has_a_warning_test.dart
@@ -5,6 +5,7 @@
 import 'package:scheduled_test/scheduled_test.dart';
 import 'package:scheduled_test/scheduled_server.dart';
 
+import '../../lib/src/exit_codes.dart' as exit_codes;
 import '../descriptor.dart' as d;
 import '../test_pub.dart';
 
@@ -20,7 +21,7 @@ main() {
     var server = new ScheduledServer();
     var pub = startPublish(server, args: ['--dry-run']);
 
-    pub.shouldExit(0);
+    pub.shouldExit(exit_codes.SUCCESS);
     expect(pub.remainingStderr(), completion(contains(
         'Suggestions:\n* Author "Nathan Weizenbaum" in pubspec.yaml should '
             'have an email address\n'
diff --git a/test/lish/preview_package_validation_has_no_warnings_test.dart b/test/lish/preview_package_validation_has_no_warnings_test.dart
index 9d4d1cb2..e83c7e88 100644
--- a/test/lish/preview_package_validation_has_no_warnings_test.dart
+++ b/test/lish/preview_package_validation_has_no_warnings_test.dart
@@ -5,6 +5,7 @@
 import 'package:scheduled_test/scheduled_test.dart';
 import 'package:scheduled_test/scheduled_server.dart';
 
+import '../../lib/src/exit_codes.dart' as exit_codes;
 import '../descriptor.dart' as d;
 import '../test_pub.dart';
 
@@ -20,7 +21,7 @@ main() {
     var server = new ScheduledServer();
     var pub = startPublish(server, args: ['--dry-run']);
 
-    pub.shouldExit(0);
+    pub.shouldExit(exit_codes.SUCCESS);
     expect(pub.remainingStderr(),
         completion(contains('Package has 0 warnings.')));
   });
diff --git a/test/pub_uploader_test.dart b/test/pub_uploader_test.dart
index 9c8b822f..7f0bde9e 100644
--- a/test/pub_uploader_test.dart
+++ b/test/pub_uploader_test.dart
@@ -11,6 +11,7 @@ import 'package:scheduled_test/scheduled_process.dart';
 import 'package:scheduled_test/scheduled_server.dart';
 import 'package:scheduled_test/scheduled_test.dart';
 
+import '../lib/src/exit_codes.dart' as exit_codes;
 import '../lib/src/io.dart';
 import '../lib/src/utils.dart';
 import 'descriptor.dart' as d;
@@ -40,17 +41,17 @@ main() {
   group('displays usage', () {
     integration('when run with no arguments', () {
       schedulePub(args: ['uploader'],
-          output: USAGE_STRING, exitCode: 64);
+          output: USAGE_STRING, exitCode: exit_codes.USAGE);
     });
 
     integration('when run with only a command', () {
       schedulePub(args: ['uploader', 'add'],
-          output: USAGE_STRING, exitCode: 64);
+          output: USAGE_STRING, exitCode: exit_codes.USAGE);
     });
 
     integration('when run with an invalid command', () {
       schedulePub(args: ['uploader', 'foo', 'email'],
-          output: USAGE_STRING, exitCode: 64);
+          output: USAGE_STRING, exitCode: exit_codes.USAGE);
     });
   });
 
@@ -73,7 +74,7 @@ main() {
     });
 
     expect(pub.nextLine(), completion(equals('Good job!')));
-    pub.shouldExit(0);
+    pub.shouldExit(exit_codes.SUCCESS);
   });
 
   integration('removes an uploader', () {
@@ -91,7 +92,7 @@ main() {
     });
 
     expect(pub.nextLine(), completion(equals('Good job!')));
-    pub.shouldExit(0);
+    pub.shouldExit(exit_codes.SUCCESS);
   });
 
   integration('defaults to the current package', () {
@@ -111,7 +112,7 @@ main() {
     });
 
     expect(pub.nextLine(), completion(equals('Good job!')));
-    pub.shouldExit(0);
+    pub.shouldExit(exit_codes.SUCCESS);
   });
 
   integration('add provides an error', () {
diff --git a/test/real_version_test.dart b/test/real_version_test.dart
index d1aa7370..75f1a22d 100644
--- a/test/real_version_test.dart
+++ b/test/real_version_test.dart
@@ -6,11 +6,12 @@ library pub_tests;
 
 import 'dart:io';
 
-import '../lib/src/sdk.dart' as sdk;
-
 import 'package:path/path.dart' as path;
 import 'package:scheduled_test/scheduled_process.dart';
 import 'package:scheduled_test/scheduled_test.dart';
+
+import '../lib/src/exit_codes.dart' as exit_codes;
+import '../lib/src/sdk.dart' as sdk;
 import 'test_pub.dart';
 
 main() {
@@ -33,6 +34,6 @@ main() {
 
     var pub = new ScheduledProcess.start(pubPath, ['version']);
     expect(pub.nextLine(), completion(startsWith("Pub")));
-    pub.shouldExit(0);
+    pub.shouldExit(exit_codes.SUCCESS);
   });
 }
diff --git a/test/test_pub.dart b/test/test_pub.dart
index 8a787138..bd8fc118 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -22,6 +22,7 @@ import 'package:unittest/compact_vm_config.dart';
 import 'package:yaml/yaml.dart';
 
 import '../lib/src/entrypoint.dart';
+import '../lib/src/exit_codes.dart' as exit_codes;
 // TODO(rnystrom): Using "gitlib" as the prefix here is ugly, but "git" collides
 // with the git descriptor method. Maybe we should try to clean up the top level
 // scope a bit?
@@ -382,7 +383,7 @@ void scheduleSymlink(String target, String symlink) {
 /// [outputJson]), [error], and [exitCode]. If [outputJson] is given, validates
 /// that pub outputs stringified JSON matching that object.
 void schedulePub({List args, Pattern output, Pattern error, outputJson,
-    Future<Uri> tokenEndpoint, int exitCode: 0}) {
+    Future<Uri> tokenEndpoint, int exitCode: exit_codes.SUCCESS}) {
   // Cannot pass both output and outputJson.
   assert(output == null || outputJson == null);
 
diff --git a/test/transformer/dart2js/minify_configuration_overrides_mode_test.dart b/test/transformer/dart2js/minify_configuration_overrides_mode_test.dart
index 3931f5f8..646d6249 100644
--- a/test/transformer/dart2js/minify_configuration_overrides_mode_test.dart
+++ b/test/transformer/dart2js/minify_configuration_overrides_mode_test.dart
@@ -4,8 +4,6 @@
 
 library pub_tests;
 
-import 'package:scheduled_test/scheduled_test.dart';
-
 import '../../descriptor.dart' as d;
 import '../../test_pub.dart';
 import '../../serve/utils.dart';
diff --git a/test/transformer/dart2js/output_can_be_consumed_by_successive_phases.dart b/test/transformer/dart2js/output_can_be_consumed_by_successive_phases.dart
index 4bdd6943..f44eb905 100644
--- a/test/transformer/dart2js/output_can_be_consumed_by_successive_phases.dart
+++ b/test/transformer/dart2js/output_can_be_consumed_by_successive_phases.dart
@@ -4,8 +4,6 @@
 
 library pub_tests;
 
-import 'package:scheduled_test/scheduled_test.dart';
-
 import '../../descriptor.dart' as d;
 import '../../test_pub.dart';
 import '../../serve/utils.dart';
diff --git a/test/transformer/dart2js/supports_configuration_with_build_test.dart b/test/transformer/dart2js/supports_configuration_with_build_test.dart
new file mode 100644
index 00000000..e407ee41
--- /dev/null
+++ b/test/transformer/dart2js/supports_configuration_with_build_test.dart
@@ -0,0 +1,91 @@
+// Copyright (c) 2013, 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.
+
+import 'dart:convert';
+
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+
+  integration("compiles dart.js and interop.js next to entrypoints when "
+              "dartjs is explicitly configured", () {
+    // Dart2js can take a long time to compile dart code, so we increase the
+    // timeout to cope with that.
+    currentSchedule.timeout *= 3;
+
+    serve([
+      d.dir('api', [
+        d.dir('packages', [
+          d.file('browser', JSON.encode({
+            'versions': [packageVersionApiMap(packageMap('browser', '1.0.0'))]
+          })),
+          d.dir('browser', [
+            d.dir('versions', [
+              d.file('1.0.0', JSON.encode(
+                  packageVersionApiMap(
+                      packageMap('browser', '1.0.0'),
+                      full: true)))
+            ])
+          ])
+        ])
+      ]),
+      d.dir('packages', [
+        d.dir('browser', [
+          d.dir('versions', [
+            d.tar('1.0.0.tar.gz', [
+              d.file('pubspec.yaml', yaml(packageMap("browser", "1.0.0"))),
+              d.dir('lib', [
+                d.file('dart.js', 'contents of dart.js'),
+                d.file('interop.js', 'contents of interop.js')
+              ])
+            ])
+          ])
+        ])
+      ])
+    ]);
+
+    d.dir(appPath, [
+      d.pubspec({
+        "name": "myapp",
+        "dependencies": {
+          "browser": "1.0.0"
+        },
+        "transformers": [{
+          "\$dart2js": {
+            "minify": true
+          }
+        }]
+      }),
+      d.dir('web', [
+        d.file('file.dart', 'void main() => print("hello");'),
+      ])
+    ]).create();
+
+    pubGet();
+
+    schedulePub(args: ["build"],
+        output: new RegExp(r"Built 5 files!"),
+        exitCode: 0);
+
+    d.dir(appPath, [
+      d.dir('build', [
+        d.dir('web', [
+          d.matcherFile('file.dart.js', isMinifiedDart2JSOutput),
+          d.matcherFile('file.dart.precompiled.js', isNot(isEmpty)),
+          d.matcherFile('file.dart.js.map', isNot(isEmpty)),
+          d.dir('packages', [
+            d.dir('browser', [
+              d.file('dart.js', 'contents of dart.js'),
+              d.file('interop.js', 'contents of interop.js')
+            ])
+          ]),
+        ])
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/transformer/mode_defaults_to_debug_in_serve_test.dart b/test/transformer/mode_defaults_to_debug_in_serve_test.dart
index 287dc41b..a20d75a0 100644
--- a/test/transformer/mode_defaults_to_debug_in_serve_test.dart
+++ b/test/transformer/mode_defaults_to_debug_in_serve_test.dart
@@ -4,8 +4,6 @@
 
 library pub_tests;
 
-import 'dart:convert';
-
 import '../descriptor.dart' as d;
 import '../test_pub.dart';
 import '../serve/utils.dart';
diff --git a/test/transformer/mode_defaults_to_release_in_build_test.dart b/test/transformer/mode_defaults_to_release_in_build_test.dart
index 5109c82e..b077d71e 100644
--- a/test/transformer/mode_defaults_to_release_in_build_test.dart
+++ b/test/transformer/mode_defaults_to_release_in_build_test.dart
@@ -47,12 +47,13 @@ main() {
     createLockFile('myapp', pkg: ['barback']);
 
     schedulePub(args: ["build"],
-        output: new RegExp(r"Built 1 file!"),
-        exitCode: 0);
+        output: new RegExp(r"Built 1 file!"));
 
     d.dir(appPath, [
       d.dir('build', [
-        d.file('foo.txt', 'release')
+        d.dir('web', [
+          d.file('foo.txt', 'release')
+        ])
       ])
     ]).validate();
   });
-- 
GitLab