From 73f82a4bac5fd3cd50451c562e512f00d32e2add Mon Sep 17 00:00:00 2001
From: "rnystrom@google.com" <rnystrom@google.com>
Date: Thu, 18 Sep 2014 22:23:06 +0000
Subject: [PATCH] Create binstubs for executables when activating a package.

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

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

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge@40471 260f80e4-7a28-3924-810f-c04153c831b5
---
 lib/src/barback/asset_environment.dart        |   5 +-
 lib/src/command/global_activate.dart          |  33 ++-
 lib/src/command/global_deactivate.dart        |   2 +-
 lib/src/global_packages.dart                  | 262 ++++++++++++++++--
 lib/src/package.dart                          |  16 +-
 lib/src/pubspec.dart                          |  55 ++++
 lib/src/utils.dart                            |  12 +
 lib/src/validator.dart                        |   2 +
 lib/src/validator/executable.dart             |  37 +++
 lib/src/validator/name.dart                   |   3 +-
 test/global/activate/bad_version_test.dart    |  14 +-
 .../activate/constraint_with_path_test.dart   |  13 +-
 .../activate/missing_package_arg_test.dart    |  12 +-
 .../activate/unexpected_arguments_test.dart   |  12 +-
 .../creates_executables_in_pubspec_test.dart  |  40 +++
 ...licit_and_no_executables_options_test.dart |  24 ++
 .../binstubs/explicit_executables_test.dart   |  40 +++
 test/global/binstubs/missing_script_test.dart |  32 +++
 test/global/binstubs/name_collision_test.dart |  61 ++++
 .../name_collision_with_overwrite_test.dart   |  62 +++++
 .../binstubs/no_executables_flag_test.dart    |  38 +++
 test/global/binstubs/path_package_test.dart   |  34 +++
 ...activate_removes_old_executables_test.dart |  48 ++++
 .../removes_even_if_not_in_pubspec_test.dart  |  45 +++
 .../removes_when_deactivated_test.dart        |  37 +++
 .../unknown_explicit_executable_test.dart     |  35 +++
 test/pubspec_test.dart                        |  53 ++++
 test/validator/executable_test.dart           |  57 ++++
 28 files changed, 1013 insertions(+), 71 deletions(-)
 create mode 100644 lib/src/validator/executable.dart
 create mode 100644 test/global/binstubs/creates_executables_in_pubspec_test.dart
 create mode 100644 test/global/binstubs/explicit_and_no_executables_options_test.dart
 create mode 100644 test/global/binstubs/explicit_executables_test.dart
 create mode 100644 test/global/binstubs/missing_script_test.dart
 create mode 100644 test/global/binstubs/name_collision_test.dart
 create mode 100644 test/global/binstubs/name_collision_with_overwrite_test.dart
 create mode 100644 test/global/binstubs/no_executables_flag_test.dart
 create mode 100644 test/global/binstubs/path_package_test.dart
 create mode 100644 test/global/binstubs/reactivate_removes_old_executables_test.dart
 create mode 100644 test/global/binstubs/removes_even_if_not_in_pubspec_test.dart
 create mode 100644 test/global/binstubs/removes_when_deactivated_test.dart
 create mode 100644 test/global/binstubs/unknown_explicit_executable_test.dart
 create mode 100644 test/validator/executable_test.dart

