Refresh binstubs after recompile in global run (#2610)

diff --git a/.travis.yml b/.travis.yml
index a763fcf..74955f8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -16,7 +16,7 @@
 env:
   global:
     - SPLIT=`which gsplit || which split`
-    - _PUB_TEST_SNAPSHOT="`pwd`/.dart_tool//pub.dart.snapshot.dart2"
+    - _PUB_TEST_SNAPSHOT="`pwd`/.dart_tool/pub.dart.snapshot.dart2"
 
 dart_task:
   - test: --preset travis `$SPLIT -n l/1/7 .dart_tool/test_files`
@@ -42,6 +42,7 @@
 before_script:
   - dart --snapshot="$_PUB_TEST_SNAPSHOT" bin/pub.dart
   - find test -name "*_test\\.dart" | sort > .dart_tool/test_files
+  - dart --disable-analytics
 
 # Only building these branches means that we don't run two builds for each pull
 # request.
diff --git a/lib/src/command/global_run.dart b/lib/src/command/global_run.dart
index 0d9bb94..087598e 100644
--- a/lib/src/command/global_run.dart
+++ b/lib/src/command/global_run.dart
@@ -66,7 +66,7 @@
 
     final vmArgs = vmArgsFromArgResults(argResults);
     final globalEntrypoint = await globals.find(package);
-    final exitCode = await runExecutable(globalEntrypoint,
+    final exitCode = await globals.runExecutable(globalEntrypoint,
         Executable.adaptProgramName(package, executable), args,
         vmArgs: vmArgs,
         enableAsserts: argResults['enable-asserts'] || argResults['checked'],
diff --git a/lib/src/executable.dart b/lib/src/executable.dart
index b647c9c..62ec5e8 100644
--- a/lib/src/executable.dart
+++ b/lib/src/executable.dart
@@ -201,10 +201,11 @@
 /// An executable in a package
 class Executable {
   String package;
-  // The relative path to the executable inside the root of [package].
+
+  /// The relative path to the executable inside the root of [package].
   String relativePath;
 
-  // Adapts the program-name following conventions of dart run
+  /// Adapts the program-name following conventions of dart run
   Executable.adaptProgramName(this.package, String program)
       : relativePath = _adaptProgramToPath(program);
 
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index d6b96f5..257f7bf 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -11,7 +11,7 @@
 
 import 'entrypoint.dart';
 import 'exceptions.dart';
-import 'executable.dart';
+import 'executable.dart' as exec;
 import 'http.dart' as http;
 import 'io.dart';
 import 'lock_file.dart';
@@ -371,16 +371,17 @@
   ///
   /// Returns the exit code from the executable.
   Future<int> runExecutable(
-      Entrypoint entrypoint, Executable executable, Iterable<String> args,
+      Entrypoint entrypoint, exec.Executable executable, Iterable<String> args,
       {bool enableAsserts = false,
       String packagesFile,
-      Future<void> Function(Executable) recompile,
+      Future<void> Function(exec.Executable) recompile,
       List<String> vmArgs = const []}) async {
-    return await runExecutable(entrypoint, executable, args,
+    return await exec.runExecutable(entrypoint, executable, args,
         enableAsserts: enableAsserts,
-        packagesFile: packagesFile,
-        recompile: recompile,
-        vmArgs: vmArgs);
+        packagesFile: packagesFile, recompile: (exectuable) async {
+      await recompile(exectuable);
+      _refreshBinStubs(entrypoint, executable);
+    }, vmArgs: vmArgs);
   }
 
   /// Gets the path to the lock file for an activated cached package with
@@ -524,6 +525,33 @@
     return Pair(successes, failures);
   }
 
+  /// Rewrites all binstubs that refer to [executable] of [entrypoint].
+  ///
+  /// This is meant to be called after a recompile due to eg. outdated
+  /// snapshots.
+  void _refreshBinStubs(Entrypoint entrypoint, exec.Executable executable) {
+    if (!dirExists(_binStubDir)) return;
+    for (var file in listDir(_binStubDir, includeDirs: false)) {
+      var contents = readTextFile(file);
+      var binStubPackage = _binStubProperty(contents, 'Package');
+      var binStubScript = _binStubProperty(contents, 'Script');
+      if (binStubPackage == null || binStubScript == null) {
+        log.fine('Could not parse binstub $file:\n$contents');
+        continue;
+      }
+      if (binStubPackage == entrypoint.root.name &&
+          binStubScript ==
+              p.basenameWithoutExtension(executable.relativePath)) {
+        log.fine('Replacing old binstub $file');
+        deleteEntry(file);
+        _createBinStub(
+            entrypoint.root, p.basenameWithoutExtension(file), binStubScript,
+            overwrite: true,
+            snapshot: entrypoint.snapshotPathOfExecutable(executable));
+      }
+    }
+  }
+
   /// Updates the binstubs for [package].
   ///
   /// A binstub is a little shell script in `PUB_CACHE/bin` that runs an
@@ -570,7 +598,7 @@
         script,
         overwrite: overwriteBinStubs,
         snapshot: entrypoint.snapshotPathOfExecutable(
-          Executable.adaptProgramName(package.name, script),
+          exec.Executable.adaptProgramName(package.name, script),
         ),
       );
       if (previousPackage != null) {
diff --git a/test/global/binstubs/outdated_binstub_runs_pub_global_test.dart b/test/global/binstubs/outdated_binstub_runs_pub_global_test.dart
index aa2a618..16e4f0e 100644
--- a/test/global/binstubs/outdated_binstub_runs_pub_global_test.dart
+++ b/test/global/binstubs/outdated_binstub_runs_pub_global_test.dart
@@ -2,6 +2,8 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'dart:io';
+
 import 'package:path/path.dart' as p;
 import 'package:test_process/test_process.dart';
 import 'package:test/test.dart';
@@ -10,22 +12,78 @@
 import '../../test_pub.dart';
 import 'utils.dart';
 
+/// The contents of the binstub for [executable], or `null` if it doesn't exist.
+String binStub(String executable) {
+  final f = File(p.join(d.sandbox, cachePath, 'bin', binStubName(executable)));
+  if (f.existsSync()) {
+    return f.readAsStringSync();
+  }
+  return null;
+}
+
 void main() {
-  test("an outdated binstub runs 'pub global run'", () async {
+  test("an outdated binstub runs 'pub global run', which replaces old binstub",
+      () async {
     await servePackages((builder) {
       builder.serve('foo', '1.0.0', pubspec: {
-        'executables': {'foo-script': 'script'}
+        'executables': {
+          'foo-script': 'script',
+          'foo-script2': 'script',
+          'foo-script-not-installed': 'script',
+          'foo-another-script': 'another-script',
+          'foo-another-script-not-installed': 'another-script'
+        }
       }, contents: [
-        d.dir('bin', [d.file('script.dart', "main(args) => print('ok');")])
+        d.dir('bin', [
+          d.file('script.dart', r"main(args) => print('ok $args');"),
+          d.file('another-script.dart',
+              r"main(args) => print('not so good $args');")
+        ])
       ]);
     });
 
-    await runPub(args: ['global', 'activate', 'foo']);
+    await runPub(args: [
+      'global',
+      'activate',
+      'foo',
+      '--executable',
+      'foo-script',
+      '--executable',
+      'foo-script2',
+      '--executable',
+      'foo-another-script',
+    ], environment: {
+      '_PUB_TEST_SDK_VERSION': '0.0.1'
+    });
 
+    expect(binStub('foo-script'), contains('script.dart-0.0.1.snapshot'));
+
+    expect(binStub('foo-script2'), contains('script.dart-0.0.1.snapshot'));
+
+    expect(
+      binStub('foo-script-not-installed'),
+      null,
+    );
+
+    expect(
+      binStub('foo-another-script'),
+      contains('another-script.dart-0.0.1.snapshot'),
+    );
+
+    expect(
+      binStub('foo-another-script-not-installed'),
+      null,
+    );
+
+    // Replace the created snapshot with one that really doesn't work with the
+    // current dart.
     await d.dir(cachePath, [
       d.dir('global_packages', [
         d.dir('foo', [
-          d.dir('bin', [d.outOfDateSnapshot('script.dart.snapshot')])
+          d.dir(
+            'bin',
+            [d.outOfDateSnapshot('script.dart-0.0.1.snapshot')],
+          )
         ])
       ])
     ]).create();
@@ -35,7 +93,43 @@
         ['arg1', 'arg2'],
         environment: getEnvironment());
 
-    expect(process.stdout, emitsThrough('ok'));
+    expect(
+        await process.stdout.rest.toList(),
+        allOf([
+          contains('Precompiled foo:script.'),
+          contains('ok [arg1, arg2]')
+        ]));
+
+    expect(
+      binStub('foo-script'),
+      contains('script.dart-0.1.2+3.snapshot'),
+    );
+
+    expect(
+      binStub('foo-script2'),
+      contains('script.dart-0.1.2+3.snapshot'),
+    );
+
+    expect(
+      binStub('foo-script-not-installed'),
+      null,
+      reason: 'global run recompile should not install new binstubs',
+    );
+
+    expect(
+      binStub('foo-another-script'),
+      contains('another-script.dart-0.0.1.snapshot'),
+      reason:
+          'global run recompile should not refresh binstubs for other scripts',
+    );
+
+    expect(
+      binStub('foo-another-script-not-installed'),
+      null,
+      reason:
+          'global run recompile should not install binstubs for other scripts',
+    );
+
     await process.shouldExit();
   });
 }
diff --git a/test/global/binstubs/utils.dart b/test/global/binstubs/utils.dart
index e7a185f..752762f 100644
--- a/test/global/binstubs/utils.dart
+++ b/test/global/binstubs/utils.dart
@@ -11,20 +11,15 @@
 /// The buildbots do not have the Dart SDK (containing "dart" and "pub") on
 /// their PATH, so we need to spawn the binstub process with a PATH that
 /// explicitly includes it.
+///
+/// The `pub`/`pub.bat` command on the PATH will be the one in tool/test-bin not
+/// the one from the sdk.
 Map getEnvironment() {
-  // TODO(rnystrom): This doesn't do the right thing when running pub's tests
-  // from pub's own repo instead of from within the Dart SDK repo. This always
-  // sets up the PATH to point to the directory where the Dart VM was run from,
-  // which will be unrelated to the path where pub itself is located when
-  // running from pub's repo.
-  //
-  // However, pub's repo doesn't actually have the shell scripts required to
-  // run "pub". Those live in the Dart SDK repo. One fix would be to make shell
-  // scripts in pub's repo that can act like those scripts but invoke pub from
-  // source from the pub repo.
-  var binDir = p.dirname(Platform.executable);
+  var binDir = p.dirname(Platform.resolvedExecutable);
   var separator = Platform.isWindows ? ';' : ':';
-  var path = "${Platform.environment["PATH"]}$separator$binDir";
+  var pubBin = p.absolute('tool', 'test-bin');
+  var path =
+      "$pubBin$separator${Platform.environment["PATH"]}$separator$binDir";
 
   var environment = getPubTestEnvironment();
   environment['PATH'] = path;
diff --git a/tool/test-bin/pub b/tool/test-bin/pub
new file mode 100755
index 0000000..c8b55a7
--- /dev/null
+++ b/tool/test-bin/pub
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+# Copyright (c) 2020, 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.
+
+# Runs bin/pub.dart with dart from PATH (or the snapshot if present).
+
+if [ -n "$_PUB_TEST_SNAPSHOT" ]; then
+    dart $_PUB_TEST_SNAPSHOT $*
+else
+    dart `dirname "$0"`/../../bin/pub.dart $*
+fi
diff --git a/tool/test-bin/pub.bat b/tool/test-bin/pub.bat
new file mode 100644
index 0000000..3254535
--- /dev/null
+++ b/tool/test-bin/pub.bat
@@ -0,0 +1,12 @@
+@echo off
+rem Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+rem for details. All rights reserved. Use of this source code is governed by a
+rem BSD-style license that can be found in the LICENSE file.
+
+rem Runs bin/pub.dart with dart from PATH (or the snapshot if present).
+
+if "%_PUB_TEST_SNAPSHOT%"=="" (
+    dart %~p0\..\..\..\bin\pub.dart %*
+) else (
+    dart %_PUB_TEST_SNAPSHOT% %*
+)