From 3246808cd26e3a35aeb2dfa5081af20b7683a5b4 Mon Sep 17 00:00:00 2001 From: Christopher Fujino Date: Wed, 14 Jun 2023 17:20:30 -0700 Subject: [PATCH] [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). --- bin/internal/shared.bat | 1 + bin/internal/shared.sh | 6 +- .../lib/src/commands/doctor.dart | 1 - .../lib/src/commands/downgrade.dart | 5 +- .../lib/src/commands/upgrade.dart | 8 +- .../flutter_tools/lib/src/context_runner.dart | 5 +- packages/flutter_tools/lib/src/doctor.dart | 2 +- .../lib/src/project_validator.dart | 5 +- .../src/runner/flutter_command_runner.dart | 11 +- packages/flutter_tools/lib/src/version.dart | 527 +++++++++++++----- packages/flutter_tools/lib/src/vmservice.dart | 7 +- .../commands.shard/hermetic/doctor_test.dart | 68 ++- .../permeable/upgrade_test.dart | 3 +- .../test/general.shard/analytics_test.dart | 18 +- .../test/general.shard/version_test.dart | 198 ++++++- .../analyze_suggestions_integration_test.dart | 1 - .../downgrade_upgrade_integration_test.dart | 2 - .../flutter_build_config_only_test.dart | 39 +- packages/flutter_tools/test/src/fakes.dart | 24 +- 19 files changed, 707 insertions(+), 224 deletions(-) diff --git a/bin/internal/shared.bat b/bin/internal/shared.bat index d2f41602e2c6..421868a9ee58 100644 --- a/bin/internal/shared.bat +++ b/bin/internal/shared.bat @@ -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%" diff --git a/bin/internal/shared.sh b/bin/internal/shared.sh index 93dc7d7a7567..3532c23114a5 100644 --- a/bin/internal/shared.sh +++ b/bin/internal/shared.sh @@ -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" diff --git a/packages/flutter_tools/lib/src/commands/doctor.dart b/packages/flutter_tools/lib/src/commands/doctor.dart index 43c1f313eaaa..7e7e3eaf8cf1 100644 --- a/packages/flutter_tools/lib/src/commands/doctor.dart +++ b/packages/flutter_tools/lib/src/commands/doctor.dart @@ -33,7 +33,6 @@ class DoctorCommand extends FlutterCommand { @override Future 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}'))) { diff --git a/packages/flutter_tools/lib/src/commands/downgrade.dart b/packages/flutter_tools/lib/src/commands/downgrade.dart index a27482aa8790..a58b75c00986 100644 --- a/packages/flutter_tools/lib/src/commands/downgrade.dart +++ b/packages/flutter_tools/lib/src/commands/downgrade.dart @@ -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; diff --git a/packages/flutter_tools/lib/src/commands/upgrade.dart b/packages/flutter_tools/lib/src/commands/upgrade.dart index bc0d8ceff9ca..b7b54a190aa6 100644 --- a/packages/flutter_tools/lib/src/commands/upgrade.dart +++ b/packages/flutter_tools/lib/src/commands/upgrade.dart @@ -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. diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index 1fae3132275b..7322a2d8f63d 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -226,7 +226,10 @@ Future runInContext( config: globals.config, platform: globals.platform, ), - FlutterVersion: () => FlutterVersion(), + FlutterVersion: () => FlutterVersion( + fs: globals.fs, + flutterRoot: Cache.flutterRoot!, + ), FuchsiaArtifacts: () => FuchsiaArtifacts.find(), FuchsiaDeviceTools: () => FuchsiaDeviceTools(), FuchsiaSdk: () => FuchsiaSdk(), diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart index 3978d0d77654..eb975fc17e04 100644 --- a/packages/flutter_tools/lib/src/doctor.dart +++ b/packages/flutter_tools/lib/src/doctor.dart @@ -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, diff --git a/packages/flutter_tools/lib/src/project_validator.dart b/packages/flutter_tools/lib/src/project_validator.dart index 791642a19bf8..57c3f8dd4fe3 100644 --- a/packages/flutter_tools/lib/src/project_validator.dart +++ b/packages/flutter_tools/lib/src/project_validator.dart @@ -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), diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart index 9f10cd0e9dff..5d6d78639fb6 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart @@ -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 { 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 jsonOut = globals.flutterVersion.toJson(); + final Map 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; diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart index bae196f14b0f..0702b35e7ec5 100644 --- a/packages/flutter_tools/lib/src/version.dart +++ b/packages/flutter_tools/lib/src/version.dart @@ -79,103 +79,152 @@ 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( + required FileSystem fs, + required String flutterRoot, + @protected + bool fetchTags = false, + }) { + final File versionFile = getVersionFile(fs, flutterRoot); + + if (!fetchTags && versionFile.existsSync()) { + final _FlutterVersionFromFile? version = _FlutterVersionFromFile.tryParseFromFile( + versionFile, + flutterRoot: flutterRoot, + ); + if (version != null) { + return version; + } + } + + // 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(); + } + } + + final String frameworkRevision = _runGit( gitLog(['-n', '1', '--pretty=format:%H']).join(' '), globals.processUtils, - _workingDirectory, + flutterRoot, + ); + + return FlutterVersion.fromRevision( + clock: clock, + frameworkRevision: frameworkRevision, + fs: fs, + flutterRoot: flutterRoot, + fetchTags: fetchTags, ); - _gitTagVersion = GitTagVersion.determine(globals.processUtils, globals.platform, workingDirectory: _workingDirectory, gitRef: _frameworkRevision); - _frameworkVersion = gitTagVersion.frameworkVersionFor(_frameworkRevision); } - final SystemClock _clock; - final String? _workingDirectory; + FlutterVersion._({ + required SystemClock clock, + required this.flutterRoot, + required this.fs, + }) : _clock = clock; - /// 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); + 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? _repositoryUrl; - String? get repositoryUrl { - if (_repositoryUrl == null) { - final String gitChannel = _runGit( - 'git rev-parse --abbrev-ref --symbolic $kGitTrackingUpstream', - globals.processUtils, - _workingDirectory, - ); - 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, - ); - } + /// 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 _repositoryUrl; + return FlutterVersion( + clock: clock, + flutterRoot: flutterRoot, + fs: fs, + fetchTags: true, + ); } - /// 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; - } + final FileSystem fs; - late GitTagVersion _gitTagVersion; - GitTagVersion get gitTagVersion => _gitTagVersion; + final SystemClock _clock; - /// The name of the local branch. - /// Use getBranchName() to read this. - String? _branch; + 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; - late String _frameworkRevision; - String get frameworkRevision => _frameworkRevision; + 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); + + // 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( - gitLog(['-n', '1', '--pretty=format:%ar']).join(' '), + FlutterVersion.gitLog(['-n', '1', '--pretty=format:%ar']).join(' '), globals.processUtils, - _workingDirectory, + flutterRoot, ); } - late String _frameworkVersion; - String get frameworkVersion => _frameworkVersion; - - String get devToolsVersion => globals.cache.devToolsVersion; - - String get dartSdkVersion => globals.cache.dartSdkVersion; - - String get engineRevision => globals.cache.engineRevision; - String get engineRevisionShort => _shortGitRevision(engineRevision); - - void ensureVersionFile() { - globals.fs.file(globals.fs.path.join(Cache.flutterRoot!, 'version')).writeAsStringSync(_frameworkVersion); - } + void ensureVersionFile(); @override String toString() { @@ -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 args = gitLog([ - 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 _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(['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 gitLog(List args) { return ['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 _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 args = FlutterVersion.gitLog([ + 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 manifest = jsonDecode(jsonContents) as Map; + + 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, ); - - // Don't ping the server too often. Return cached value if it's fresh. - if (timeSinceLastCheck < VersionFreshnessValidator.checkAgeConsideredUpToDate) { - return versionCheckStamp.lastKnownRemoteVersion; + // 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; } + } - // 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, + @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, ); - return null; + 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; + } + + @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 command, { bool lenient = true }) { +String _runSync( + List 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 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(); } diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index 9f9d59663252..24ad33db6650 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -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 setUpVmService({ } vmService.registerServiceCallback(kFlutterVersionServiceName, (Map params) async { - final FlutterVersion version = context.get() ?? FlutterVersion(); + final FlutterVersion version = context.get() ?? FlutterVersion( + fs: globals.fs, + flutterRoot: Cache.flutterRoot!, + ); final Map versionJson = version.toJson(); versionJson['frameworkRevisionShort'] = version.frameworkRevisionShort; versionJson['engineRevisionShort'] = version.engineRevisionShort; diff --git a/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart index cd569c54a7a3..65fe083b2be9 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart @@ -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()), ); }, overrides: { - FileSystem: () => MemoryFileSystem.test(), + FileSystem: () => fs, ProcessManager: () => fakeProcessManager, }); - testUsingContext('Fetches tags to get the right version', () async { - Cache.disableLocking(); - - final DoctorCommand doctorCommand = DoctorCommand(); - final CommandRunner commandRunner = createTestCommandRunner(doctorCommand); + group('FlutterValidator', () { + late FakeFlutterVersion initialVersion; + late FakeFlutterVersion secondVersion; + late TestFeatureFlags featureFlags; - await commandRunner.run(['doctor']); - - expect(flutterVersion.didFetchTagsAndUpdate, true); - Cache.enableLocking(); - }, overrides: { - 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(command: ['which', 'java']), + ]); + final List validators = DoctorValidatorsProvider.test( + featureFlags: featureFlags, + platform: FakePlatform(), + ).validators; + final FlutterValidator flutterValidator = validators.whereType().first; + final ValidationResult result = await flutterValidator.validate(); + expect( + result.messages.map((ValidationMessage msg) => msg.message), + contains(contains('Framework revision 222')), + ); + }, overrides: { + 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? startedValidatorTasks, bool sendEvent = true, + FlutterVersion? version, }) async => true; @override diff --git a/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart b/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart index 2146ca44745d..cb32e9ea2564 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart @@ -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.unmodifiable({ diff --git a/packages/flutter_tools/test/general.shard/analytics_test.dart b/packages/flutter_tools/test/general.shard/analytics_test.dart index b8bfd57a47c7..c6cdf97745b9 100644 --- a/packages/flutter_tools/test/general.shard/analytics_test.dart +++ b/packages/flutter_tools/test/general.shard/analytics_test.dart @@ -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: { - 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: { - 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: { - FlutterVersion: () => FlutterVersion(), + FlutterVersion: () => FakeFlutterVersion(), Config: () => testConfig, Platform: () => FakePlatform(environment: { '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: { - FlutterVersion: () => FlutterVersion(), + FlutterVersion: () => FakeFlutterVersion(), Config: () => testConfig, Platform: () => FakePlatform(environment: { '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? startedValidatorTasks, bool sendEvent = true, + FlutterVersion? version, }) async { return diagnoseSucceeds; } diff --git a/packages/flutter_tools/test/general.shard/version_test.dart b/packages/flutter_tools/test/general.shard/version_test.dart index 6fe00447e6c4..ab4c730d444d 100644 --- a/packages/flutter_tools/test/general.shard/version_test.dart +++ b/packages/flutter_tools/test/general.shard/version_test.dart @@ -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: { - 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: { + ProcessManager: () => processManager, + Cache: () => cache, + }); + + testUsingContext('ensureVersionFile() writes version information to disk', () async { + processManager.addCommands([ + const FakeCommand( + command: ['git', '-c', 'log.showSignature=false', 'log', '-n', '1', '--pretty=format:%H'], + stdout: '1234abcd', + ), + const FakeCommand( + command: ['git', 'tag', '--points-at', '1234abcd'], + ), + const FakeCommand( + command: ['git', 'describe', '--match', '*.*.*', '--long', '--tags', '1234abcd'], + stdout: '0.1.2-3-1234abcd', + ), + const FakeCommand( + command: ['git', 'symbolic-ref', '--short', 'HEAD'], + stdout: 'feature-branch', + ), + const FakeCommand( + command: ['git', 'rev-parse', '--abbrev-ref', '--symbolic', '@{upstream}'], + ), + FakeCommand( + command: const [ + '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: { + 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 versionJson = { + '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: { + 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([ + const FakeCommand( + command: ['git', '-c', 'log.showSignature=false', 'log', '-n', '1', '--pretty=format:%H'], + stdout: '1234abcd', + ), + const FakeCommand( + command: ['git', 'tag', '--points-at', '1234abcd'], + ), + const FakeCommand( + command: ['git', 'describe', '--match', '*.*.*', '--long', '--tags', '1234abcd'], + stdout: '0.1.2-3-1234abcd', + ), + const FakeCommand( + command: ['git', 'symbolic-ref', '--short', 'HEAD'], + stdout: 'feature-branch', + ), + const FakeCommand( + command: ['git', 'rev-parse', '--abbrev-ref', '--symbolic', '@{upstream}'], + stdout: 'feature-branch', + ), + FakeCommand( + command: const [ + '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: { - FlutterVersion: () => FlutterVersion(clock: _testClock), ProcessManager: () => processManager, Cache: () => cache, }); diff --git a/packages/flutter_tools/test/integration.shard/analyze_suggestions_integration_test.dart b/packages/flutter_tools/test/integration.shard/analyze_suggestions_integration_test.dart index c4c37ab48a55..bac0d2f80c71 100644 --- a/packages/flutter_tools/test/integration.shard/analyze_suggestions_integration_test.dart +++ b/packages/flutter_tools/test/integration.shard/analyze_suggestions_integration_test.dart @@ -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); diff --git a/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart b/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart index 26ac8752cc1c..8ca82c0d25cd 100644 --- a/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart +++ b/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart @@ -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([ 'git', 'config', '--system', 'core.longpaths', 'true', diff --git a/packages/flutter_tools/test/integration.shard/flutter_build_config_only_test.dart b/packages/flutter_tools/test/integration.shard/flutter_build_config_only_test.dart index 55d793056b0e..92f38142534f 100644 --- a/packages/flutter_tools/test/integration.shard/flutter_build_config_only_test.dart +++ b/packages/flutter_tools/test/integration.shard/flutter_build_config_only_test.dart @@ -24,14 +24,18 @@ void main() { ); exampleAppDir = tempDir.childDirectory('bbb').childDirectory('example'); - processManager.runSync([ - flutterBin, - ...getLocalEngineArguments(), - 'create', - '--template=plugin', - '--platforms=android', - 'bbb', - ], workingDirectory: tempDir.path); + processManager.runSync( + [ + 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([ - flutterBin, - ...getLocalEngineArguments(), - 'build', - 'apk', - '--target-platform=android-arm', - '--config-only', - ], workingDirectory: exampleAppDir.path); + final ProcessResult result = processManager.runSync( + [ + flutterBin, + ...getLocalEngineArguments(), + 'build', + 'apk', + '--target-platform=android-arm', + '--config-only', + ], + workingDirectory: exampleAppDir.path, + ); expect(gradleFile, exists); expect(result.stdout, contains(RegExp(r'Config complete'))); diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart index b7c58560dd7d..65fb5578cf18 100644 --- a/packages/flutter_tools/test/src/fakes.dart +++ b/packages/flutter_tools/test/src/fakes.dart @@ -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 checkFlutterVersionFreshness() async {