Merge pull request #1901 from dart-lang/dart-2-snapshot

Generate Dart 2 snapshots when running in Dart 2 mode
diff --git a/.travis.yml b/.travis.yml
index 477740d..2b14c55 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -17,6 +17,7 @@
 # snapshot if it's available.
 before_script:
   - dart --no-checked --snapshot=bin/pub.dart.snapshot --snapshot-kind=app-jit bin/pub.dart --help
+  - dart --preview-dart-2 --snapshot=bin/pub.dart.snapshot.dart2 bin/pub.dart
 
 # Only building these branches means that we don't run two builds for each pull
 # request.
diff --git a/lib/src/command.dart b/lib/src/command.dart
index 4b5772b..cfc7545 100644
--- a/lib/src/command.dart
+++ b/lib/src/command.dart
@@ -39,7 +39,7 @@
   Entrypoint get entrypoint {
     // Lazy load it.
     if (_entrypoint == null) {
-      _entrypoint = new Entrypoint('.', cache);
+      _entrypoint = new Entrypoint.current(cache);
     }
     return _entrypoint;
   }
diff --git a/lib/src/command/run.dart b/lib/src/command/run.dart
index e480337..93afa3c 100644
--- a/lib/src/command/run.dart
+++ b/lib/src/command/run.dart
@@ -57,8 +57,23 @@
       log.warning("The --mode flag is deprecated and has no effect.");
     }
 
+    // The user may pass in an executable without an extension, but the file
+    // to actually execute will always have one.
+    if (p.extension(executable) != ".dart") executable += ".dart";
+
+    var snapshotPath =
+        p.join(entrypoint.cachePath, "bin", package, "$executable.snapshot");
+
+    // Don't ever compile snapshots for mutable packages, since their code may
+    // change later on.
+    var useSnapshot = fileExists(snapshotPath) ||
+        (package != entrypoint.root.name &&
+            !entrypoint.packageGraph.isPackageMutable(package));
+
     var exitCode = await runExecutable(entrypoint, package, executable, args,
-        checked: argResults['checked']);
+        checked: argResults['checked'],
+        snapshotPath: useSnapshot ? snapshotPath : null,
+        recompile: entrypoint.precompileExecutables);
     await flushThenExit(exitCode);
   }
 }
diff --git a/lib/src/command/uploader.dart b/lib/src/command/uploader.dart
index 6d16e46..43fd9e5 100644
--- a/lib/src/command/uploader.dart
+++ b/lib/src/command/uploader.dart
@@ -4,10 +4,7 @@
 
 import 'dart:async';
 
-import 'package:path/path.dart' as path;
-
 import '../command.dart';
-import '../entrypoint.dart';
 import '../exit_codes.dart' as exit_codes;
 import '../http.dart';
 import '../io.dart';
