From 07b1962edc0228a1a76e070ebb31d1f4c77ed83c Mon Sep 17 00:00:00 2001
From: "nweiz@google.com" <nweiz@google.com>
Date: Wed, 20 Aug 2014 20:06:20 +0000
Subject: [PATCH] Precompile dependencies' executables for use with "pub run".

R=rnystrom@google.com
BUG=20482

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

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge@39424 260f80e4-7a28-3924-810f-c04153c831b5
---
 lib/src/entrypoint.dart                       | 128 ++++++++++++++++++
 lib/src/executable.dart                       |  49 ++++++-
 lib/src/package.dart                          |  14 +-
 lib/src/package_graph.dart                    |  22 +++
 lib/src/utils.dart                            |  26 ++++
 ...for_immediate_and_transitive_dep_test.dart |  56 ++++++++
 test/snapshot/creates_a_snapshot_test.dart    |  53 ++++++++
 ...napshot_an_entrypoint_dependency_test.dart |  32 +++++
 .../doesnt_snapshot_path_dependency_test.dart |  29 ++++
 ...snapshot_transitive_dependencies_test.dart |  30 ++++
 ...ints_errors_for_broken_snapshots_test.dart |  49 +++++++
 ...mpiles_if_the_sdk_is_out_of_date_test.dart |  44 ++++++
 .../snapshots_transformed_code_test.dart      |  61 +++++++++
 ...upgrades_snapshot_for_dependency_test.dart |  55 ++++++++
 test/snapshot/upgrades_snapshot_test.dart     |  46 +++++++
 test/test_pub.dart                            |  35 ++++-
 16 files changed, 719 insertions(+), 10 deletions(-)
 create mode 100644 test/snapshot/creates_a_snapshot_for_immediate_and_transitive_dep_test.dart
 create mode 100644 test/snapshot/creates_a_snapshot_test.dart
 create mode 100644 test/snapshot/doesnt_snapshot_an_entrypoint_dependency_test.dart
 create mode 100644 test/snapshot/doesnt_snapshot_path_dependency_test.dart
 create mode 100644 test/snapshot/doesnt_snapshot_transitive_dependencies_test.dart
 create mode 100644 test/snapshot/prints_errors_for_broken_snapshots_test.dart
 create mode 100644 test/snapshot/recompiles_if_the_sdk_is_out_of_date_test.dart
 create mode 100644 test/snapshot/snapshots_transformed_code_test.dart
 create mode 100644 test/snapshot/upgrades_snapshot_for_dependency_test.dart
 create mode 100644 test/snapshot/upgrades_snapshot_test.dart

diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 9d2f815b..16b19a59 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -5,14 +5,19 @@
 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;
 import 'package.dart';
 import 'package_graph.dart';
+import 'sdk.dart' as sdk;
 import 'solver/version_solver.dart';
 import 'source/cached.dart';
 import 'system_cache.dart';
@@ -133,10 +138,133 @@ class Entrypoint {
         _linkOrDeleteSecondaryPackageDirs();
 
         result.summarizeChanges(type, dryRun: dryRun);
+
+        // TODO(nweiz): we've already parsed all the pubspecs and we know the
+        // lockfile is up to date; there's got to be a way to re-use that
+        // information here.
+        //
+        // Also, don't precompile stuff when the transitive dependencies
+        // haven't changed.
+        return precompileExecutables().catchError((error, stackTrace) {
+          // Just log exceptions here. Since the method is just about acquiring
+          // dependencies, it shouldn't fail unless that fails.
+          log.exception(error, stackTrace);
+        });
       });
     });
   }
 
