From 6ce214fe705bebcf1a0411e71e22c6367ecca31c Mon Sep 17 00:00:00 2001
From: "nweiz@google.com" <nweiz@google.com>
Date: Wed, 3 Sep 2014 00:20:51 +0000
Subject: [PATCH] Precompile immutable globally-installed pub executables.

R=rnystrom@google.com
BUG=20483

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

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge@39787 260f80e4-7a28-3924-810f-c04153c831b5
---
 lib/src/barback/asset_environment.dart        |  49 ++++++
 lib/src/command/global_run.dart               |  13 +-
 lib/src/command/run.dart                      |  12 +-
 lib/src/entrypoint.dart                       | 118 +++++---------
 lib/src/executable.dart                       |  41 +++--
 lib/src/global_packages.dart                  | 145 +++++++++++++++---
 lib/src/package.dart                          |  23 ++-
 .../activate_git_after_hosted_test.dart       |   3 +
 .../activate_hosted_after_git_test.dart       |   3 +
 .../activate_hosted_after_path_test.dart      |   3 +
 test/global/activate/cached_package_test.dart |   4 +-
 test/global/activate/constraint_test.dart     |   2 +-
 .../activate/different_version_test.dart      |   2 +
 ...doesnt_snapshot_path_executables_test.dart |  32 ++++
 test/global/activate/git_package_test.dart    |   3 +
 .../activate/ignores_active_version_test.dart |   2 +
 .../reactivating_git_upgrades_test.dart       |   4 +
 .../activate/removes_old_lockfile_test.dart   |  33 ++++
 .../snaphots_hosted_executables_test.dart     |  46 ++++++
 .../snapshots_git_executables_test.dart       |  48 ++++++
 ...orts_version_solver_backtracking_test.dart |   2 +-
 .../activate/uncached_package_test.dart       |   4 +-
 ...eactivate_and_reactivate_package_test.dart |   2 +
 .../removes_precompiled_snapshots_test.dart   |  22 +++
 test/global/list_test.dart                    |   2 +-
 ...recompiles_if_sdk_is_out_of_date_test.dart |  53 +++++++
 test/global/run/uses_old_lockfile_test.dart   |  54 +++++++
 27 files changed, 594 insertions(+), 131 deletions(-)
 create mode 100644 test/global/activate/doesnt_snapshot_path_executables_test.dart
 create mode 100644 test/global/activate/removes_old_lockfile_test.dart
 create mode 100644 test/global/activate/snaphots_hosted_executables_test.dart
 create mode 100644 test/global/activate/snapshots_git_executables_test.dart
 create mode 100644 test/global/deactivate/removes_precompiled_snapshots_test.dart
 create mode 100644 test/global/run/recompiles_if_sdk_is_out_of_date_test.dart
 create mode 100644 test/global/run/uses_old_lockfile_test.dart

diff --git a/lib/src/barback/asset_environment.dart b/lib/src/barback/asset_environment.dart
index 54d30f15..1eab02d6 100644
--- a/lib/src/barback/asset_environment.dart
+++ b/lib/src/barback/asset_environment.dart
@@ -12,6 +12,7 @@ import 'package:path/path.dart' as path;
 import 'package:watcher/watcher.dart';
 
 import '../entrypoint.dart';
+import '../exceptions.dart';
 import '../io.dart';
 import '../log.dart' as log;
 import '../package.dart';
@@ -232,6 +233,54 @@ class AssetEnvironment {
             rootDirectory: "bin"));
   }
 
+  /// Precompiles all of [packageName]'s executables to snapshots in
+  /// [directory].
+  ///
+  /// If [executableIds] is passed, only those executables are precompiled.
+  Future precompileExecutables(String packageName, String directory,
+      {Iterable<AssetId> executableIds}) {
+    if (executableIds == null) {
+      executableIds = graph.packages[packageName].executableIds;
+    }
+    log.fine("executables for $packageName: $executableIds");
+    if (executableIds.isEmpty) return null;
+
+    var package = graph.packages[packageName];
+    return servePackageBinDirectory(packageName).then((server) {
+      return waitAndPrintErrors(executableIds.map((id) {
+        var basename = path.url.basename(id.path);
+        var snapshotPath = path.join(directory, "$basename.snapshot");
+        return runProcess(Platform.executable, [
+          '--snapshot=$snapshotPath',
+          server.url.resolve(basename).toString()
+        ]).then((result) {
+          if (result.success) {
+            log.message("Precompiled ${_formatExecutable(id)}.");
+          } else {
+            // TODO(nweiz): Stop manually deleting this when issue 20504 is
+            // fixed.
+            deleteEntry(snapshotPath);
+            throw new ApplicationException(
+                log.yellow("Failed to precompile "
+                    "${_formatExecutable(id)}:\n") +
+                result.stderr.join('\n'));
+          }
+        });
+      })).whenComplete(() {
+        // Don't return this future, since we have no need to wait for the
+        // server to fully shut down.
+        server.close();
+      });
+    });
+  }
+
+  /// Returns the executable name for [id].
+  ///
+  /// [id] is assumed to be an executable in a bin directory. The return value
+  /// is intended for log output and may contain formatting.
+  String _formatExecutable(AssetId id) =>
+      log.bold("${id.package}:${path.basenameWithoutExtension(id.path)}");
+
   /// Stops the server bound to [rootDirectory].
   ///
   /// Also removes any source files within that directory from barback. Returns
diff --git a/lib/src/command/global_run.dart b/lib/src/command/global_run.dart
index 86eb30bc..ae47a052 100644
--- a/lib/src/command/global_run.dart
+++ b/lib/src/command/global_run.dart
@@ -6,8 +6,9 @@ library pub.command.global_run;
 
 import 'dart:async';
 