@@ -58,7 +55,7 @@
     return new Future.sync(() {
       var package = argResults['package'];
       if (package != null) return package;
-      return new Entrypoint(path.current, cache).root.name;
+      return entrypoint.root.name;
     })
         .then((package) {
           var uploader = rest[0];
diff --git a/lib/src/dart.dart b/lib/src/dart.dart
index d445b20..6ce6700 100644
--- a/lib/src/dart.dart
+++ b/lib/src/dart.dart
@@ -11,6 +11,7 @@
 import 'exceptions.dart';
 import 'io.dart';
 import 'log.dart' as log;
+import 'utils.dart';
 
 /// Returns whether [dart] looks like an entrypoint file.
 bool isEntrypoint(CompilationUnit dart) {
@@ -51,18 +52,41 @@
 ///
 /// If [name] is passed, it is used to describe the executable in logs and error
 /// messages.
+///
+/// When running in Dart 2 mode, this automatically creates a Dart 2-compatible
+/// snapshot as well at `$snapshotPath.dart2`.
 Future snapshot(Uri executableUrl, String snapshotPath,
     {Uri packagesFile, String name}) async {
   name = log.bold(name ?? executableUrl.toString());
 
-  var args = ['--snapshot=$snapshotPath', executableUrl.toString()];
-  if (packagesFile != null) args.insert(0, "--packages=$packagesFile");
-  var result = await runProcess(Platform.executable, args);
+  var dart1Args = ['--snapshot=$snapshotPath', executableUrl.toString()];
 
-  if (result.success) {
+  var dart2Path = '$snapshotPath.dart2';
+  var dart2Args = isDart2
+      ? ['--preview-dart-2', '--snapshot=$dart2Path', executableUrl.toString()]
+      : null;
+
+  if (packagesFile != null) {
+    dart1Args.insert(0, "--packages=$packagesFile");
+
+    // Resolve [packagesFile] in case it's relative to work around sdk#33177.
+    dart2Args?.insert(0, "--packages=${Uri.base.resolveUri(packagesFile)}");
+  }
+
+  var processes = [runProcess(Platform.executable, dart1Args)];
+  if (isDart2) processes.add(runProcess(Platform.executable, dart2Args));
+  var results = await Future.wait(processes);
+
+  var failure =
+      results.firstWhere((result) => !result.success, orElse: () => null);
+  if (failure == null) {
     log.message("Precompiled $name.");
   } else {
-    throw new ApplicationException(
-        log.yellow("Failed to precompile $name:\n") + result.stderr.join('\n'));
+    // Don't leave partial results.
+    deleteEntry(snapshotPath);
+    deleteEntry(dart2Path);
+
+    throw new ApplicationException(log.yellow("Failed to precompile $name:\n") +
+        failure.stderr.join('\n'));
   }
 }
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index b44cc06..c266723 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -74,6 +74,9 @@
   /// real directory on disk.
   final bool _inMemory;
 
+  /// Whether this entrypoint exists within the package cache.
+  bool get isCached => root.dir != null && p.isWithin(cache.rootDir, root.dir);
+
   /// Whether this is an entrypoint for a globally-activated package.
   final bool isGlobal;
 
@@ -138,26 +141,40 @@
     return newPath;
   }
 
+  /// Returns the contents of the `.packages` file for this entrypoint.
+  ///
+  /// This is based on the package's lockfile, so it works whether or not a
+  /// `.packages` file has been written.
+  String get packagesFileContents => lockFile.packagesFile(cache, root.name);
+
   /// The path to the directory containing dependency executable snapshots.
   String get _snapshotPath => p.join(cachePath, 'bin');
 
+  /// Loads the entrypoint for the package at the current directory.
+  Entrypoint.current(SystemCache cache)
+      : root = new Package.load(null, '.', cache.sources, isRootPackage: true),
+        cache = cache,
+        _inMemory = false,
+        isGlobal = false;
+
   /// Loads the entrypoint from a package at [rootDir].
-  Entrypoint(String rootDir, SystemCache cache, {this.isGlobal: false})
+  Entrypoint(String rootDir, SystemCache cache)
       : root =
             new Package.load(null, rootDir, cache.sources, isRootPackage: true),
         cache = cache,
-        _inMemory = false;
+        _inMemory = false,
+        isGlobal = true;
 
   /// Creates an entrypoint given package and lockfile objects.
-  Entrypoint.inMemory(this.root, this._lockFile, this.cache,
-      {this.isGlobal: false})
-      : _inMemory = true;
+  Entrypoint.inMemory(this.root, this._lockFile, this.cache)
+      : _inMemory = true,
+        isGlobal = true;
 
   /// Creates an entrypoint given a package and a [solveResult], from which the
   /// package graph and lockfile will be computed.
-  Entrypoint.fromSolveResult(this.root, this.cache, SolveResult solveResult,
-      {this.isGlobal: false})
-      : _inMemory = true {
+  Entrypoint.fromSolveResult(this.root, this.cache, SolveResult solveResult)
+      : _inMemory = true,
+        isGlobal = true {
     _packageGraph = new PackageGraph.fromSolveResult(this, solveResult);
     _lockFile = _packageGraph.lockFile;
   }
@@ -238,7 +255,7 @@
     /// have to reload and reparse all the pubspecs.
     _packageGraph = new PackageGraph.fromSolveResult(this, result);
 
-    writeTextFile(packagesFile, lockFile.packagesFile(cache, root.name));
+    writeTextFile(packagesFile, packagesFileContents);
 
     try {
       if (precompile) {
@@ -350,8 +367,12 @@
     // executable will save us a few IO operations over checking each one. If
     // some executables do exist and some do not, the directory is corrupted and
     // it's good to start from scratch anyway.
-    var executablesExist = executables.every((executable) => fileExists(p.join(
-        _snapshotPath, packageName, "${p.basename(executable)}.snapshot")));
+    var executablesExist = executables.every((executable) {
+      var snapshotPath = p.join(
+          _snapshotPath, packageName, "${p.basename(executable)}.snapshot");
+      if (!fileExists(snapshotPath)) return false;
+      if (isDart2 && !fileExists("$snapshotPath.dart2")) return false;
+    });
     if (!executablesExist) return executables;
 
     // Otherwise, we don't need to recompile.
diff --git a/lib/src/executable.dart b/lib/src/executable.dart
index 796b326..6ef158f 100644
--- a/lib/src/executable.dart
+++ b/lib/src/executable.dart
@@ -14,25 +14,34 @@
 import 'isolate.dart' as isolate;
 import 'log.dart' as log;
 import 'utils.dart';
-import 'system_cache.dart';
 
 /// Runs [executable] from [package] reachable from [entrypoint].
 ///
-/// The executable string is a relative Dart file path using native path
-/// separators with or without a trailing ".dart" extension. It is contained
-/// within [package], which should either be the entrypoint package or an
-/// immediate dependency of it.
+/// The [executable] is a relative path to a Dart file within [package], which
+/// should either be the entrypoint package or an immediate dependency of it.
 ///
 /// Arguments from [args] will be passed to the spawned Dart application.
 ///
-/// If [checked] is true, the program is run in checked mode. If [mode] is
-/// passed, it's used as the barback mode; it defaults to [BarbackMode.RELEASE].
+/// If [checked] is true, the program is run in checked mode.
+///
+/// If [packagesFile] is passed, it's used as the package config file path for
+/// the executable. Otherwise, `entrypoint.packagesFile` is used.
+///
+/// If [snapshotPath] is passed, this will run the executable from that snapshot
+/// if it exists. If [recompile] is passed, it's called if the snapshot is
+/// out-of-date or nonexistent. It's expected to regenerate a snapshot at
+/// [snapshotPath], after which the snapshot will be re-run. It's ignored if
+/// [snapshotPath] isn't passed.
 ///
 /// Returns the exit code of the spawned app.
 Future<int> runExecutable(Entrypoint entrypoint, String package,
     String executable, Iterable<String> args,
-    {bool isGlobal: false, bool checked: false, SystemCache cache}) async {
-  assert(!isGlobal || cache != null);
+    {bool checked: false,
+    String packagesFile,
+    String snapshotPath,
+    Future<void> recompile()}) async {
+  packagesFile ??= entrypoint.packagesFile;
+
   // Make sure the package is an immediate dependency of the entrypoint or the
   // entrypoint itself.
   if (entrypoint.root.name != package &&
@@ -49,25 +58,24 @@
   entrypoint.migrateCache();
 
   // Unless the user overrides the verbosity, we want to filter out the
-  // normal pub output shown while loading the environment.
+  // normal pub output that may be shown when recompiling snapshots.
   if (log.verbosity == log.Verbosity.NORMAL) {
     log.verbosity = log.Verbosity.WARNING;
   }
 
-  // Ensure that there's a trailing extension.
-  if (p.extension(executable) != ".dart") executable += ".dart";
-
-  var localSnapshotPath =
-      p.join(entrypoint.cachePath, "bin", package, "$executable.snapshot");
   // Snapshots are compiled in Dart 1 mode, so in Dart 2 mode we always run
   // executables from source.
-  if (!isDart2 && !isGlobal && fileExists(localSnapshotPath)) {
+  if (snapshotPath != null) {
+    // Look for the Dart 2-specific snapshot when running in Dart 2 mode.
+    if (isDart2) snapshotPath += '.dart2';
+
     // Since we don't access the package graph, this doesn't happen
     // automatically.
     entrypoint.assertUpToDate();
 
-    return _runCachedExecutable(entrypoint, localSnapshotPath, args,
-        checked: checked);
+    var result = await _runOrCompileSnapshot(snapshotPath, args,
+        packagesFile: packagesFile, checked: checked, recompile: recompile);
+    if (result != null) return result;
   }
 
   // If the command has a path separator, then it's a path relative to the
@@ -75,12 +83,11 @@
   // "bin".
   if (p.split(executable).length == 1) executable = p.join("bin", executable);
 
-  var executablePath = await _executablePath(entrypoint, package, executable,
-      isGlobal: isGlobal, cache: cache);
+  var executablePath = await _executablePath(entrypoint, package, executable);
 
   if (executablePath == null) {
     var message = "Could not find ${log.bold(executable)}";
-    if (isGlobal || package != entrypoint.root.name) {
+    if (entrypoint.isGlobal || package != entrypoint.root.name) {
       message += " in package ${log.bold(package)}";
     }
     log.error("$message.");
@@ -91,7 +98,7 @@
   // helpful for the subprocess to be able to spawn Dart with
   // Platform.executableArguments and have that work regardless of the working
   // directory.
-  var packageConfig = p.toUri(p.absolute(entrypoint.packagesFile));
+  var packageConfig = p.toUri(p.absolute(packagesFile));
 
   await isolate.runUri(p.toUri(executablePath), args.toList(), null,
       checked: checked,
@@ -106,28 +113,37 @@
 /// returns `null`. If the executable is global and doesn't already have a
 /// `.packages` file one will be created.
 Future<String> _executablePath(
-    Entrypoint entrypoint, String package, String path,
-    {bool isGlobal: false, SystemCache cache}) async {
+    Entrypoint entrypoint, String package, String path) async {
   assert(p.isRelative(path));
 
-  if (!fileExists(entrypoint.packagesFile)) {
-    if (!isGlobal) return null;
-    // A .packages file may not already exist if the global executable has a
-    // 1.6-style lock file instead.
-    await writeTextFile(
-        entrypoint.packagesFile, entrypoint.lockFile.packagesFile(cache));
-  }
   var fullPath = entrypoint.packageGraph.packages[package].path(path);
   if (!fileExists(fullPath)) return null;
   return p.absolute(fullPath);
 }
 
+/// Like [runSnapshot], but runs [recompile] if [path] doesn't exist yet.
+///
+/// Returns `null` if [path] doesn't exist and isn't generated by [recompile].
+Future<int> _runOrCompileSnapshot(String path, Iterable<String> args,
+    {Future<void> recompile(),
+    String packagesFile,
+    bool checked: false}) async {
+  if (!fileExists(path)) {
+    if (recompile == null) return null;
+    await recompile();
+    if (!fileExists(path)) return null;
+  }
+
+  return await runSnapshot(path, args,
+      recompile: recompile, packagesFile: packagesFile, checked: checked);
+}
+
 /// Runs the snapshot at [path] with [args] and hooks its stdout, stderr, and
 /// sdtin to this process's.
 ///
 /// If [recompile] is passed, it's called if the snapshot is out-of-date. It's
 /// expected to regenerate a snapshot at [path], after which the snapshot will
-/// be re-run. It may return a Future.
+/// be re-run.
 ///
 /// If [checked] is set, runs the snapshot in checked mode.
 ///
@@ -135,7 +151,9 @@
 ///
 /// This doesn't do any validation of the snapshot's SDK version.
 Future<int> runSnapshot(String path, Iterable<String> args,
-    {recompile(), String packagesFile, bool checked: false}) async {
+    {Future<void> recompile(),
+    String packagesFile,
+    bool checked: false}) async {
   Uri packageConfig;
   if (packagesFile != null) {
     // We use an absolute path here not because the VM insists but because it's
@@ -155,6 +173,8 @@
   } on IsolateSpawnException catch (error) {
     if (recompile == null) rethrow;
     if (!error.message.contains("Wrong script snapshot version")) rethrow;
+
+    log.fine("Precompiled executable is out of date.");
     await recompile();
     await isolate.runUri(url, argList, null,
         checked: checked, packageConfig: packageConfig);
@@ -162,14 +182,3 @@
 
   return exitCode;
 }
-
-/// Runs the executable snapshot at [snapshotPath].
-Future<int> _runCachedExecutable(
-    Entrypoint entrypoint, String snapshotPath, List<String> args,
-    {bool checked: false}) {
-  return runSnapshot(snapshotPath, args,
-      packagesFile: entrypoint.packagesFile, checked: checked, recompile: () {
-    log.fine("Precompiled executable is out of date.");
-    return entrypoint.precompileExecutables();
-  });
-}
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index c99f9f3..a69d5c4 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -137,7 +137,7 @@
   /// Otherwise, the previous ones will be preserved.
   Future activatePath(String path, List<String> executables,
       {bool overwriteBinStubs}) async {
-    var entrypoint = new Entrypoint(path, cache, isGlobal: true);
+    var entrypoint = new Entrypoint(path, cache);
 
     // Get the package's dependencies.
     await entrypoint.acquireDependencies(SolveType.GET);
@@ -204,8 +204,7 @@
 
     // Load the package graph from [result] so we don't need to re-parse all
     // the pubspecs.
-    var entrypoint =
-        new Entrypoint.fromSolveResult(root, cache, result, isGlobal: true);
+    var entrypoint = new Entrypoint.fromSolveResult(root, cache, result);
     var snapshots = await _precompileExecutables(entrypoint, dep.name);
 
     _updateBinStubs(entrypoint.packageGraph.packages[dep.name], executables,
@@ -226,6 +225,13 @@
       var binDir = p.join(_directory, packageName, 'bin');
       cleanDir(binDir);
 
+      var packagesFilePath = p.join(_directory, packageName, '.packages');
+      if (!fileExists(packagesFilePath)) {
+        // A .packages file may not already exist if the global executable has a
+        // 1.6-style lock file instead.
+        await writeTextFile(packagesFilePath, entrypoint.packagesFileContents);
+      }
+
       // Try to avoid starting up an asset server to precompile packages if
       // possible. This is faster and produces better error messages.
       var package = entrypoint.packageGraph.packages[packageName];
@@ -335,14 +341,12 @@
     if (source is CachedSource) {
       // For cached sources, the package itself is in the cache and the
       // lockfile is the one we just loaded.
-      entrypoint = new Entrypoint.inMemory(cache.load(id), lockFile, cache,
-          isGlobal: true);
+      entrypoint = new Entrypoint.inMemory(cache.load(id), lockFile, cache);
     } else {
       // For uncached sources (i.e. path), the ID just points to the real
       // directory for the package.
       entrypoint = new Entrypoint(
-          (id.source as PathSource).pathFromDescription(id.description), cache,
-          isGlobal: true);
+          (id.source as PathSource).pathFromDescription(id.description), cache);
     }
 
     entrypoint.root.pubspec.sdkConstraints.forEach((sdkName, constraint) {
@@ -375,28 +379,17 @@
   Future<int> runExecutable(
       String package, String executable, Iterable<String> args,
       {bool checked: false}) {
-    var binDir = p.join(_directory, package, 'bin');
-    // Snapshots are compiled in Dart 1 mode, so in Dart 2 mode we always run
-    // executables from source.
-    if (isDart2 || !fileExists(p.join(binDir, '$executable.dart.snapshot'))) {
-      return exe.runExecutable(find(package), package, executable, args,
-          isGlobal: true, checked: checked, cache: cache);
-    }
-
-    // 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(binDir, '$executable.dart.snapshot');
-    return exe.runSnapshot(snapshotPath, args, checked: checked,
-        recompile: () async {
-      log.fine("$package:$executable is out of date and needs to be "
-          "recompiled.");
-      await _precompileExecutables(
-          find(package).packageGraph.entrypoint, package);
-    });
+    var entrypoint = find(package);
+    return exe.runExecutable(
+        entrypoint, package, p.join('bin', '$executable.dart'), args,
+        checked: checked,
+        packagesFile:
+            entrypoint.isCached ? _getPackagesFilePath(package) : null,
+        // Don't use snapshots for executables activated from paths.
+        snapshotPath: entrypoint.isCached
+            ? p.join(_directory, package, 'bin', '$executable.dart.snapshot')
+            : null,
+        recompile: () => _precompileExecutables(entrypoint, package));
   }
 
   /// Gets the path to the lock file for an activated cached package with
diff --git a/test/global/dart2_test.dart b/test/global/dart2_test.dart
new file mode 100644
index 0000000..e619453
--- /dev/null
+++ b/test/global/dart2_test.dart
@@ -0,0 +1,89 @@
+// Copyright (c) 2018, 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:path/path.dart' as p;
+import 'package:test/test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+main() {
+  test("doesn't create a Dart 2 snapshot in Dart 1", () async {
+    await servePackages((builder) {
+      builder.serve("foo", "1.0.0", contents: [
+        d.dir('bin', [d.file("hello.dart", "void main() => print('hello!');")])
+      ]);
+    });
+
+    await runPub(
+        args: ["global", "activate", "foo"],
+        output: allOf([contains('Precompiled foo:hello.')]));
+
+    await d.dir(p.join(cachePath, 'global_packages', 'foo', 'bin'),
+        [d.nothing('hello.dart.snapshot.dart2')]).validate();
+  });
+
+  test("creates both a Dart 1 and Dart 2 snapshot in Dart 2", () async {
+    await servePackages((builder) {
+      builder.serve("foo", "1.0.0", contents: [
+        d.dir('bin', [d.file("hello.dart", "void main() => print('hello!');")])
+      ]);
+    });
+
+    await runPub(
+        args: ["global", "activate", "foo"],
+        output: allOf([contains('Precompiled foo:hello.')]),
+        dart2: true);
+
+    await d.dir(p.join(cachePath, 'global_packages', 'foo', 'bin'), [
+      d.file('hello.dart.snapshot', contains('hello!')),
+      d.file('hello.dart.snapshot.dart2', contains('hello!'))
+    ]).validate();
+  });
+
+  test("creates a Dart 2 snapshot when reactivated with Dart 2", () async {
+    await servePackages((builder) {
+      builder.serve("foo", "1.0.0", contents: [
+        d.dir('bin', [d.file("hello.dart", "void main() => print('hello!');")])
+      ]);
+    });
+
+    await runPub(
+        args: ["global", "activate", "foo"],
+        output: allOf([contains('Precompiled foo:hello.')]));
+    await runPub(
+        args: ["global", "activate", "foo"],
+        output: allOf([contains('Precompiled foo:hello.')]),
+        dart2: true);
+
+    await d.dir(p.join(cachePath, 'global_packages', 'foo', 'bin'), [
+      d.file('hello.dart.snapshot', contains('hello!')),
+      d.file('hello.dart.snapshot.dart2', contains('hello!'))
+    ]).validate();
+  });
+
+  test("creates a Dart 2 snapshot when run with Dart 2", () async {
+    await servePackages((builder) {
+      builder.serve("foo", "1.0.0", contents: [
+        d.dir('bin', [d.file("hello.dart", "void main() => print('hello!');")])
+      ]);
+    });
+
+    await runPub(
+        args: ["global", "activate", "foo"],
+        output: allOf([contains('Precompiled foo:hello.')]));
+
+    var pub = await pubRun(global: true, args: ["foo:hello"], dart2: true);
+    // In the real world this would just print "hello!", but since we
+    // collect all output we see the precompilation messages as well.
+    expect(pub.stdout, emits("Precompiling executables..."));
+    expect(pub.stdout, emitsThrough("hello!"));
+    await pub.shouldExit();
+
+    await d.dir(p.join(cachePath, 'global_packages', 'foo', 'bin'), [
+      d.file('hello.dart.snapshot', contains('hello!')),
+      d.file('hello.dart.snapshot.dart2', contains('hello!'))
+    ]).validate();
+  });
+}
diff --git a/test/global/run/uses_old_lockfile_test.dart b/test/global/run/uses_old_lockfile_test.dart
index 38e850b..983a61a 100644
--- a/test/global/run/uses_old_lockfile_test.dart
+++ b/test/global/run/uses_old_lockfile_test.dart
@@ -42,7 +42,7 @@
     ]).create();
 
     var pub = await pubRun(global: true, args: ["foo:script"]);
-    expect(pub.stdout, emits("bar 1.0.0"));
+    expect(pub.stdout, emitsThrough("bar 1.0.0"));
     await pub.shouldExit();
 
     await d.dir(cachePath, [
diff --git a/test/snapshot_test.dart b/test/snapshot_test.dart
index 9f9837a..64609c3 100644
--- a/test/snapshot_test.dart
+++ b/test/snapshot_test.dart
@@ -91,6 +91,49 @@
       await process.shouldExit();
     });
 
+    test("only for Dart 1 in Dart 1 mode", () async {
+      await servePackages((builder) {
+        builder.serve("foo", "1.2.3", contents: [
+          d.dir(
+              "bin", [d.file("hello.dart", "void main() => print('hello!');")])
+        ]);
+      });
+
+      await d.appDir({"foo": "1.2.3"}).create();
+
+      await pubGet(output: allOf([contains("Precompiled foo:hello.")]));
+
+      await d.dir(p.join(appPath, '.dart_tool', 'pub', 'bin', 'foo'),
+          [d.nothing('hello.dart.snapshot.dart2')]).validate();
+    });
+
+    test("for Dart 1 and 2 in Dart 2 mode", () async {
+      await servePackages((builder) {
+        builder.serve("foo", "1.2.3", contents: [
+          d.dir(
+              "bin", [d.file("hello.dart", "void main() => print('hello!');")])
+        ]);
+      });
+
+      await d.appDir({"foo": "1.2.3"}).create();
+
+      await pubGet(
+          output: allOf([contains("Precompiled foo:hello.")]), dart2: true);
+
+      await d.dir(p.join(appPath, '.dart_tool', 'pub', 'bin', 'foo'), [
+        d.file('hello.dart.snapshot', contains('hello!')),
+        d.file('hello.dart.snapshot.dart2', contains('hello!'))
+      ]).validate();
+
+      var process = await pubRun(args: ['foo:hello']);
+      expect(process.stdout, emits("hello!"));
+      await process.shouldExit();
+
+      process = await pubRun(args: ['foo:hello'], dart2: true);
+      expect(process.stdout, emits("hello!"));
+      await process.shouldExit();
+    });
+
     group("again if", () {
       test("its package is updated", () async {
         await servePackages((builder) {
@@ -227,6 +270,83 @@
           d.dir('foo', [d.file('hello.dart.snapshot', contains('hello!'))])
         ]).validate();
       });
+
+      test("the SDK is out of date", () async {
+        await servePackages((builder) {
+          builder.serve("foo", "5.6.7", contents: [
+            d.dir("bin",
+                [d.file("hello.dart", "void main() => print('hello!');")])
+          ]);
+        });
+
+        await d.appDir({"foo": "5.6.7"}).create();
+
+        await pubGet(output: contains("Precompiled foo:hello."));
+
+        await d.dir(p.join(appPath, '.dart_tool', 'pub', 'bin'), [
+          d.dir('foo', [d.outOfDateSnapshot('hello.dart.snapshot')])
+        ]).create();
+
+        var process = await 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.
+        expect(process.stdout, emits("Precompiling executables..."));
+        expect(process.stdout, emitsThrough("hello!"));
+        await process.shouldExit();
+
+        await d.dir(p.join(appPath, '.dart_tool', 'pub', 'bin'), [
+          d.file('sdk-version', '0.1.2+3\n'),
+          d.dir('foo', [d.file('hello.dart.snapshot', contains('hello!'))])
+        ]).validate();
+      });
+
+      group("Dart 2 is newly in use for", () {
+        test("pub get", () async {
+          await servePackages((builder) {
+            builder.serve("foo", "5.6.7", contents: [
+              d.dir("bin",
+                  [d.file("hello.dart", "void main() => print('hello!');")])
+            ]);
+          });
+
+          await d.appDir({"foo": "5.6.7"}).create();
+
+          await pubGet(output: contains("Precompiled foo:hello."));
+          await pubGet(output: contains("Precompiled foo:hello."), dart2: true);
+
+          await d.dir(p.join(appPath, '.dart_tool', 'pub', 'bin', 'foo'), [
+            d.file('hello.dart.snapshot', contains('hello!')),
+            d.file('hello.dart.snapshot.dart2', contains('hello!'))
+          ]).validate();
+        });
+
+        test("pub run", () async {
+          await servePackages((builder) {
+            builder.serve("foo", "5.6.7", contents: [
+              d.dir("bin",
+                  [d.file("hello.dart", "void main() => print('hello!');")])
+            ]);
+          });
+
+          await d.appDir({"foo": "5.6.7"}).create();
+
+          await pubGet(output: contains("Precompiled foo:hello."));
+
+          var process = await pubRun(args: ['foo:hello'], dart2: true);
+
+          // In the real world this would just print "hello!", but since we
+          // collect all output we see the precompilation messages as well.
+          expect(process.stdout, emits("Precompiling executables..."));
+          expect(process.stdout, emitsThrough("hello!"));
+          await process.shouldExit();
+
+          await d.dir(p.join(appPath, '.dart_tool', 'pub', 'bin', 'foo'), [
+            d.file('hello.dart.snapshot', contains('hello!')),
+            d.file('hello.dart.snapshot.dart2', contains('hello!'))
+          ]).validate();
+        });
+      });
     });
   });
 }
diff --git a/test/test_pub.dart b/test/test_pub.dart
index bd4afdf..42574a1 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -123,7 +123,8 @@
     silent,
     warning,
     int exitCode,
-    Map<String, String> environment}) async {
+    Map<String, String> environment,
+    bool dart2: false}) async {
   if (error != null && warning != null) {
     throw new ArgumentError("Cannot pass both 'error' and 'warning'.");
   }
@@ -145,7 +146,8 @@
       error: error,
       silent: silent,
       exitCode: exitCode,
-      environment: environment);
+      environment: environment,
+      dart2: dart2);
 }
 
 Future pubGet(
@@ -154,14 +156,16 @@
         error,
         warning,
         int exitCode,
-        Map<String, String> environment}) =>
+        Map<String, String> environment,
+        bool dart2: false}) =>
     pubCommand(RunCommand.get,
         args: args,
         output: output,
         error: error,
         warning: warning,
         exitCode: exitCode,
-        environment: environment);
+        environment: environment,
+        dart2: dart2);
 
 Future pubUpgrade(
         {Iterable<String> args,
@@ -169,14 +173,16 @@
         error,
         warning,
         int exitCode,
-        Map<String, String> environment}) =>
+        Map<String, String> environment,
+        bool dart2: false}) =>
     pubCommand(RunCommand.upgrade,
         args: args,
         output: output,
         error: error,
         warning: warning,
         exitCode: exitCode,
-        environment: environment);
+        environment: environment,
+        dart2: dart2);
 
 Future pubDowngrade(
         {Iterable<String> args,
@@ -184,14 +190,16 @@
         error,
         warning,
         int exitCode,
-        Map<String, String> environment}) =>
+        Map<String, String> environment,
+        bool dart2: false}) =>
     pubCommand(RunCommand.downgrade,
         args: args,
         output: output,
         error: error,
         warning: warning,
         exitCode: exitCode,
-        environment: environment);
+        environment: environment,
+        dart2: dart2);
 
 /// Schedules starting the "pub [global] run" process and validates the
 /// expected startup output.