+  /// Precompiles all executables from dependencies that don't transitively
+  /// depend on [this] or on a path dependency.
+  Future precompileExecutables() {
+    return loadPackageGraph().then((graph) {
+      var executables = new Map.fromIterable(root.immediateDependencies,
+          key: (dep) => dep.name,
+          value: (dep) => _executablesForPackage(graph, dep.name));
+
+      for (var package in executables.keys.toList()) {
+        if (executables[package].isEmpty) executables.remove(package);
+      }
+
+      var binDir = path.join('.pub', 'bin');
+      deleteEntry(binDir);
+      if (executables.isEmpty) return null;
+
+      return log.progress("Precompiling executables", () {
+        // TODO(nweiz): Only add assets touchable by the executables we're
+        // precompiling.
+        ensureDir(binDir);
+
+        // Make sure there's a trailing newline so our version file matches the
+        // SDK's.
+        writeTextFile(path.join(binDir, 'sdk-version'), "${sdk.version}\n");
+        return AssetEnvironment.create(this, BarbackMode.RELEASE,
+            WatcherType.NONE, useDart2JS: false).then((environment) {
+          environment.barback.errors.listen((error) {
+            log.error(log.red("Build error:\n$error"));
+          });
+
+          return waitAndPrintErrors(executables.keys.map((package) {
+            return _precompileExecutablesForPackage(
+                environment, package, executables[package]);
+          }));
+        });
+      });
+    });
+  }
+
+  /// Returns the list of all executable assets for [packageName] that should be
+  /// precompiled.
+  ///
+  /// If [changed] isn't `null`, executables for [packageName] will only be
+  /// compiled if they might depend on a package in [changed].
+  List<AssetId> _executablesForPackage(PackageGraph graph, String packageName) {
+    var package = graph.packages[packageName];
+    var binDir = path.join(package.dir, 'bin');
+    if (!dirExists(binDir)) return [];
+
+    // If the lockfile has a dependency on the entrypoint or on a path
+    // dependency, its executables could change at any point, so we
+    // shouldn't precompile them.
+    var hasUncachedDependency = graph.transitiveDependencies(packageName)
+        .any((package) {
+      var source = cache.sources[
+          graph.lockFile.packages[package.name].source];
+      return source is! CachedSource;
+    });
+    if (hasUncachedDependency) return [];
+
+    return 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();
+  }
+
+  /// 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);
+    ensureDir(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'));
+          }
+        });
+      }));
+    });
+  }
+
+  /// 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
diff --git a/lib/src/executable.dart b/lib/src/executable.dart
index 9d62188b..2551bae8 100644
--- a/lib/src/executable.dart
+++ b/lib/src/executable.dart
@@ -15,7 +15,9 @@ import 'barback/asset_environment.dart';
 import 'command.dart';
 import 'entrypoint.dart';
 import 'exit_codes.dart' as exit_codes;
+import 'io.dart';
 import 'log.dart' as log;
+import 'sdk.dart' as sdk;
 import 'utils.dart';
 
 /// Runs [executable] from [package] reachable from [entrypoint].
@@ -31,6 +33,18 @@ import 'utils.dart';
 Future<int> runExecutable(PubCommand command, 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,
+      "$executable.dart.snapshot");
+  if (!isGlobal && fileExists(snapshotPath)) {
+    return _runCachedExecutable(entrypoint, snapshotPath, args);
+  }
+
   // If the command has a path separator, then it's a path relative to the
   // root of the package. Otherwise, it's implicitly understood to be in
   // "bin".
@@ -50,12 +64,6 @@ Future<int> runExecutable(PubCommand command, Entrypoint entrypoint,
     executable = p.join("bin", executable);
   }
 
-  // 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 environment;
   return AssetEnvironment.create(entrypoint, BarbackMode.RELEASE,
       WatcherType.NONE, useDart2JS: false).then((_environment) {
@@ -128,3 +136,32 @@ Future<int> runExecutable(PubCommand command, Entrypoint entrypoint,
     });
   });
 }
