Write .dart_tool/package_graph.json when writing package_config.json (#4524)

diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 9eab39f..61d564a 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -302,6 +302,10 @@
     p.normalize(p.join(workspaceRoot.dir, '.dart_tool', 'package_config.json')),
   );
 
+  late final String packageGraphPath = p.relative(
+    p.normalize(p.join(workspaceRoot.dir, '.dart_tool', 'package_graph.json')),
+  );
+
   /// The path to the entrypoint workspace's lockfile.
   String get lockFilePath =>
       p.normalize(p.join(workspaceRoot.dir, 'pubspec.lock'));
@@ -396,10 +400,12 @@
   /// Writes the .dart_tool/package_config.json file and workspace references to
   /// it.
   ///
+  /// Also writes the .dart_tool.package_graph.json file.
+  ///
   /// If the workspace is non-trivial: For each package in the workspace write:
   /// `.dart_tool/pub/workspace_ref.json` with a pointer to the workspace root
   /// package dir.
-  Future<void> writePackageConfigFile() async {
+  Future<void> writePackageConfigFiles() async {
     ensureDir(p.dirname(packageConfigPath));
     writeTextFile(
       packageConfigPath,
@@ -409,6 +415,7 @@
             .pubspec.sdkConstraints[sdk.identifier]?.effectiveConstraint,
       ),
     );
+    writeTextFile(packageGraphPath, await _packageGraphFile(cache));
     if (workspaceRoot.workspaceChildren.isNotEmpty) {
       for (final package in workspaceRoot.transitiveWorkspace) {
         final workspaceRefDir = p.join(package.dir, '.dart_tool', 'pub');
@@ -426,6 +433,30 @@
     }
   }
 
+  Future<String> _packageGraphFile(SystemCache cache) async {
+    return const JsonEncoder.withIndent('  ').convert({
+      'roots': workspaceRoot.transitiveWorkspace.map((p) => p.name).toList()
+        ..sort(),
+      'packages': [
+        for (final p in workspaceRoot.transitiveWorkspace)
+          {
+            'name': p.name,
+            'version': p.version.toString(),
+            'dependencies': p.dependencies.keys.toList()..sort(),
+            'devDependencies': p.devDependencies.keys.toList()..sort(),
+          },
+        for (final p in lockFile.packages.values)
+          {
+            'name': p.name,
+            'version': p.version.toString(),
+            'dependencies': (await cache.describe(p)).dependencies.keys.toList()
+              ..sort(),
+          },
+      ],
+      'configVersion': 1,
+    });
+  }
+
   /// Returns the contents of the `.dart_tool/package_config` file generated
   /// from this entrypoint based on [lockFile].
   ///
@@ -605,7 +636,7 @@
       /// have to reload and reparse all the pubspecs.
       _packageGraph = Future.value(PackageGraph.fromSolveResult(this, result));
 
-      await writePackageConfigFile();
+      await writePackageConfigFiles();
 
       try {
         if (precompile) {
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index fd722ef..43a5263 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -298,7 +298,7 @@
         solveResult: result,
       );
 
-      await entrypoint.writePackageConfigFile();
+      await entrypoint.writePackageConfigFiles();
 
       await entrypoint.precompileExecutables();
 
diff --git a/test/package_graph_file_test.dart b/test/package_graph_file_test.dart
new file mode 100644
index 0000000..823ba00
--- /dev/null
+++ b/test/package_graph_file_test.dart
@@ -0,0 +1,127 @@
+// Copyright (c) 2025, 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 'dart:convert';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+import 'descriptor.dart' as d;
+import 'test_pub.dart';
+
+void main() {
+  test('package_config.json file is created', () async {
+    await servePackages()
+      ..serve(
+        'foo',
+        '1.2.3',
+        deps: {'baz': '2.2.2'},
+        sdk: '^3.5.0',
+        pubspec: {
+          // dev_dependencies of non-workspace packages should not be listed
+          // in the package_graph.
+          'dev_dependencies': {'test': '^1.0.0'},
+        },
+      )
+      ..serve(
+        'bar',
+        '3.2.1',
+        sdk: '^3.5.0',
+      )
+      ..serve(
+        'baz',
+        '2.2.2',
+        sdk: '^3.5.0',
+        deps: {'bar': '3.2.1'},
+        contents: [d.dir('lib', [])],
+      )
+      ..serve(
+        'test',
+        '1.0.0',
+        sdk: '^3.5.0',
+      )
+      ..serve(
+        'test',
+        '2.0.0',
+        sdk: '^3.5.0',
+      );
+
+    await d.dir('boo', [
+      d.libPubspec(
+        'boo',
+        '2.0.0',
+        sdk: '^3.5.0',
+        deps: {'bar': 'any'},
+        devDeps: {'test': '^1.0.0'},
+      ),
+    ]).create();
+
+    await d.dir(appPath, [
+      d.appPubspec(
+        dependencies: {
+          'foo': '1.2.3',
+          'boo': {'path': '../boo'},
+        },
+        extras: {
+          'environment': {
+            'sdk': '^3.5.0',
+          },
+          'dev_dependencies': {'test': '^2.0.0'},
+          'workspace': ['helper/'],
+        },
+      ),
+      d.dir('helper', [
+        d.libPubspec(
+          'helper',
+          '2.0.0',
+          resolutionWorkspace: true,
+        ),
+      ]),
+    ]).create();
+
+    await pubGet(
+      environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
+    );
+
+    final packageGraph = jsonDecode(
+      File(p.join(d.sandbox, packageGraphFilePath)).readAsStringSync(),
+    );
+    expect(packageGraph, {
+      'roots': ['helper', 'myapp'],
+      'packages': [
+        {
+          'name': 'myapp',
+          'version': '0.0.0',
+          'dependencies': ['boo', 'foo'],
+          'devDependencies': ['test'],
+        },
+        {
+          'name': 'helper',
+          'version': '2.0.0',
+          'dependencies': <Object?>[],
+          'devDependencies': <Object?>[],
+        },
+        {'name': 'test', 'version': '2.0.0', 'dependencies': <Object?>[]},
+        {
+          'name': 'boo',
+          'version': '2.0.0',
+          'dependencies': ['bar'],
+        },
+        {
+          'name': 'foo',
+          'version': '1.2.3',
+          'dependencies': ['baz'],
+        },
+        {'name': 'bar', 'version': '3.2.1', 'dependencies': <Object?>[]},
+        {
+          'name': 'baz',
+          'version': '2.2.2',
+          'dependencies': ['bar'],
+        }
+      ],
+      'configVersion': 1,
+    });
+  });
+}
diff --git a/test/test_pub.dart b/test/test_pub.dart
index 160c155..ac4bf6c 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -75,6 +75,11 @@
 String packageConfigFilePath =
     p.join(appPath, '.dart_tool', 'package_config.json');
 
+/// The path of the ".dart_tool/package_graph.json" file in the mock app used
+/// for tests, relative to the sandbox directory.
+String packageGraphFilePath =
+    p.join(appPath, '.dart_tool', 'package_graph.json');
+
 /// The entry from the `.dart_tool/package_config.json` file for [packageName].
 Map<String, dynamic> packageSpec(String packageName) => dig(
       json.decode(File(d.path(packageConfigFilePath)).readAsStringSync()),
diff --git a/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
index 25d1baa..58cf843 100644
--- a/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
+++ b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
@@ -127,6 +127,29 @@
 [E]    |   "generatorVersion": "3.1.2+3",
 [E]    |   "pubCache": "file://$SANDBOX/cache"
 [E]    | }
+[E] IO  : Writing $N characters to text file .dart_tool/package_graph.json.
+[E] FINE: Contents:
+[E]    | {
+[E]    |   "roots": [
+[E]    |   "myapp"
+[E]    |   ],
+[E]    |   "packages": [
+[E]    |   {
+[E]    |   "name": "myapp",
+[E]    |   "version": "0.0.0",
+[E]    |   "dependencies": [
+[E]    |   "foo"
+[E]    |   ],
+[E]    |   "devDependencies": []
+[E]    |   },
+[E]    |   {
+[E]    |   "name": "foo",
+[E]    |   "version": "1.0.0",
+[E]    |   "dependencies": []
+[E]    |   }
+[E]    |   ],
+[E]    |   "configVersion": 1
+[E]    | }
 [E] IO  : Writing $N characters to text file $SANDBOX/cache/log/pub_log.txt.
 
 -------------------------------- END OF OUTPUT ---------------------------------
@@ -291,6 +314,29 @@
    |   "generatorVersion": "3.1.2+3",
    |   "pubCache": "file://$SANDBOX/cache"
    | }
+IO  : Writing $N characters to text file .dart_tool/package_graph.json.
+FINE: Contents:
+   | {
+   |   "roots": [
+   |   "myapp"
+   |   ],
+   |   "packages": [
+   |   {
+   |   "name": "myapp",
+   |   "version": "0.0.0",
+   |   "dependencies": [
+   |   "foo"
+   |   ],
+   |   "devDependencies": []
+   |   },
+   |   {
+   |   "name": "foo",
+   |   "version": "1.0.0",
+   |   "dependencies": []
+   |   }
+   |   ],
+   |   "configVersion": 1
+   | }
 ---- End log transcript ----
 -------------------------------- END OF OUTPUT ---------------------------------