Consider sdk packages immutable (#4394)

diff --git a/lib/src/package_graph.dart b/lib/src/package_graph.dart
index c366bfa..b0a85f5 100644
--- a/lib/src/package_graph.dart
+++ b/lib/src/package_graph.dart
@@ -3,11 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:graphs/graphs.dart';
-import 'package:path/path.dart' as p;
 
 import 'entrypoint.dart';
 import 'package.dart';
 import 'solver.dart';
+import 'source/cached.dart';
+import 'source/sdk.dart';
 import 'utils.dart';
 
 /// A holistic view of the entire transitive dependency graph for an entrypoint.
@@ -78,12 +79,12 @@
     return _transitiveDependencies![package]!;
   }
 
-  bool _isPackageCached(String package) {
-    // The root package is not included in the lock file, so we instead ask
-    // the entrypoint.
-    // TODO(sigurdm): there should be a way to get the id of any package
-    // including the root.
-    return p.isWithin(entrypoint.cache.rootDir, packages[package]!.dir);
+  bool _isPackageFromImmutableSource(String package) {
+    final id = entrypoint.lockFile.packages[package];
+    if (id == null) {
+      return false; // This is a root package.
+    }
+    return id.source is CachedSource || id.source is SdkSource;
   }
 
   /// Returns whether [package] is mutable.
@@ -93,9 +94,9 @@
   /// without modifying the pub cache. Information generated from mutable
   /// packages is generally not safe to cache, since it may change frequently.
   bool isPackageMutable(String package) {
-    if (!_isPackageCached(package)) return true;
+    if (!_isPackageFromImmutableSource(package)) return true;
 
     return transitiveDependencies(package)
-        .any((dep) => !_isPackageCached(dep.name));
+        .any((dep) => !_isPackageFromImmutableSource(dep.name));
   }
 }
diff --git a/test/embedding/embedding_test.dart b/test/embedding/embedding_test.dart
index 388768c..3ec6a00 100644
--- a/test/embedding/embedding_test.dart
+++ b/test/embedding/embedding_test.dart
@@ -367,6 +367,83 @@
     );
   });
 
+  test(
+      '`embedding run` does not recompile executables '
+      'from packages depending on sdk packages', () async {
+    final server = await servePackages();
+    server.serve(
+      'hosted',
+      '1.0.0',
+      deps: {
+        'foo': {'sdk': 'flutter'},
+      },
+      contents: [
+        d.dir('bin', [d.file('hosted.dart', 'main() {print(42);}')]),
+      ],
+    );
+    await d.dir('flutter', [
+      d.dir('bin', [
+        d.dir('cache', [
+          d.file(
+            'flutter.version.json',
+            '{"flutterVersion": "1.2.3"}',
+          ),
+        ]),
+      ]),
+      d.dir('packages', [
+        d.dir('foo', [
+          d.libPubspec('foo', '1.2.3'),
+        ]),
+      ]),
+    ]).create();
+
+    await d.dir(appPath, [
+      d.pubspec({
+        'name': 'myapp',
+        'dependencies': {
+          'hosted': '^1.0.0',
+        },
+      }),
+    ]).create();
+
+    final buffer = StringBuffer();
+    await runEmbeddingToBuffer(
+      ['run', 'hosted'],
+      buffer,
+      workingDirectory: d.path(appPath),
+      environment: {
+        'FLUTTER_ROOT': p.join(d.sandbox, 'flutter'),
+        EnvironmentKeys.forceTerminalOutput: '1',
+      },
+    );
+
+    expect(
+      buffer.toString(),
+      allOf(
+        contains('Built hosted:hosted'),
+        contains('42'),
+      ),
+    );
+
+    final buffer2 = StringBuffer();
+    await runEmbeddingToBuffer(
+      ['run', 'hosted'],
+      buffer2,
+      workingDirectory: d.path(appPath),
+      environment: {
+        'FLUTTER_ROOT': p.join(d.sandbox, 'flutter'),
+        EnvironmentKeys.forceTerminalOutput: '1',
+      },
+    );
+    expect(
+      buffer2.toString(),
+      allOf(
+        isNot(contains('Built hosted:hosted')),
+        contains('42'),
+      ),
+    );
+  });
+
   test('"pkg" and "packages" will trigger a suggestion of "pub"', () async {
     await servePackages();
     await d.appDir().create();