+
+/// Runs the executable snapshot at [snapshotPath].
+Future _runCachedExecutable(Entrypoint entrypoint, String snapshotPath,
+    List<String> args) {
+  return syncFuture(() {
+    // If the snapshot was compiled with a different SDK version, we need to
+    // recompile it.
+    var sdkVersionPath = p.join(".pub", "bin", "sdk-version");
+    if (fileExists(sdkVersionPath) &&
+        readTextFile(sdkVersionPath) == "${sdk.version}\n") {
+      return null;
+    }
+
+    log.fine("Precompiled executables are out of date.");
+    return entrypoint.precompileExecutables();
+  }).then((_) {
+    var vmArgs = ["--checked", snapshotPath]..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;
+    });
+  });
+}
diff --git a/lib/src/package.dart b/lib/src/package.dart
index 43336484..438c34b2 100644
--- a/lib/src/package.dart
+++ b/lib/src/package.dart
@@ -124,8 +124,10 @@ 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.
-  List<String> listFiles({String beneath}) {
+  /// If [beneath] is passed, this will only return files beneath that path. 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;
 
     // This is used in some performance-sensitive paths and can list many, many
@@ -145,6 +147,12 @@ class Package {
           ["ls-files", "--cached", "--others", "--exclude-standard",
            relativeBeneath],
           workingDir: dir);
+
+      // 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('/'));
+
       // Git always prints files relative to the repository root, but we want
       // them relative to the working directory. It also prints forward slashes
       // on Windows which we normalize away for easier testing.
@@ -156,7 +164,7 @@ class Package {
         return fileExists(file);
       });
     } else {
-      files = listDir(beneath, recursive: true, includeDirs: false,
+      files = listDir(beneath, recursive: recursive, includeDirs: false,
           whitelist: _WHITELISTED_FILES);
     }
 
diff --git a/lib/src/package_graph.dart b/lib/src/package_graph.dart
index 14913787..4470e51b 100644
--- a/lib/src/package_graph.dart
+++ b/lib/src/package_graph.dart
@@ -7,6 +7,7 @@ library pub.package_graph;
 import 'entrypoint.dart';
 import 'lock_file.dart';
 import 'package.dart';
+import 'utils.dart';
 
 /// A holistic view of the entire transitive dependency graph for an entrypoint.
 ///
@@ -24,5 +25,26 @@ class PackageGraph {
   /// All transitive dependencies of the entrypoint (including itself).
   final Map<String, Package> packages;
 
+  /// A map of transitive dependencies for each package.
+  Map<String, Set<Package>> _transitiveDependencies;
+
   PackageGraph(this.entrypoint, this.lockFile, this.packages);
+
+  /// Returns all transitive dependencies of [package].
+  ///
+  /// For the entrypoint this returns all packages in [packages], which includes
+  /// dev and override. For any other package, it ignores dev and override
+  /// dependencies.
+  Set<Package> transitiveDependencies(String package) {
+    if (package == entrypoint.root.name) return packages.values.toSet();
+
+    if (_transitiveDependencies == null) {
+      var closure = transitiveClosure(mapMap(packages,
+          value: (_, package) => package.dependencies.map((dep) => dep.name)));
+      _transitiveDependencies = mapMap(closure,
+          value: (_, names) => names.map((name) => packages[name]).toSet());
+    }
+
+    return _transitiveDependencies[package];
+  }
 }
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 4e1a369b..3abeefdb 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -331,6 +331,32 @@ Future<Map> mapFromIterableAsync(Iterable iter, {key(element),
   })).then((_) => map);
 }
 
+/// Returns the transitive closure of [graph].
+///
+/// This assumes [graph] represents a graph with a vertex for each key and an
+/// edge betweek each key and the values for that key.
+Map<dynamic, Set> transitiveClosure(Map<dynamic, Iterable> graph) {
+  // This uses the Floyd-Warshall algorithm
+  // (https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm).
+  var result = {};
+  graph.forEach((vertex, edges) {
+    result[vertex] = new Set.from(edges)..add(vertex);
+  });
+
+  for (var vertex1 in graph.keys) {
+    for (var vertex2 in graph.keys) {
+      for (var vertex3 in graph.keys) {
+        if (result[vertex2].contains(vertex1) &&
+            result[vertex1].contains(vertex3)) {
+          result[vertex2].add(vertex3);
+        }
+      }
+    }
+  }
+
+  return result;
+}
+
 /// Given a list of filenames, returns a set of patterns that can be used to
 /// filter for those filenames.
 ///