diff --git a/lib/src/barback/asset_environment.dart b/lib/src/barback/asset_environment.dart
index 613d94e8..7b0fca29 100644
--- a/lib/src/barback/asset_environment.dart
+++ b/lib/src/barback/asset_environment.dart
@@ -583,15 +583,12 @@ class AssetEnvironment {
   /// this is optimized for our needs in here instead of using the more general
   /// but slower [listDir].
   Iterable<AssetId> _listDirectorySources(Package package, String dir) {
-    var subdirectory = path.join(package.dir, dir);
-    if (!dirExists(subdirectory)) return [];
-
     // This is used in some performance-sensitive paths and can list many, many
     // files. As such, it leans more havily towards optimization as opposed to
     // readability than most code in pub. In particular, it avoids using the
     // path package, since re-parsing a path is very expensive relative to
     // string operations.
-    return package.listFiles(beneath: subdirectory).map((file) {
+    return package.listFiles(beneath: dir).map((file) {
       // From profiling, path.relative here is just as fast as a raw substring
       // and is correct in the case where package.dir has a trailing slash.
       var relative = path.relative(file, from: package.dir);
diff --git a/lib/src/command/global_activate.dart b/lib/src/command/global_activate.dart
index 8448af58..e78641b4 100644
--- a/lib/src/command/global_activate.dart
+++ b/lib/src/command/global_activate.dart
@@ -22,9 +22,33 @@ class GlobalActivateCommand extends PubCommand {
         help: "The source used to find the package.",
         allowed: ["git", "hosted", "path"],
         defaultsTo: "hosted");
+
+    commandParser.addFlag("no-executables", negatable: false,
+        help: "Do not put executables on PATH.");
+
+    commandParser.addOption("executable", abbr: "x",
+        help: "Executable(s) to place on PATH.",
+        allowMultiple: true);
+
+    commandParser.addFlag("overwrite", negatable: false,
+        help: "Overwrite executables from other packages with the same name.");
   }
 
   Future onRun() {
+    // Default to `null`, which means all executables.
+    var executables;
+    if (commandOptions.wasParsed("executable")) {
+      if (commandOptions.wasParsed("no-executables")) {
+        usageError("Cannot pass both --no-executables and --executable.");
+      }
+
+      executables = commandOptions["executable"];
+    } else if (commandOptions["no-executables"]) {
+      // An empty list means no executables.
+      executables = [];
+    }
+
+    var overwrite = commandOptions["overwrite"];
     var args = commandOptions.rest;
 
     readArg([String error]) {
@@ -46,7 +70,8 @@ class GlobalActivateCommand extends PubCommand {
         var repo = readArg("No Git repository given.");
         // TODO(rnystrom): Allow passing in a Git ref too.
         validateNoExtraArgs();
-        return globals.activateGit(repo);
+        return globals.activateGit(repo, executables,
+            overwriteBinStubs: overwrite);
 
       case "hosted":
         var package = readArg("No package to activate given.");
@@ -62,12 +87,14 @@ class GlobalActivateCommand extends PubCommand {
         }
 
         validateNoExtraArgs();
-        return globals.activateHosted(package, constraint);
+        return globals.activateHosted(package, constraint, executables,
+            overwriteBinStubs: overwrite);
 
       case "path":
         var path = readArg("No package to activate given.");
         validateNoExtraArgs();
-        return globals.activatePath(path);
+        return globals.activatePath(path, executables,
+            overwriteBinStubs: overwrite);
     }
 
     throw "unreachable";
diff --git a/lib/src/command/global_deactivate.dart b/lib/src/command/global_deactivate.dart
index 21483479..4e966552 100644
--- a/lib/src/command/global_deactivate.dart
+++ b/lib/src/command/global_deactivate.dart
@@ -29,7 +29,7 @@ class GlobalDeactivateCommand extends PubCommand {
       usageError("Unexpected $arguments ${toSentence(unexpected)}.");
     }
 
-    if (!globals.deactivate(commandOptions.rest.first, logDeactivate: true)) {
+    if (!globals.deactivate(commandOptions.rest.first)) {
       dataError("No active package ${log.bold(commandOptions.rest.first)}.");
     }
 
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index f5a7d658..2b9d5edb 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -18,14 +18,19 @@ import 'lock_file.dart';
 import 'log.dart' as log;
 import 'package.dart';
 import 'pubspec.dart';
-import 'system_cache.dart';
+import 'sdk.dart' as sdk;
 import 'solver/version_solver.dart';
 import 'source/cached.dart';
 import 'source/git.dart';
 import 'source/path.dart';
+import 'system_cache.dart';
 import 'utils.dart';
 import 'version.dart';
 
+/// Matches the package name that a binstub was created for inside the contents
+/// of the shell script.
+final _binStubPackagePattern = new RegExp(r"Package: ([a-zA-Z0-9_-]+)");
+
 /// Maintains the set of packages that have been globally activated.
 ///
 /// These have been hand-chosen by the user to make their executables in bin/
@@ -57,6 +62,9 @@ class GlobalPackages {
   /// The directory where the lockfiles for activated packages are stored.
   String get _directory => p.join(cache.rootDir, "global_packages");
 
+  /// The directory where binstubs for global package executables are stored.
+  String get _binStubDir => p.join(cache.rootDir, "bin");
+
   /// Creates a new global package registry backed by the given directory on
   /// the user's file system.
   ///
@@ -66,7 +74,16 @@ class GlobalPackages {
 
   /// Caches the package located in the Git repository [repo] and makes it the
   /// active global version.
-  Future activateGit(String repo) async {
+  ///
+  /// [executables] is the names of the executables that should have binstubs.
+  /// If `null`, all executables in the package will get binstubs. If empty, no
+  /// binstubs will be created.
+  ///
+  /// if [overwriteBinStubs] is `true`, any binstubs that collide with
+  /// existing binstubs in other packages will be overwritten by this one's.
+  /// Otherwise, the previous ones will be preserved.
+  Future activateGit(String repo, List<String> executables,
+      {bool overwriteBinStubs}) async {
     var source = cache.sources["git"] as GitSource;
     var name = await source.getPackageNameFromRepo(repo);
     // Call this just to log what the current active package is, if any.
@@ -76,19 +93,42 @@ class GlobalPackages {
     // dependencies. Their executables shouldn't be cached, and there should
     // be a mechanism for redoing dependency resolution if a path pubspec has
     // changed (see also issue 20499).
-    await _installInCache(
+    var package = await _installInCache(
         new PackageDep(name, "git", VersionConstraint.any, repo));
+    _updateBinStubs(package, executables,
+        overwriteBinStubs: overwriteBinStubs);
   }
 
   /// Finds the latest version of the hosted package with [name] that matches
   /// [constraint] and makes it the active global version.
-  Future activateHosted(String name, VersionConstraint constraint) {
+  ///
+  /// [executables] is the names of the executables that should have binstubs.
+  /// If `null`, all executables in the package will get binstubs. If empty, no
+  /// binstubs will be created.
+  ///
+  /// if [overwriteBinStubs] is `true`, any binstubs that collide with
+  /// existing binstubs in other packages will be overwritten by this one's.
+  /// Otherwise, the previous ones will be preserved.
+  Future activateHosted(String name, VersionConstraint constraint,
+      List<String> executables, {bool overwriteBinStubs}) async {
     _describeActive(name);
-    return _installInCache(new PackageDep(name, "hosted", constraint, name));
+    var package = await _installInCache(
+        new PackageDep(name, "hosted", constraint, name));
+    _updateBinStubs(package, executables,
+        overwriteBinStubs: overwriteBinStubs);
   }
 
   /// Makes the local package at [path] globally active.
-  Future activatePath(String path) async {
+  ///
+  /// [executables] is the names of the executables that should have binstubs.
+  /// If `null`, all executables in the package will get binstubs. If empty, no
+  /// binstubs will be created.
+  ///
+  /// if [overwriteBinStubs] is `true`, any binstubs that collide with
+  /// existing binstubs in other packages will be overwritten by this one's.
+  /// Otherwise, the previous ones will be preserved.
+  Future activatePath(String path, List<String> executables,
+      {bool overwriteBinStubs}) async {
     var entrypoint = new Entrypoint(path, cache);
 
     // Get the package's dependencies.
@@ -109,10 +149,15 @@ class GlobalPackages {
 
     var binDir = p.join(_directory, name, 'bin');
     if (dirExists(binDir)) deleteEntry(binDir);
+
+    _updateBinStubs(entrypoint.root, executables,
+        overwriteBinStubs: overwriteBinStubs);
   }
 
   /// Installs the package [dep] and its dependencies into the system cache.
-  Future _installInCache(PackageDep dep) async {
+  ///
+  /// Returns the cached root [Package].
+  Future<Package> _installInCache(PackageDep dep) async {
     var source = cache.sources[dep.source];
 
     // Create a dummy package with just [dep] so we can do resolution on it.
@@ -140,6 +185,8 @@ class GlobalPackages {
         .loadPackageGraph(result);
     await _precompileExecutables(graph.entrypoint, dep.name);
     _writeLockFile(dep.name, lockFile);
+
+    return graph.packages[dep.name];
   }
 
   /// Precompiles the executables for [package] and saves them in the global
@@ -188,7 +235,6 @@ class GlobalPackages {
 
     var id = lockFile.packages[package];
     log.message('Activated ${_formatPackage(id)}.');
-
   }
 
   /// Shows the user the currently active package with [name], if any.
@@ -217,19 +263,16 @@ class GlobalPackages {
 
   /// Deactivates a previously-activated package named [name].
   ///
-  /// If [logDeactivate] is true, displays to the user when a package is
-  /// deactivated. Otherwise, deactivates silently.
-  ///
   /// Returns `false` if no package with [name] was currently active.
-  bool deactivate(String name, {bool logDeactivate: false}) {
+  bool deactivate(String name) {
     var dir = p.join(_directory, name);
     if (!dirExists(dir)) return false;
 
-    if (logDeactivate) {
-      var lockFile = new LockFile.load(_getLockFilePath(name), cache.sources);
-      var id = lockFile.packages[name];
-      log.message('Deactivated package ${_formatPackage(id)}.');
-    }
+    _deleteBinStubs(name);
+
+    var lockFile = new LockFile.load(_getLockFilePath(name), cache.sources);
+    var id = lockFile.packages[name];
+    log.message('Deactivated package ${_formatPackage(id)}.');
 
     deleteEntry(dir);
 
@@ -366,4 +409,189 @@ class GlobalPackages {
       return '${log.bold(id.name)} ${id.version}';
     }
   }
+
+  /// Updates the binstubs for [package].
+  ///
+  /// A binstub is a little shell script in `PUB_CACHE/bin` that runs an
+  /// executable from a globally activated package. This removes any old
+  /// binstubs from the previously activated version of the package and
+  /// (optionally) creates new ones for the executables listed in the package's
+  /// pubspec.
+  ///
+  /// [executables] is the names of the executables that should have binstubs.
+  /// If `null`, all executables in the package will get binstubs. If empty, no
+  /// binstubs will be created.
+  ///
+  /// if [overwriteBinStubs] is `true`, any binstubs that collide with
+  /// existing binstubs in other packages will be overwritten by this one's.
+  /// Otherwise, the previous ones will be preserved.
+  void _updateBinStubs(Package package, List<String> executables,
+      {bool overwriteBinStubs}) {
+    // Remove any previously activated binstubs for this package, in case the
+    // list of executables has changed.
+    _deleteBinStubs(package.name);
+
+    if ((executables != null && executables.isEmpty) ||
+        package.pubspec.executables.isEmpty) {
+      return;
+    }
+
+    ensureDir(_binStubDir);
+
+    var installed = [];
+    var collided = {};
+    var allExecutables = ordered(package.pubspec.executables.keys);
+    for (var executable in allExecutables) {
+      if (executables != null && !executables.contains(executable)) continue;
+
+      var script = package.pubspec.executables[executable];
+
+      var previousPackage = _createBinStub(package, executable, script,
+          overwrite: overwriteBinStubs);
+      if (previousPackage != null) {
+        collided[executable] = previousPackage;
+
+        if (!overwriteBinStubs) continue;
+      }
+
+      installed.add(executable);
+    }
+
+    if (installed.isNotEmpty) {
+      var names = namedSequence("executable", installed.map(log.bold));
+      log.message("Installed $names.");
+      // TODO(rnystrom): Show the user how to add the binstub directory to
+      // their PATH if not already on it.
+    }
+
+    // Show errors for any collisions.
+    if (collided.isNotEmpty) {
+      for (var command in ordered(collided.keys)) {
+        if (overwriteBinStubs) {
+          log.warning("Replaced ${log.bold(command)} previously installed from "
+              "${log.bold(collided[command])}.");
+        } else {
+          log.warning("Executable ${log.bold(command)} was already installed "
+              "from ${log.bold(collided[command])}.");
+        }
+      }
+
+      if (!overwriteBinStubs) {
+        log.warning("Deactivate the other package(s) or activate "
+            "${log.bold(package.name)} using --overwrite.");
+      }
+    }
+
+    // Show errors for any unknown executables.
+    if (executables != null) {
+      var unknown = ordered(executables.where(
+          (exe) => !package.pubspec.executables.keys.contains(exe)));
+      if (unknown.isNotEmpty) {
+        dataError("Unknown ${namedSequence('executable', unknown)}.");
+      }
+    }
+
+    // Show errors for any missing scripts.
+    // TODO(rnystrom): This can print false positives since a script may be
+    // produced by a transformer. Do something better.
+    var binFiles = package.listFiles(beneath: "bin", recursive: false)
+        .map((path) => p.relative(path, from: package.dir))
+        .toList();
+    for (var executable in installed) {
+      var script = package.pubspec.executables[executable];
+      var scriptPath = p.join("bin", "$script.dart");
+      if (!binFiles.contains(scriptPath)) {
+        log.warning('Warning: Executable "$executable" runs "$scriptPath", '
+            'which was not found in ${log.bold(package.name)}.');
+      }
+    }
+  }
+
+  /// Creates a binstub named [executable] that runs [script] from [package].
+  ///
+  /// If [overwrite] is `true`, this will replace an existing binstub with that
+  /// name for another package.
+  ///
+  /// If a collision occurs, returns the name of the package that owns the
+  /// existing binstub. Otherwise returns `null`.
+  String _createBinStub(Package package, String executable, String script,
+      {bool overwrite}) {
+    var binStubPath = p.join(_binStubDir, executable);
+
+    // See if the binstub already exists. If so, it's for another package
+    // since we already deleted all of this package's binstubs.
+    var previousPackage;
+    if (fileExists(binStubPath)) {
+      var contents = readTextFile(binStubPath);
+      var match = _binStubPackagePattern.firstMatch(contents);
+      if (match != null) {
+        previousPackage = match[1];
+        if (!overwrite) return previousPackage;
+      } else {
+        log.fine("Could not parse binstub $binStubPath:\n$contents");
+      }
+    }
+
+    // TODO(rnystrom): If the script was precompiled to a snapshot, make the
+    // script just invoke that directly and skip pub global run entirely.
+
+    if (Platform.operatingSystem == "windows") {
+      var batch = """
+@echo off
+rem This file was created by pub v${sdk.version}.
+rem Package: ${package.name}
+rem Version: ${package.version}
+rem Executable: ${executable}
+rem Script: ${script}
+pub global run ${package.name}:$script "%*"
+""";
+      writeTextFile(binStubPath, batch);
+    } else {
+      var bash = """
+# This file was created by pub v${sdk.version}.
+# Package: ${package.name}
+# Version: ${package.version}
+# Executable: ${executable}
+# Script: ${script}
+pub global run ${package.name}:$script "\$@"
+""";
+      writeTextFile(binStubPath, bash);
+
+      // Make it executable.
+      var result = Process.runSync('chmod', ['+x', binStubPath]);
+      if (result.exitCode != 0) {
+        // Couldn't make it executable so don't leave it laying around.
+        try {
+          deleteEntry(binStubPath);
+        } on IOException catch (err) {
+          // Do nothing. We're going to fail below anyway.
+          log.fine("Could not delete binstub:\n$err");
+        }
+
+        fail('Could not make "$binStubPath" executable (exit code '
+            '${result.exitCode}):\n${result.stderr}');
+      }
+    }
+
+    return previousPackage;
+  }
+
+  /// Deletes all existing binstubs for [package].
+  void _deleteBinStubs(String package) {
+    if (!dirExists(_binStubDir)) return;
+
+    for (var file in listDir(_binStubDir, includeDirs: false)) {
+      var contents = readTextFile(file);
+      var match = _binStubPackagePattern.firstMatch(contents);
+      if (match == null) {
+        log.fine("Could not parse binstub $file:\n$contents");
+        continue;
+      }
+
+      if (match[1] == package) {
+        log.fine("Deleting old binstub $file");
+        deleteEntry(file);
+      }
+    }
+  }
 }
diff --git a/lib/src/package.dart b/lib/src/package.dart
index 3dd84e2d..e5fff8b6 100644
--- a/lib/src/package.dart
+++ b/lib/src/package.dart
@@ -79,10 +79,7 @@ class Package {
   /// Returns a list of asset ids for all Dart executables in this package's bin
   /// directory.
   List<AssetId> get executableIds {
-    var binDir = path.join(dir, 'bin');
-    if (!dirExists(binDir)) return [];
-
-    return ordered(listFiles(beneath: binDir, recursive: false))
+    return ordered(listFiles(beneath: "bin", recursive: false))
         .where((executable) => path.extension(executable) == '.dart')
         .map((executable) {
       return new AssetId(
@@ -142,11 +139,18 @@ class Package {
   /// If this is a Git repository, this will respect .gitignore; otherwise, it
   /// will return all non-hidden, non-blacklisted files.
   ///
-  /// If [beneath] is passed, this will only return files beneath that path. If
+  /// If [beneath] is passed, this will only return files beneath that path,
+  /// which is expected to be relative to the package's root directory. If
   /// [recursive] is true, this will return all files beneath that path;
   /// otherwise, it will only return files one level beneath it.
   List<String> listFiles({String beneath, recursive: true}) {
-    if (beneath == null) beneath = dir;
+    if (beneath == null) {
+      beneath = dir;
+    } else {
+      beneath = path.join(dir, beneath);
+    }
+
+    if (!dirExists(beneath)) return [];
 
     // This is used in some performance-sensitive paths and can list many, many
     // files. As such, it leans more havily towards optimization as opposed to
diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart
index 7aa57350..fad03b2a 100644
--- a/lib/src/pubspec.dart
+++ b/lib/src/pubspec.dart
@@ -239,6 +239,61 @@ class Pubspec {
   bool _parsedPublishTo = false;
   String _publishTo;
 
+  /// The executables that should be placed on the user's PATH when this
+  /// package is globally activated.
+  ///
+  /// It is a map of strings to string. Each key is the name of the command
+  /// that will be placed on the user's PATH. The value is the name of the
+  /// .dart script (without extension) in the package's `bin` directory that
+  /// should be run for that command. Both key and value must be "simple"
+  /// strings: alphanumerics, underscores and hypens only. If a value is
+  /// omitted, it is inferred to use the same name as the key.
+  Map<String, String> get executables {
+    if (_executables != null) return _executables;
+
+    _executables = {};
+    var yaml = fields['executables'];
+    if (yaml == null) return _executables;
+
+    if (yaml is! Map) {
+      _error('"executables" field must be a map.',
+          fields.nodes['executables'].span);
+    }
+
+    yaml.nodes.forEach((key, value) {
+      // Don't allow path separators or other stuff meaningful to the shell.
+      validateName(name, description) {
+      }
+
+      if (key.value is! String) {
+        _error('"executables" keys must be strings.', key.span);
+      }
+
+      final keyPattern = new RegExp(r"^[a-zA-Z0-9_-]+$");
+      if (!keyPattern.hasMatch(key.value)) {
+        _error('"executables" keys may only contain letters, '
+            'numbers, hyphens and underscores.', key.span);
+      }
+
+      if (value.value == null) {
+        value = key;
+      } else if (value.value is! String) {
+        _error('"executables" values must be strings or null.', value.span);
+      }
+
+      final valuePattern = new RegExp(r"[/\\]");
+      if (valuePattern.hasMatch(value.value)) {
+        _error('"executables" values may not contain path separators.',
+            value.span);
+      }
+
+      _executables[key.value] = value.value;
+    });
+
+    return _executables;
+  }
+  Map<String, String> _executables;
+
   /// Whether the package is private and cannot be published.
   ///
   /// This is specified in the pubspec by setting "publish_to" to "none".
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 7af41e5f..e8bbb1d5 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -167,6 +167,18 @@ String padRight(String source, int length) {
   return result.toString();
 }
 
+/// Returns a labelled sentence fragment starting with [name] listing the
+/// elements [iter].
+///
+/// If [iter] does not have one item, name will be pluralized by adding "s" or
+/// using [plural], if given.
+String namedSequence(String name, Iterable iter, [String plural]) {
+  if (iter.length == 1) return "$name ${iter.single}";
+
+  if (plural == null) plural = "${name}s";
+  return "$plural ${toSentence(iter)}";
+}
+
 /// Returns a sentence fragment listing the elements of [iter].
 ///
 /// This converts each element of [iter] to a string and separates them with
diff --git a/lib/src/validator.dart b/lib/src/validator.dart
index 32194fec..17e46458 100644
--- a/lib/src/validator.dart
+++ b/lib/src/validator.dart
@@ -13,6 +13,7 @@ import 'validator/compiled_dartdoc.dart';
 import 'validator/dependency.dart';
 import 'validator/dependency_override.dart';
 import 'validator/directory.dart';
+import 'validator/executable.dart';
 import 'validator/license.dart';
 import 'validator/name.dart';
 import 'validator/pubspec_field.dart';
@@ -62,6 +63,7 @@ abstract class Validator {
       new DependencyValidator(entrypoint),
       new DependencyOverrideValidator(entrypoint),
       new DirectoryValidator(entrypoint),
+      new ExecutableValidator(entrypoint),
       new CompiledDartdocValidator(entrypoint),
       new Utf8ReadmeValidator(entrypoint)
     ];
diff --git a/lib/src/validator/executable.dart b/lib/src/validator/executable.dart
new file mode 100644
index 00000000..23bc5e53
--- /dev/null
+++ b/lib/src/validator/executable.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2012, 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.validator.executable;
+
+import 'dart:async';
+
+import 'package:path/path.dart' as p;
+
+import '../entrypoint.dart';
+import '../io.dart';
+import '../utils.dart';
+import '../validator.dart';
+
+/// Validates that a package's pubspec doesn't contain executables that
+/// reference non-existent scripts.
+class ExecutableValidator extends Validator {
+  ExecutableValidator(Entrypoint entrypoint)
+    : super(entrypoint);
+
+  Future validate() async {
+    // TODO(rnystrom): This can print false positives since a script may be
+    // produced by a transformer. Do something better.
+    var binFiles = entrypoint.root.listFiles(beneath: "bin", recursive: false)
+        .map((path) => p.relative(path, from: entrypoint.root.dir))
+        .toList();
+
+    entrypoint.root.pubspec.executables.forEach((executable, script) {
+      var scriptPath = p.join("bin", "$script.dart");
+      if (binFiles.contains(scriptPath)) return;
+
+      warnings.add('Your pubspec.yaml lists an executable "$executable" that '
+          'points to a script "$scriptPath" that does not exist.');
+    });
+  }
+}
diff --git a/lib/src/validator/name.dart b/lib/src/validator/name.dart
index 14da9120..585a3df8 100644
--- a/lib/src/validator/name.dart
+++ b/lib/src/validator/name.dart
@@ -52,8 +52,7 @@ class NameValidator extends Validator {
   /// to the package's root directory.
   List<String> get _libraries {
     var libDir = path.join(entrypoint.root.dir, "lib");
-    if (!dirExists(libDir)) return [];
-    return entrypoint.root.listFiles(beneath: libDir)
+    return entrypoint.root.listFiles(beneath: "lib")
         .map((file) => path.relative(file, from: path.dirname(libDir)))
         .where((file) => !path.split(file).contains("src") &&
                          path.extension(file) == '.dart')
diff --git a/test/global/activate/bad_version_test.dart b/test/global/activate/bad_version_test.dart
index 73cb2ae3..9db057ad 100644
--- a/test/global/activate/bad_version_test.dart
+++ b/test/global/activate/bad_version_test.dart
@@ -2,6 +2,8 @@
 // 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 '../../../lib/src/exit_codes.dart' as exit_codes;
 import '../../test_pub.dart';
 
@@ -9,16 +11,8 @@ main() {
   initConfig();
   integration('fails if the version constraint cannot be parsed', () {
     schedulePub(args: ["global", "activate", "foo", "1.0"],
-        error: """
-            Could not parse version "1.0". Unknown text at "1.0".
-
-            Usage: pub global activate <package...>
-            -h, --help      Print usage information for this command.
-            -s, --source    The source used to find the package.
-                            [git, hosted (default), path]
-
-            Run "pub help" to see global options.
-            """,
+        error: contains(
+            'Could not parse version "1.0". Unknown text at "1.0".'),
         exitCode: exit_codes.USAGE);
   });
 }
diff --git a/test/global/activate/constraint_with_path_test.dart b/test/global/activate/constraint_with_path_test.dart
index 2795ca41..1e5412f4 100644
--- a/test/global/activate/constraint_with_path_test.dart
+++ b/test/global/activate/constraint_with_path_test.dart
@@ -2,6 +2,8 @@
 // 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 '../../../lib/src/exit_codes.dart' as exit_codes;
 import '../../test_pub.dart';
 
@@ -9,16 +11,7 @@ main() {
   initConfig();
   integration('fails if a version is passed with the path source', () {
     schedulePub(args: ["global", "activate", "-spath", "foo", "1.2.3"],
-        error: """
-            Unexpected argument "1.2.3".
-
-            Usage: pub global activate <package...>
-            -h, --help      Print usage information for this command.
-            -s, --source    The source used to find the package.
-                            [git, hosted (default), path]
-
-            Run "pub help" to see global options.
-            """,
+        error: contains('Unexpected argument "1.2.3".'),
         exitCode: exit_codes.USAGE);
   });
 }
diff --git a/test/global/activate/missing_package_arg_test.dart b/test/global/activate/missing_package_arg_test.dart
index 9469de2d..72fbc972 100644
--- a/test/global/activate/missing_package_arg_test.dart
+++ b/test/global/activate/missing_package_arg_test.dart
@@ -2,6 +2,8 @@
 // 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 '../../../lib/src/exit_codes.dart' as exit_codes;
 import '../../test_pub.dart';
 
@@ -9,15 +11,7 @@ main() {
   initConfig();
   integration('fails if no package was given', () {
     schedulePub(args: ["global", "activate"],
-        error: """
-            No package to activate given.
-
-            Usage: pub global activate <package...>
-            -h, --help      Print usage information for this command.
-            -s, --source    The source used to find the package.
-                            [git, hosted (default), path]
-
-            Run "pub help" to see global options.""",
+        error: contains("No package to activate given."),
         exitCode: exit_codes.USAGE);
   });
 }
diff --git a/test/global/activate/unexpected_arguments_test.dart b/test/global/activate/unexpected_arguments_test.dart
index 5d519177..24d0e2ce 100644
--- a/test/global/activate/unexpected_arguments_test.dart
+++ b/test/global/activate/unexpected_arguments_test.dart
@@ -2,6 +2,8 @@
 // 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 '../../../lib/src/exit_codes.dart' as exit_codes;
 import '../../test_pub.dart';
 
@@ -9,15 +11,7 @@ main() {
   initConfig();
   integration('fails if there are extra arguments', () {
     schedulePub(args: ["global", "activate", "foo", "1.0.0", "bar", "baz"],
-        error: """
-            Unexpected arguments "bar" and "baz".
-
-            Usage: pub global activate <package...>
-            -h, --help      Print usage information for this command.
-            -s, --source    The source used to find the package.
-                            [git, hosted (default), path]
-
-            Run "pub help" to see global options.""",
+        error: contains('Unexpected arguments "bar" and "baz".'),
         exitCode: exit_codes.USAGE);
   });
 }
diff --git a/test/global/binstubs/creates_executables_in_pubspec_test.dart b/test/global/binstubs/creates_executables_in_pubspec_test.dart
new file mode 100644
index 00000000..616ce006
--- /dev/null
+++ b/test/global/binstubs/creates_executables_in_pubspec_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 'package:scheduled_test/scheduled_test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("creates binstubs for each executable in the pubspec", () {
+    servePackages((builder) {
+      builder.serve("foo", "1.0.0", pubspec: {
+        "executables": {
+          "one": null,
+          "two-renamed": "two"
+        }
+      }, contents: [
+        d.dir("bin", [
+          d.file("one.dart", "main(args) => print('one');"),
+          d.file("two.dart", "main(args) => print('two');"),
+          d.file("nope.dart", "main(args) => print('nope');")
+        ])
+      ]);
+    });
+
+    schedulePub(args: ["global", "activate", "foo"], output:
+        contains("Installed executables one and two-renamed."));
+
+    d.dir(cachePath, [
+      d.dir("bin", [
+        d.matcherFile("one", contains("pub global run foo:one")),
+        d.matcherFile("two-renamed", contains("pub global run foo:two")),
+        d.nothing("two"),
+        d.nothing("nope")
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/global/binstubs/explicit_and_no_executables_options_test.dart b/test/global/binstubs/explicit_and_no_executables_options_test.dart
new file mode 100644
index 00000000..65c67ada
--- /dev/null
+++ b/test/global/binstubs/explicit_and_no_executables_options_test.dart
@@ -0,0 +1,24 @@
+// 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 '../../../lib/src/exit_codes.dart' as exit_codes;
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("errors if -x and --no-executables are both passed", () {
+    d.dir("foo", [
+      d.libPubspec("foo", "1.0.0")
+    ]).create();
+
+    schedulePub(args: [
+      "global", "activate", "--source", "path", "../foo",
+      "-x", "anything", "--no-executables"
+    ], error: contains("Cannot pass both --no-executables and --executable."),
+        exitCode: exit_codes.USAGE);
+  });
+}
diff --git a/test/global/binstubs/explicit_executables_test.dart b/test/global/binstubs/explicit_executables_test.dart
new file mode 100644
index 00000000..632eb461
--- /dev/null
+++ b/test/global/binstubs/explicit_executables_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 'package:scheduled_test/scheduled_test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("only creates binstubs for the listed executables", () {
+    d.dir("foo", [
+      d.pubspec({
+        "name": "foo",
+        "executables": {
+          "one": "script",
+          "two": "script",
+          "three": "script"
+        }
+      }),
+      d.dir("bin", [
+        d.file("script.dart", "main() => print('ok');")
+      ])
+    ]).create();
+
+    schedulePub(args: [
+      "global", "activate", "--source", "path", "../foo",
+      "-x", "one", "--executable", "three"
+    ], output: contains("Installed executables one and three."));
+
+    d.dir(cachePath, [
+      d.dir("bin", [
+        d.matcherFile("one", contains("pub global run foo:script")),
+        d.nothing("two"),
+        d.matcherFile("three", contains("pub global run foo:script"))
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/global/binstubs/missing_script_test.dart b/test/global/binstubs/missing_script_test.dart
new file mode 100644
index 00000000..084c0c76
--- /dev/null
+++ b/test/global/binstubs/missing_script_test.dart
@@ -0,0 +1,32 @@
+// 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_stream.dart';
+
+import '../../../lib/src/exit_codes.dart' as exit_codes;
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("errors if an executable's script can't be found", () {
+    d.dir("foo", [
+      d.pubspec({
+        "name": "foo",
+        "executables": {
+          "missing": "not_here",
+          "nope": null
+        }
+      })
+    ]).create();
+
+    var pub = startPub(args: ["global", "activate", "-spath", "../foo"]);
+
+    pub.stderr.expect('Warning: Executable "missing" runs '
+        '"bin/not_here.dart", which was not found in foo.');
+    pub.stderr.expect('Warning: Executable "nope" runs "bin/nope.dart", which '
+        'was not found in foo.');
+    pub.shouldExit();
+  });
+}
diff --git a/test/global/binstubs/name_collision_test.dart b/test/global/binstubs/name_collision_test.dart
new file mode 100644
index 00000000..7a3120d6
--- /dev/null
+++ b/test/global/binstubs/name_collision_test.dart
@@ -0,0 +1,61 @@
+// 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_stream.dart';
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("does not overwrite an existing binstub", () {
+    d.dir("foo", [
+      d.pubspec({
+        "name": "foo",
+        "executables": {
+          "foo": "foo",
+          "collide1": "foo",
+          "collide2": "foo"
+        }
+      }),
+      d.dir("bin", [
+        d.file("foo.dart", "main() => print('ok');")
+      ])
+    ]).create();
+
+    d.dir("bar", [
+      d.pubspec({
+        "name": "bar",
+        "executables": {
+          "bar": "bar",
+          "collide1": "bar",
+          "collide2": "bar"
+        }
+      }),
+      d.dir("bin", [
+        d.file("bar.dart", "main() => print('ok');")
+      ])
+    ]).create();
+
+    schedulePub(args: ["global", "activate", "-spath", "../foo"]);
+
+    var pub = startPub(args: ["global", "activate", "-spath", "../bar"]);
+    pub.stdout.expect(consumeThrough("Installed executable bar."));
+    pub.stderr.expect("Executable collide1 was already installed from foo.");
+    pub.stderr.expect("Executable collide2 was already installed from foo.");
+    pub.stderr.expect("Deactivate the other package(s) or activate bar using "
+        "--overwrite.");
+    pub.shouldExit();
+
+    d.dir(cachePath, [
+      d.dir("bin", [
+        d.matcherFile("foo", contains("foo:foo")),
+        d.matcherFile("bar", contains("bar:bar")),
+        d.matcherFile("collide1", contains("foo:foo")),
+        d.matcherFile("collide2", contains("foo:foo"))
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/global/binstubs/name_collision_with_overwrite_test.dart b/test/global/binstubs/name_collision_with_overwrite_test.dart
new file mode 100644
index 00000000..41a2dcda
--- /dev/null
+++ b/test/global/binstubs/name_collision_with_overwrite_test.dart
@@ -0,0 +1,62 @@
+// 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_stream.dart';
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("overwrites an existing binstub if --overwrite is passed", () {
+    d.dir("foo", [
+      d.pubspec({
+        "name": "foo",
+        "executables": {
+          "foo": "foo",
+          "collide1": "foo",
+          "collide2": "foo"
+        }
+      }),
+      d.dir("bin", [
+        d.file("foo.dart", "main() => print('ok');")
+      ])
+    ]).create();
+
+    d.dir("bar", [
+      d.pubspec({
+        "name": "bar",
+        "executables": {
+          "bar": "bar",
+          "collide1": "bar",
+          "collide2": "bar"
+        }
+      }),
+      d.dir("bin", [
+        d.file("bar.dart", "main() => print('ok');")
+      ])
+    ]).create();
+
+    schedulePub(args: ["global", "activate", "-spath", "../foo"]);
+
+    var pub = startPub(args: [
+      "global", "activate", "-spath", "../bar", "--overwrite"
+    ]);
+    pub.stdout.expect(consumeThrough(
+        "Installed executables bar, collide1 and collide2."));
+    pub.stderr.expect("Replaced collide1 previously installed from foo.");
+    pub.stderr.expect("Replaced collide2 previously installed from foo.");
+    pub.shouldExit();
+
+    d.dir(cachePath, [
+      d.dir("bin", [
+        d.matcherFile("foo", contains("foo:foo")),
+        d.matcherFile("bar", contains("bar:bar")),
+        d.matcherFile("collide1", contains("bar:bar")),
+        d.matcherFile("collide2", contains("bar:bar"))
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/global/binstubs/no_executables_flag_test.dart b/test/global/binstubs/no_executables_flag_test.dart
new file mode 100644
index 00000000..0f7816a4
--- /dev/null
+++ b/test/global/binstubs/no_executables_flag_test.dart
@@ -0,0 +1,38 @@
+// 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("does not create binstubs if --no-executables is passed", () {
+    d.dir("foo", [
+      d.pubspec({
+        "name": "foo",
+        "executables": {
+          "one": null
+        }
+      }),
+      d.dir("bin", [
+        d.file("one.dart", "main() => print('ok');")
+      ])
+    ]).create();
+
+    schedulePub(args: ["global", "activate", "--source", "path", "../foo"]);
+
+    schedulePub(args: [
+      "global", "activate", "--source", "path", "../foo", "--no-executables"
+    ]);
+
+    // Should still delete old one.
+    d.dir(cachePath, [
+      d.dir("bin", [
+        d.nothing("one")
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/global/binstubs/path_package_test.dart b/test/global/binstubs/path_package_test.dart
new file mode 100644
index 00000000..a837f73a
--- /dev/null
+++ b/test/global/binstubs/path_package_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 'package:scheduled_test/scheduled_test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("creates binstubs when activating a path package", () {
+    d.dir("foo", [
+      d.pubspec({
+        "name": "foo",
+        "executables": {
+          "foo": null
+        }
+      }),
+      d.dir("bin", [
+        d.file("foo.dart", "main() => print('ok');")
+      ])
+    ]).create();
+
+    schedulePub(args: ["global", "activate", "--source", "path", "../foo"],
+        output: contains("Installed executable foo."));
+
+    d.dir(cachePath, [
+      d.dir("bin", [
+        d.matcherFile("foo", contains("pub global run foo:foo"))
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/global/binstubs/reactivate_removes_old_executables_test.dart b/test/global/binstubs/reactivate_removes_old_executables_test.dart
new file mode 100644
index 00000000..c468d09a
--- /dev/null
+++ b/test/global/binstubs/reactivate_removes_old_executables_test.dart
@@ -0,0 +1,48 @@
+// 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("removes previous binstubs when reactivating a package", () {
+    d.dir("foo", [
+      d.pubspec({
+        "name": "foo",
+        "executables": {
+          "one": null,
+          "two": null
+        }
+      }),
+      d.dir("bin", [
+        d.file("one.dart", "main() => print('ok');"),
+        d.file("two.dart", "main() => print('ok');")
+      ])
+    ]).create();
+
+    schedulePub(args: ["global", "activate", "--source", "path", "../foo"]);
+
+    d.dir("foo", [
+      d.pubspec({
+        "name": "foo",
+        "executables": {
+          // Remove "one".
+          "two": null
+        }
+      }),
+    ]).create();
+
+    schedulePub(args: ["global", "activate", "--source", "path", "../foo"]);
+
+    d.dir(cachePath, [
+      d.dir("bin", [
+        d.nothing("one"),
+        d.matcherFile("two", contains("two"))
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/global/binstubs/removes_even_if_not_in_pubspec_test.dart b/test/global/binstubs/removes_even_if_not_in_pubspec_test.dart
new file mode 100644
index 00000000..03cd2f74
--- /dev/null
+++ b/test/global/binstubs/removes_even_if_not_in_pubspec_test.dart
@@ -0,0 +1,45 @@
+// 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("removes all binstubs for package", () {
+    d.dir("foo", [
+      d.pubspec({
+        "name": "foo",
+        "executables": {
+          "foo": null
+        }
+      }),
+      d.dir("bin", [
+        d.file("foo.dart", "main() => print('ok');")
+      ])
+    ]).create();
+
+    // Create the binstub for foo.
+    schedulePub(args: ["global", "activate", "--source", "path", "../foo"]);
+
+    // Remove it from the pubspec.
+    d.dir("foo", [
+      d.pubspec({
+        "name": "foo"
+      })
+    ]).create();
+
+    // Deactivate.
+    schedulePub(args: ["global", "deactivate", "foo"]);
+
+    // It should still be deleted.
+    d.dir(cachePath, [
+      d.dir("bin", [
+        d.nothing("foo")
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/global/binstubs/removes_when_deactivated_test.dart b/test/global/binstubs/removes_when_deactivated_test.dart
new file mode 100644
index 00000000..3a8e149c
--- /dev/null
+++ b/test/global/binstubs/removes_when_deactivated_test.dart
@@ -0,0 +1,37 @@
+// 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("removes binstubs when the package is deactivated", () {
+    servePackages((builder) {
+      builder.serve("foo", "1.0.0", pubspec: {
+        "executables": {
+          "one": null,
+          "two": null
+        }
+      }, contents: [
+        d.dir("bin", [
+          d.file("one.dart", "main(args) => print('one');"),
+          d.file("two.dart", "main(args) => print('two');")
+        ])
+      ]);
+    });
+
+    schedulePub(args: ["global", "activate", "foo"]);
+    schedulePub(args: ["global", "deactivate", "foo"]);
+
+    d.dir(cachePath, [
+      d.dir("bin", [
+        d.nothing("one"),
+        d.nothing("two")
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/global/binstubs/unknown_explicit_executable_test.dart b/test/global/binstubs/unknown_explicit_executable_test.dart
new file mode 100644
index 00000000..ae1e0e0f
--- /dev/null
+++ b/test/global/binstubs/unknown_explicit_executable_test.dart
@@ -0,0 +1,35 @@
+// 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_stream.dart';
+
+import '../../../lib/src/exit_codes.dart' as exit_codes;
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("errors on an unknown explicit executable", () {
+    d.dir("foo", [
+      d.pubspec({
+        "name": "foo",
+        "executables": {
+          "one": "one"
+        }
+      }),
+      d.dir("bin", [
+        d.file("one.dart", "main() => print('ok');")
+      ])
+    ]).create();
+
+    var pub = startPub(args: [
+      "global", "activate", "--source", "path", "../foo",
+      "-x", "who", "-x", "one", "--executable", "wat"
+    ]);
+
+    pub.stdout.expect(consumeThrough("Installed executable one."));
+    pub.stderr.expect("Unknown executables wat and who.");
+    pub.shouldExit(exit_codes.DATA);
+  });
+}
diff --git a/test/pubspec_test.dart b/test/pubspec_test.dart
index e34ce431..fec3077d 100644
--- a/test/pubspec_test.dart
+++ b/test/pubspec_test.dart
@@ -444,5 +444,58 @@ publish_to: none
             (pubspec) => pubspec.publishTo);
       });
     });
+
+    group("executables", () {
+      test("defaults to an empty map if omitted", () {
+        var pubspec = new Pubspec.parse('', sources);
+        expect(pubspec.executables, isEmpty);
+      });
+
+      test("allows simple names for keys and most characters in values", () {
+        var pubspec = new Pubspec.parse('''
+executables:
+  abcDEF-123_: "abc DEF-123._"
+''', sources);
+        expect(pubspec.executables['abcDEF-123_'], equals('abc DEF-123._'));
+      });
+
+      test("throws if not a map", () {
+        expectPubspecException('executables: not map',
+            (pubspec) => pubspec.executables);
+      });
+
+      test("throws if key is not a string", () {
+        expectPubspecException('executables: {123: value}',
+            (pubspec) => pubspec.executables);
+      });
+
+      test("throws if a key isn't a simple name", () {
+        expectPubspecException('executables: {funny/name: ok}',
+            (pubspec) => pubspec.executables);
+      });
+
+      test("throws if a value is not a string", () {
+        expectPubspecException('executables: {command: 123}',
+            (pubspec) => pubspec.executables);
+      });
+
+      test("throws if a value contains a path separator", () {
+        expectPubspecException('executables: {command: funny_name/part}',
+            (pubspec) => pubspec.executables);
+      });
+
+      test("throws if a value contains a windows path separator", () {
+        expectPubspecException(r'executables: {command: funny_name\part}',
+            (pubspec) => pubspec.executables);
+      });
+
+      test("uses the key if the value is null", () {
+        var pubspec = new Pubspec.parse('''
+executables:
+  command:
+''', sources);
+        expect(pubspec.executables['command'], equals('command'));
+      });
+    });
   });
 }
diff --git a/test/validator/executable_test.dart b/test/validator/executable_test.dart
new file mode 100644
index 00000000..6d1fb816
--- /dev/null
+++ b/test/validator/executable_test.dart
@@ -0,0 +1,57 @@
+// 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 'package:scheduled_test/scheduled_test.dart';
+
+import '../../lib/src/entrypoint.dart';
+import '../../lib/src/validator.dart';
+import '../../lib/src/validator/executable.dart';
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+import 'utils.dart';
+
+Validator executable(Entrypoint entrypoint) =>
+  new ExecutableValidator(entrypoint);
+
+main() {
+  initConfig();
+
+  setUp(d.validPackage.create);
+
+  group('should consider a package valid if it', () {
+    integration('has executables that are present', () {
+      d.dir(appPath, [
+        d.pubspec({
+          "name": "test_pkg",
+          "version": "1.0.0",
+          "executables": {
+            "one": "one_script",
+            "two": null
+          }
+        }),
+        d.dir("bin", [
+          d.file("one_script.dart", "main() => print('ok');"),
+          d.file("two.dart", "main() => print('ok');")
+        ])
+      ]).create();
+      expectNoValidationError(executable);
+    });
+  });
+
+  group("should consider a package invalid if it", () {
+    integration('is missing one or more listed executables', () {
+      d.dir(appPath, [
+        d.pubspec({
+          "name": "test_pkg",
+          "version": "1.0.0",
+          "executables": {
+            "nope": "not_there",
+            "nada": null
+          }
+        })
+      ]).create();
+      expectValidationWarning(executable);
+    });
+  });
+}
-- 
GitLab