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 ---------------------------------