import 'package:meta/meta.dart';
import 'base/common.dart';
import 'base/file_system.dart';
import 'base/logger.dart';
import 'base/platform.dart';
import 'base/process.dart';
import 'base/time.dart';
import 'base/utils.dart';
import 'cache.dart';
import 'convert.dart';
import 'features.dart';
import 'git.dart';
import 'globals.dart' as globals;
const kUnknownFrameworkVersion = '0.0.0-unknown';
const kGitTrackingUpstream = '@{upstream}';
const kUserBranch = '[user-branch]';
const kObsoleteBranches = <String, String>{'dev': 'beta'};
enum Channel { master, main, beta, stable }
const kOfficialChannels = <String>{'master', 'main', 'beta', 'stable'};
const kChannelDescriptions = <String, String>{
'master': 'latest development branch, for contributors',
'main': 'latest development branch, follows master channel',
'beta': 'updated monthly, recommended for experienced users',
'stable': 'updated quarterly, for new users and for production app releases',
};
const kDevelopmentChannels = <String>{'master', 'main'};
String getNameForChannel(Channel channel) {
return kOfficialChannels.elementAt(channel.index);
}
Channel? getChannelForName(String name) {
if (kOfficialChannels.contains(name)) {
return Channel.values[kOfficialChannels.toList().indexOf(name)];
}
return null;
}
abstract class FlutterVersion {
factory FlutterVersion({
required String flutterRoot,
required FileSystem fs,
required Git git,
Cache? cache,
SystemClock clock = const SystemClock(),
@protected bool fetchTags = false,
Logger? logger,
Platform? platform,
}) {
final File versionFile = getVersionFile(fs, flutterRoot);
if (!fetchTags && versionFile.existsSync()) {
final _FlutterVersionFromFile? version = _FlutterVersionFromFile.tryParseFromFile(
versionFile,
git: git,
flutterRoot: flutterRoot,
logger: logger,
platform: platform,
cache: cache,
);
if (version != null) {
final String gitPath = fs.path.join(flutterRoot, '.git');
final bool isGitRepo = fs.directory(gitPath).existsSync() || fs.file(gitPath).existsSync();
if (!isGitRepo) {
return version;
}
final bool isSuspicious =
version.frameworkVersion == kUnknownFrameworkVersion ||
version.repositoryUrl == 'unknown source' ||
!_isStandardRemote(version.repositoryUrl);
if (!isSuspicious) {
return version;
}
}
}
if (fetchTags && versionFile.existsSync()) {
versionFile.deleteSync();
final File legacyVersionFile = fs.file(fs.path.join(flutterRoot, 'version'));
if (legacyVersionFile.existsSync()) {
legacyVersionFile.deleteSync();
}
}
final String frameworkRevision = git
.logSync(['-n', '1', '--pretty=format:%H'], workingDirectory: flutterRoot)
.stdout
.trim();
return FlutterVersion.fromRevision(
cache: cache,
clock: clock,
fetchTags: fetchTags,
flutterRoot: flutterRoot,
frameworkRevision: frameworkRevision,
fs: fs,
git: git,
logger: logger,
platform: platform ?? globals.platform,
);
}
FlutterVersion._({
required this._clock,
required this.flutterRoot,
required this.fs,
required this._git,
this._cache,
this._logger,
this._platform,
});
factory FlutterVersion.fromRevision({
required String flutterRoot,
required String frameworkRevision,
required FileSystem fs,
required Git git,
required Platform platform,
Cache? cache,
SystemClock clock = const SystemClock(),
bool fetchTags = false,
Logger? logger,
}) {
final GitTagVersion gitTagVersion = GitTagVersion.determine(
platform,
git: git,
gitRef: frameworkRevision,
workingDirectory: flutterRoot,
fetchTags: fetchTags,
logger: logger,
);
final String frameworkVersion = gitTagVersion.frameworkVersionFor(frameworkRevision);
final result = _FlutterVersionGit._(
cache: cache,
clock: clock,
flutterRoot: flutterRoot,
frameworkRevision: frameworkRevision,
frameworkVersion: frameworkVersion,
fs: fs,
git: git,
gitTagVersion: gitTagVersion,
logger: logger,
platform: platform,
);
if (fetchTags) {
result.ensureVersionFile();
}
return result;
}
final Platform? _platform;
final Logger? _logger;
final Cache? _cache;
FlutterVersion fetchTagsAndGetVersion({SystemClock clock = const SystemClock()}) {
if (channel != 'master' && channel != 'main') {
return this;
}
return FlutterVersion(
clock: clock,
flutterRoot: flutterRoot,
fs: fs,
fetchTags: true,
git: _git,
);
}
final FileSystem fs;
final Git _git;
final SystemClock _clock;
String? get repositoryUrl;
GitTagVersion get gitTagVersion;
String get channel;
String get frameworkRevision;
String get frameworkRevisionShort => _shortGitRevision(frameworkRevision);
String get frameworkVersion;
String get devToolsVersion;
String get dartSdkVersion;
String get engineRevision;
String? get engineContentHash;
String get engineRevisionShort => _shortGitRevision(engineRevision);
static File getVersionFile(FileSystem fs, String flutterRoot) {
return fs.file(fs.path.join(flutterRoot, 'bin', 'cache', 'flutter.version.json'));
}
final String flutterRoot;
String _getTimeSinceCommit({String? revision}) {
return _git
.logSync(['-n', '1', '--pretty=format:%ar', ?revision], workingDirectory: flutterRoot)
.stdout
.trim();
}
late final String frameworkAge = _getTimeSinceCommit();
late final String engineAge = engineBuildDate != null
? _clock.now().difference(DateTime.parse(engineBuildDate!)).ago()
: _getTimeSinceCommit(revision: engineRevision);
void ensureVersionFile();
void deleteVersionFile() {
final File versionFile = getVersionFile(fs, flutterRoot);
if (versionFile.existsSync()) {
versionFile.deleteSync();
}
}
@override
String toString() {
final versionText = frameworkVersion == kUnknownFrameworkVersion ? '' : ' $frameworkVersion';
final flutterText =
'Flutter$versionText • channel $channel • ${repositoryUrl ?? 'unknown source'}';
final frameworkText =
'Framework • revision $frameworkRevisionShort ($frameworkAge) • $frameworkCommitDate';
String engineText;
if (engineContentHash != null) {
engineText = 'Engine • hash $engineContentHash (revision $engineRevisionShort) ($engineAge)';
} else {
engineText = 'Engine • revision $engineRevisionShort ($engineAge)';
}
if (engineCommitDate != null) {
engineText = '$engineText • $engineCommitDate';
}
final toolsText = 'Tools • Dart $dartSdkVersion • DevTools $devToolsVersion';
return '$flutterText\n$frameworkText\n$engineText\n$toolsText';
}
Map<String, Object> toJson() => <String, Object>{
'frameworkVersion': frameworkVersion,
'channel': channel,
'repositoryUrl': repositoryUrl ?? 'unknown source',
'frameworkRevision': frameworkRevision,
'frameworkCommitDate': frameworkCommitDate,
'engineRevision': engineRevision,
'engineCommitDate': ?engineCommitDate,
'engineContentHash': ?engineContentHash,
'engineBuildDate': ?engineBuildDate,
'dartSdkVersion': dartSdkVersion,
'devToolsVersion': devToolsVersion,
'flutterVersion': frameworkVersion,
};
String get frameworkCommitDate;
String? get engineCommitDate;
String? get engineBuildDate;
Future<void> checkFlutterVersionFreshness() async {
final Cache effectiveCache = _cache ?? globals.cache;
final Logger effectiveLogger = _logger ?? globals.logger;
final Platform effectivePlatform = _platform ?? globals.platform;
if (!kOfficialChannels.contains(channel)) {
return;
}
if (VersionUpstreamValidator(version: this, platform: effectivePlatform).run() != null) {
return;
}
DateTime localFrameworkCommitDate;
try {
localFrameworkCommitDate = DateTime.parse(
_gitCommitDate(git: _git, workingDirectory: flutterRoot),
);
} on VersionCheckError {
return;
} on FormatException {
return;
}
final DateTime? latestFlutterCommitDate = await _getLatestAvailableFlutterDate(
cache: effectiveCache,
logger: effectiveLogger,
);
return VersionFreshnessValidator(
version: this,
clock: _clock,
localFrameworkCommitDate: localFrameworkCommitDate,
latestFlutterCommitDate: latestFlutterCommitDate,
logger: effectiveLogger,
cache: effectiveCache,
pauseTime: VersionFreshnessValidator.timeToPauseToLetUserReadTheMessage,
).run();
}
Future<DateTime?> _getLatestAvailableFlutterDate({
required Cache cache,
required Logger logger,
}) async {
cache.checkLockAcquired();
final VersionCheckStamp versionCheckStamp = await VersionCheckStamp.load(cache, logger);
final DateTime now = _clock.now();
if (versionCheckStamp.lastTimeVersionWasChecked != null) {
final Duration timeSinceLastCheck = now.difference(
versionCheckStamp.lastTimeVersionWasChecked!,
);
if (timeSinceLastCheck < VersionFreshnessValidator.checkAgeConsideredUpToDate) {
return versionCheckStamp.lastKnownRemoteVersion;
}
}
try {
final DateTime remoteFrameworkCommitDate = DateTime.parse(
await _fetchRemoteFrameworkCommitDate(logger: logger),
);
await versionCheckStamp.store(
newTimeVersionWasChecked: now,
newKnownRemoteVersion: remoteFrameworkCommitDate,
cache: cache,
);
return remoteFrameworkCommitDate;
} on VersionCheckError catch (error) {
logger.printTrace('Failed to check Flutter version in the remote repository: $error');
await versionCheckStamp.store(newTimeVersionWasChecked: now, cache: cache);
return null;
}
}
Future<String> _fetchRemoteFrameworkCommitDate({required Logger logger}) async {
try {
await _run(_git, ['fetch', '--tags'], workingDirectory: flutterRoot);
return _gitCommitDate(git: _git, gitRef: kGitTrackingUpstream, workingDirectory: flutterRoot);
} on VersionCheckError catch (error) {
logger.printError(error.message);
rethrow;
}
}
String getVersionString({bool redactUnknownBranches = false}) {
if (frameworkVersion != kUnknownFrameworkVersion) {
return '${getBranchName(redactUnknownBranches: redactUnknownBranches)}/$frameworkVersion';
}
return '${getBranchName(redactUnknownBranches: redactUnknownBranches)}/$frameworkRevisionShort';
}
String? _branch;
String getBranchName({bool redactUnknownBranches = false}) {
_branch ??= () {
final String branch = _git
.runSync(['symbolic-ref', '--short', 'HEAD'], workingDirectory: flutterRoot)
.stdout
.trim();
return branch == 'HEAD' ? '' : branch;
}();
if (redactUnknownBranches || _branch!.isEmpty) {
if (!kOfficialChannels.contains(_branch) && !kObsoleteBranches.containsKey(_branch)) {
return kUserBranch;
}
}
return _branch!;
}
static Future<void> resetFlutterVersionFreshnessCheck([Cache? cache]) async {
try {
final Cache effectiveCache = cache ?? globals.cache;
await effectiveCache.getStampFileFor(VersionCheckStamp.flutterVersionCheckStampFile).delete();
} on FileSystemException {
} on UnsupportedError {
}
}
}
String _gitCommitDate({
String gitRef = 'HEAD',
bool lenient = false,
required Git git,
required String? workingDirectory,
}) {
final RunResult result = git.logSync([
gitRef,
'-n',
'1',
'--pretty=format:%ad',
'--date=iso',
], workingDirectory: workingDirectory);
if (result.exitCode == 0) {
return result.stdout.trim();
}
final error = VersionCheckError(
'Command exited with code ${result.exitCode}: ${result.command.join(' ')}\n'
'Standard out: ${result.stdout}\n'
'Standard error: ${result.stderr}',
);
if (lenient) {
final dummyDate = DateTime.fromMillisecondsSinceEpoch(0);
globals.printError(
'Failed to find the latest git commit date: $error\n'
'Returning $dummyDate instead.',
);
return dummyDate.toString();
}
throw error;
}
class _FlutterVersionFromFile extends FlutterVersion {
_FlutterVersionFromFile._({
required super.clock,
required this.frameworkVersion,
required this.channel,
required this.repositoryUrl,
required this.frameworkRevision,
required this.frameworkCommitDate,
required this.engineRevision,
required this.engineCommitDate,
required this.dartSdkVersion,
required this.devToolsVersion,
required this.gitTagVersion,
required this.engineContentHash,
required this.engineBuildDate,
required super.flutterRoot,
required super.fs,
required super.git,
super.platform,
super.logger,
super.cache,
}) : super._();
static _FlutterVersionFromFile? tryParseFromFile(
File jsonFile, {
required String flutterRoot,
required Git git,
SystemClock clock = const SystemClock(),
Logger? logger,
Platform? platform,
Cache? cache,
}) {
try {
final String jsonContents = jsonFile.readAsStringSync();
final manifest = jsonDecode(jsonContents) as Map<String, Object?>;
return _FlutterVersionFromFile._(
clock: clock,
git: git,
frameworkVersion: manifest['frameworkVersion']! as String,
channel: manifest['channel']! as String,
repositoryUrl: manifest['repositoryUrl']! as String,
frameworkRevision: manifest['frameworkRevision']! as String,
frameworkCommitDate: manifest['frameworkCommitDate']! as String,
engineRevision: manifest['engineRevision']! as String,
engineCommitDate: manifest['engineCommitDate'] as String?,
engineContentHash: manifest['engineContentHash'] as String?,
engineBuildDate: manifest['engineBuildDate'] as String?,
dartSdkVersion: manifest['dartSdkVersion']! as String,
devToolsVersion: manifest['devToolsVersion']! as String,
gitTagVersion: GitTagVersion.parse(manifest['flutterVersion']! as String, logger: logger),
flutterRoot: flutterRoot,
fs: jsonFile.fileSystem,
platform: platform,
logger: logger,
cache: cache,
);
} catch (err) {
if (logger != null) {
logger.printTrace('Failed to parse ${jsonFile.path} with $err');
}
try {
jsonFile.deleteSync();
} on FileSystemException {
if (logger != null) {
logger.printTrace('Failed to delete ${jsonFile.path}');
}
}
return null;
}
}
@override
final GitTagVersion gitTagVersion;
@override
final String frameworkVersion;
@override
final String channel;
@override
String getBranchName({bool redactUnknownBranches = false}) => channel;
@override
final String repositoryUrl;
@override
final String frameworkRevision;
@override
final String frameworkCommitDate;
@override
final String? engineCommitDate;
@override
final String? engineBuildDate;
@override
final String engineRevision;
@override
final String? engineContentHash;
@override
final String dartSdkVersion;
@override
final String devToolsVersion;
@override
void ensureVersionFile() {
_ensureLegacyVersionFile(fs: fs, flutterRoot: flutterRoot, frameworkVersion: frameworkVersion);
}
}
class _FlutterVersionGit extends FlutterVersion {
_FlutterVersionGit._({
required super.clock,
required super.flutterRoot,
required this.frameworkRevision,
required this.frameworkVersion,
required this.gitTagVersion,
required super.fs,
required super.git,
super.platform,
super.logger,
super.cache,
}) : super._();
late final FlutterEngineStampFromFile? _engineStamp = FlutterEngineStampFromFile.tryParseFromFile(
fs.file(fs.path.join(flutterRoot, 'bin', 'cache', 'engine_stamp.json')),
);
@override
final GitTagVersion gitTagVersion;
@override
final String frameworkRevision;
@override
String get frameworkCommitDate =>
_gitCommitDate(git: _git, lenient: true, workingDirectory: flutterRoot);
@override
late final String engineCommitDate =
_engineStamp?.gitRevisionDate.toString() ??
_gitCommitDate(
git: _git,
gitRef: engineRevision,
lenient: true,
workingDirectory: flutterRoot,
);
String? _repositoryUrl;
@override
String? get repositoryUrl {
if (_repositoryUrl == null) {
final RunResult trackingResult = _git.runSync([
'rev-parse',
'--abbrev-ref',
'--symbolic',
kGitTrackingUpstream,
], workingDirectory: flutterRoot);
if (trackingResult.exitCode == 0) {
final String gitChannel = trackingResult.stdout.trim();
final int slash = gitChannel.indexOf('/');
if (slash != -1) {
final String remote = gitChannel.substring(0, slash);
final RunResult remoteResult = _git.runSync([
'ls-remote',
'--get-url',
remote,
], workingDirectory: flutterRoot);
if (remoteResult.exitCode == 0) {
_repositoryUrl = remoteResult.stdout.trim();
}
}
}
if (_repositoryUrl == null || _repositoryUrl!.isEmpty) {
final RunResult originResult = _git.runSync([
'remote',
'get-url',
'origin',
], workingDirectory: flutterRoot);
if (originResult.exitCode == 0) {
_repositoryUrl = originResult.stdout.trim();
}
}
}
return _repositoryUrl;
}
@override
String get devToolsVersion => globals.cache.devToolsVersion;
@override
String get dartSdkVersion => globals.cache.dartSdkVersion;
@override
String get engineRevision => _engineStamp?.gitRevision ?? globals.cache.engineRevision;
@override
final String frameworkVersion;
@override
String get channel {
final String channel = getBranchName(redactUnknownBranches: true);
assert(
kOfficialChannels.contains(channel) ||
kObsoleteBranches.containsKey(channel) ||
channel == kUserBranch,
'Potential PII leak in channel name: "$channel"',
);
return channel;
}
@override
void ensureVersionFile() {
_ensureLegacyVersionFile(fs: fs, flutterRoot: flutterRoot, frameworkVersion: frameworkVersion);
const encoder = JsonEncoder.withIndent(' ');
final File newVersionFile = FlutterVersion.getVersionFile(fs, flutterRoot);
newVersionFile.writeAsStringSync(encoder.convert(toJson()));
}
@override
String? get engineBuildDate => _engineStamp?.buildDate.toString();
@override
String? get engineContentHash => _engineStamp?.contentHash;
}
void _ensureLegacyVersionFile({
required FileSystem fs,
required String flutterRoot,
required String frameworkVersion,
}) {
if (featureFlags.isOmitLegacyVersionFileEnabled) {
return;
}
final File legacyVersionFile = fs.file(fs.path.join(flutterRoot, 'version'));
if (!legacyVersionFile.existsSync()) {
legacyVersionFile.writeAsStringSync(frameworkVersion);
}
}
class VersionUpstreamValidator {
VersionUpstreamValidator({required this.version, required this.platform});
final Platform platform;
final FlutterVersion version;
VersionCheckError? run() {
final String? flutterGit = platform.environment['FLUTTER_GIT_URL'];
final String? repositoryUrl = version.repositoryUrl;
if (repositoryUrl == null) {
return VersionCheckError(
'The tool could not determine the remote upstream which is being '
'tracked by the SDK.',
);
}
final List<String> sanitizedStandardRemotes = [
if (flutterGit != null)
flutterGit
else
..._standardRemotes,
].map((String remote) => stripDotGit(remote)).toList();
final String sanitizedRepositoryUrl = stripDotGit(repositoryUrl);
if (!sanitizedStandardRemotes.contains(sanitizedRepositoryUrl)) {
if (flutterGit != null) {
return VersionCheckError(
'The Flutter SDK is tracking "$repositoryUrl" but "FLUTTER_GIT_URL" '
'is set to "$flutterGit".\n'
'Either remove "FLUTTER_GIT_URL" from the environment or set it to '
'"$repositoryUrl". '
'If this is intentional, it is recommended to use "git" directly to '
'manage the SDK.',
);
}
return VersionCheckError(
'The Flutter SDK is tracking a non-standard remote "$repositoryUrl".\n'
'Set the environment variable "FLUTTER_GIT_URL" to '
'"$repositoryUrl". '
'If this is intentional, it is recommended to use "git" directly to '
'manage the SDK.\n'
r'If this is NOT intentional, try deleting the version cache: `rm $FLUTTER_ROOT/bin/cache/flutter.version.json`.',
);
}
return null;
}
static final _standardRemotes = [
'https://github.com/flutter/flutter.git',
'git@github.com:flutter/flutter.git',
'ssh://git@github.com/flutter/flutter.git',
];
static final _patternUrlDotGit = RegExp(r'(.*)(\.git)$');
static String stripDotGit(String url) {
return _patternUrlDotGit.firstMatch(url)?.group(1)! ?? url;
}
}
@visibleForTesting
class VersionCheckStamp {
const VersionCheckStamp({
this.lastTimeVersionWasChecked,
this.lastKnownRemoteVersion,
this.lastTimeWarningWasPrinted,
});
final DateTime? lastTimeVersionWasChecked;
final DateTime? lastKnownRemoteVersion;
final DateTime? lastTimeWarningWasPrinted;
@visibleForTesting
static const flutterVersionCheckStampFile = 'flutter_version_check';
static Future<VersionCheckStamp> load(Cache cache, Logger logger) async {
final String? versionCheckStamp = cache.getStampFor(flutterVersionCheckStampFile);
if (versionCheckStamp != null) {
try {
final dynamic jsonObject = json.decode(versionCheckStamp);
if (jsonObject is Map<String, dynamic>) {
return fromJson(jsonObject);
} else {
logger.printTrace('Warning: expected version stamp to be a Map but found: $jsonObject');
}
} on Exception catch (error, stackTrace) {
logger.printTrace('${error.runtimeType}: $error\n$stackTrace');
}
}
return const VersionCheckStamp();
}
static VersionCheckStamp fromJson(Map<String, dynamic> jsonObject) {
DateTime? readDateTime(String property) {
return jsonObject.containsKey(property)
? DateTime.parse(jsonObject[property] as String)
: null;
}
return VersionCheckStamp(
lastTimeVersionWasChecked: readDateTime('lastTimeVersionWasChecked'),
lastKnownRemoteVersion: readDateTime('lastKnownRemoteVersion'),
lastTimeWarningWasPrinted: readDateTime('lastTimeWarningWasPrinted'),
);
}
Future<void> store({
DateTime? newTimeVersionWasChecked,
DateTime? newKnownRemoteVersion,
DateTime? newTimeWarningWasPrinted,
Cache? cache,
}) async {
final Map<String, String> jsonData = toJson();
if (newTimeVersionWasChecked != null) {
jsonData['lastTimeVersionWasChecked'] = '$newTimeVersionWasChecked';
}
if (newKnownRemoteVersion != null) {
jsonData['lastKnownRemoteVersion'] = '$newKnownRemoteVersion';
}
if (newTimeWarningWasPrinted != null) {
jsonData['lastTimeWarningWasPrinted'] = '$newTimeWarningWasPrinted';
}
const prettyJsonEncoder = JsonEncoder.withIndent(' ');
(cache ?? globals.cache).setStampFor(
flutterVersionCheckStampFile,
prettyJsonEncoder.convert(jsonData),
);
}
Map<String, String> toJson({
DateTime? updateTimeVersionWasChecked,
DateTime? updateKnownRemoteVersion,
DateTime? updateTimeWarningWasPrinted,
}) {
updateTimeVersionWasChecked = updateTimeVersionWasChecked ?? lastTimeVersionWasChecked;
updateKnownRemoteVersion = updateKnownRemoteVersion ?? lastKnownRemoteVersion;
updateTimeWarningWasPrinted = updateTimeWarningWasPrinted ?? lastTimeWarningWasPrinted;
final jsonData = <String, String>{};
if (updateTimeVersionWasChecked != null) {
jsonData['lastTimeVersionWasChecked'] = '$updateTimeVersionWasChecked';
}
if (updateKnownRemoteVersion != null) {
jsonData['lastKnownRemoteVersion'] = '$updateKnownRemoteVersion';
}
if (updateTimeWarningWasPrinted != null) {
jsonData['lastTimeWarningWasPrinted'] = '$updateTimeWarningWasPrinted';
}
return jsonData;
}
}
class VersionCheckError implements Exception {
VersionCheckError(this.message);
final String message;
@override
String toString() => '$VersionCheckError: $message';
}
Future<String> _run(Git git, List<String> command, {String? workingDirectory}) async {
final RunResult results = await git.run(
command,
workingDirectory: workingDirectory ?? Cache.flutterRoot,
);
if (results.exitCode == 0) {
return results.stdout.trim();
}
throw VersionCheckError(
'Command exited with code ${results.exitCode}: ${command.join(' ')}\n'
'Standard error: ${results.stderr}',
);
}
String _shortGitRevision(String? revision) {
if (revision == null) {
return '';
}
return revision.length > 10 ? revision.substring(0, 10) : revision;
}
class GitTagVersion {
const GitTagVersion({
required this.x,
required this.y,
required this.z,
required this.hash,
required this.gitTag,
this.hotfix,
this.devVersion,
this.devPatch,
this.commits,
});
const GitTagVersion.unknown()
: x = null,
y = null,
z = null,
hotfix = null,
commits = 0,
devVersion = null,
devPatch = null,
hash = '',
gitTag = '';
final int? x;
final int? y;
final int? z;
final int? hotfix;
final int? commits;
final String hash;
final int? devVersion;
final int? devPatch;
final String gitTag;
static GitTagVersion determine(
Platform platform, {
required Git git,
String? workingDirectory,
bool fetchTags = false,
String gitRef = 'HEAD',
Logger? logger,
}) {
if (fetchTags) {
final String channel = git
.runSync(['symbolic-ref', '--short', 'HEAD'], workingDirectory: workingDirectory)
.stdout
.trim();
if (!kDevelopmentChannels.contains(channel) && kOfficialChannels.contains(channel)) {
if (logger != null) {
logger.printTrace('Skipping request to fetchTags - on well known channel $channel.');
} else {
try {
globals.printTrace('Skipping request to fetchTags - on well known channel $channel.');
} on Exception {
}
}
} else {
final String flutterGit =
platform.environment['FLUTTER_GIT_URL'] ?? 'https://github.com/flutter/flutter.git';
git.runSync(['fetch', flutterGit, '--tags', '-f'], workingDirectory: workingDirectory);
}
}
final List<String> tags = git
.runSync(['tag', '--points-at', gitRef], workingDirectory: workingDirectory)
.stdout
.trim()
.split('\n')
.reversed
.toList();
final stableTagPattern = RegExp(r'^\d+\.\d+\.\d+$');
for (final tag in tags) {
if (stableTagPattern.hasMatch(tag.trim())) {
return parse(tag, logger: logger);
}
}
final devTagPattern = RegExp(r'^\d+\.\d+\.\d+-\d+\.\d+\.pre$');
for (final tag in tags) {
if (devTagPattern.hasMatch(tag.trim())) {
return parse(tag, logger: logger);
}
}
return _useNewestTagAndCommitsPastFallback(
git: git,
workingDirectory: workingDirectory,
gitRef: gitRef,
logger: logger,
);
}
static GitTagVersion _useNewestTagAndCommitsPastFallback({
required Git git,
required String? workingDirectory,
required String gitRef,
Logger? logger,
}) {
final String latestTag = git
.runSync([
'for-each-ref',
'--sort=-v:refname',
'--count=1',
'--format=%(refname:short)',
'refs/tags/[0-9]*.*.*',
], workingDirectory: workingDirectory)
.stdout
.trim();
final String ancestorRef = git
.runSync(['merge-base', gitRef, latestTag], workingDirectory: workingDirectory)
.stdout
.trim();
final String commitCount = git
.runSync([
'rev-list',
'--count',
'$ancestorRef..$gitRef',
], workingDirectory: workingDirectory)
.stdout
.trim();
return parse('$latestTag-$commitCount', logger: logger);
}
static GitTagVersion parseVersion(String version) {
final versionPattern = RegExp(
r'^(\d+)\.(\d+)\.(\d+)(-\d+\.\d+\.pre)?(?:[-\.](\d+)(?:-g([a-f0-9]+))?)?$',
);
final Match? match = versionPattern.firstMatch(version.trim());
if (match == null) {
return const GitTagVersion.unknown();
}
final List<String?> matchGroups = match.groups(<int>[1, 2, 3, 4, 5, 6]);
final int? x = matchGroups[0] == null ? null : int.tryParse(matchGroups[0]!);
final int? y = matchGroups[1] == null ? null : int.tryParse(matchGroups[1]!);
final int? z = matchGroups[2] == null ? null : int.tryParse(matchGroups[2]!);
final String? devString = matchGroups[3];
int? devVersion, devPatch;
if (devString != null) {
final Match? devMatch = RegExp(r'^-(\d+)\.(\d+)\.pre$').firstMatch(devString);
final List<String?>? devGroups = devMatch?.groups(<int>[1, 2]);
devVersion = devGroups?[0] == null ? null : int.tryParse(devGroups![0]!);
devPatch = devGroups?[1] == null ? null : int.tryParse(devGroups![1]!);
}
final int? commits = matchGroups[4] == null ? 0 : int.tryParse(matchGroups[4]!);
final String hash = matchGroups[5] ?? '';
return GitTagVersion(
x: x,
y: y,
z: z,
devVersion: devVersion,
devPatch: devPatch,
commits: commits,
hash: hash,
gitTag: '$x.$y.$z${devString ?? ''}',
);
}
@visibleForTesting
static GitTagVersion parse(String version, {Logger? logger}) {
GitTagVersion gitTagVersion;
gitTagVersion = parseVersion(version);
if (gitTagVersion != const GitTagVersion.unknown()) {
return gitTagVersion;
}
if (logger != null) {
logger.printTrace('Could not interpret results of "git describe": $version');
} else {
try {
globals.printTrace('Could not interpret results of "git describe": $version');
} on Exception {
}
}
return const GitTagVersion.unknown();
}
String frameworkVersionFor(String revision) {
if (x == null || y == null || z == null || !revision.startsWith(hash)) {
return kUnknownFrameworkVersion;
}
if (commits == 0) {
return gitTag;
}
if (hotfix != null) {
return '$x.$y.$z+hotfix.${hotfix! + 1}.pre-$commits';
}
if (devPatch != null && devVersion != null) {
return '$x.$y.0-${devVersion! + 1}.0.pre-$commits';
}
return '$x.$y.${z! + 1}-0.0.pre-$commits';
}
}
enum VersionCheckResult {
unknown,
versionIsCurrent,
newVersionAvailable,
}
class VersionFreshnessValidator {
VersionFreshnessValidator({
required this.version,
required this.localFrameworkCommitDate,
required this.clock,
required this.cache,
required this.logger,
this.latestFlutterCommitDate,
this.pauseTime = Duration.zero,
});
final FlutterVersion version;
final DateTime localFrameworkCommitDate;
final SystemClock clock;
final Cache cache;
final Logger logger;
final Duration pauseTime;
final DateTime? latestFlutterCommitDate;
late final DateTime now = clock.now();
late final Duration frameworkAge = now.difference(localFrameworkCommitDate);
@visibleForTesting
static const checkAgeConsideredUpToDate = Duration(days: 3);
@visibleForTesting
static const maxTimeSinceLastWarning = Duration(days: 1);
@visibleForTesting
static Duration timeToPauseToLetUserReadTheMessage = const Duration(seconds: 2);
@visibleForTesting
bool canShowWarning(VersionCheckResult remoteVersionStatus) {
final bool installationSeemsOutdated =
frameworkAge > versionAgeConsideredUpToDate(version.channel);
if (remoteVersionStatus == VersionCheckResult.newVersionAvailable) {
return true;
}
if (!installationSeemsOutdated) {
return false;
}
return remoteVersionStatus == VersionCheckResult.unknown;
}
@visibleForTesting
static Duration versionAgeConsideredUpToDate(String channel) {
return switch (channel) {
'stable' => const Duration(days: 365 ~/ 2),
'beta' => const Duration(days: 7 * 8),
_ => const Duration(days: 7 * 3),
};
}
Future<void> run() async {
final VersionCheckResult remoteVersionStatus;
if (latestFlutterCommitDate == null) {
remoteVersionStatus = VersionCheckResult.unknown;
} else {
if (latestFlutterCommitDate!.isAfter(localFrameworkCommitDate)) {
remoteVersionStatus = VersionCheckResult.newVersionAvailable;
} else {
remoteVersionStatus = VersionCheckResult.versionIsCurrent;
}
}
final VersionCheckStamp stamp = await VersionCheckStamp.load(cache, logger);
final DateTime lastTimeWarningWasPrinted =
stamp.lastTimeWarningWasPrinted ?? clock.ago(maxTimeSinceLastWarning * 2);
final bool beenAWhileSinceWarningWasPrinted =
now.difference(lastTimeWarningWasPrinted) > maxTimeSinceLastWarning;
if (!beenAWhileSinceWarningWasPrinted) {
return;
}
final bool canShowWarningResult = canShowWarning(remoteVersionStatus);
if (!canShowWarningResult) {
return;
}
final String updateMessage;
switch (remoteVersionStatus) {
case VersionCheckResult.newVersionAvailable:
updateMessage = _newVersionAvailableMessage;
case VersionCheckResult.versionIsCurrent:
case VersionCheckResult.unknown:
updateMessage = versionOutOfDateMessage(frameworkAge);
}
logger.printBox(updateMessage);
await Future.wait<void>(<Future<void>>[
stamp.store(newTimeWarningWasPrinted: now, cache: cache),
Future<void>.delayed(pauseTime),
]);
}
}
@visibleForTesting
String versionOutOfDateMessage(Duration frameworkAge) {
return '''
WARNING: your installation of Flutter is ${frameworkAge.inDays} days old.
To update to the latest version, run "flutter upgrade".''';
}
const _newVersionAvailableMessage = '''
A new version of Flutter is available!
To update to the latest version, run "flutter upgrade".''';
@visibleForTesting
final class FlutterEngineStampFromFile {
FlutterEngineStampFromFile._({
required this.buildDate,
required this.gitRevision,
required this.gitRevisionDate,
required this.contentHash,
});
final DateTime buildDate;
final String gitRevision;
final DateTime gitRevisionDate;
final String contentHash;
static FlutterEngineStampFromFile? tryParseFromFile(File jsonFile) {
final Map<String, Object?> data;
try {
data = json.decode(jsonFile.readAsStringSync()) as Map<String, Object?>;
if (data case {
'build_time_ms': final int buildTimeMs,
'git_revision': final String gitRevision,
'git_revision_date': final String gitRevisionDate,
'content_hash': final String contentHash,
}) {
return FlutterEngineStampFromFile._(
buildDate: DateTime.fromMillisecondsSinceEpoch(buildTimeMs),
gitRevision: gitRevision,
gitRevisionDate: DateTime.parse(gitRevisionDate),
contentHash: contentHash,
);
}
} catch (_) {
return null;
}
return null;
}
}
bool _isStandardRemote(String remote) {
final String sanitized = VersionUpstreamValidator.stripDotGit(remote);
final String? customGitUrl = globals.platform.environment['FLUTTER_GIT_URL'];
if (customGitUrl != null && VersionUpstreamValidator.stripDotGit(customGitUrl) == sanitized) {
return true;
}
for (final standard in const <String>[
'https://github.com/flutter/flutter.git',
'git@github.com:flutter/flutter.git',
'ssh://git@github.com/flutter/flutter.git',
]) {
if (VersionUpstreamValidator.stripDotGit(standard) == sanitized) {
return true;
}
}
return false;
}