Skip to content

Commit 923a5cb

Browse files
committed
Initial support for experiments
1 parent f6457fd commit 923a5cb

23 files changed

+520
-20
lines changed

lib/src/command/global_activate.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ class GlobalActivateCommand extends PubCommand {
5252
hide: true,
5353
);
5454

55+
argParser.addMultiOption('experiments', help: 'Experiments(s) to enable.');
56+
5557
argParser.addFlag(
5658
'no-executables',
5759
negatable: false,
@@ -131,6 +133,7 @@ class GlobalActivateCommand extends PubCommand {
131133
overwriteBinStubs: overwrite,
132134
path: argResults.option('git-path'),
133135
ref: argResults.option('git-ref'),
136+
allowedExperiments: argResults.multiOption('experiments'),
134137
);
135138

136139
case 'hosted':
@@ -171,6 +174,7 @@ class GlobalActivateCommand extends PubCommand {
171174
ref.withConstraint(constraint),
172175
executables,
173176
overwriteBinStubs: overwrite,
177+
allowedExperiments: argResults.multiOption('experiments'),
174178
);
175179

176180
case 'path':

lib/src/entrypoint.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,9 @@ See $workspacesDocUrl for more information.''',
404404
/// package dir.
405405
///
406406
/// Also marks the package active in `PUB_CACHE/active_roots/`.
407-
Future<void> writePackageConfigFiles() async {
407+
Future<void> writePackageConfigFiles({
408+
required List<String> experiments,
409+
}) async {
408410
ensureDir(p.dirname(packageConfigPath));
409411

410412
writeTextFileIfDifferent(
@@ -416,6 +418,7 @@ See $workspacesDocUrl for more information.''',
416418
.pubspec
417419
.sdkConstraints[sdk.identifier]
418420
?.effectiveConstraint,
421+
experiments: experiments,
419422
),
420423
);
421424
writeTextFileIfDifferent(packageGraphPath, await _packageGraphFile(cache));
@@ -471,6 +474,7 @@ See $workspacesDocUrl for more information.''',
471474
Future<String> _packageConfigFile(
472475
SystemCache cache, {
473476
VersionConstraint? entrypointSdkConstraint,
477+
required List<String> experiments,
474478
}) async {
475479
final entries = <PackageConfigEntry>[];
476480
if (lockFile.packages.isNotEmpty) {
@@ -515,6 +519,7 @@ See $workspacesDocUrl for more information.''',
515519
packages: entries,
516520
generator: 'pub',
517521
generatorVersion: sdk.version,
522+
experiments: experiments,
518523
additionalProperties: {
519524
if (FlutterSdk().isAvailable) ...{
520525
'flutterRoot':
@@ -616,6 +621,7 @@ Try running `$topLevelProgram pub get` to create `$lockFilePath`.''');
616621
lockFile,
617622
newLockFile,
618623
result.availableVersions,
624+
result.experiments,
619625
cache,
620626
dryRun: dryRun,
621627
enforceLockfile: enforceLockfile,
@@ -644,7 +650,7 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without
644650
/// have to reload and reparse all the pubspecs.
645651
_packageGraph = Future.value(PackageGraph.fromSolveResult(this, result));
646652

647-
await writePackageConfigFiles();
653+
await writePackageConfigFiles(experiments: result.experiments);
648654

649655
try {
650656
if (precompile) {

lib/src/experiment.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
/// An experiment as described by an sdk_experiments file
6+
class Experiment {
7+
final String name;
8+
9+
/// A description of the experiment
10+
final String description;
11+
12+
/// Where you can read more about the experiment
13+
final String docUrl;
14+
15+
Experiment(this.name, this.description, this.docUrl);
16+
}

lib/src/global_packages.dart

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ class GlobalPackages {
9090
Future<void> activateGit(
9191
String repo,
9292
List<String>? executables, {
93+
required List<String> allowedExperiments,
9394
required bool overwriteBinStubs,
9495
String? path,
9596
String? ref,
@@ -127,10 +128,15 @@ class GlobalPackages {
127128
packageRef.withConstraint(VersionConstraint.any),
128129
executables,
129130
overwriteBinStubs: overwriteBinStubs,
131+
allowedExperiments: allowedExperiments,
130132
);
131133
}
132134

133-
Package packageForConstraint(PackageRange dep, String dir) {
135+
Package packageForConstraint(
136+
PackageRange dep,
137+
String dir,
138+
List<String> allowedExperiments,
139+
) {
134140
return Package(
135141
Pubspec(
136142
'pub global activate',
@@ -142,6 +148,7 @@ class GlobalPackages {
142148
defaultUpperBoundConstraint: null,
143149
),
144150
},
151+
experiments: allowedExperiments,
145152
),
146153
dir,
147154
[],
@@ -164,12 +171,14 @@ class GlobalPackages {
164171
Future<void> activateHosted(
165172
PackageRange range,
166173
List<String>? executables, {
174+
required List<String> allowedExperiments,
167175
required bool overwriteBinStubs,
168176
String? url,
169177
}) async {
170178
await _installInCache(
171179
range,
172180
executables,
181+
allowedExperiments: allowedExperiments,
173182
overwriteBinStubs: overwriteBinStubs,
174183
);
175184
}
@@ -231,6 +240,7 @@ class GlobalPackages {
231240
Future<void> _installInCache(
232241
PackageRange dep,
233242
List<String>? executables, {
243+
required List<String> allowedExperiments,
234244
required bool overwriteBinStubs,
235245
bool silent = false,
236246
}) async {
@@ -239,7 +249,7 @@ class GlobalPackages {
239249

240250
final tempDir = cache.createTempDir();
241251
// Create a dummy package with just [dep] so we can do resolution on it.
242-
final root = packageForConstraint(dep, tempDir);
252+
final root = packageForConstraint(dep, tempDir, allowedExperiments);
243253

244254
// Resolve it and download its dependencies.
245255
SolveResult result;
@@ -284,6 +294,7 @@ To recompile executables, first run `$topLevelProgram pub global deactivate $nam
284294
originalLockFile ?? LockFile.empty(),
285295
lockFile,
286296
result.availableVersions,
297+
result.experiments,
287298
cache,
288299
dryRun: false,
289300
quiet: false,
@@ -300,19 +311,19 @@ To recompile executables, first run `$topLevelProgram pub global deactivate $nam
300311
// Load the package graph from [result] so we don't need to re-parse all
301312
// the pubspecs.
302313
final entrypoint = Entrypoint.global(
303-
packageForConstraint(dep, packageDir),
314+
packageForConstraint(dep, packageDir, result.experiments),
304315
lockFile,
305316
cache,
306317
solveResult: result,
307318
);
308319

309-
await entrypoint.writePackageConfigFiles();
320+
await entrypoint.writePackageConfigFiles(experiments: result.experiments);
310321

311322
await entrypoint.precompileExecutables();
312323
}
313324

314325
final entrypoint = Entrypoint.global(
315-
packageForConstraint(dep, _packageDir(dep.name)),
326+
packageForConstraint(dep, _packageDir(dep.name), allowedExperiments),
316327
lockFile,
317328
cache,
318329
solveResult: result,
@@ -427,7 +438,11 @@ Consider `$topLevelProgram pub global deactivate $name`''');
427438
// For cached sources, the package itself is in the cache and the
428439
// lockfile is the one we just loaded.
429440
entrypoint = Entrypoint.global(
430-
packageForConstraint(id.toRange(), _packageDir(id.name)),
441+
packageForConstraint(
442+
id.toRange(),
443+
_packageDir(id.name),
444+
[], // XXX load experiments here
445+
),
431446
lockFile,
432447
cache,
433448
);
@@ -536,6 +551,7 @@ Try reactivating the package.
536551
entrypoint.lockFile,
537552
newLockFile,
538553
result.availableVersions,
554+
result.experiments,
539555
cache,
540556
dryRun: true,
541557
enforceLockfile: true,
@@ -678,6 +694,7 @@ Try reactivating the package.
678694
id.toRange(),
679695
packageExecutables,
680696
overwriteBinStubs: true,
697+
allowedExperiments: [], // XXX
681698
silent: true,
682699
);
683700
} else {

lib/src/package.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ class Package {
8787
...package.pubspec.dependencyOverrides,
8888
};
8989

90+
/// A collection of the union of all experiments used in the workspace.
91+
late final Set<String> allExperimentsInWorkspace = {
92+
for (final package in transitiveWorkspace) ...package.pubspec.experiments,
93+
};
94+
9095
/// The immediate dependencies this package specifies in its pubspec.
9196
Map<String, PackageRange> get dependencies => pubspec.dependencies;
9297

lib/src/package_config.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,14 @@ class PackageConfig {
3838
/// `.dart_tool/package_config.json` file.
3939
Map<String, dynamic> additionalProperties;
4040

41+
List<String> experiments;
42+
4143
PackageConfig({
4244
required this.configVersion,
4345
required this.packages,
4446
this.generator,
4547
this.generatorVersion,
48+
required this.experiments,
4649
Map<String, dynamic>? additionalProperties,
4750
}) : additionalProperties = additionalProperties ?? {} {
4851
final names = <String>{};
@@ -100,6 +103,21 @@ class PackageConfig {
100103
);
101104
}
102105

106+
// Read the 'experiments' property
107+
final experiments = root['experiments'] ?? <String>[];
108+
if (experiments is! List) {
109+
throw const FormatException(
110+
'"experiments" in package_config.json must be a list, if given',
111+
);
112+
}
113+
for (final experiment in experiments) {
114+
if (experiment is! String) {
115+
throw const FormatException(
116+
'"experiments" in package_config.json must all be strings',
117+
);
118+
}
119+
}
120+
103121
// Read the 'generatorVersion' property
104122
Version? generatorVersion;
105123
final generatorVersionRaw = root['generatorVersion'];
@@ -122,6 +140,7 @@ class PackageConfig {
122140
packages: packages,
123141
generator: generator,
124142
generatorVersion: generatorVersion,
143+
experiments: experiments.cast<String>(),
125144
additionalProperties: Map.fromEntries(
126145
root.entries.where(
127146
(e) =>
@@ -131,6 +150,7 @@ class PackageConfig {
131150
'generated',
132151
'generator',
133152
'generatorVersion',
153+
'experiments',
134154
}.contains(e.key),
135155
),
136156
),
@@ -141,6 +161,7 @@ class PackageConfig {
141161
Map<String, Object?> toJson() => {
142162
'configVersion': configVersion,
143163
'packages': packages.map((p) => p.toJson()).toList(),
164+
'experiments': experiments,
144165
'generator': generator,
145166
'generatorVersion': generatorVersion?.toString(),
146167
}..addAll(additionalProperties);

lib/src/pubspec.dart

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,51 @@ environment:
143143
_containingDescription,
144144
);
145145

146+
List<String>? _experiments;
147+
List<String> get experiments => _experiments ??= parseExperiments();
148+
149+
List<String> parseExperiments() {
150+
final experimentsNode = fields.nodes['experiments'];
151+
if (experimentsNode == null || experimentsNode.value == null) {
152+
return [];
153+
}
154+
if (experimentsNode is! YamlList) {
155+
_error('`experiments` must be a list of strings', experimentsNode.span);
156+
}
157+
final result = <String>[];
158+
for (final e in experimentsNode.nodes) {
159+
final value = e.value;
160+
if (value is! String) {
161+
_error('`experiments` must be a list of strings', e.span);
162+
}
163+
164+
/// For root packages, validate that all experiments are known by at least
165+
/// one of the current sdks.
166+
///
167+
/// Dependencies will only be chosen by the solver if their experiments
168+
/// are a subset of those of the root packages, so we don't filter here.
169+
if (_containingDescription is ResolvedRootDescription &&
170+
!availableExperiments.containsKey(value)) {
171+
final availableExperimentsDescription =
172+
availableExperiments.isEmpty
173+
? '''There are no available experiments.'''
174+
: '''
175+
Available experiments are:
176+
${availableExperiments.values.map((experiment) => '* ${experiment.name}: ${experiment.description}, ${experiment.docUrl}').join('\n')}''';
177+
_error('''
178+
$value is not a known experiment.
179+
180+
$availableExperimentsDescription
181+
182+
Read more about experiments at https://dart.dev/go/experiments.
183+
''', e.span);
184+
} else {
185+
result.add(value);
186+
}
187+
}
188+
return result;
189+
}
190+
146191
Map<String, PackageRange>? _dependencies;
147192

148193
/// The packages this package depends on when it is the root package.
@@ -341,6 +386,7 @@ environment:
341386
this.workspace = const <String>[],
342387
this.dependencyOverridesFromOverridesFile = false,
343388
this.resolution = Resolution.none,
389+
List<String> experiments = const <String>[],
344390
}) : _dependencies =
345391
dependencies == null
346392
? null
@@ -364,6 +410,7 @@ environment:
364410
// This is a dummy value. Dependencies should already be resolved, so we
365411
// never need to do relative resolutions.
366412
_containingDescription = ResolvedRootDescription.fromDir('.'),
413+
_experiments = experiments,
367414
super(
368415
fields == null ? YamlMap() : YamlMap.wrap(fields),
369416
name: name,

0 commit comments

Comments
 (0)