Skip to content

Initial support for experiments #4614

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/src/command/global_activate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class GlobalActivateCommand extends PubCommand {
hide: true,
);

argParser.addMultiOption('experiments', help: 'Experiments(s) to enable.');

argParser.addFlag(
'no-executables',
negatable: false,
Expand Down Expand Up @@ -131,6 +133,7 @@ class GlobalActivateCommand extends PubCommand {
overwriteBinStubs: overwrite,
path: argResults.option('git-path'),
ref: argResults.option('git-ref'),
allowedExperiments: argResults.multiOption('experiments'),
);

case 'hosted':
Expand Down Expand Up @@ -171,6 +174,7 @@ class GlobalActivateCommand extends PubCommand {
ref.withConstraint(constraint),
executables,
overwriteBinStubs: overwrite,
allowedExperiments: argResults.multiOption('experiments'),
);

case 'path':
Expand Down
10 changes: 8 additions & 2 deletions lib/src/entrypoint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,9 @@ See $workspacesDocUrl for more information.''',
/// package dir.
///
/// Also marks the package active in `PUB_CACHE/active_roots/`.
Future<void> writePackageConfigFiles() async {
Future<void> writePackageConfigFiles({
required List<String> experiments,
}) async {
ensureDir(p.dirname(packageConfigPath));

writeTextFileIfDifferent(
Expand All @@ -416,6 +418,7 @@ See $workspacesDocUrl for more information.''',
.pubspec
.sdkConstraints[sdk.identifier]
?.effectiveConstraint,
experiments: experiments,
),
);
writeTextFileIfDifferent(packageGraphPath, await _packageGraphFile(cache));
Expand Down Expand Up @@ -471,6 +474,7 @@ See $workspacesDocUrl for more information.''',
Future<String> _packageConfigFile(
SystemCache cache, {
VersionConstraint? entrypointSdkConstraint,
required List<String> experiments,
}) async {
final entries = <PackageConfigEntry>[];
if (lockFile.packages.isNotEmpty) {
Expand Down Expand Up @@ -515,6 +519,7 @@ See $workspacesDocUrl for more information.''',
packages: entries,
generator: 'pub',
generatorVersion: sdk.version,
experiments: experiments,
additionalProperties: {
if (FlutterSdk().isAvailable) ...{
'flutterRoot':
Expand Down Expand Up @@ -616,6 +621,7 @@ Try running `$topLevelProgram pub get` to create `$lockFilePath`.''');
lockFile,
newLockFile,
result.availableVersions,
result.experiments,
cache,
dryRun: dryRun,
enforceLockfile: enforceLockfile,
Expand Down Expand Up @@ -644,7 +650,7 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without
/// have to reload and reparse all the pubspecs.
_packageGraph = Future.value(PackageGraph.fromSolveResult(this, result));

await writePackageConfigFiles();
await writePackageConfigFiles(experiments: result.experiments);

try {
if (precompile) {
Expand Down
16 changes: 16 additions & 0 deletions lib/src/experiment.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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.

/// An experiment as described by an sdk_experiments file
class Experiment {
final String name;

/// A description of the experiment
final String description;

/// Where you can read more about the experiment
final String docUrl;

Experiment(this.name, this.description, this.docUrl);
}
29 changes: 23 additions & 6 deletions lib/src/global_packages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class GlobalPackages {
Future<void> activateGit(
String repo,
List<String>? executables, {
required List<String> allowedExperiments,
required bool overwriteBinStubs,
String? path,
String? ref,
Expand Down Expand Up @@ -127,10 +128,15 @@ class GlobalPackages {
packageRef.withConstraint(VersionConstraint.any),
executables,
overwriteBinStubs: overwriteBinStubs,
allowedExperiments: allowedExperiments,
);
}

Package packageForConstraint(PackageRange dep, String dir) {
Package packageForConstraint(
PackageRange dep,
String dir,
List<String> allowedExperiments,
) {
return Package(
Pubspec(
'pub global activate',
Expand All @@ -142,6 +148,7 @@ class GlobalPackages {
defaultUpperBoundConstraint: null,
),
},
experiments: allowedExperiments,
),
dir,
[],
Expand All @@ -164,12 +171,14 @@ class GlobalPackages {
Future<void> activateHosted(
PackageRange range,
List<String>? executables, {
required List<String> allowedExperiments,
required bool overwriteBinStubs,
String? url,
}) async {
await _installInCache(
range,
executables,
allowedExperiments: allowedExperiments,
overwriteBinStubs: overwriteBinStubs,
);
}
Expand Down Expand Up @@ -231,6 +240,7 @@ class GlobalPackages {
Future<void> _installInCache(
PackageRange dep,
List<String>? executables, {
required List<String> allowedExperiments,
required bool overwriteBinStubs,
bool silent = false,
}) async {
Expand All @@ -239,7 +249,7 @@ class GlobalPackages {

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

// Resolve it and download its dependencies.
SolveResult result;
Expand Down Expand Up @@ -284,6 +294,7 @@ To recompile executables, first run `$topLevelProgram pub global deactivate $nam
originalLockFile ?? LockFile.empty(),
lockFile,
result.availableVersions,
result.experiments,
cache,
dryRun: false,
quiet: false,
Expand All @@ -300,19 +311,19 @@ To recompile executables, first run `$topLevelProgram pub global deactivate $nam
// Load the package graph from [result] so we don't need to re-parse all
// the pubspecs.
final entrypoint = Entrypoint.global(
packageForConstraint(dep, packageDir),
packageForConstraint(dep, packageDir, result.experiments),
lockFile,
cache,
solveResult: result,
);

await entrypoint.writePackageConfigFiles();
await entrypoint.writePackageConfigFiles(experiments: result.experiments);

await entrypoint.precompileExecutables();
}

final entrypoint = Entrypoint.global(
packageForConstraint(dep, _packageDir(dep.name)),
packageForConstraint(dep, _packageDir(dep.name), allowedExperiments),
lockFile,
cache,
solveResult: result,
Expand Down Expand Up @@ -427,7 +438,11 @@ Consider `$topLevelProgram pub global deactivate $name`''');
// For cached sources, the package itself is in the cache and the
// lockfile is the one we just loaded.
entrypoint = Entrypoint.global(
packageForConstraint(id.toRange(), _packageDir(id.name)),
packageForConstraint(
id.toRange(),
_packageDir(id.name),
[], // XXX load experiments here
),
lockFile,
cache,
);
Expand Down Expand Up @@ -536,6 +551,7 @@ Try reactivating the package.
entrypoint.lockFile,
newLockFile,
result.availableVersions,
result.experiments,
cache,
dryRun: true,
enforceLockfile: true,
Expand Down Expand Up @@ -678,6 +694,7 @@ Try reactivating the package.
id.toRange(),
packageExecutables,
overwriteBinStubs: true,
allowedExperiments: [], // XXX
silent: true,
);
} else {
Expand Down
5 changes: 5 additions & 0 deletions lib/src/package.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ class Package {
...package.pubspec.dependencyOverrides,
};

/// A collection of the union of all experiments used in the workspace.
late final Set<String> allExperimentsInWorkspace = {
for (final package in transitiveWorkspace) ...package.pubspec.experiments,
};

/// The immediate dependencies this package specifies in its pubspec.
Map<String, PackageRange> get dependencies => pubspec.dependencies;

Expand Down
21 changes: 21 additions & 0 deletions lib/src/package_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,14 @@ class PackageConfig {
/// `.dart_tool/package_config.json` file.
Map<String, dynamic> additionalProperties;

List<String> experiments;

PackageConfig({
required this.configVersion,
required this.packages,
this.generator,
this.generatorVersion,
required this.experiments,
Map<String, dynamic>? additionalProperties,
}) : additionalProperties = additionalProperties ?? {} {
final names = <String>{};
Expand Down Expand Up @@ -100,6 +103,21 @@ class PackageConfig {
);
}

// Read the 'experiments' property
final experiments = root['experiments'] ?? <String>[];
if (experiments is! List) {
throw const FormatException(
'"experiments" in package_config.json must be a list, if given',
);
}
for (final experiment in experiments) {
if (experiment is! String) {
throw const FormatException(
'"experiments" in package_config.json must all be strings',
);
}
}

// Read the 'generatorVersion' property
Version? generatorVersion;
final generatorVersionRaw = root['generatorVersion'];
Expand All @@ -122,6 +140,7 @@ class PackageConfig {
packages: packages,
generator: generator,
generatorVersion: generatorVersion,
experiments: experiments.cast<String>(),
additionalProperties: Map.fromEntries(
root.entries.where(
(e) =>
Expand All @@ -131,6 +150,7 @@ class PackageConfig {
'generated',
'generator',
'generatorVersion',
'experiments',
}.contains(e.key),
),
),
Expand All @@ -141,6 +161,7 @@ class PackageConfig {
Map<String, Object?> toJson() => {
'configVersion': configVersion,
'packages': packages.map((p) => p.toJson()).toList(),
'experiments': experiments,
'generator': generator,
'generatorVersion': generatorVersion?.toString(),
}..addAll(additionalProperties);
Expand Down
47 changes: 47 additions & 0 deletions lib/src/pubspec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,51 @@ environment:
_containingDescription,
);

List<String>? _experiments;
List<String> get experiments => _experiments ??= parseExperiments();

List<String> parseExperiments() {
final experimentsNode = fields.nodes['experiments'];
if (experimentsNode == null || experimentsNode.value == null) {
return [];
}
if (experimentsNode is! YamlList) {
_error('`experiments` must be a list of strings', experimentsNode.span);
}
final result = <String>[];
for (final e in experimentsNode.nodes) {
final value = e.value;
if (value is! String) {
_error('`experiments` must be a list of strings', e.span);
}

/// For root packages, validate that all experiments are known by at least
/// one of the current sdks.
///
/// Dependencies will only be chosen by the solver if their experiments
/// are a subset of those of the root packages, so we don't filter here.
if (_containingDescription is ResolvedRootDescription &&
!availableExperiments.containsKey(value)) {
final availableExperimentsDescription =
availableExperiments.isEmpty
? '''There are no available experiments.'''
: '''
Available experiments are:
${availableExperiments.values.map((experiment) => '* ${experiment.name}: ${experiment.description}, ${experiment.docUrl}').join('\n')}''';
_error('''
$value is not a known experiment.

$availableExperimentsDescription

Read more about experiments at https://dart.dev/go/experiments.
''', e.span);
} else {
result.add(value);
}
}
return result;
}

Map<String, PackageRange>? _dependencies;

/// The packages this package depends on when it is the root package.
Expand Down Expand Up @@ -341,6 +386,7 @@ environment:
this.workspace = const <String>[],
this.dependencyOverridesFromOverridesFile = false,
this.resolution = Resolution.none,
List<String> experiments = const <String>[],
}) : _dependencies =
dependencies == null
? null
Expand All @@ -364,6 +410,7 @@ environment:
// This is a dummy value. Dependencies should already be resolved, so we
// never need to do relative resolutions.
_containingDescription = ResolvedRootDescription.fromDir('.'),
_experiments = experiments,
super(
fields == null ? YamlMap() : YamlMap.wrap(fields),
name: name,
Expand Down
Loading