Have executableForCommand rerun pub get if sdk changed minor version (#3652)

diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 76bcdcd..4626559 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -625,8 +625,10 @@
       //  * Force `pub get` if a path dependency has changed language version.
       _checkPackageConfigUpToDate();
       touch(packageConfigPath);
-    } else if (touchedLockFile) {
-      touch(packageConfigPath);
+    } else {
+      if (touchedLockFile) {
+        touch(packageConfigPath);
+      }
     }
 
     for (var match in _sdkConstraint.allMatches(lockFileText)) {
@@ -645,6 +647,12 @@
             "dependencies' SDK constraints. Please run \"$topLevelProgram pub get\" again.");
       }
     }
+    // We want to do ensure a pub get gets run when updating a minor version of
+    // the Dart SDK.
+    //
+    // Putting this check last because it leads to less specific messages than
+    // the 'incompatible sdk' check above.
+    _checkPackageConfigSameDartSdk();
   }
 
   /// Determines whether or not the lockfile is out of date with respect to the
@@ -840,6 +848,26 @@
     }
   }
 
+  /// Checks whether or not the `.dart_tool/package_config.json` file is was
+  /// generated by a different sdk down changes in minor versions.
+  ///
+  /// For pre-releases we always consider the package_config.json out of date
+  /// when the version changes.
+  ///
+  /// Throws [DataException], if `.dart_tool/package_config.json` the version
+  /// changed sufficiently.
+  void _checkPackageConfigSameDartSdk() {
+    final generatorVersion = packageConfig.generatorVersion;
+    if (generatorVersion == null ||
+        generatorVersion.major != sdk.version.major ||
+        generatorVersion.minor != sdk.version.minor ||
+        generatorVersion.isPreRelease ||
+        sdk.version.isPreRelease) {
+      dataError('The sdk was updated since last package resolution. Please run '
+          '"$topLevelProgram pub get" again.');
+    }
+  }
+
   /// If the entrypoint uses the old-style `.pub` cache directory, migrates it
   /// to the new-style `.dart_tool/pub` directory.
   void migrateCache() {
diff --git a/lib/src/executable.dart b/lib/src/executable.dart
index b3d8be6..e895a3c 100644
--- a/lib/src/executable.dart
+++ b/lib/src/executable.dart
@@ -307,7 +307,8 @@
   try {
     // TODO(sigurdm): it would be nicer with a 'isUpToDate' function.
     entrypoint.assertUpToDate();
-  } on DataException {
+  } on DataException catch (e) {
+    log.fine('Resolution not up to date: ${e.message}. Redoing.');
     try {
       await warningsOnlyUnlessTerminal(
         () => entrypoint.acquireDependencies(
diff --git a/test/embedding/embedding_test.dart b/test/embedding/embedding_test.dart
index 9f74405..4419c15 100644
--- a/test/embedding/embedding_test.dart
+++ b/test/embedding/embedding_test.dart
@@ -258,6 +258,67 @@
       environment: getPubTestEnvironment(),
     );
   });
+
+  test('`embedding run` does `pub get` if sdk updated', () async {
+    await d.dir(appPath, [
+      d.pubspec({
+        'name': 'myapp',
+        'environment': {'sdk': '^2.18.0'},
+        'dependencies': {'foo': '^1.0.0'}
+      }),
+      d.dir('bin', [
+        d.file('myapp.dart', 'main() {print(42);}'),
+      ])
+    ]).create();
+
+    final server = await servePackages();
+    server.serve('foo', '1.0.0', pubspec: {
+      'environment': {'sdk': '^2.18.0'}
+    });
+
+    await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '2.18.3'});
+    // Deleting the version-listing cache will cause it to be refetched, and the
+    // warning will happen.
+    File(p.join(globalServer.cachingPath, '.cache', 'foo-versions.json'))
+        .deleteSync();
+    server.serve('foo', '1.0.1', pubspec: {
+      'environment': {'sdk': '^2.18.0'}
+    });
+
+    final buffer = StringBuffer();
+
+    // Just changing the patch version should not trigger a pub get.
+    await runEmbeddingToBuffer(
+      ['--verbose', 'run', 'myapp'],
+      buffer,
+      workingDirectory: d.path(appPath),
+      environment: {'_PUB_TEST_SDK_VERSION': '2.18.4'},
+    );
+
+    expect(
+      buffer.toString(),
+      allOf(contains('42'), isNot(contains('Resolving dependencies'))),
+    );
+
+    File(p.join(globalServer.cachingPath, '.cache', 'foo-versions.json'));
+    buffer.clear();
+
+    // Changing the minor version should.
+    await runEmbeddingToBuffer(
+      ['--verbose', 'run', 'myapp'],
+      buffer,
+      workingDirectory: d.path(appPath),
+      environment: {'_PUB_TEST_SDK_VERSION': '2.19.3'},
+    );
+    expect(
+      buffer.toString(),
+      allOf(
+        contains('42'),
+        contains('Resolving dependencies'),
+        contains('1.0.1 available'),
+      ),
+    );
+  });
 }
 
 String _filter(String input) {
diff --git a/tool/test-bin/pub_command_runner.dart b/tool/test-bin/pub_command_runner.dart
index 3c1bef9..756cd91 100644
--- a/tool/test-bin/pub_command_runner.dart
+++ b/tool/test-bin/pub_command_runner.dart
@@ -33,6 +33,31 @@
   }
 }
 
+class RunCommand extends Command<int> {
+  @override
+  String get name => 'run';
+
+  @override
+  String get description => 'runs a dart app';
+
+  @override
+  Future<int> run() async {
+    final executable = await getExecutableForCommand(argResults!.rest.first);
+    final packageConfig = executable.packageConfig;
+    final process = await Process.start(
+      Platform.executable,
+      [
+        if (packageConfig != null) '--packages=$packageConfig',
+        executable.executable,
+        ...argResults!.rest.skip(1)
+      ],
+      mode: ProcessStartMode.inheritStdio,
+    );
+
+    return await process.exitCode;
+  }
+}
+
 class Runner extends CommandRunner<int> {
   late ArgResults _options;
 
@@ -44,6 +69,7 @@
     addCommand(
         pubCommand(analytics: analytics, isVerbose: () => _options['verbose'])
           ..addSubcommand(ThrowingCommand()));
+    addCommand(RunCommand());
     argParser.addFlag('verbose');
   }
 
@@ -51,7 +77,9 @@
   Future<int> run(Iterable<String> args) async {
     try {
       _options = super.parse(args);
-
+      if (_options['verbose']) {
+        log.verbosity = log.Verbosity.all;
+      }
       return await runCommand(_options);
     } on UsageException catch (error) {
       log.exception(error);