diff --git a/test/snapshot/creates_a_snapshot_for_immediate_and_transitive_dep_test.dart b/test/snapshot/creates_a_snapshot_for_immediate_and_transitive_dep_test.dart
new file mode 100644
index 00000000..a084aa79
--- /dev/null
+++ b/test/snapshot/creates_a_snapshot_for_immediate_and_transitive_dep_test.dart
@@ -0,0 +1,56 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import 'package:path/path.dart' as p;
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("creates a snapshot for an immediate dependency that's also a "
+      "transitive dependency", () {
+    servePackages([
+      packageMap("foo", "1.2.3"),
+      packageMap("bar", "1.2.3", {"foo": "1.2.3"})
+    ], 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!');")
+        ])
+      ])
+    ]);
+
+    d.appDir({"foo": "1.2.3"}).create();
+
+    pubGet(output: allOf([
+      contains("Precompiled foo:hello."),
+      contains("Precompiled foo:goodbye.")
+    ]));
+
+    d.dir(p.join(appPath, '.pub', 'bin'), [
+      d.file('sdk-version', '0.1.2+3\n'),
+      d.dir('foo', [
+        d.matcherFile('hello.dart.snapshot', contains('hello!')),
+        d.matcherFile('goodbye.dart.snapshot', contains('goodbye!')),
+        d.nothing('shell.sh.snapshot'),
+        d.nothing('subdir')
+      ])
+    ]).validate();
+
+    var process = pubRun(args: ['foo:hello']);
+    process.stdout.expect("hello!");
+    process.shouldExit();
+
+    process = pubRun(args: ['foo:goodbye']);
+    process.stdout.expect("goodbye!");
+    process.shouldExit();
+  });
+}
diff --git a/test/snapshot/creates_a_snapshot_test.dart b/test/snapshot/creates_a_snapshot_test.dart
new file mode 100644
index 00000000..27411dc1
--- /dev/null
+++ b/test/snapshot/creates_a_snapshot_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.
+
+library pub_tests;
+
+import 'package:path/path.dart' as p;
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("creates a snapshot for an immediate dependency's executables",
+      () {
+    servePackages([packageMap("foo", "1.2.3")], 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!');")
+        ])
+      ])
+    ]);
+
+    d.appDir({"foo": "1.2.3"}).create();
+
+    pubGet(output: allOf([
+      contains("Precompiled foo:hello."),
+      contains("Precompiled foo:goodbye.")
+    ]));
+
+    d.dir(p.join(appPath, '.pub', 'bin'), [
+      d.file('sdk-version', '0.1.2+3\n'),
+      d.dir('foo', [
+        d.matcherFile('hello.dart.snapshot', contains('hello!')),
+        d.matcherFile('goodbye.dart.snapshot', contains('goodbye!')),
+        d.nothing('shell.sh.snapshot'),
+        d.nothing('subdir')
+      ])
+    ]).validate();
+
+    var process = pubRun(args: ['foo:hello']);
+    process.stdout.expect("hello!");
+    process.shouldExit();
+
+    process = pubRun(args: ['foo:goodbye']);
+    process.stdout.expect("goodbye!");
+    process.shouldExit();
+  });
+}
diff --git a/test/snapshot/doesnt_snapshot_an_entrypoint_dependency_test.dart b/test/snapshot/doesnt_snapshot_an_entrypoint_dependency_test.dart
new file mode 100644
index 00000000..164344ce
--- /dev/null
+++ b/test/snapshot/doesnt_snapshot_an_entrypoint_dependency_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.
+
+library pub_tests;
+
+import 'package:path/path.dart' as p;
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("doesn't create a snapshot for a package that depends on the "
+      "entrypoint", () {
+    servePackages([
+      packageMap("foo", "1.2.3", {'bar': '1.2.3'}),
+      packageMap("bar", "1.2.3", {'myapp': 'any'})
+    ], contents: [
+      d.dir("bin", [d.file("hello.dart", "void main() => print('hello!');")])
+    ]);
+
+    d.appDir({"foo": "1.2.3"}).create();
+
+    pubGet();
+
+    // No local cache should be created, since all dependencies transitively
+    // depend on the entrypoint.
+    d.nothing(p.join(appPath, '.pub', 'bin')).validate();
+  });
+}
diff --git a/test/snapshot/doesnt_snapshot_path_dependency_test.dart b/test/snapshot/doesnt_snapshot_path_dependency_test.dart
new file mode 100644
index 00000000..5c1b1c48
--- /dev/null
+++ b/test/snapshot/doesnt_snapshot_path_dependency_test.dart
@@ -0,0 +1,29 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import 'package:path/path.dart' as p;
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("doesn't create a snapshot for a path dependency", () {
+    d.dir("foo", [
+      d.libPubspec("foo", "1.2.3"),
+      d.dir("bin", [
+        d.dir("bin", [d.file("hello.dart", "void main() => print('hello!');")])
+      ])
+    ]).create();
+
+    d.appDir({"foo": {"path": "../foo"}}).create();
+
+    pubGet();
+
+    d.nothing(p.join(appPath, '.pub', 'bin')).validate();
+  });
+}
diff --git a/test/snapshot/doesnt_snapshot_transitive_dependencies_test.dart b/test/snapshot/doesnt_snapshot_transitive_dependencies_test.dart
new file mode 100644
index 00000000..9b4512e7
--- /dev/null
+++ b/test/snapshot/doesnt_snapshot_transitive_dependencies_test.dart
@@ -0,0 +1,30 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import 'package:path/path.dart' as p;
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("doesn't create a snapshot for transitive dependencies' "
+      "executables", () {
+    servePackages([
+      packageMap("foo", "1.2.3", {'bar': '1.2.3'}),
+      packageMap("bar", "1.2.3")
+    ], contents: [
+      d.dir("bin", [d.file("hello.dart", "void main() => print('hello!');")])
+    ]);
+
+    d.appDir({"foo": "1.2.3"}).create();
+
+    pubGet();
+
+    d.nothing(p.join(appPath, '.pub', 'bin', 'bar')).validate();
+  });
+}
diff --git a/test/snapshot/prints_errors_for_broken_snapshots_test.dart b/test/snapshot/prints_errors_for_broken_snapshots_test.dart
new file mode 100644
index 00000000..1c8af8f1
--- /dev/null
+++ b/test/snapshot/prints_errors_for_broken_snapshots_test.dart
@@ -0,0 +1,49 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import 'package:path/path.dart' as p;
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("prints errors for broken snapshot compilation", () {
+    servePackages([
+      packageMap("foo", "1.2.3"),
+      packageMap("bar", "1.2.3")
+    ], contents: [
+      d.dir("bin", [
+        d.file("hello.dart", "void main() { no closing brace"),
+        d.file("goodbye.dart", "void main() { no closing brace"),
+      ])
+    ]);
+
+    d.appDir({"foo": "1.2.3", "bar": "1.2.3"}).create();
+
+    // This should still have a 0 exit code, since installation succeeded even
+    // if precompilation didn't.
+    pubGet(error: allOf([
+      contains("Failed to precompile foo:hello"),
+      contains("Failed to precompile foo:goodbye"),
+      contains("Failed to precompile bar:hello"),
+      contains("Failed to precompile bar:goodbye")
+    ]), exitCode: 0);
+
+    d.dir(p.join(appPath, '.pub', 'bin'), [
+      d.file('sdk-version', '0.1.2+3\n'),
+      d.dir('foo', [
+        d.nothing('hello.dart.snapshot'),
+        d.nothing('goodbye.dart.snapshot')
+      ]),
+      d.dir('bar', [
+        d.nothing('hello.dart.snapshot'),
+        d.nothing('goodbye.dart.snapshot')
+      ])
+    ]).validate();
+  });
+}
diff --git a/test/snapshot/recompiles_if_the_sdk_is_out_of_date_test.dart b/test/snapshot/recompiles_if_the_sdk_is_out_of_date_test.dart
new file mode 100644
index 00000000..7712798d
--- /dev/null
+++ b/test/snapshot/recompiles_if_the_sdk_is_out_of_date_test.dart
@@ -0,0 +1,44 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import 'package:path/path.dart' as p;
+import 'package:scheduled_test/scheduled_test.dart';
+import 'package:scheduled_test/scheduled_stream.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("creates a snapshot for an immediate dependency's executables",
+      () {
+    servePackages([packageMap("foo", "5.6.7")], contents: [
+      d.dir("bin", [d.file("hello.dart", "void main() => print('hello!');")])
+    ]);
+
+    d.appDir({"foo": "5.6.7"}).create();
+
+    pubGet(output: contains("Precompiled foo:hello."));
+
+    d.dir(p.join(appPath, '.pub', 'bin'), [
+      d.file('sdk-version', '0.0.1'),
+      d.dir('foo', [d.file('hello.dart.snapshot', 'junk')])
+    ]).create();
+
+    var process = pubRun(args: ['foo:hello']);
+
+    // In the real world this would just print "hello!", but since we collect
+    // all output we see the precompilation messages as well.
+    process.stdout.expect("Precompiling executables...");
+    process.stdout.expect(consumeThrough("hello!"));
+    process.shouldExit();
+
+    d.dir(p.join(appPath, '.pub', 'bin'), [
+      d.file('sdk-version', '0.1.2+3'),
+      d.dir('foo', [d.matcherFile('hello.dart.snapshot', contains('hello!'))])
+    ]).create();
+  });
+}
diff --git a/test/snapshot/snapshots_transformed_code_test.dart b/test/snapshot/snapshots_transformed_code_test.dart
new file mode 100644
index 00000000..38018ced
--- /dev/null
+++ b/test/snapshot/snapshots_transformed_code_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.
+
+library pub_tests;
+
+import 'package:path/path.dart' as p;
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+const REPLACE_TRANSFORMER = """
+import 'dart:async';
+
+import 'package:barback/barback.dart';
+
+class ReplaceTransformer extends Transformer {
+  ReplaceTransformer.asPlugin();
+
+  String get allowedExtensions => '.dart';
+
+  Future apply(Transform transform) {
+    return transform.primaryInput.readAsString().then((contents) {
+      transform.addOutput(new Asset.fromString(transform.primaryInput.id,
+          contents.replaceAll("REPLACE ME", "hello!")));
+    });
+  }
+}
+""";
+
+main() {
+  initConfig();
+  integration("snapshots the transformed version of an executable", () {
+    servePackages([
+      packageMap("foo", "1.2.3", {"barback": "any"})
+          ..addAll({'transformers': ['foo']})
+    ], contents: [
+      d.dir("lib", [d.file("foo.dart", REPLACE_TRANSFORMER)]),
+      d.dir("bin", [
+        d.file("hello.dart", """
+final message = 'REPLACE ME';
+
+void main() => print(message);
+"""),
+      ])
+    ], serveBarback: true);
+
+    d.appDir({"foo": "1.2.3"}).create();
+
+    pubGet(output: contains("Precompiled foo:hello."));
+
+    d.dir(p.join(appPath, '.pub', 'bin'), [
+      d.dir('foo', [d.matcherFile('hello.dart.snapshot', contains('hello!'))])
+    ]).validate();
+
+    var process = pubRun(args: ['foo:hello']);
+    process.stdout.expect("hello!");
+    process.shouldExit();
+  });
+}
diff --git a/test/snapshot/upgrades_snapshot_for_dependency_test.dart b/test/snapshot/upgrades_snapshot_for_dependency_test.dart
new file mode 100644
index 00000000..f25d4768
--- /dev/null
+++ b/test/snapshot/upgrades_snapshot_for_dependency_test.dart
@@ -0,0 +1,55 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import 'package:path/path.dart' as p;
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("upgrades a snapshot when a dependency is upgraded", () {
+    servePackages([
+      packageMap("foo", "1.2.3", {"bar": "any"}),
+      packageMap("bar", "1.2.3")
+    ], contents: [
+      d.dir("lib", [d.file("bar.dart", "final message = 'hello!';")]),
+      d.dir("bin", [
+        d.file("hello.dart", """
+import 'package:bar/bar.dart';
+
+void main() => print(message);
+""")
+      ])
+    ]);
+
+    d.appDir({"foo": "any"}).create();
+
+    pubGet(output: contains("Precompiled foo:hello."));
+
+    d.dir(p.join(appPath, '.pub', 'bin', 'foo'), [
+      d.matcherFile('hello.dart.snapshot', contains('hello!'))
+    ]).validate();
+
+    servePackages([
+      packageMap("foo", "1.2.3", {"bar": "any"}),
+      packageMap("bar", "1.2.4")
+    ], contents: [
+      d.dir("lib", [d.file("bar.dart", "final message = 'hello 2!';")]),
+    ], replace: true);
+
+    pubUpgrade(output: contains("Precompiled foo:hello."));
+
+    d.dir(p.join(appPath, '.pub', 'bin', 'foo'), [
+      d.matcherFile('hello.dart.snapshot', contains('hello 2!'))
+    ]).validate();
+
+    var process = pubRun(args: ['foo:hello']);
+    process.stdout.expect("hello 2!");
+    process.shouldExit();
+  });
+}
diff --git a/test/snapshot/upgrades_snapshot_test.dart b/test/snapshot/upgrades_snapshot_test.dart
new file mode 100644
index 00000000..35316821
--- /dev/null
+++ b/test/snapshot/upgrades_snapshot_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.
+
+library pub_tests;
+
+import 'package:path/path.dart' as p;
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("upgrades a snapshot when its package is upgraded", () {
+    servePackages([packageMap("foo", "1.2.3")], contents: [
+      d.dir("bin", [
+        d.file("hello.dart", "void main() => print('hello!');")
+      ])
+    ]);
+
+    d.appDir({"foo": "any"}).create();
+
+    pubGet(output: contains("Precompiled foo:hello."));
+
+    d.dir(p.join(appPath, '.pub', 'bin', 'foo'), [
+      d.matcherFile('hello.dart.snapshot', contains('hello!'))
+    ]).validate();
+
+    servePackages([packageMap("foo", "1.2.4")], contents: [
+      d.dir("bin", [
+        d.file("hello.dart", "void main() => print('hello 2!');")
+      ])
+    ]);
+
+    pubUpgrade(output: contains("Precompiled foo:hello."));
+
+    d.dir(p.join(appPath, '.pub', 'bin', 'foo'), [
+      d.matcherFile('hello.dart.snapshot', contains('hello 2!'))
+    ]).validate();
+
+    var process = pubRun(args: ['foo:hello']);
+    process.stdout.expect("hello 2!");
+    process.shouldExit();
+  });
+}
diff --git a/test/test_pub.dart b/test/test_pub.dart
index 54e30dec..ff5d9f9d 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -255,8 +255,11 @@ Map<String, List<Map>> _servedPackages;
 ///
 /// If [contents] is given, its contents are added to every served
 /// package.