@@ -200,10 +208,11 @@
 /// "pub run".
 ///
 /// Returns the `pub run` process.
-Future<PubProcess> pubRun({bool global: false, Iterable<String> args}) async {
+Future<PubProcess> pubRun(
+    {bool global: false, Iterable<String> args, bool dart2: false}) async {
   var pubArgs = global ? ["global", "run"] : ["run"];
   pubArgs.addAll(args);
-  var pub = await startPub(args: pubArgs);
+  var pub = await startPub(args: pubArgs, dart2: dart2);
 
   // Loading sources and transformers isn't normally printed, but the pub test
   // infrastructure runs pub in verbose mode, which enables this.
@@ -244,12 +253,16 @@
     silent,
     int exitCode: exit_codes.SUCCESS,
     String workingDirectory,
-    Map<String, String> environment}) async {
+    Map<String, String> environment,
+    bool dart2: false}) async {
   // Cannot pass both output and outputJson.
   assert(output == null || outputJson == null);
 
   var pub = await startPub(
-      args: args, workingDirectory: workingDirectory, environment: environment);
+      args: args,
+      workingDirectory: workingDirectory,
+      environment: environment,
+      dart2: dart2);
   await pub.shouldExit(exitCode);
 
   expect(() async {
@@ -338,7 +351,8 @@
     {Iterable<String> args,
     String tokenEndpoint,
     String workingDirectory,
-    Map<String, String> environment}) async {
+    Map<String, String> environment,
+    bool dart2: false}) async {
   args ??= [];
 
   ensureDir(_pathInSandbox(appPath));
@@ -359,13 +373,13 @@
   // TODO(nweiz): When the test runner supports plugins, create one to
   // auto-generate the snapshot before each run.
   var pubPath = p.absolute(p.join(pubRoot, 'bin/pub.dart'));
-  if (fileExists('$pubPath.snapshot')) pubPath += '.snapshot';
+  var snapshotPath = '$pubPath.snapshot';
+  if (dart2) snapshotPath += '.dart2';
+  if (fileExists(snapshotPath)) pubPath = snapshotPath;
 
-  var dartArgs = [
-    await PackageResolver.current.processArgument,
-    pubPath,
-    '--verbose'
-  ]..addAll(args);
+  var dartArgs = [await PackageResolver.current.processArgument];
+  if (dart2) dartArgs.add('--preview-dart-2');
+  dartArgs..addAll([pubPath, '--verbose'])..addAll(args);
 
   return await PubProcess.start(dartBin, dartArgs,
       environment: getPubTestEnvironment(tokenEndpoint)