+import 'package:path/path.dart' as p;
+
 import '../command.dart';
-import '../executable.dart';
 import '../io.dart';
 import '../utils.dart';
 
@@ -37,10 +38,14 @@ class GlobalRunCommand extends PubCommand {
     }
 
     var args = commandOptions.rest.skip(1).toList();
+    if (p.split(executable).length > 1) {
+      // TODO(nweiz): Use adjacent strings when the new async/await compiler
+      // lands.
+      usageError('Cannot run an executable in a subdirectory of a global ' +
+          'package.');
+    }
 
-    var entrypoint = await globals.find(package);
-    var exitCode = await runExecutable(this, entrypoint, package, executable,
-          args, isGlobal: true);
+    var exitCode = await globals.runExecutable(package, executable, args);
     await flushThenExit(exitCode);
   }
 }
diff --git a/lib/src/command/run.dart b/lib/src/command/run.dart
index 45c0931a..1e3a5013 100644
--- a/lib/src/command/run.dart
+++ b/lib/src/command/run.dart
@@ -6,6 +6,8 @@ library pub.command.run;
 
 import 'dart:async';
 
+import 'package:path/path.dart' as p;
+
 import '../command.dart';
 import '../executable.dart';
 import '../io.dart';
@@ -34,10 +36,16 @@ class RunCommand extends PubCommand {
       var components = split1(executable, ":");
       package = components[0];
       executable = components[1];
+
+      if (p.split(executable).length > 1) {
+      // TODO(nweiz): Use adjacent strings when the new async/await compiler
+      // lands.
+        usageError("Cannot run an executable in a subdirectory of a " +
+            "dependency.");
+      }
     }
 
-    var exitCode = await runExecutable(this, entrypoint, package, executable,
-        args);
+    var exitCode = await runExecutable(entrypoint, package, executable, args);
     await flushThenExit(exitCode);
   }
 }
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 25bb14f1..c1f0e7d5 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -5,13 +5,11 @@
 library pub.entrypoint;
 
 import 'dart:async';
-import 'dart:io';
 
 import 'package:path/path.dart' as path;
 import 'package:barback/barback.dart';
 
 import 'barback/asset_environment.dart';
-import 'exceptions.dart';
 import 'io.dart';
 import 'lock_file.dart';
 import 'log.dart' as log;
@@ -144,15 +142,8 @@ class Entrypoint {
 
         /// Build a package graph from the version solver results so we don't
         /// have to reload and reparse all the pubspecs.
-        return Future.wait(ids.map((id) {
-          return cache.sources[id.source].getDirectory(id).then((dir) {
-            return new Package(result.pubspecs[id.name], dir);
-          });
-        }));
-      }).then((packages) {
-        _packageGraph = new PackageGraph(this, _lockFile,
-            new Map.fromIterable(packages, key: (package) => package.name));
-
+        return loadPackageGraph(result);
+      }).then((packageGraph) {
         return precompileExecutables(changed: result.changedPackages)
             .catchError((error, stackTrace) {
           // Just log exceptions here. Since the method is just about acquiring
@@ -207,8 +198,11 @@ class Entrypoint {
           });
 
           return waitAndPrintErrors(executables.keys.map((package) {
-            return _precompileExecutablesForPackage(
-                environment, package, executables[package]);
+            var dir = path.join(binDir, package);
+            cleanDir(dir);
+            return environment.precompileExecutables(
+                package, dir,
+                executableIds: executables[package]);
           }));
         });
       });
@@ -236,15 +230,7 @@ class Entrypoint {
     });
     if (hasUncachedDependency) return [];
 
-    var executables =
-        ordered(package.listFiles(beneath: binDir, recursive: false))
-        .where((executable) => path.extension(executable) == '.dart')
-        .map((executable) {
-      return new AssetId(
-          package.name,
-          path.toUri(path.relative(executable, from: package.dir))
-              .toString());
-    }).toList();
+    var executables = package.executableIds;
 
     // If we don't know which packages were changed, always precompile the
     // executables.
@@ -267,51 +253,6 @@ class Entrypoint {
     return [];
   }
 
-  /// Precompiles all [executables] for [package].	
-  ///	
-  /// [executables] is assumed to be a list of Dart executables in [package]'s	
-  /// bin directory.
-  Future _precompileExecutablesForPackage(
-      AssetEnvironment environment, String package, List<AssetId> executables) {
-    var cacheDir = path.join('.pub', 'bin', package);
-    cleanDir(cacheDir);
-
-    // TODO(nweiz): Unserve this directory when we're done with it.
-    return environment.servePackageBinDirectory(package).then((server) {
-      return waitAndPrintErrors(executables.map((id) {
-        var basename = path.url.basename(id.path);
-        var snapshotPath = path.join(cacheDir, "$basename.snapshot");
-        return runProcess(Platform.executable, [
-          '--snapshot=$snapshotPath',
-          server.url.resolve(basename).toString()
-        ]).then((result) {
-          if (result.success) {
-            log.message("Precompiled ${_executableName(id)}.");
-          } else {
-            // TODO(nweiz): Stop manually deleting this when issue 20504 is
-            // fixed.
-            deleteEntry(snapshotPath);
-            throw new ApplicationException(
-                log.yellow("Failed to precompile "
-                    "${_executableName(id)}:\n") +
-                result.stderr.join('\n'));
-          }
-        });
-      })).whenComplete(() {
-        // Don't return this future, since we have no need to wait for the
-        // server to fully shut down.
-        server.close();
-      });
-    });
-  }
-
-  /// Returns the executable name for [id].
-  ///
-  /// [id] is assumed to be an executable in a bin directory. The return value
-  /// is intended for log output and may contain formatting.
-  String _executableName(AssetId id) =>
-      log.bold("${id.package}:${path.basenameWithoutExtension(id.path)}");
-
   /// Makes sure the package at [id] is locally available.
   ///
   /// This automatically downloads the package to the system-wide cache as well
