blob: a828096e30c08f389adfe14e62b6dab77aa9caf9 [file] [edit]
// Copyright (c) 2026, 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 'process.dart';
import 'upstream.dart';
import 'version.dart';
const _cipdVersionPrefix = 'version:';
/// An in-memory view of the SDK's DEPS file.
final class DepsFile {
final String contents;
DepsFile(this.contents);
/// Returns the string value of the DEPS variable [name].
String readVar(String name) {
final match = _varPattern(name).firstMatch(contents);
if (match == null) throw StateError('DEPS has no "$name" variable.');
return match[2]!;
}
/// Returns a copy of this file with variable [name] set to [value].
DepsFile withVar(String name, String value) {
readVar(name); // Fail loudly if the variable doesn't exist.
return DepsFile(contents.replaceFirstMapped(
_varPattern(name),
(match) => '${match[1]}$value${match[3]}',
));
}
/// Matches `"name": "value"`, capturing the text around the value so it can
/// be replaced without touching formatting or trailing comments.
static RegExp _varPattern(String name) =>
RegExp('("${RegExp.escape(name)}":\\s*")([^"]*)(")');
}
/// When an engine's DEPS pin should be rolled to a newer upstream release.
final class RollPolicy {
/// How old the current pin must be before a minor or patch update rolls.
final Duration minimumAge;
/// Whether a new major version rolls immediately regardless of [minimumAge].
final bool rollOnMajorVersion;
const RollPolicy({
required this.minimumAge,
this.rollOnMajorVersion = false,
});
/// Returns why rolling from [current] (pinned [pinAge] ago) to [latest]
/// should wait, or `null` if the roll should go ahead.
String? holdReason(Version current, Version latest, Duration pinAge) {
if (rollOnMajorVersion && latest.major > current.major) return null;
if (pinAge >= minimumAge) return null;
return [
if (rollOnMajorVersion) 'same major version (${current.major})',
'pinned ${pinAge.inDays} days ago '
'(waiting for ${minimumAge.inDays} days)',
].join(', ');
}
}
/// Where an [Engine]'s new versions come from.
sealed class VersionSource {
const VersionSource();
}
/// Tracks upstream releases, rolling to them according to [policy].
final class UpstreamReleases extends VersionSource {
/// When to roll to a newer upstream release.
final RollPolicy policy;
/// Looks up the latest upstream release given the current pinned version.
final Future<Release> Function(Upstream upstream, Version current)
fetchLatest;
const UpstreamReleases({required this.policy, required this.fetchLatest});
}
/// Keeps the pin at the same version as [leader], rolling in the same CL
/// whenever [leader] rolls.
final class SameVersionAs extends VersionSource {
final Engine leader;
const SameVersionAs(this.leader);
}
/// A browser or JS engine pinned in DEPS and packaged in CIPD.
final class Engine {
/// The name used by the tool proxy `browser` grant and by `--only`.
final String name;
/// The name used in commit messages and notifications.
final String displayName;
/// The DEPS variable holding the pinned version.
final String depsVar;
/// Whether the DEPS value includes the CIPD `version:` prefix.
final bool depsValueHasCipdPrefix;
/// The CIPD packages that must exist before the DEPS pin can be rolled.
final List<String> cipdPackages;
/// Try builders to add to roll CLs, beyond the default CQ set.
final List<String> trybots;
/// Where new versions come from and when to roll to them.
final VersionSource source;
const Engine({
required this.name,
required this.displayName,
required this.depsVar,
required this.depsValueHasCipdPrefix,
required this.cipdPackages,
required this.trybots,
required this.source,
});
/// The engine whose version this engine tracks (itself unless [source] is
/// [SameVersionAs]). Engines with the same [versionLeader] roll together.
Engine get versionLeader => switch (source) {
SameVersionAs(:final leader) => leader,
UpstreamReleases() => this,
};
/// The version pinned in [deps].
Version pinnedVersion(DepsFile deps) {
final value = deps.readVar(depsVar);
if (!depsValueHasCipdPrefix) return Version.parse(value);
if (!value.startsWith(_cipdVersionPrefix)) {
throw FormatException('Expected "$_cipdVersionPrefix" prefix', value);
}
return Version.parse(value.substring(_cipdVersionPrefix.length));
}
/// Returns a copy of [deps] with this engine pinned to [version].
DepsFile withPinnedVersion(DepsFile deps, Version version) => deps.withVar(
depsVar,
depsValueHasCipdPrefix ? _cipdTag(version) : '$version',
);
/// Returns the [cipdPackages] that do not have an instance for [version].
Future<List<String>> missingCipdPackages(Version version) async {
final tag = _cipdTag(version);
final results = await Future.wait([
for (final package in cipdPackages)
runProcess('cipd', ['resolve', package, '-version', tag]),
]);
return [
for (var i = 0; i < cipdPackages.length; i++)
if (results[i].exitCode != 0) cipdPackages[i],
];
}
String _cipdTag(Version version) => '$_cipdVersionPrefix$version';
}
/// The pinned and latest versions of an engine and whether to roll it.
final class EngineStatus {
final Engine engine;
final Version current;
final Release latest;
/// Why the engine shouldn't be rolled, or `null` if it should.
final String? holdReason;
EngineStatus({
required this.engine,
required this.current,
required this.latest,
required this.holdReason,
});
bool get shouldRoll => holdReason == null;
/// The new DEPS pin when rolling.
Version get target => latest.version;
/// E.g. "Firefox 127.0 -> 128.0".
String get rollSummary => '${engine.displayName} $current -> $target';
/// The engine and builder version, e.g. "Firefox 128.0+2".
String get buildName => '${engine.displayName} ${latest.builderVersion}';
/// Returns the CIPD packages for [target] that have not been published yet.
Future<List<String>> missingCipdPackages() =>
engine.missingCipdPackages(target);
}
/// Returns the CIPD package names for [prefix] on each of [platforms].
List<String> _packages(String prefix, List<String> platforms) =>
[for (final platform in platforms) '$prefix/$platform'];
// The platform lists mirror what the packaging builders upload; see
// https://dart.googlesource.com/deps/+/refs/heads/main/bin/update_browsers.dart
const _chromePlatforms = [
'linux-amd64',
'mac-amd64',
'mac-arm64',
'windows-amd64',
];
const _d8Platforms = [
'linux-amd64',
'linux-arm64',
'mac-amd64',
'mac-arm64',
'windows-amd64',
];
const _firefoxPlatforms = [
'linux-amd64',
'linux-arm64',
'windows-amd64',
'windows-arm64',
];
const _jsshellPlatforms = [
'linux-amd64',
'linux-arm64',
'mac-amd64',
'windows-amd64',
'windows-arm64',
];
/// Chrome and Firefox roll on a new major version, and otherwise every two
/// weeks.
const _majorOrBiweekly = RollPolicy(
minimumAge: Duration(days: 14),
rollOnMajorVersion: true,
);
/// d8 and jsc are rolled monthly.
const _monthly = RollPolicy(minimumAge: Duration(days: 30));
/// We follow Chrome beta to catch issues before they reach stable.
final chrome = Engine(
name: 'chrome',
displayName: 'Chrome',
depsVar: 'chrome_tag',
depsValueHasCipdPrefix: false,
cipdPackages: [
..._packages('dart/browsers/chrome', _chromePlatforms),
..._packages('dart/third_party/chromedriver', _chromePlatforms),
],
trybots: const [
'dart2js-linux-chrome-try',
'ddc-linux-chrome-try',
'dart2wasm-linux-chrome-try',
],
source: UpstreamReleases(
policy: _majorOrBiweekly,
fetchLatest: (upstream, _) => upstream.latestChromeBeta(),
),
);
/// We follow Firefox stable.
final firefox = Engine(
name: 'firefox',
displayName: 'Firefox',
depsVar: 'firefox_tag',
depsValueHasCipdPrefix: false,
cipdPackages: _packages('dart/browsers/firefox', _firefoxPlatforms),
trybots: const [
'dart2js-linux-firefox-try',
'ddc-linux-firefox-try',
'dart2wasm-linux-firefox-try',
],
source: UpstreamReleases(
policy: _majorOrBiweekly,
fetchLatest: (upstream, _) => upstream.latestFirefox(),
),
);
final d8 = Engine(
name: 'd8',
displayName: 'd8',
depsVar: 'd8_tag',
depsValueHasCipdPrefix: true,
cipdPackages: _packages('dart/third_party/d8', _d8Platforms),
trybots: const [
'dart2js-minified-linux-d8-try',
'ddc-hostasserts-linux-d8-try',
'dart2wasm-linux-d8-try',
],
source: UpstreamReleases(
policy: _monthly,
fetchLatest: (upstream, current) => upstream.latestD8(current),
),
);
/// jsshell is built from the same release as Firefox, so it is kept at the
/// Firefox version and rolled in the same CL.
final jsshell = Engine(
name: 'jsshell',
displayName: 'jsshell',
depsVar: 'jsshell_tag',
depsValueHasCipdPrefix: true,
cipdPackages: _packages('dart/third_party/jsshell', _jsshellPlatforms),
trybots: const [],
source: SameVersionAs(firefox),
);
final jsc = Engine(
name: 'jsc',
displayName: 'jsc',
depsVar: 'jsc_tag',
depsValueHasCipdPrefix: true,
cipdPackages: const ['dart/third_party/jsc/linux-amd64'],
trybots: const ['dart2wasm-linux-optimized-jsc-try'],
source: UpstreamReleases(
policy: _monthly,
fetchLatest: (upstream, _) => upstream.latestJsc(),
),
);
/// All engines managed by the roller, in the order they are reported.
final allEngines = [chrome, firefox, d8, jsshell, jsc];