Don't re-precompile if current global installed was same version (#2810)

diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index 43cf996..bde09a2 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -85,8 +85,6 @@
   Future<void> activateGit(String repo, List<String> executables,
       {Map<String, FeatureDependency> features, bool overwriteBinStubs}) async {
     var name = await cache.git.getPackageNameFromRepo(repo);
-    // 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
@@ -119,7 +117,6 @@
       {Map<String, FeatureDependency> features,
       bool overwriteBinStubs,
       String url}) async {
-    _describeActive(name);
     await _installInCache(
         cache.hosted.source
             .refFor(name, url: url)
@@ -146,8 +143,14 @@
     await entrypoint.acquireDependencies(SolveType.GET);
     var name = entrypoint.root.name;
 
-    // Call this just to log what the current active package is, if any.
-    _describeActive(name);
+    try {
+      var originalLockFile =
+          LockFile.load(_getLockFilePath(name), cache.sources);
+      // Call this just to log what the current active package is, if any.
+      _describeActive(originalLockFile, name);
+    } on IOException {
+      // Couldn't read the lock file. It probably doesn't exist.
+    }
 
     // Write a lockfile that points to the local package.
     var fullPath = canonicalize(entrypoint.root.dir);
@@ -168,6 +171,16 @@
   /// Installs the package [dep] and its dependencies into the system cache.
   Future<void> _installInCache(PackageRange dep, List<String> executables,
       {bool overwriteBinStubs}) async {
+    LockFile originalLockFile;
+    try {
+      originalLockFile =
+          LockFile.load(_getLockFilePath(dep.name), cache.sources);
+      // Call this just to log what the current active package is, if any.
+      _describeActive(originalLockFile, dep.name);
+    } on IOException {
+      // Couldn't read the lock file. It probably doesn't exist.
+    }
+
     // Create a dummy package with just [dep] so we can do resolution on it.
     var root = Package.inMemory(Pubspec('pub global activate',
         dependencies: [dep], sources: cache.sources));
@@ -190,7 +203,17 @@
       rethrow;
     }
 
-    result.showReport(SolveType.GET);
+    final sameVersions = originalLockFile != null &&
+        originalLockFile.samePackageIds(result.lockFile);
+
+    if (sameVersions) {
+      log.message('''
+The package ${dep.name} is already activated at newest available version.
+To recompile executables, first run `global decativate ${dep.name}`.
+''');
+    } else {
+      result.showReport(SolveType.GET);
+    }
 
     // Make sure all of the dependencies are locally installed.
     await Future.wait(result.packages.map((id) {
@@ -215,10 +238,13 @@
     final entrypoint = Entrypoint.global(
         Package(result.pubspecs[dep.name],
             cache.source(dep.source).getDirectory(id)),
-        result.lockFile,
+        lockFile,
         cache,
         solveResult: result);
-    await entrypoint.precompileExecutables();
+    if (!sameVersions) {
+      // Only precompile binaries if we have a new resolution.
+      await entrypoint.precompileExecutables();
+    }
 
     _updateBinStubs(
       entrypoint,
@@ -255,27 +281,21 @@
   }
 
   /// Shows the user the currently active package with [name], if any.
-  void _describeActive(String name) {
-    try {
-      var lockFile = LockFile.load(_getLockFilePath(name), cache.sources);
-      var id = lockFile.packages[name];
+  void _describeActive(LockFile lockFile, String name) {
+    var id = lockFile.packages[name];
 
-      var source = id.source;
-      if (source is GitSource) {
-        var url = source.urlFromDescription(id.description);
-        log.message('Package ${log.bold(name)} is currently active from Git '
-            'repository "$url".');
-      } else if (source is PathSource) {
-        var path = source.pathFromDescription(id.description);
-        log.message('Package ${log.bold(name)} is currently active at path '
-            '"$path".');
-      } else {
-        log.message('Package ${log.bold(name)} is currently active at version '
-            '${log.bold(id.version)}.');
-      }
-    } on IOException {
-      // If we couldn't read the lock file, it's not activated.
-      return;
+    var source = id.source;
+    if (source is GitSource) {
+      var url = source.urlFromDescription(id.description);
+      log.message('Package ${log.bold(name)} is currently active from Git '
+          'repository "$url".');
+    } else if (source is PathSource) {
+      var path = source.pathFromDescription(id.description);
+      log.message('Package ${log.bold(name)} is currently active at path '
+          '"$path".');
+    } else {
+      log.message('Package ${log.bold(name)} is currently active at version '
+          '${log.bold(id.version)}.');
     }
   }
 
diff --git a/lib/src/lock_file.dart b/lib/src/lock_file.dart
index 795dfc4..8616778 100644
--- a/lib/src/lock_file.dart
+++ b/lib/src/lock_file.dart
@@ -338,4 +338,17 @@
     }
     return 'transitive';
   }
+
+  /// `true` if [other] has the same packages as `this` in the same versions
+  /// from the same sources.
+  bool samePackageIds(LockFile other) {
+    if (packages.length != other.packages.length) {
+      return false;
+    }
+    for (final id in packages.values) {
+      final otherId = other.packages[id.name];
+      if (id != otherId) return false;
+    }
+    return true;
+  }
 }
diff --git a/test/global/activate/activate_hosted_twice_test.dart b/test/global/activate/activate_hosted_twice_test.dart
new file mode 100644
index 0000000..a90e52c
--- /dev/null
+++ b/test/global/activate/activate_hosted_twice_test.dart
@@ -0,0 +1,68 @@
+// 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.
+
+import 'package:test/test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+void main() {
+  test('activating a hosted package twice will not precompile', () async {
+    await servePackages((builder) => builder
+      ..serve('foo', '1.0.0', deps: {
+        'bar': 'any'
+      }, contents: [
+        d.dir('bin', [
+          d.file('foo.dart', r'''
+import 'package:bar/bar.dart';
+main(args) => print('bar $version');''')
+        ])
+      ])
+      ..serve('bar', '1.0.0', contents: [
+        d.dir('lib', [d.file('bar.dart', 'final version = "1.0.0";')])
+      ]));
+
+    await runPub(args: ['global', 'activate', 'foo'], output: '''
+Resolving dependencies...
++ bar 1.0.0
++ foo 1.0.0
+Downloading foo 1.0.0...
+Downloading bar 1.0.0...
+Precompiling executables...
+Precompiled foo:foo.
+Activated foo 1.0.0.''');
+
+    await runPub(args: ['global', 'activate', 'foo'], output: '''
+Package foo is currently active at version 1.0.0.
+Resolving dependencies...
+The package foo is already activated at newest available version.
+To recompile executables, first run `global decativate foo`.
+Activated foo 1.0.0.''');
+
+    var pub = await pubRun(global: true, args: ['foo']);
+    expect(pub.stdout, emits('bar 1.0.0'));
+    await pub.shouldExit();
+
+    await runPub(args: ['global', 'activate', 'foo']);
+
+    globalPackageServer
+        .add((builder) => builder.serve('bar', '2.0.0', contents: [
+              d.dir('lib', [d.file('bar.dart', 'final version = "2.0.0";')])
+            ]));
+
+    await runPub(args: ['global', 'activate', 'foo'], output: '''
+Package foo is currently active at version 1.0.0.
+Resolving dependencies...
++ bar 2.0.0
++ foo 1.0.0
+Downloading bar 2.0.0...
+Precompiling executables...
+Precompiled foo:foo.
+Activated foo 1.0.0.''');
+
+    var pub2 = await pubRun(global: true, args: ['foo']);
+    expect(pub2.stdout, emits('bar 2.0.0'));
+    await pub2.shouldExit();
+  });
+}