@@ -421,22 +362,37 @@ class Entrypoint {
   /// Loads the package graph for the application and all of its transitive
   /// dependencies.
   ///
-  /// Before loading, makes sure the lockfile and dependencies are installed
-  /// and up to date.
-  Future<PackageGraph> loadPackageGraph() {
+  /// If [result] is passed, this loads the graph from it without re-parsing the
+  /// lockfile or any pubspecs. Otherwise, before loading, this makes sure the
+  /// lockfile and dependencies are installed and up to date.
+  Future<PackageGraph> loadPackageGraph([SolveResult result]) {
     if (_packageGraph != null) return new Future.value(_packageGraph);
 
-    return ensureLockFileIsUpToDate().then((_) {
-      return Future.wait(lockFile.packages.values.map((id) {
-        var source = cache.sources[id.source];
-        return source.getDirectory(id)
-            .then((dir) => new Package.load(id.name, dir, cache.sources));
-      })).then((packages) {
-        var packageMap = new Map.fromIterable(packages, key: (p) => p.name);
-        packageMap[root.name] = root;
-        _packageGraph = new PackageGraph(this, lockFile, packageMap);
-        return _packageGraph;
-      });
+    return syncFuture(() {
+      if (result != null) {
+        return Future.wait(result.packages.map((id) {
+          return cache.sources[id.source].getDirectory(id)
+              .then((dir) => new Package(result.pubspecs[id.name], dir));
+        })).then((packages) {
+          return new PackageGraph(this, new LockFile(result.packages),
+              new Map.fromIterable(packages, key: (package) => package.name));
+        });
+      } else {
+        return ensureLockFileIsUpToDate().then((_) {
+          return Future.wait(lockFile.packages.values.map((id) {
+            var source = cache.sources[id.source];
+            return source.getDirectory(id)
+                .then((dir) => new Package.load(id.name, dir, cache.sources));
+          })).then((packages) {
+            var packageMap = new Map.fromIterable(packages, key: (p) => p.name);
+            packageMap[root.name] = root;
+            return new PackageGraph(this, lockFile, packageMap);
+          });
+        });
+      }
+    }).then((graph) {
+      _packageGraph = graph;
+      return graph;
     });
   }
 
diff --git a/lib/src/executable.dart b/lib/src/executable.dart
index 5cecc13d..79513923 100644
--- a/lib/src/executable.dart
+++ b/lib/src/executable.dart
@@ -12,7 +12,6 @@ import 'package:path/path.dart' as p;
 import 'package:stack_trace/stack_trace.dart';
 
 import 'barback/asset_environment.dart';
-import 'command.dart';
 import 'entrypoint.dart';
 import 'exit_codes.dart' as exit_codes;
 import 'io.dart';
@@ -30,19 +29,18 @@ import 'utils.dart';
 /// Arguments from [args] will be passed to the spawned Dart application.
 ///
 /// Returns the exit code of the spawned app.
-Future<int> runExecutable(PubCommand command, Entrypoint entrypoint,
-    String package, String executable, Iterable<String> args,
-    {bool isGlobal: false}) {
+Future<int> runExecutable(Entrypoint entrypoint, String package,
+    String executable, Iterable<String> args, {bool isGlobal: false}) {
   // Unless the user overrides the verbosity, we want to filter out the
   // normal pub output shown while loading the environment.
   if (log.verbosity == log.Verbosity.NORMAL) {
     log.verbosity = log.Verbosity.WARNING;
   }
 
-  var snapshotPath = p.join(".pub", "bin", package,
+  var localSnapshotPath = p.join(".pub", "bin", package,
       "$executable.dart.snapshot");
-  if (!isGlobal && fileExists(snapshotPath)) {
-    return _runCachedExecutable(entrypoint, snapshotPath, args);
+  if (!isGlobal && fileExists(localSnapshotPath)) {
+    return _runCachedExecutable(entrypoint, localSnapshotPath, args);
   }
 
   // If the command has a path separator, then it's a path relative to the
@@ -51,14 +49,7 @@ Future<int> runExecutable(PubCommand command, Entrypoint entrypoint,
   var rootDir = "bin";
   var parts = p.split(executable);
   if (parts.length > 1) {
-    if (isGlobal) {
-      command.usageError(
-          'Cannot run an executable in a subdirectory of a global package.');
-    } else if (package != entrypoint.root.name) {
-      command.usageError(
-          "Cannot run an executable in a subdirectory of a dependency.");
-    }
-
+    assert(!isGlobal && package == entrypoint.root.name);
     rootDir = parts.first;
   } else {
     executable = p.join("bin", executable);
@@ -139,6 +130,26 @@ Future<int> runExecutable(PubCommand command, Entrypoint entrypoint,
   });
 }
 
+/// Runs the snapshot at [path] with [args] and hooks its stdout, stderr, and
+/// sdtin to this process's.
+///
+/// Returns the snapshot's exit code.
+///
+/// This doesn't do any validation of the snapshot's SDK version.
+Future<int> runSnapshot(String path, Iterable<String> args) {
+  var vmArgs = [path]..addAll(args);
+
+  return Process.start(Platform.executable, vmArgs).then((process) {
+    // Note: we're not using process.std___.pipe(std___) here because
+    // that prevents pub from also writing to the output streams.
+    process.stderr.listen(stderr.add);
+    process.stdout.listen(stdout.add);
+    stdin.listen(process.stdin.add);
+
+    return process.exitCode;
+  });
+}
+
 /// Runs the executable snapshot at [snapshotPath].
 Future _runCachedExecutable(Entrypoint entrypoint, String snapshotPath,
     List<String> args) {
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index bbb289a5..08f9fb1a 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -8,14 +8,19 @@ import 'dart:async';
 import 'dart:io';
 
 import 'package:path/path.dart' as p;
+import 'package:barback/barback.dart';
 
+import 'barback/asset_environment.dart';
 import 'entrypoint.dart';
+import 'executable.dart' as exe;
 import 'io.dart';
 import 'lock_file.dart';
 import 'log.dart' as log;
 import 'package.dart';
 import 'pubspec.dart';
+import 'package_graph.dart';
 import 'system_cache.dart';
+import 'sdk.dart' as sdk;
 import 'solver/version_solver.dart';
 import 'source/cached.dart';
 import 'source/git.dart';
@@ -69,6 +74,10 @@ class GlobalPackages {
       // Call this just to log what the current active package is, if any.
       _describeActive(name);
 
+      // TODO(nweiz): Add some special handling for git repos that contain path
+      // 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).
       return _installInCache(
           new PackageDep(name, "git", VersionConstraint.any, repo));
     });
@@ -96,7 +105,13 @@ class GlobalPackages {
       var fullPath = canonicalize(entrypoint.root.dir);
       var id = new PackageId(name, "path", entrypoint.root.version,
           PathSource.describePath(fullPath));
+
+      // TODO(rnystrom): Look in "bin" and display list of binaries that
+      // user can run.
       _writeLockFile(name, new LockFile([id]));
+
+      var binDir = p.join(_directory, name, 'bin');
+      if (dirExists(binDir)) deleteEntry(binDir);
     });
   }
 
@@ -120,9 +135,36 @@ class GlobalPackages {
       result.showReport(SolveType.GET);
 
       // Make sure all of the dependencies are locally installed.
-      return Future.wait(result.packages.map(_cacheDependency));
-    }).then((ids) {
-      _writeLockFile(dep.name, new LockFile(ids));
+      return Future.wait(result.packages.map(_cacheDependency)).then((ids) {
+        var lockFile = new LockFile(ids);
+
+        // Load the package graph from [result] so we don't need to re-parse all
+        // the pubspecs.
+        return new Entrypoint.inMemory(root, lockFile, cache)
+            .loadPackageGraph(result)
+            .then((graph) => _precompileExecutables(graph.entrypoint, dep.name))
+            .then((_) => _writeLockFile(dep.name, lockFile));
+      });
+    });
+  }
+
+  /// Precompiles the executables for [package] and saves them in the global
+  /// cache.
+  Future _precompileExecutables(Entrypoint entrypoint, String package) {
+    return log.progress("Precompiling executables", () {
+      var binDir = p.join(_directory, package, 'bin');
+      var sdkVersionPath = p.join(binDir, 'sdk-version');
+      cleanDir(binDir);
+      writeTextFile(sdkVersionPath, "${sdk.version}\n");
+
+      return AssetEnvironment.create(entrypoint, BarbackMode.RELEASE,
+          useDart2JS: false).then((environment) {
+        environment.barback.errors.listen((error) {
+          log.error(log.red("Build error:\n$error"));
+        });
+
+        return environment.precompileExecutables(package, binDir);
+      });
     });
   }
 
@@ -142,15 +184,20 @@ class GlobalPackages {
 
   /// Finishes activating package [package] by saving [lockFile] in the cache.
   void _writeLockFile(String package, LockFile lockFile) {
-    ensureDir(_directory);
+    ensureDir(p.join(_directory, package));
+
+    // TODO(nweiz): This cleans up Dart 1.6's old lockfile location. Remove it
+    // when Dart 1.6 is old enough that we don't think anyone will have these
+    // lockfiles anymore (issue 20703).
+    var oldPath = p.join(_directory, "$package.lock");
+    if (fileExists(oldPath)) deleteEntry(oldPath);
+
     writeTextFile(_getLockFilePath(package),
         lockFile.serialize(cache.rootDir, cache.sources));
 
     var id = lockFile.packages[package];
     log.message('Activated ${_formatPackage(id)}.');
 
-    // TODO(rnystrom): Look in "bin" and display list of binaries that
-    // user can run.
   }
 
   /// Shows the user the currently active package with [name], if any.
@@ -184,18 +231,17 @@ class GlobalPackages {
   ///
   /// Returns `false` if no package with [name] was currently active.
   bool deactivate(String name, {bool logDeactivate: false}) {
-    var lockFilePath = _getLockFilePath(name);
-    if (!fileExists(lockFilePath)) return false;
-
-    var lockFile = new LockFile.load(lockFilePath, cache.sources);
-    var id = lockFile.packages[name];
-
-    deleteEntry(lockFilePath);
+    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)}.');
     }
 
+    deleteEntry(dir);
+
     return true;
   }
 
@@ -204,12 +250,25 @@ class GlobalPackages {
   /// Returns an [Entrypoint] loaded with the active package if found.
   Future<Entrypoint> find(String name) {
     return syncFuture(() {
+      var lockFilePath = _getLockFilePath(name);
       var lockFile;
       try {
-        lockFile = new LockFile.load(_getLockFilePath(name), cache.sources);
+        lockFile = new LockFile.load(lockFilePath, cache.sources);
       } on IOException catch (error) {
-        // If we couldn't read the lock file, it's not activated.
-        dataError("No active package ${log.bold(name)}.");
+        var oldLockFilePath = p.join(_directory, '$name.lock');
+        try {
+          // TODO(nweiz): This looks for Dart 1.6's old lockfile location.
+          // Remove it when Dart 1.6 is old enough that we don't think anyone
+          // will have these lockfiles anymore (issue 20703).
+          lockFile = new LockFile.load(oldLockFilePath, cache.sources);
+        } on IOException catch (error) {
+          // If we couldn't read the lock file, it's not activated.
+          dataError("No active package ${log.bold(name)}.");
+        }
+
+        // Move the old lockfile to its new location.
+        ensureDir(p.dirname(lockFilePath));
+        new File(oldLockFilePath).renameSync(lockFilePath);
       }
 
       // Load the package from the cache.
@@ -235,25 +294,65 @@ class GlobalPackages {
     });
   }
 
+  /// Runs [package]'s [executable] with [args].
+  ///
+  /// If [executable] is available in its precompiled form, that will be
+  /// recompiled if the SDK has been upgraded since it was first compiled and
+  /// then run. Otherwise, it will be run from source.
+  ///
+  /// Returns the exit code from the executable.
+  Future<int> runExecutable(String package, String executable,
+      Iterable<String> args) {
+    var binDir = p.join(_directory, package, 'bin');
+    if (!fileExists(p.join(binDir, '$executable.dart.snapshot'))) {
+      return find(package).then((entrypoint) {
+        return exe.runExecutable(entrypoint, package, executable, args,
+            isGlobal: true);
+      });
+    }
+
+    // Unless the user overrides the verbosity, we want to filter out the
+    // normal pub output shown while loading the environment.
+    if (log.verbosity == log.Verbosity.NORMAL) {
+      log.verbosity = log.Verbosity.WARNING;
+    }
+
+    return syncFuture(() {
+      var sdkVersionPath = p.join(binDir, 'sdk-version');
+      var snapshotVersion = readTextFile(sdkVersionPath);
+      if (snapshotVersion == "${sdk.version}\n") return null;
+      log.fine("$package:$executable was compiled with Dart "
+          "${snapshotVersion.trim()} and needs to be recompiled.");
+
+      return find(package)
+          .then((entrypoint) => entrypoint.loadPackageGraph())
+          .then((graph) => _precompileExecutables(graph.entrypoint, package));
+    }).then((_) =>
+        exe.runSnapshot(p.join(binDir, '$executable.dart.snapshot'), args));
+  }
+
   /// Gets the path to the lock file for an activated cached package with
   /// [name].
-  String _getLockFilePath(name) => p.join(_directory, name + ".lock");
+  String _getLockFilePath(String name) =>
+      p.join(_directory, name, "pubspec.lock");
 
   /// Shows to the user formatted list of globally activated packages.
   void listActivePackages() {
     if (!dirExists(_directory)) return;
 
     // Loads lock [file] and returns [PackageId] of the activated package.
-    loadPackageId(file) {
-      var name = p.basenameWithoutExtension(file);
+    loadPackageId(file, name) {
       var lockFile = new LockFile.load(p.join(_directory, file), cache.sources);
       return lockFile.packages[name];
     }
 
-    var packages = listDir(_directory, includeDirs: false)
-        .where((file) => p.extension(file) == '.lock')
-        .map(loadPackageId)
-        .toList();
+    var packages = listDir(_directory).map((entry) {
+      if (fileExists(entry)) {
+        return loadPackageId(entry, p.basenameWithoutExtension(entry));
+      } else {
+        return loadPackageId(p.join(entry, 'pubspec.lock'), p.basename(entry));
+      }
+    }).toList();
 
     packages
         ..sort((id1, id2) => id1.name.compareTo(id2.name))
diff --git a/lib/src/package.dart b/lib/src/package.dart
index 211eb37e..3dd84e2d 100644
--- a/lib/src/package.dart
+++ b/lib/src/package.dart
@@ -7,6 +7,7 @@ library pub.package;
 import 'dart:io';
 
 import 'package:path/path.dart' as path;
+import 'package:barback/barback.dart';
 
 import 'io.dart';
 import 'git.dart' as git;
@@ -75,6 +76,20 @@ class Package {
     return deps.values.toSet();
   }
 
+  /// 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))
+        .where((executable) => path.extension(executable) == '.dart')
+        .map((executable) {
+      return new AssetId(
+          name, path.toUri(path.relative(executable, from: dir)).toString());
+    }).toList();
+  }
+
   /// Returns the path to the README file at the root of the entrypoint, or null
   /// if no README file is found.
   ///
@@ -154,7 +169,13 @@ class Package {
       // If we're not listing recursively, strip out paths that contain
       // separators. Since git always prints forward slashes, we always detect
       // them.
-      if (!recursive) files = files.where((file) => !file.contains('/'));
+      if (!recursive) {
+        // If we're listing a subdirectory, we only want to look for slashes
+        // after the subdirectory prefix.
+        var relativeStart = relativeBeneath == '.' ? 0 :
+            relativeBeneath.length + 1;
+        files = files.where((file) => !file.contains('/', relativeStart));
+      }
 
       // Git always prints files relative to the repository root, but we want
       // them relative to the working directory. It also prints forward slashes
diff --git a/test/global/activate/activate_git_after_hosted_test.dart b/test/global/activate/activate_git_after_hosted_test.dart
index 30082928..d50561e3 100644
--- a/test/global/activate/activate_git_after_hosted_test.dart
+++ b/test/global/activate/activate_git_after_hosted_test.dart
@@ -32,6 +32,9 @@ main() {
             Package foo is currently active at version 1.0.0.
             Resolving dependencies...
             + foo 1.0.0 from git ../foo.git
+            Precompiling executables...
+            Loading source assets...
+            Precompiled foo:foo.
             Activated foo 1.0.0 from Git repository "../foo.git".""");
 
     // Should now run the git one.
diff --git a/test/global/activate/activate_hosted_after_git_test.dart b/test/global/activate/activate_hosted_after_git_test.dart
index 09ac4151..88e9913a 100644
--- a/test/global/activate/activate_hosted_after_git_test.dart
+++ b/test/global/activate/activate_hosted_after_git_test.dart
@@ -34,6 +34,9 @@ main() {
         Resolving dependencies...
         + foo 2.0.0
         Downloading foo 2.0.0...
+        Precompiling executables...
+        Loading source assets...
+        Precompiled foo:foo.
         Activated foo 2.0.0.""");
 
     // Should now run the hosted one.
diff --git a/test/global/activate/activate_hosted_after_path_test.dart b/test/global/activate/activate_hosted_after_path_test.dart
index 3115695c..a3d42590 100644
--- a/test/global/activate/activate_hosted_after_path_test.dart
+++ b/test/global/activate/activate_hosted_after_path_test.dart
@@ -34,6 +34,9 @@ main() {
         Resolving dependencies...
         + foo 2.0.0
         Downloading foo 2.0.0...
+        Precompiling executables...
+        Loading source assets...
+        Precompiled foo:foo.
         Activated foo 2.0.0.""");
 
     // Should now run the hosted one.
diff --git a/test/global/activate/cached_package_test.dart b/test/global/activate/cached_package_test.dart
index 348e7677..72671713 100644
--- a/test/global/activate/cached_package_test.dart
+++ b/test/global/activate/cached_package_test.dart
@@ -19,12 +19,14 @@ main() {
     schedulePub(args: ["global", "activate", "foo"], output: """
         Resolving dependencies...
         + foo 1.0.0
+        Precompiling executables...
+        Loading source assets...
         Activated foo 1.0.0.""");
 
     // Should be in global package cache.
     d.dir(cachePath, [
       d.dir('global_packages', [
-        d.matcherFile('foo.lock', contains('1.0.0'))
+        d.dir('foo', [d.matcherFile('pubspec.lock', contains('1.0.0'))])
       ])
     ]).validate();
   });
diff --git a/test/global/activate/constraint_test.dart b/test/global/activate/constraint_test.dart
index 714e06c5..d651709c 100644
--- a/test/global/activate/constraint_test.dart
+++ b/test/global/activate/constraint_test.dart
@@ -21,7 +21,7 @@ main() {
 
     d.dir(cachePath, [
       d.dir('global_packages', [
-        d.matcherFile('foo.lock', contains('1.0.1'))
+        d.dir('foo', [d.matcherFile('pubspec.lock', contains('1.0.1'))])
       ])
     ]).validate();
   });
diff --git a/test/global/activate/different_version_test.dart b/test/global/activate/different_version_test.dart
index a21387cb..6a406eac 100644
--- a/test/global/activate/different_version_test.dart
+++ b/test/global/activate/different_version_test.dart
@@ -22,6 +22,8 @@ main() {
         Resolving dependencies...
         + foo 2.0.0
         Downloading foo 2.0.0...
+        Precompiling executables...
+        Loading source assets...
         Activated foo 2.0.0.""");
   });
 }
diff --git a/test/global/activate/doesnt_snapshot_path_executables_test.dart b/test/global/activate/doesnt_snapshot_path_executables_test.dart
new file mode 100644
index 00000000..fafc6df3
--- /dev/null
+++ b/test/global/activate/doesnt_snapshot_path_executables_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_test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("doesn't snapshots the executables for a path package", () {
+    d.dir('foo', [
+      d.libPubspec("foo", "1.0.0"),
+      d.dir("bin", [
+        d.file("hello.dart", "void main() => print('hello!');")
+      ])
+    ]).create();
+
+    schedulePub(args: ["global", "activate", "-spath", "../foo"],
+        output: isNot(contains('Precompiled foo:hello.')));
+
+    d.dir(cachePath, [
+      d.dir('global_packages', [
+        d.dir('foo', [
+          d.matcherFile('pubspec.lock', contains('1.0.0')),
+          d.nothing('bin')
+        ])
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/global/activate/git_package_test.dart b/test/global/activate/git_package_test.dart
index eea65e8e..d6a887bd 100644
--- a/test/global/activate/git_package_test.dart
+++ b/test/global/activate/git_package_test.dart
@@ -21,6 +21,9 @@ main() {
         output: '''
             Resolving dependencies...
             + foo 1.0.0 from git ../foo.git
+            Precompiling executables...
+            Loading source assets...
+            Precompiled foo:foo.
             Activated foo 1.0.0 from Git repository "../foo.git".''');
   });
 }
diff --git a/test/global/activate/ignores_active_version_test.dart b/test/global/activate/ignores_active_version_test.dart
index a34e0c68..3905f5ff 100644
--- a/test/global/activate/ignores_active_version_test.dart
+++ b/test/global/activate/ignores_active_version_test.dart
@@ -22,6 +22,8 @@ main() {
         Resolving dependencies...
         + foo 1.3.0
         Downloading foo 1.3.0...
+        Precompiling executables...
+        Loading source assets...
         Activated foo 1.3.0.""");
   });
 }
diff --git a/test/global/activate/reactivating_git_upgrades_test.dart b/test/global/activate/reactivating_git_upgrades_test.dart
index a07d667e..f328d269 100644
--- a/test/global/activate/reactivating_git_upgrades_test.dart
+++ b/test/global/activate/reactivating_git_upgrades_test.dart
@@ -19,6 +19,8 @@ main() {
         output: '''
             Resolving dependencies...
             + foo 1.0.0 from git ../foo.git
+            Precompiling executables...
+            Loading source assets...
             Activated foo 1.0.0 from Git repository "../foo.git".''');
 
     d.git('foo.git', [
@@ -31,6 +33,8 @@ main() {
             Package foo is currently active from Git repository "../foo.git".
             Resolving dependencies...
             + foo 1.0.1 from git ../foo.git
+            Precompiling executables...
+            Loading source assets...
             Activated foo 1.0.1 from Git repository "../foo.git".''');
   });
 }
diff --git a/test/global/activate/removes_old_lockfile_test.dart b/test/global/activate/removes_old_lockfile_test.dart
new file mode 100644
index 00000000..8816bcd6
--- /dev/null
+++ b/test/global/activate/removes_old_lockfile_test.dart
@@ -0,0 +1,33 @@
+// 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 the 1.6-style lockfile', () {
+    servePackages((builder) {
+      builder.serve("foo", "1.0.0");
+    });
+
+    d.dir(cachePath, [
+      d.dir('global_packages', [
+        d.file('foo.lock', 'packages: {foo: {description: foo, source: hosted, '
+            'version: "1.0.0"}}}')
+      ])
+    ]).create();
+
+    schedulePub(args: ["global", "activate", "foo"]);
+
+    d.dir(cachePath, [
+      d.dir('global_packages', [
+        d.nothing('foo.lock'),
+        d.dir('foo', [d.matcherFile('pubspec.lock', contains('1.0.0'))])
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/global/activate/snaphots_hosted_executables_test.dart b/test/global/activate/snaphots_hosted_executables_test.dart
new file mode 100644
index 00000000..79ce0d7e
--- /dev/null
+++ b/test/global/activate/snaphots_hosted_executables_test.dart
@@ -0,0 +1,46 @@
+// 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('snapshots the executables for a hosted package', () {
+    servePackages((builder) {
+      builder.serve("foo", "1.0.0", contents: [
+        d.dir('bin', [
+          d.file("hello.dart", "void main() => print('hello!');"),
+          d.file("goodbye.dart", "void main() => print('goodbye!');"),
+          d.file("shell.sh", "echo shell"),
+          d.dir("subdir", [
+            d.file("sub.dart", "void main() => print('sub!');")
+          ])
+        ])
+      ]);
+    });
+
+    schedulePub(args: ["global", "activate", "foo"], output: allOf([
+      contains('Precompiled foo:hello.'),
+      contains("Precompiled foo:goodbye.")
+    ]));
+
+    d.dir(cachePath, [
+      d.dir('global_packages', [
+        d.dir('foo', [
+          d.matcherFile('pubspec.lock', contains('1.0.0')),
+          d.dir('bin', [
+            d.file('sdk-version', '0.1.2+3\n'),
+            d.matcherFile('hello.dart.snapshot', contains('hello!')),
+            d.matcherFile('goodbye.dart.snapshot', contains('goodbye!')),
+            d.nothing('shell.sh.snapshot'),
+            d.nothing('subdir')
+          ])
+        ])
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/global/activate/snapshots_git_executables_test.dart b/test/global/activate/snapshots_git_executables_test.dart
new file mode 100644
index 00000000..35274c14
--- /dev/null
+++ b/test/global/activate/snapshots_git_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('snapshots the executables for a Git repo', () {
+    ensureGit();
+
+    d.git('foo.git', [
+      d.libPubspec("foo", "1.0.0"),
+      d.dir("bin", [
+        d.file("hello.dart", "void main() => print('hello!');"),
+        d.file("goodbye.dart", "void main() => print('goodbye!');"),
+        d.file("shell.sh", "echo shell"),
+        d.dir("subdir", [
+          d.file("sub.dart", "void main() => print('sub!');")
+        ])
+      ])
+    ]).create();
+
+    schedulePub(args: ["global", "activate", "-sgit", "../foo.git"],
+        output: allOf([
+      contains('Precompiled foo:hello.'),
+      contains("Precompiled foo:goodbye.")
+    ]));
+
+    d.dir(cachePath, [
+      d.dir('global_packages', [
+        d.dir('foo', [
+          d.matcherFile('pubspec.lock', contains('1.0.0')),
+          d.dir('bin', [
+            d.file('sdk-version', '0.1.2+3\n'),
+            d.matcherFile('hello.dart.snapshot', contains('hello!')),
+            d.matcherFile('goodbye.dart.snapshot', contains('goodbye!')),
+            d.nothing('shell.sh.snapshot'),
+            d.nothing('subdir')
+          ])
+        ])
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/global/activate/supports_version_solver_backtracking_test.dart b/test/global/activate/supports_version_solver_backtracking_test.dart
index c4647c2b..97d83de8 100644
--- a/test/global/activate/supports_version_solver_backtracking_test.dart
+++ b/test/global/activate/supports_version_solver_backtracking_test.dart
@@ -25,7 +25,7 @@ main() {
     // dummy SDK version 0.1.2+3.
     d.dir(cachePath, [
       d.dir('global_packages', [
-        d.matcherFile('foo.lock', contains('1.1.0'))
+        d.dir('foo', [d.matcherFile('pubspec.lock', contains('1.1.0'))])
       ])
     ]).validate();
   });
diff --git a/test/global/activate/uncached_package_test.dart b/test/global/activate/uncached_package_test.dart
index 7194b99a..92b27ff8 100644
--- a/test/global/activate/uncached_package_test.dart
+++ b/test/global/activate/uncached_package_test.dart
@@ -20,12 +20,14 @@ main() {
         Resolving dependencies...
         + foo 1.2.3 (2.0.0-wildly.unstable available)
         Downloading foo 1.2.3...
+        Precompiling executables...
+        Loading source assets...
         Activated foo 1.2.3.""");
 
     // Should be in global package cache.
     d.dir(cachePath, [
       d.dir('global_packages', [
-        d.matcherFile('foo.lock', contains('1.2.3'))
+        d.dir('foo', [d.matcherFile('pubspec.lock', contains('1.2.3'))])
       ])
     ]).validate();
   });
diff --git a/test/global/deactivate/deactivate_and_reactivate_package_test.dart b/test/global/deactivate/deactivate_and_reactivate_package_test.dart
index 20b1b7eb..1e8a37cf 100644
--- a/test/global/deactivate/deactivate_and_reactivate_package_test.dart
+++ b/test/global/deactivate/deactivate_and_reactivate_package_test.dart
@@ -23,6 +23,8 @@ main() {
         Resolving dependencies...
         + foo 2.0.0
         Downloading foo 2.0.0...
+        Precompiling executables...
+        Loading source assets...
         Activated foo 2.0.0.""");
   });
 }
diff --git a/test/global/deactivate/removes_precompiled_snapshots_test.dart b/test/global/deactivate/removes_precompiled_snapshots_test.dart
new file mode 100644
index 00000000..482a3f41
--- /dev/null
+++ b/test/global/deactivate/removes_precompiled_snapshots_test.dart
@@ -0,0 +1,22 @@
+// 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('removes precompiled snapshots', () {
+    servePackages((builder) => builder.serve("foo", "1.0.0"));
+
+    schedulePub(args: ["global", "activate", "foo"]);
+
+    schedulePub(args: ["global", "deactivate", "foo"],
+        output: "Deactivated package foo 1.0.0.");
+
+    d.dir(cachePath, [
+      d.dir('global_packages', [d.nothing('foo')])
+    ]).validate();
+  });
+}
diff --git a/test/global/list_test.dart b/test/global/list_test.dart
index 782c9b0f..02c231ec 100644
--- a/test/global/list_test.dart
+++ b/test/global/list_test.dart
@@ -11,7 +11,7 @@ import '../test_pub.dart';
 main() {
   initConfig();
 
-  integration('lists an activated hosted package', () {
+  solo_integration('lists an activated hosted package', () {
     servePackages((builder) {
       builder.serve('foo', '1.0.0');
     });
diff --git a/test/global/run/recompiles_if_sdk_is_out_of_date_test.dart b/test/global/run/recompiles_if_sdk_is_out_of_date_test.dart
new file mode 100644
index 00000000..df4b0297
--- /dev/null
+++ b/test/global/run/recompiles_if_sdk_is_out_of_date_test.dart
@@ -0,0 +1,53 @@
+// 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('recompiles a script if the SDK version is out-of-date', () {
+    servePackages((builder) {
+      builder.serve("foo", "1.0.0", contents: [
+        d.dir("bin", [
+          d.file("script.dart", "main(args) => print('ok');")
+        ])
+      ]);
+    });
+
+    schedulePub(args: ["global", "activate", "foo"]);
+
+    d.dir(cachePath, [
+      d.dir('global_packages', [
+        d.dir('foo', [
+          d.dir('bin', [
+            d.file('sdk-version', '0.0.1\n'),
+            d.file('script.dart.snapshot', 'junk')
+          ])
+        ])
+      ])
+    ]).create();
+
+    var pub = pubRun(global: true, args: ["foo:script"]);
+    // In the real world this would just print "hello!", but since we collect
+    // all output we see the precompilation messages as well.
+    pub.stdout.expect("Precompiling executables...");
+    pub.stdout.expect(consumeThrough("ok"));
+    pub.shouldExit();
+
+    d.dir(cachePath, [
+      d.dir('global_packages', [
+        d.dir('foo', [
+          d.dir('bin', [
+            d.file('sdk-version', '0.1.2+3\n'),
+            d.matcherFile('script.dart.snapshot', contains('ok'))
+          ])
+        ])
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/global/run/uses_old_lockfile_test.dart b/test/global/run/uses_old_lockfile_test.dart
new file mode 100644
index 00000000..21b41ead
--- /dev/null
+++ b/test/global/run/uses_old_lockfile_test.dart
@@ -0,0 +1,54 @@
+// 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('uses the 1.6-style lockfile if necessary', () {
+    servePackages((builder) {
+      builder.serve("bar", "1.0.0");
+      builder.serve("foo", "1.0.0", deps: {"bar": "any"}, contents: [
+        d.dir("bin", [
+          d.file("script.dart", """
+              import 'package:bar/bar.dart' as bar;
+
+              main(args) => print(bar.main());""")
+        ])
+      ]);
+    });
+
+    schedulePub(args: ["cache", "add", "foo"]);
+    schedulePub(args: ["cache", "add", "bar"]);
+
+    d.dir(cachePath, [
+      d.dir('global_packages', [
+        d.file('foo.lock', '''
+packages:
+  foo:
+    description: foo
+    source: hosted
+    version: "1.0.0"
+  bar:
+    description: bar
+    source: hosted
+    version: "1.0.0"''')
+      ])
+    ]).create();
+
+    var pub = pubRun(global: true, args: ["foo:script"]);
+    pub.stdout.expect("bar 1.0.0");
+    pub.shouldExit();
+
+    d.dir(cachePath, [
+      d.dir('global_packages', [
+        d.nothing('foo.lock'),
+        d.dir('foo', [d.matcherFile('pubspec.lock', contains('1.0.0'))])
+      ])
+    ]).validate();
+  });
+}
-- 
GitLab