+///
+/// If [serveBarback] is true, the repo versions of barback and its dependencies
+/// will be served as well.
 void servePackages(List<Map> pubspecs, {bool replace: false,
-    Iterable<d.Descriptor> contents}) {
+    Iterable<d.Descriptor> contents, bool serveBarback: false}) {
   if (_servedPackages == null || _servedPackageDir == null) {
     _servedPackages = <String, List<Map>>{};
     _servedApiPackageDir = d.dir('packages', []);
@@ -284,6 +287,28 @@ void servePackages(List<Map> pubspecs, {bool replace: false,
         versions.add(pubspec);
       }
 
+      var repoPackages = new Set();
+      if (serveBarback) {
+        _addPackage(name) {
+          if (_servedPackages.containsKey(name)) return;
+          repoPackages.add(name);
+
+          var pubspec = new Map.from(loadYaml(
+              readTextFile(path.join(repoRoot, 'pkg', name, 'pubspec.yaml'))));
+
+          // Remove any SDK constraints since we don't have a valid SDK version
+          // while testing.
+          pubspec.remove('environment');
+
+          _servedPackages[name] = [pubspec];
+          if (pubspec.containsKey('dependencies')) {
+            pubspec['dependencies'].keys.forEach(_addPackage);
+          }
+        }
+
+        _addPackage('barback');
+      }
+
       _servedApiPackageDir.contents.clear();
       _servedPackageDir.contents.clear();
       for (var name in _servedPackages.keys) {
@@ -305,6 +330,14 @@ void servePackages(List<Map> pubspecs, {bool replace: false,
           d.dir('versions', _servedPackages[name].map((pubspec) {
             var version = pubspec['version'];
 
+            if (repoPackages.contains(name)) {
+              return d.tar('$version.tar.gz', [
+                d.file('pubspec.yaml', JSON.encode(pubspec)),
+                new d.DirectoryDescriptor.fromFilesystem('lib',
+                    path.join(repoRoot, 'pkg', name, 'lib'))
+              ]);
+            }
+
             var archiveContents = [
                 d.file('pubspec.yaml', JSON.encode(pubspec)),
                 d.libDir(name, '$name $version')
-- 
GitLab