[flutter_tools] cache flutter sdk version to disk (#124558)

Fixes https://github.com/flutter/flutter/issues/112833

Most of the actual changes here are in [packages/flutter_tools/lib/src/version.dart](https://github.com/flutter/flutter/pull/124558/files#diff-092e00109d9e1589fbc7c6de750e29a6ae512b2dd44e85d60028953561201605), while the rest is largely just addressing changes to the constructor of `FlutterVersion` which now has different dependencies.

This change makes `FlutterVersion` an interface with two concrete implementations:

1. `_FlutterVersionGit` which is mostly the previous implementation, and
2. `_FlutterVersionFromFile` which will read a new `.version.json` file from the root of the repo

The [`FlutterVersion` constructor](https://github.com/flutter/flutter/pull/124558/files#diff-092e00109d9e1589fbc7c6de750e29a6ae512b2dd44e85d60028953561201605R70) is now a factory that first checks if `.version.json` exists, and if so returns an instance of `_FlutterVersionFromGit` else it returns the fallback `_FlutterVersionGit` which will end up writing `.version.json` so that we don't need to re-calculate the version on the next invocation.

`.version.json` will be deleted in the bash/batch entrypoints any time we need to rebuild he tool (this will usually be because the user did `flutter upgrade` or `flutter channel`, or manually changed the commit with git).
This commit is contained in:
Christopher Fujino 2023-06-14 17:20:30 -07:00 committed by GitHub
parent 6d2b5ea30e
commit 3246808cd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 714 additions and 231 deletions

View File

@ -128,6 +128,7 @@ GOTO :after_subroutine
:do_snapshot
IF EXIST "%FLUTTER_ROOT%\version" DEL "%FLUTTER_ROOT%\version"
IF EXIST "%FLUTTER_ROOT%\bin\cache\flutter.version.json" DEL "%FLUTTER_ROOT%\bin\cache\flutter.version.json"
ECHO: > "%cache_dir%\.dartignore"
ECHO Building flutter tool... 1>&2
PUSHD "%flutter_tools_dir%"

View File

@ -123,7 +123,10 @@ function upgrade_flutter () (
# * STAMP_PATH is an empty file, or
# * Contents of STAMP_PATH is not what we are going to compile, or
# * pubspec.yaml last modified after pubspec.lock
if [[ ! -f "$SNAPSHOT_PATH" || ! -s "$STAMP_PATH" || "$(cat "$STAMP_PATH")" != "$compilekey" || "$FLUTTER_TOOLS_DIR/pubspec.yaml" -nt "$FLUTTER_TOOLS_DIR/pubspec.lock" ]]; then
if [[ ! -f "$SNAPSHOT_PATH" || \
! -s "$STAMP_PATH" || \
"$(cat "$STAMP_PATH")" != "$compilekey" || \
"$FLUTTER_TOOLS_DIR/pubspec.yaml" -nt "$FLUTTER_TOOLS_DIR/pubspec.lock" ]]; then
# Waits for the update lock to be acquired. Placing this check inside the
# conditional allows the majority of flutter/dart installations to bypass
# the lock entirely, but as a result this required a second verification that
@ -137,6 +140,7 @@ function upgrade_flutter () (
# Fetch Dart...
rm -f "$FLUTTER_ROOT/version"
rm -f "$FLUTTER_ROOT/bin/cache/flutter.version.json"
touch "$FLUTTER_ROOT/bin/cache/.dartignore"
"$FLUTTER_ROOT/bin/internal/update_dart_sdk.sh"

View File

@ -33,7 +33,6 @@ class DoctorCommand extends FlutterCommand {
@override
Future<FlutterCommandResult> runCommand() async {
globals.flutterVersion.fetchTagsAndUpdate();
if (argResults?.wasParsed('check-for-remote-artifacts') ?? false) {
final String engineRevision = stringArg('check-for-remote-artifacts')!;
if (engineRevision.startsWith(RegExp(r'[a-f0-9]{1,40}'))) {

View File

@ -92,7 +92,10 @@ class DowngradeCommand extends FlutterCommand {
String workingDirectory = Cache.flutterRoot!;
if (argResults!.wasParsed('working-directory')) {
workingDirectory = stringArg('working-directory')!;
_flutterVersion = FlutterVersion(workingDirectory: workingDirectory);
_flutterVersion = FlutterVersion(
fs: _fileSystem!,
flutterRoot: workingDirectory,
);
}
final String currentChannel = _flutterVersion!.channel;

View File

@ -80,7 +80,7 @@ class UpgradeCommand extends FlutterCommand {
gitTagVersion: GitTagVersion.determine(globals.processUtils, globals.platform),
flutterVersion: stringArg('working-directory') == null
? globals.flutterVersion
: FlutterVersion(workingDirectory: _commandRunner.workingDirectory),
: FlutterVersion(flutterRoot: _commandRunner.workingDirectory!, fs: globals.fs),
verifyOnly: boolArg('verify-only'),
);
}
@ -297,7 +297,11 @@ class UpgradeCommandRunner {
'for instructions.'
);
}
return FlutterVersion(workingDirectory: workingDirectory, frameworkRevision: revision);
return FlutterVersion.fromRevision(
flutterRoot: workingDirectory!,
frameworkRevision: revision,
fs: globals.fs,
);
}
/// Attempts a hard reset to the given revision.

View File

@ -226,7 +226,10 @@ Future<T> runInContext<T>(
config: globals.config,
platform: globals.platform,
),
FlutterVersion: () => FlutterVersion(),
FlutterVersion: () => FlutterVersion(
fs: globals.fs,
flutterRoot: Cache.flutterRoot!,
),
FuchsiaArtifacts: () => FuchsiaArtifacts.find(),
FuchsiaDeviceTools: () => FuchsiaDeviceTools(),
FuchsiaSdk: () => FuchsiaSdk(),

View File

@ -123,7 +123,7 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
FlutterValidator(
fileSystem: globals.fs,
platform: globals.platform,
flutterVersion: () => globals.flutterVersion,
flutterVersion: () => globals.flutterVersion.fetchTagsAndGetVersion(clock: globals.systemClock),
devToolsVersion: () => globals.cache.devToolsVersion,
processManager: globals.processManager,
userMessages: userMessages,

View File

@ -136,7 +136,10 @@ class VariableDumpMachineProjectValidator extends MachineProjectValidator {
));
// FlutterVersion
final FlutterVersion version = FlutterVersion(workingDirectory: project.directory.absolute.path);
final FlutterVersion version = FlutterVersion(
flutterRoot: Cache.flutterRoot!,
fs: fileSystem,
);
result.add(ProjectValidatorResult(
name: 'FlutterVersion.frameworkRevision',
value: _toJsonValue(version.frameworkRevision),

View File

@ -18,6 +18,7 @@ import '../cache.dart';
import '../convert.dart';
import '../globals.dart' as globals;
import '../tester/flutter_tester.dart';
import '../version.dart';
import '../web/web_device.dart';
/// Common flutter command line options.
@ -318,14 +319,16 @@ class FlutterCommandRunner extends CommandRunner<void> {
if ((topLevelResults[FlutterGlobalOptions.kVersionFlag] as bool?) ?? false) {
globals.flutterUsage.sendCommand(FlutterGlobalOptions.kVersionFlag);
globals.flutterVersion.fetchTagsAndUpdate();
String status;
final FlutterVersion version = globals.flutterVersion.fetchTagsAndGetVersion(
clock: globals.systemClock,
);
final String status;
if (machineFlag) {
final Map<String, Object> jsonOut = globals.flutterVersion.toJson();
final Map<String, Object> jsonOut = version.toJson();
jsonOut['flutterRoot'] = Cache.flutterRoot!;
status = const JsonEncoder.withIndent(' ').convert(jsonOut);
} else {
status = globals.flutterVersion.toString();
status = version.toString();
}
globals.printStatus(status);
return;

View File

@ -79,104 +79,153 @@ Channel? getChannelForName(String name) {
return null;
}
class FlutterVersion {
abstract class FlutterVersion {
/// Parses the Flutter version from currently available tags in the local
/// repo.
///
/// Call [fetchTagsAndUpdate] to update the version based on the latest tags
/// available upstream.
FlutterVersion({
factory FlutterVersion({
SystemClock clock = const SystemClock(),
String? workingDirectory,
String? frameworkRevision,
}) : _clock = clock,
_workingDirectory = workingDirectory {
_frameworkRevision = frameworkRevision ?? _runGit(
gitLog(<String>['-n', '1', '--pretty=format:%H']).join(' '),
globals.processUtils,
_workingDirectory,
);
_gitTagVersion = GitTagVersion.determine(globals.processUtils, globals.platform, workingDirectory: _workingDirectory, gitRef: _frameworkRevision);
_frameworkVersion = gitTagVersion.frameworkVersionFor(_frameworkRevision);
}
required FileSystem fs,
required String flutterRoot,
@protected
bool fetchTags = false,
}) {
final File versionFile = getVersionFile(fs, flutterRoot);
final SystemClock _clock;
final String? _workingDirectory;
/// Fetches tags from the upstream Flutter repository and re-calculates the
/// version.
///
/// This carries a performance penalty, and should only be called when the
/// user explicitly wants to get the version, e.g. for `flutter --version` or
/// `flutter doctor`.
void fetchTagsAndUpdate() {
_gitTagVersion = GitTagVersion.determine(globals.processUtils, globals.platform, workingDirectory: _workingDirectory, fetchTags: true);
_frameworkVersion = gitTagVersion.frameworkVersionFor(_frameworkRevision);
}
String? _repositoryUrl;
String? get repositoryUrl {
if (_repositoryUrl == null) {
final String gitChannel = _runGit(
'git rev-parse --abbrev-ref --symbolic $kGitTrackingUpstream',
globals.processUtils,
_workingDirectory,
if (!fetchTags && versionFile.existsSync()) {
final _FlutterVersionFromFile? version = _FlutterVersionFromFile.tryParseFromFile(
versionFile,
flutterRoot: flutterRoot,
);
final int slash = gitChannel.indexOf('/');
if (slash != -1) {
final String remote = gitChannel.substring(0, slash);
_repositoryUrl = _runGit(
'git ls-remote --get-url $remote',
globals.processUtils,
_workingDirectory,
);
if (version != null) {
return version;
}
}
return _repositoryUrl;
}
/// The channel is the current branch if we recognize it, or "[user-branch]" (kUserBranch).
/// `master`, `beta`, `stable`; or old ones, like `alpha`, `hackathon`, `dev`, ...
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;
}
// if we are fetching tags, ignore cached versionFile
if (fetchTags && versionFile.existsSync()) {
versionFile.deleteSync();
final File legacyVersionFile = fs.file(fs.path.join(flutterRoot, 'version'));
if (legacyVersionFile.existsSync()) {
legacyVersionFile.deleteSync();
}
}
late GitTagVersion _gitTagVersion;
GitTagVersion get gitTagVersion => _gitTagVersion;
/// The name of the local branch.
/// Use getBranchName() to read this.
String? _branch;
late String _frameworkRevision;
String get frameworkRevision => _frameworkRevision;
String get frameworkRevisionShort => _shortGitRevision(frameworkRevision);
String? _frameworkAge;
String get frameworkAge {
return _frameworkAge ??= _runGit(
gitLog(<String>['-n', '1', '--pretty=format:%ar']).join(' '),
final String frameworkRevision = _runGit(
gitLog(<String>['-n', '1', '--pretty=format:%H']).join(' '),
globals.processUtils,
_workingDirectory,
flutterRoot,
);
return FlutterVersion.fromRevision(
clock: clock,
frameworkRevision: frameworkRevision,
fs: fs,
flutterRoot: flutterRoot,
fetchTags: fetchTags,
);
}
late String _frameworkVersion;
String get frameworkVersion => _frameworkVersion;
FlutterVersion._({
required SystemClock clock,
required this.flutterRoot,
required this.fs,
}) : _clock = clock;
String get devToolsVersion => globals.cache.devToolsVersion;
factory FlutterVersion.fromRevision({
SystemClock clock = const SystemClock(),
required String flutterRoot,
required String frameworkRevision,
required FileSystem fs,
bool fetchTags = false,
}) {
final GitTagVersion gitTagVersion = GitTagVersion.determine(
globals.processUtils,
globals.platform,
gitRef: frameworkRevision,
workingDirectory: flutterRoot,
fetchTags: fetchTags,
);
final String frameworkVersion = gitTagVersion.frameworkVersionFor(frameworkRevision);
return _FlutterVersionGit._(
clock: clock,
flutterRoot: flutterRoot,
frameworkRevision: frameworkRevision,
frameworkVersion: frameworkVersion,
gitTagVersion: gitTagVersion,
fs: fs,
);
}
String get dartSdkVersion => globals.cache.dartSdkVersion;
/// Ensure the latest git tags are fetched and recalculate [FlutterVersion].
///
/// This is only required when not on beta or stable and we need to calculate
/// the current version relative to upstream tags.
///
/// This is a method and not a factory constructor so that test classes can
/// override it.
FlutterVersion fetchTagsAndGetVersion({
SystemClock clock = const SystemClock(),
}) {
// We don't need to fetch tags on beta and stable to calculate the version,
// we should already exactly be on a tag that was pushed when this release
// was published.
if (channel != 'master' && channel != 'main') {
return this;
}
return FlutterVersion(
clock: clock,
flutterRoot: flutterRoot,
fs: fs,
fetchTags: true,
);
}
String get engineRevision => globals.cache.engineRevision;
final FileSystem fs;
final SystemClock _clock;
String? get repositoryUrl;
GitTagVersion get gitTagVersion;
/// The channel is the upstream branch.
///
/// `master`, `dev`, `beta`, `stable`; or old ones, like `alpha`, `hackathon`, ...
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 engineRevisionShort => _shortGitRevision(engineRevision);
void ensureVersionFile() {
globals.fs.file(globals.fs.path.join(Cache.flutterRoot!, 'version')).writeAsStringSync(_frameworkVersion);
// This is static as it is called from a constructor.
static File getVersionFile(FileSystem fs, String flutterRoot) {
return fs.file(fs.path.join(flutterRoot, 'bin', 'cache', 'flutter.version.json'));
}
final String flutterRoot;
String? _frameworkAge;
// TODO(fujino): calculate this relative to frameworkCommitDate for
// _FlutterVersionFromFile so we don't need a git call.
String get frameworkAge {
return _frameworkAge ??= _runGit(
FlutterVersion.gitLog(<String>['-n', '1', '--pretty=format:%ar']).join(' '),
globals.processUtils,
flutterRoot,
);
}
void ensureVersionFile();
@override
String toString() {
final String versionText = frameworkVersion == _unknownFrameworkVersion ? '' : ' $frameworkVersion';
@ -202,47 +251,13 @@ class FlutterVersion {
'engineRevision': engineRevision,
'dartSdkVersion': dartSdkVersion,
'devToolsVersion': devToolsVersion,
'flutterVersion': frameworkVersion,
};
String get frameworkDate => frameworkCommitDate;
/// A date String describing the last framework commit.
///
/// If a git command fails, this will return a placeholder date.
String get frameworkCommitDate => _gitCommitDate(lenient: true);
// The date of the given commit hash as [gitRef]. If no hash is specified,
// then it is the HEAD of the current local branch.
//
// If lenient is true, and the git command fails, a placeholder date is
// returned. Otherwise, the VersionCheckError exception is propagated.
static String _gitCommitDate({
String gitRef = 'HEAD',
bool lenient = false,
}) {
final List<String> args = gitLog(<String>[
gitRef,
'-n',
'1',
'--pretty=format:%ad',
'--date=iso',
]);
try {
// Don't plumb 'lenient' through directly so that we can print an error
// if something goes wrong.
return _runSync(args, lenient: false);
} on VersionCheckError catch (e) {
if (lenient) {
final DateTime dummyDate = DateTime.fromMillisecondsSinceEpoch(0);
globals.printError('Failed to find the latest git commit date: $e\n'
'Returning $dummyDate instead.');
// Return something that DateTime.parse() can parse.
return dummyDate.toString();
} else {
rethrow;
}
}
}
String get frameworkCommitDate;
/// Checks if the currently installed version of Flutter is up-to-date, and
/// warns the user if it isn't.
@ -261,7 +276,7 @@ class FlutterVersion {
DateTime localFrameworkCommitDate;
try {
// Don't perform the update check if fetching the latest local commit failed.
localFrameworkCommitDate = DateTime.parse(_gitCommitDate());
localFrameworkCommitDate = DateTime.parse(_gitCommitDate(workingDirectory: flutterRoot));
} on VersionCheckError {
return;
}
@ -278,6 +293,53 @@ class FlutterVersion {
).run();
}
/// Gets the release date of the latest available Flutter version.
///
/// This method sends a server request if it's been more than
/// [checkAgeConsideredUpToDate] since the last version check.
///
/// Returns null if the cached version is out-of-date or missing, and we are
/// unable to reach the server to get the latest version.
Future<DateTime?> _getLatestAvailableFlutterDate() async {
globals.cache.checkLockAcquired();
final VersionCheckStamp versionCheckStamp = await VersionCheckStamp.load(globals.cache, globals.logger);
final DateTime now = _clock.now();
if (versionCheckStamp.lastTimeVersionWasChecked != null) {
final Duration timeSinceLastCheck = now.difference(
versionCheckStamp.lastTimeVersionWasChecked!,
);
// Don't ping the server too often. Return cached value if it's fresh.
if (timeSinceLastCheck < VersionFreshnessValidator.checkAgeConsideredUpToDate) {
return versionCheckStamp.lastKnownRemoteVersion;
}
}
// Cache is empty or it's been a while since the last server ping. Ping the server.
try {
final DateTime remoteFrameworkCommitDate = DateTime.parse(
await fetchRemoteFrameworkCommitDate(),
);
await versionCheckStamp.store(
newTimeVersionWasChecked: now,
newKnownRemoteVersion: remoteFrameworkCommitDate,
);
return remoteFrameworkCommitDate;
} on VersionCheckError catch (error) {
// This happens when any of the git commands fails, which can happen when
// there's no Internet connectivity. Remote version check is best effort
// only. We do not prevent the command from running when it fails.
globals.printTrace('Failed to check Flutter version in the remote repository: $error');
// Still update the timestamp to avoid us hitting the server on every single
// command if for some reason we cannot connect (eg. we may be offline).
await versionCheckStamp.store(
newTimeVersionWasChecked: now,
);
return null;
}
}
/// The date of the latest framework commit in the remote repository.
///
/// Throws [VersionCheckError] if a git command fails, for example, when the
@ -286,7 +348,7 @@ class FlutterVersion {
try {
// Fetch upstream branch's commit and tags
await _run(<String>['git', 'fetch', '--tags']);
return _gitCommitDate(gitRef: kGitTrackingUpstream);
return _gitCommitDate(gitRef: kGitTrackingUpstream, workingDirectory: Cache.flutterRoot);
} on VersionCheckError catch (error) {
globals.printError(error.message);
rethrow;
@ -301,13 +363,18 @@ class FlutterVersion {
return '${getBranchName(redactUnknownBranches: redactUnknownBranches)}/$frameworkRevisionShort';
}
/// The name of the local branch.
///
/// Use getBranchName() to read this.
String? _branch;
/// Return the branch name.
///
/// If [redactUnknownBranches] is true and the branch is unknown,
/// the branch name will be returned as `'[user-branch]'` ([kUserBranch]).
String getBranchName({ bool redactUnknownBranches = false }) {
_branch ??= () {
final String branch = _runGit('git symbolic-ref --short HEAD', globals.processUtils, _workingDirectory);
final String branch = _runGit('git symbolic-ref --short HEAD', globals.processUtils, flutterRoot);
return branch == 'HEAD' ? '' : branch;
}();
if (redactUnknownBranches || _branch!.isEmpty) {
@ -342,51 +409,205 @@ class FlutterVersion {
static List<String> gitLog(List<String> args) {
return <String>['git', '-c', 'log.showSignature=false', 'log'] + args;
}
}
/// Gets the release date of the latest available Flutter version.
///
/// This method sends a server request if it's been more than
/// [checkAgeConsideredUpToDate] since the last version check.
///
/// Returns null if the cached version is out-of-date or missing, and we are
/// unable to reach the server to get the latest version.
Future<DateTime?> _getLatestAvailableFlutterDate() async {
globals.cache.checkLockAcquired();
final VersionCheckStamp versionCheckStamp = await VersionCheckStamp.load(globals.cache, globals.logger);
// The date of the given commit hash as [gitRef]. If no hash is specified,
// then it is the HEAD of the current local branch.
//
// If lenient is true, and the git command fails, a placeholder date is
// returned. Otherwise, the VersionCheckError exception is propagated.
String _gitCommitDate({
String gitRef = 'HEAD',
bool lenient = false,
required String? workingDirectory,
}) {
final List<String> args = FlutterVersion.gitLog(<String>[
gitRef,
'-n',
'1',
'--pretty=format:%ad',
'--date=iso',
]);
try {
// Don't plumb 'lenient' through directly so that we can print an error
// if something goes wrong.
return _runSync(
args,
lenient: false,
workingDirectory: workingDirectory,
);
} on VersionCheckError catch (e) {
if (lenient) {
final DateTime dummyDate = DateTime.fromMillisecondsSinceEpoch(0);
globals.printError('Failed to find the latest git commit date: $e\n'
'Returning $dummyDate instead.');
// Return something that DateTime.parse() can parse.
return dummyDate.toString();
} else {
rethrow;
}
}
}
final DateTime now = _clock.now();
if (versionCheckStamp.lastTimeVersionWasChecked != null) {
final Duration timeSinceLastCheck = now.difference(
versionCheckStamp.lastTimeVersionWasChecked!,
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.dartSdkVersion,
required this.devToolsVersion,
required this.gitTagVersion,
required super.flutterRoot,
required super.fs,
}) : super._();
static _FlutterVersionFromFile? tryParseFromFile(
File jsonFile, {
required String flutterRoot,
SystemClock clock = const SystemClock(),
}) {
try {
final String jsonContents = jsonFile.readAsStringSync();
final Map<String, Object?> manifest = jsonDecode(jsonContents) as Map<String, Object?>;
return _FlutterVersionFromFile._(
clock: clock,
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,
dartSdkVersion: manifest['dartSdkVersion']! as String,
devToolsVersion: manifest['devToolsVersion']! as String,
gitTagVersion: GitTagVersion.parse(manifest['flutterVersion']! as String),
flutterRoot: flutterRoot,
fs: jsonFile.fileSystem,
);
// ignore: avoid_catches_without_on_clauses
} catch (err) {
globals.printTrace('Failed to parse ${jsonFile.path} with $err');
try {
jsonFile.deleteSync();
} on FileSystemException {
globals.printTrace('Failed to delete ${jsonFile.path}');
}
// Returning null means fallback to git implementation.
return null;
}
}
// Don't ping the server too often. Return cached value if it's fresh.
if (timeSinceLastCheck < VersionFreshnessValidator.checkAgeConsideredUpToDate) {
return versionCheckStamp.lastKnownRemoteVersion;
@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 engineRevision;
@override
final String dartSdkVersion;
@override
final String devToolsVersion;
@override
void ensureVersionFile() {}
}
class _FlutterVersionGit extends FlutterVersion {
_FlutterVersionGit._({
required super.clock,
required super.flutterRoot,
required this.frameworkRevision,
required this.frameworkVersion,
required this.gitTagVersion,
required super.fs,
}) : super._();
@override
final GitTagVersion gitTagVersion;
@override
final String frameworkRevision;
@override
String get frameworkCommitDate => _gitCommitDate(lenient: true, workingDirectory: flutterRoot);
String? _repositoryUrl;
@override
String? get repositoryUrl {
if (_repositoryUrl == null) {
final String gitChannel = _runGit(
'git rev-parse --abbrev-ref --symbolic $kGitTrackingUpstream',
globals.processUtils,
flutterRoot,
);
final int slash = gitChannel.indexOf('/');
if (slash != -1) {
final String remote = gitChannel.substring(0, slash);
_repositoryUrl = _runGit(
'git ls-remote --get-url $remote',
globals.processUtils,
flutterRoot,
);
}
}
return _repositoryUrl;
}
// Cache is empty or it's been a while since the last server ping. Ping the server.
try {
final DateTime remoteFrameworkCommitDate = DateTime.parse(
await FlutterVersion.fetchRemoteFrameworkCommitDate(),
);
await versionCheckStamp.store(
newTimeVersionWasChecked: now,
newKnownRemoteVersion: remoteFrameworkCommitDate,
);
return remoteFrameworkCommitDate;
} on VersionCheckError catch (error) {
// This happens when any of the git commands fails, which can happen when
// there's no Internet connectivity. Remote version check is best effort
// only. We do not prevent the command from running when it fails.
globals.printTrace('Failed to check Flutter version in the remote repository: $error');
// Still update the timestamp to avoid us hitting the server on every single
// command if for some reason we cannot connect (eg. we may be offline).
await versionCheckStamp.store(
newTimeVersionWasChecked: now,
);
return null;
@override
String get devToolsVersion => globals.cache.devToolsVersion;
@override
String get dartSdkVersion => globals.cache.dartSdkVersion;
@override
String get engineRevision => globals.cache.engineRevision;
@override
final String frameworkVersion;
/// The channel is the current branch if we recognize it, or "[user-branch]" (kUserBranch).
/// `master`, `beta`, `stable`; or old ones, like `alpha`, `hackathon`, `dev`, ...
@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() {
final File legacyVersionFile = fs.file(fs.path.join(flutterRoot, 'version'));
if (!legacyVersionFile.existsSync()) {
legacyVersionFile.writeAsStringSync(frameworkVersion);
}
const JsonEncoder encoder = JsonEncoder.withIndent(' ');
final File newVersionFile = FlutterVersion.getVersionFile(fs, flutterRoot);
if (!newVersionFile.existsSync()) {
newVersionFile.writeAsStringSync(encoder.convert(toJson()));
}
}
}
@ -606,10 +827,14 @@ class VersionCheckError implements Exception {
///
/// If [lenient] is true and the command fails, returns an empty string.
/// Otherwise, throws a [ToolExit] exception.
String _runSync(List<String> command, { bool lenient = true }) {
String _runSync(
List<String> command, {
bool lenient = true,
required String? workingDirectory,
}) {
final ProcessResult results = globals.processManager.runSync(
command,
workingDirectory: Cache.flutterRoot,
workingDirectory: workingDirectory,
);
if (results.exitCode == 0) {
@ -630,7 +855,7 @@ String _runSync(List<String> command, { bool lenient = true }) {
String _runGit(String command, ProcessUtils processUtils, String? workingDirectory) {
return processUtils.runSync(
command.split(' '),
workingDirectory: workingDirectory ?? Cache.flutterRoot,
workingDirectory: workingDirectory,
).stdout.trim();
}

View File

@ -13,8 +13,10 @@ import 'base/context.dart';
import 'base/io.dart' as io;
import 'base/logger.dart';
import 'base/utils.dart';
import 'cache.dart';
import 'convert.dart';
import 'device.dart';
import 'globals.dart' as globals;
import 'ios/xcodeproj.dart';
import 'project.dart';
import 'version.dart';
@ -244,7 +246,10 @@ Future<vm_service.VmService> setUpVmService({
}
vmService.registerServiceCallback(kFlutterVersionServiceName, (Map<String, Object?> params) async {
final FlutterVersion version = context.get<FlutterVersion>() ?? FlutterVersion();
final FlutterVersion version = context.get<FlutterVersion>() ?? FlutterVersion(
fs: globals.fs,
flutterRoot: Cache.flutterRoot!,
);
final Map<String, Object> versionJson = version.toJson();
versionJson['frameworkRevisionShort'] = version.frameworkRevisionShort;
versionJson['engineRevisionShort'] = version.engineRevisionShort;

View File

@ -4,7 +4,6 @@
import 'dart:async';
import 'package:args/command_runner.dart';
import 'package:fake_async/fake_async.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_studio_validator.dart';
@ -16,7 +15,6 @@ import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/doctor.dart';
import 'package:flutter_tools/src/custom_devices/custom_device_workflow.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/doctor.dart';
@ -32,17 +30,16 @@ import 'package:test/fake.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fakes.dart';
import '../../src/test_flutter_command_runner.dart';
void main() {
late FakeFlutterVersion flutterVersion;
late BufferLogger logger;
late FakeProcessManager fakeProcessManager;
late MemoryFileSystem fs;
setUp(() {
flutterVersion = FakeFlutterVersion();
logger = BufferLogger.test();
fakeProcessManager = FakeProcessManager.empty();
fs = MemoryFileSystem.test();
});
testWithoutContext('ValidationMessage equality and hashCode includes contextUrl', () {
@ -761,27 +758,55 @@ void main() {
contains(isA<CustomDeviceWorkflow>()),
);
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(),
FileSystem: () => fs,
ProcessManager: () => fakeProcessManager,
});
testUsingContext('Fetches tags to get the right version', () async {
Cache.disableLocking();
group('FlutterValidator', () {
late FakeFlutterVersion initialVersion;
late FakeFlutterVersion secondVersion;
late TestFeatureFlags featureFlags;
final DoctorCommand doctorCommand = DoctorCommand();
final CommandRunner<void> commandRunner = createTestCommandRunner(doctorCommand);
await commandRunner.run(<String>['doctor']);
expect(flutterVersion.didFetchTagsAndUpdate, true);
Cache.enableLocking();
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FileSystem: () => MemoryFileSystem.test(),
FlutterVersion: () => flutterVersion,
Doctor: () => NoOpDoctor(),
}, initializeFlutterRoot: false);
setUp(() {
secondVersion = FakeFlutterVersion(frameworkRevisionShort: '222');
initialVersion = FakeFlutterVersion(
frameworkRevisionShort: '111',
nextFlutterVersion: secondVersion,
);
featureFlags = TestFeatureFlags();
});
testUsingContext('FlutterValidator fetches tags and gets fresh version', () async {
final Directory devtoolsDir = fs.directory('/path/to/flutter/bin/cache/dart-sdk/bin/resources/devtools')
..createSync(recursive: true);
fs.directory('/path/to/flutter/bin/cache/artifacts').createSync(recursive: true);
devtoolsDir.childFile('version.json').writeAsStringSync('{"version": "123"}');
fakeProcessManager.addCommands(const <FakeCommand>[
FakeCommand(command: <String>['which', 'java']),
]);
final List<DoctorValidator> validators = DoctorValidatorsProvider.test(
featureFlags: featureFlags,
platform: FakePlatform(),
).validators;
final FlutterValidator flutterValidator = validators.whereType<FlutterValidator>().first;
final ValidationResult result = await flutterValidator.validate();
expect(
result.messages.map((ValidationMessage msg) => msg.message),
contains(contains('Framework revision 222')),
);
}, overrides: <Type, Generator>{
Cache: () => Cache.test(
rootOverride: fs.directory('/path/to/flutter'),
fileSystem: fs,
processManager: fakeProcessManager,
),
FileSystem: () => fs,
FlutterVersion: () => initialVersion,
Platform: () => FakePlatform(),
ProcessManager: () => fakeProcessManager,
TestFeatureFlags: () => featureFlags,
});
});
testUsingContext('If android workflow is disabled, AndroidStudio validator is not included', () {
final DoctorValidatorsProvider provider = DoctorValidatorsProvider.test(
featureFlags: TestFeatureFlags(isAndroidEnabled: false),
@ -826,6 +851,7 @@ class NoOpDoctor implements Doctor {
bool showPii = true,
List<ValidatorTask>? startedValidatorTasks,
bool sendEvent = true,
FlutterVersion? version,
}) async => true;
@override

View File

@ -37,7 +37,8 @@ void main() {
setUp(() {
fakeCommandRunner = FakeUpgradeCommandRunner();
realCommandRunner = UpgradeCommandRunner();
realCommandRunner = UpgradeCommandRunner()
..workingDirectory = Cache.flutterRoot;
processManager = FakeProcessManager.empty();
fakeCommandRunner.willHaveUncommittedChanges = false;
fakePlatform = FakePlatform()..environment = Map<String, String>.unmodifiable(<String, String>{

View File

@ -41,11 +41,14 @@ void main() {
group('analytics', () {
late Directory tempDir;
late Config testConfig;
late FileSystem fs;
const String flutterRoot = '/path/to/flutter';
setUp(() {
Cache.flutterRoot = '../..';
Cache.flutterRoot = flutterRoot;
tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_analytics_test.');
testConfig = Config.test();
fs = MemoryFileSystem.test();
});
tearDown(() {
@ -77,7 +80,7 @@ void main() {
expect(count, 0);
}, overrides: <Type, Generator>{
FlutterVersion: () => FlutterVersion(),
FlutterVersion: () => FakeFlutterVersion(),
Usage: () => Usage(
configDirOverride: tempDir.path,
logFile: tempDir.childFile('analytics.log').path,
@ -101,7 +104,7 @@ void main() {
expect(count, 0);
}, overrides: <Type, Generator>{
FlutterVersion: () => FlutterVersion(),
FlutterVersion: () => FakeFlutterVersion(),
Usage: () => Usage(
configDirOverride: tempDir.path,
logFile: tempDir.childFile('analytics.log').path,
@ -118,12 +121,12 @@ void main() {
expect(globals.fs.file('test').readAsStringSync(), contains('$featuresKey: enable-web'));
}, overrides: <Type, Generator>{
FlutterVersion: () => FlutterVersion(),
FlutterVersion: () => FakeFlutterVersion(),
Config: () => testConfig,
Platform: () => FakePlatform(environment: <String, String>{
'FLUTTER_ANALYTICS_LOG_FILE': 'test',
}),
FileSystem: () => MemoryFileSystem.test(),
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
@ -141,12 +144,12 @@ void main() {
contains('$featuresKey: enable-web,enable-linux-desktop,enable-macos-desktop'),
);
}, overrides: <Type, Generator>{
FlutterVersion: () => FlutterVersion(),
FlutterVersion: () => FakeFlutterVersion(),
Config: () => testConfig,
Platform: () => FakePlatform(environment: <String, String>{
'FLUTTER_ANALYTICS_LOG_FILE': 'test',
}),
FileSystem: () => MemoryFileSystem.test(),
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
});
@ -384,6 +387,7 @@ class FakeDoctor extends Fake implements Doctor {
bool showPii = true,
List<ValidatorTask>? startedValidatorTasks,
bool sendEvent = true,
FlutterVersion? version,
}) async {
return diagnoseSucceeds;
}

View File

@ -4,12 +4,13 @@
import 'dart:convert';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/version.dart';
import 'package:test/fake.dart';
@ -18,7 +19,7 @@ import '../src/context.dart';
import '../src/fake_process_manager.dart';
import '../src/fakes.dart' show FakeFlutterVersion;
final SystemClock _testClock = SystemClock.fixed(DateTime(2015));
final SystemClock _testClock = SystemClock.fixed(DateTime.utc(2015));
final DateTime _stampUpToDate = _testClock.ago(VersionFreshnessValidator.checkAgeConsideredUpToDate ~/ 2);
final DateTime _stampOutOfDate = _testClock.ago(VersionFreshnessValidator.checkAgeConsideredUpToDate * 2);
@ -49,7 +50,11 @@ void main() {
}
group('$FlutterVersion for $channel', () {
late FileSystem fs;
const String flutterRoot = '/path/to/flutter';
setUpAll(() {
fs = MemoryFileSystem.test();
Cache.disableLocking();
VersionFreshnessValidator.timeToPauseToLetUserReadTheMessage = Duration.zero;
});
@ -101,7 +106,7 @@ void main() {
),
]);
final FlutterVersion flutterVersion = globals.flutterVersion;
final FlutterVersion flutterVersion = FlutterVersion(clock: _testClock, fs: fs, flutterRoot: flutterRoot);
await flutterVersion.checkFlutterVersionFreshness();
expect(flutterVersion.channel, channel);
expect(flutterVersion.repositoryUrl, flutterUpstreamUrl);
@ -124,7 +129,6 @@ void main() {
expect(testLogger.statusText, isEmpty);
expect(processManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
FlutterVersion: () => FlutterVersion(clock: _testClock),
ProcessManager: () => processManager,
Cache: () => cache,
});
@ -419,15 +423,197 @@ void main() {
),
]);
final FlutterVersion flutterVersion = globals.flutterVersion;
final MemoryFileSystem fs = MemoryFileSystem.test();
final FlutterVersion flutterVersion = FlutterVersion(
clock: _testClock,
fs: fs,
flutterRoot: '/path/to/flutter',
);
expect(flutterVersion.channel, '[user-branch]');
expect(flutterVersion.getVersionString(), 'feature-branch/1234abcd');
expect(flutterVersion.getBranchName(), 'feature-branch');
expect(flutterVersion.getVersionString(redactUnknownBranches: true), '[user-branch]/1234abcd');
expect(flutterVersion.getBranchName(redactUnknownBranches: true), '[user-branch]');
expect(processManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
FlutterVersion: () => FlutterVersion(clock: _testClock),
ProcessManager: () => processManager,
Cache: () => cache,
});
testUsingContext('ensureVersionFile() writes version information to disk', () async {
processManager.addCommands(<FakeCommand>[
const FakeCommand(
command: <String>['git', '-c', 'log.showSignature=false', 'log', '-n', '1', '--pretty=format:%H'],
stdout: '1234abcd',
),
const FakeCommand(
command: <String>['git', 'tag', '--points-at', '1234abcd'],
),
const FakeCommand(
command: <String>['git', 'describe', '--match', '*.*.*', '--long', '--tags', '1234abcd'],
stdout: '0.1.2-3-1234abcd',
),
const FakeCommand(
command: <String>['git', 'symbolic-ref', '--short', 'HEAD'],
stdout: 'feature-branch',
),
const FakeCommand(
command: <String>['git', 'rev-parse', '--abbrev-ref', '--symbolic', '@{upstream}'],
),
FakeCommand(
command: const <String>[
'git',
'-c',
'log.showSignature=false',
'log',
'HEAD',
'-n',
'1',
'--pretty=format:%ad',
'--date=iso',
],
stdout: _testClock.ago(VersionFreshnessValidator.versionAgeConsideredUpToDate('stable') ~/ 2).toString(),
),
]);
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory flutterRoot = fs.directory('/path/to/flutter');
flutterRoot.childDirectory('bin').childDirectory('cache').createSync(recursive: true);
final FlutterVersion flutterVersion = FlutterVersion(
clock: _testClock,
fs: fs,
flutterRoot: flutterRoot.path,
);
final File versionFile = fs.file('/path/to/flutter/bin/cache/flutter.version.json');
expect(versionFile.existsSync(), isFalse);
flutterVersion.ensureVersionFile();
expect(versionFile.existsSync(), isTrue);
expect(versionFile.readAsStringSync(), '''
{
"frameworkVersion": "0.0.0-unknown",
"channel": "[user-branch]",
"repositoryUrl": "unknown source",
"frameworkRevision": "1234abcd",
"frameworkCommitDate": "2014-10-02 00:00:00.000Z",
"engineRevision": "abcdefg",
"dartSdkVersion": "2.12.0",
"devToolsVersion": "2.8.0",
"flutterVersion": "0.0.0-unknown"
}''');
expect(processManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
ProcessManager: () => processManager,
Cache: () => cache,
});
testUsingContext('version does not call git if a .version.json file exists', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory flutterRoot = fs.directory('/path/to/flutter');
final Directory cacheDir = flutterRoot
.childDirectory('bin')
.childDirectory('cache')
..createSync(recursive: true);
const String devToolsVersion = '0000000';
const Map<String, Object> versionJson = <String, Object>{
'channel': 'stable',
'frameworkVersion': '1.2.3',
'repositoryUrl': 'https://github.com/flutter/flutter.git',
'frameworkRevision': '1234abcd',
'frameworkCommitDate': '2023-04-28 12:34:56 -0400',
'engineRevision': 'deadbeef',
'dartSdkVersion': 'deadbeef2',
'devToolsVersion': devToolsVersion,
'flutterVersion': 'foo',
};
cacheDir.childFile('flutter.version.json').writeAsStringSync(
jsonEncode(versionJson),
);
final FlutterVersion flutterVersion = FlutterVersion(
clock: _testClock,
fs: fs,
flutterRoot: flutterRoot.path,
);
expect(flutterVersion.channel, 'stable');
expect(flutterVersion.getVersionString(), 'stable/1.2.3');
expect(flutterVersion.getBranchName(), 'stable');
expect(flutterVersion.dartSdkVersion, 'deadbeef2');
expect(flutterVersion.devToolsVersion, devToolsVersion);
expect(flutterVersion.engineRevision, 'deadbeef');
expect(processManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
ProcessManager: () => processManager,
Cache: () => cache,
});
testUsingContext('FlutterVersion() falls back to git if .version.json is malformed', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory flutterRoot = fs.directory(fs.path.join('path', 'to', 'flutter'));
final Directory cacheDir = flutterRoot
.childDirectory('bin')
.childDirectory('cache')
..createSync(recursive: true);
final File legacyVersionFile = flutterRoot.childFile('version');
final File versionFile = cacheDir.childFile('flutter.version.json')..writeAsStringSync(
'{',
);
processManager.addCommands(<FakeCommand>[
const FakeCommand(
command: <String>['git', '-c', 'log.showSignature=false', 'log', '-n', '1', '--pretty=format:%H'],
stdout: '1234abcd',
),
const FakeCommand(
command: <String>['git', 'tag', '--points-at', '1234abcd'],
),
const FakeCommand(
command: <String>['git', 'describe', '--match', '*.*.*', '--long', '--tags', '1234abcd'],
stdout: '0.1.2-3-1234abcd',
),
const FakeCommand(
command: <String>['git', 'symbolic-ref', '--short', 'HEAD'],
stdout: 'feature-branch',
),
const FakeCommand(
command: <String>['git', 'rev-parse', '--abbrev-ref', '--symbolic', '@{upstream}'],
stdout: 'feature-branch',
),
FakeCommand(
command: const <String>[
'git',
'-c',
'log.showSignature=false',
'log',
'HEAD',
'-n',
'1',
'--pretty=format:%ad',
'--date=iso',
],
stdout: _testClock.ago(VersionFreshnessValidator.versionAgeConsideredUpToDate('stable') ~/ 2).toString(),
),
]);
// version file exists in a malformed state
expect(versionFile.existsSync(), isTrue);
final FlutterVersion flutterVersion = FlutterVersion(
clock: _testClock,
fs: fs,
flutterRoot: flutterRoot.path,
);
// version file was deleted because it couldn't be parsed
expect(versionFile.existsSync(), isFalse);
expect(legacyVersionFile.existsSync(), isFalse);
// version file was written to disk
flutterVersion.ensureVersionFile();
expect(processManager, hasNoRemainingExpectations);
expect(versionFile.existsSync(), isTrue);
expect(legacyVersionFile.existsSync(), isTrue);
}, overrides: <Type, Generator>{
ProcessManager: () => processManager,
Cache: () => cache,
});

View File

@ -158,7 +158,6 @@ void main() {
expect(decoded['FlutterProject.isModule'], false);
expect(decoded['FlutterProject.isPlugin'], false);
expect(decoded['FlutterProject.manifest.appname'], 'test_project');
expect(decoded['FlutterVersion.frameworkRevision'], '');
expect(decoded['Platform.isAndroid'], false);
expect(decoded['Platform.isIOS'], false);

View File

@ -43,8 +43,6 @@ void main() {
final Directory testDirectory = parentDirectory.childDirectory('flutter');
testDirectory.createSync(recursive: true);
int exitCode = 0;
// Enable longpaths for windows integration test.
await processManager.run(<String>[
'git', 'config', '--system', 'core.longpaths', 'true',

View File

@ -24,14 +24,18 @@ void main() {
);
exampleAppDir = tempDir.childDirectory('bbb').childDirectory('example');
processManager.runSync(<String>[
flutterBin,
...getLocalEngineArguments(),
'create',
'--template=plugin',
'--platforms=android',
'bbb',
], workingDirectory: tempDir.path);
processManager.runSync(
<String>[
flutterBin,
...getLocalEngineArguments(),
'create',
'--template=plugin',
'--platforms=android',
'bbb',
'-v',
],
workingDirectory: tempDir.path,
);
});
tearDown(() async {
@ -48,14 +52,17 @@ void main() {
// Ensure file is gone prior to configOnly running.
await gradleFile.delete();
final ProcessResult result = processManager.runSync(<String>[
flutterBin,
...getLocalEngineArguments(),
'build',
'apk',
'--target-platform=android-arm',
'--config-only',
], workingDirectory: exampleAppDir.path);
final ProcessResult result = processManager.runSync(
<String>[
flutterBin,
...getLocalEngineArguments(),
'build',
'apk',
'--target-platform=android-arm',
'--config-only',
],
workingDirectory: exampleAppDir.path,
);
expect(gradleFile, exists);
expect(result.stdout, contains(RegExp(r'Config complete')));

View File

@ -13,6 +13,7 @@ import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/convert.dart';
@ -337,6 +338,8 @@ class FakeFlutterVersion implements FlutterVersion {
this.frameworkAge = '0 hours ago',
this.frameworkCommitDate = '12/01/01',
this.gitTagVersion = const GitTagVersion.unknown(),
this.flutterRoot = '/path/to/flutter',
this.nextFlutterVersion,
});
final String branch;
@ -344,6 +347,17 @@ class FakeFlutterVersion implements FlutterVersion {
bool get didFetchTagsAndUpdate => _didFetchTagsAndUpdate;
bool _didFetchTagsAndUpdate = false;
/// Will be returned by [fetchTagsAndGetVersion] if not null.
final FlutterVersion? nextFlutterVersion;
@override
FlutterVersion fetchTagsAndGetVersion({
SystemClock clock = const SystemClock(),
}) {
_didFetchTagsAndUpdate = true;
return nextFlutterVersion ?? this;
}
bool get didCheckFlutterVersionFreshness => _didCheckFlutterVersionFreshness;
bool _didCheckFlutterVersionFreshness = false;
@ -355,6 +369,9 @@ class FakeFlutterVersion implements FlutterVersion {
return kUserBranch;
}
@override
final String flutterRoot;
@override
final String devToolsVersion;
@ -385,16 +402,11 @@ class FakeFlutterVersion implements FlutterVersion {
@override
final String frameworkCommitDate;
@override
String get frameworkDate => frameworkCommitDate;
@override
final GitTagVersion gitTagVersion;
@override
void fetchTagsAndUpdate() {
_didFetchTagsAndUpdate = true;
}
FileSystem get fs => throw UnimplementedError('FakeFlutterVersion.fs is not implemented');
@override
Future<void> checkFlutterVersionFreshness() async {