diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart index 571beae47b..69971db78a 100644 --- a/packages/flutter_tools/lib/src/version.dart +++ b/packages/flutter_tools/lib/src/version.dart @@ -168,16 +168,26 @@ abstract class FlutterVersion { /// `master`, `dev`, `beta`, `stable`; or old ones, like `alpha`, `hackathon`, ... String get channel; + /// The SHA describing the commit being used for the SDK and tools provide in `flutter/flutter`. + /// + /// The _exception_ is the _engine artifacts_, which are downloaded separately as [engineRevision]. String get frameworkRevision; - String get frameworkRevisionShort => _shortGitRevision(frameworkRevision); + /// The shorter Git commit SHA of [frameworkRevion]. + String get frameworkRevisionShort => _shortGitRevision(frameworkRevision); String get frameworkVersion; String get devToolsVersion; - String get dartSdkVersion; + /// The SHA describing the commit being used for the engine artifacts, which are compiled from the `engine/` sub-directory. + /// + /// When using a standard release build, or master channel, [engineRevision] will be identical to [frameworkRevision] since + /// the monorepository merge (as of 2025); however when modifying the framework (or engine) locally, or using a flag such + /// as `FLUTTER_PREBUILT_ENGINE_VERSION=...`, the engine SHA will be _different_ than the [frameworkRevision]. String get engineRevision; + + /// The shorter Git commit SHA of [engineRevision]. String get engineRevisionShort => _shortGitRevision(engineRevision); // This is static as it is called from a constructor. @@ -187,18 +197,24 @@ abstract class FlutterVersion { 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(['-n', '1', '--pretty=format:%ar']).join(' '), + String _getTimeSinceCommit({String? revision}) { + return _runGit( + FlutterVersion.gitLog([ + '-n', + '1', + '--pretty=format:%ar', + if (revision != null) revision, + ]).join(' '), globals.processUtils, flutterRoot, ); } + // TODO(fujino): calculate this relative to frameworkCommitDate for + // _FlutterVersionFromFile so we don't need a git call. + late final String frameworkAge = _getTimeSinceCommit(); + late final String engineAge = _getTimeSinceCommit(revision: engineRevision); + void ensureVersionFile(); @override @@ -209,12 +225,16 @@ abstract class FlutterVersion { 'Flutter$versionText • channel $channel • ${repositoryUrl ?? 'unknown source'}'; final String frameworkText = 'Framework • revision $frameworkRevisionShort ($frameworkAge) • $frameworkCommitDate'; - final String engineText = 'Engine • revision $engineRevisionShort'; + String engineText = 'Engine • revision $engineRevisionShort ($engineAge)'; + if (engineCommitDate != null) { + engineText = '$engineText • $engineCommitDate'; + } + final String toolsText = 'Tools • Dart $dartSdkVersion • DevTools $devToolsVersion'; // Flutter 1.10.2-pre.69 • channel master • https://github.com/flutter/flutter.git // Framework • revision 340c158f32 (85 minutes ago) • 2018-10-26 11:27:22 -0400 - // Engine • revision 9c46333e14 + // Engine • revision 9c46333e14 (96 minutes ago) • 2018-10-26 11:16:22 -0400 // Tools • Dart 2.1.0 (build 2.1.0-dev.8.0 bf26f760b1) return '$flutterText\n$frameworkText\n$engineText\n$toolsText'; @@ -227,16 +247,25 @@ abstract class FlutterVersion { 'frameworkRevision': frameworkRevision, 'frameworkCommitDate': frameworkCommitDate, 'engineRevision': engineRevision, + if (engineCommitDate != null) 'engineCommitDate': engineCommitDate!, 'dartSdkVersion': dartSdkVersion, 'devToolsVersion': devToolsVersion, 'flutterVersion': frameworkVersion, }; - /// A date String describing the last framework commit. + /// A date String describing the [frameworkRevision] commit. /// /// If a git command fails, this will return a placeholder date. String get frameworkCommitDate; + /// A date String describing the [engineRevision] commit. + /// + /// If a git command fails, this will return a placeholder date. + /// + /// If no date was recorded ([engineCommitDate] is a newly stored field), + /// the date is omitted, and left `null`. + String? get engineCommitDate; + /// Checks if the currently installed version of Flutter is up-to-date, and /// warns the user if it isn't. /// @@ -439,6 +468,7 @@ class _FlutterVersionFromFile extends FlutterVersion { required this.frameworkRevision, required this.frameworkCommitDate, required this.engineRevision, + required this.engineCommitDate, required this.dartSdkVersion, required this.devToolsVersion, required this.gitTagVersion, @@ -463,6 +493,7 @@ class _FlutterVersionFromFile extends FlutterVersion { frameworkRevision: manifest['frameworkRevision']! as String, frameworkCommitDate: manifest['frameworkCommitDate']! as String, engineRevision: manifest['engineRevision']! as String, + engineCommitDate: manifest['engineCommitDate'] as String?, dartSdkVersion: manifest['dartSdkVersion']! as String, devToolsVersion: manifest['devToolsVersion']! as String, gitTagVersion: GitTagVersion.parse(manifest['flutterVersion']! as String), @@ -503,6 +534,9 @@ class _FlutterVersionFromFile extends FlutterVersion { @override final String frameworkCommitDate; + @override + final String? engineCommitDate; + @override final String engineRevision; @@ -537,6 +571,16 @@ class _FlutterVersionGit extends FlutterVersion { @override String get frameworkCommitDate => _gitCommitDate(lenient: true, workingDirectory: flutterRoot); + // This uses 'late final' instead of 'String get' because unlike frameworkCommitDate, it is + // operating based on a 'gitRef: ...', which we can assume to be immutable in the context of + // this invocation (possibly HEAD could change, but gitRef should not). + @override + late final String engineCommitDate = _gitCommitDate( + gitRef: engineRevision, + lenient: true, + workingDirectory: flutterRoot, + ); + String? _repositoryUrl; @override String? get repositoryUrl { diff --git a/packages/flutter_tools/test/general.shard/version_test.dart b/packages/flutter_tools/test/general.shard/version_test.dart index 426a1d6429..ecd4221faf 100644 --- a/packages/flutter_tools/test/general.shard/version_test.dart +++ b/packages/flutter_tools/test/general.shard/version_test.dart @@ -168,6 +168,33 @@ void main() { ], stdout: getChannelUpToDateVersion().toString(), ), + const FakeCommand( + command: [ + 'git', + '-c', + 'log.showSignature=false', + 'log', + '-n', + '1', + '--pretty=format:%ar', + 'abcdefg', + ], + stdout: '2 seconds ago', + ), + FakeCommand( + command: const [ + 'git', + '-c', + 'log.showSignature=false', + 'log', + 'abcdefg', + '-n', + '1', + '--pretty=format:%ad', + '--date=iso', + ], + stdout: getChannelUpToDateVersion().toString(), + ), ]); final FlutterVersion flutterVersion = FlutterVersion( @@ -185,7 +212,7 @@ void main() { flutterVersion.toString(), 'Flutter • channel $channel • $flutterUpstreamUrl\n' 'Framework • revision 1234abcd (1 second ago) • ${getChannelUpToDateVersion()}\n' - 'Engine • revision abcdefg\n' + 'Engine • revision abcdefg (2 seconds ago) • ${getChannelUpToDateVersion()}\n' 'Tools • Dart 2.12.0 • DevTools 2.8.0', ); expect(flutterVersion.frameworkAge, '1 second ago'); @@ -689,6 +716,23 @@ void main() { .ago(VersionFreshnessValidator.versionAgeConsideredUpToDate('stable') ~/ 2) .toString(), ), + FakeCommand( + command: const [ + 'git', + '-c', + 'log.showSignature=false', + 'log', + 'abcdefg', + '-n', + '1', + '--pretty=format:%ad', + '--date=iso', + ], + stdout: + _testClock + .ago(VersionFreshnessValidator.versionAgeConsideredUpToDate('stable') ~/ 2) + .toString(), + ), ]); final MemoryFileSystem fs = MemoryFileSystem.test(); @@ -713,6 +757,7 @@ void main() { "frameworkRevision": "1234abcd", "frameworkCommitDate": "2014-10-02 00:00:00.000Z", "engineRevision": "abcdefg", + "engineCommitDate": "2014-10-02 00:00:00.000Z", "dartSdkVersion": "2.12.0", "devToolsVersion": "2.8.0", "flutterVersion": "0.0.0-unknown" @@ -793,6 +838,67 @@ void main() { overrides: {ProcessManager: () => processManager, Cache: () => cache}, ); + testUsingContext( + '_FlutterVersionFromFile ignores engineCommitDate if historically omitted', + () 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 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': '0000000', + 'flutterVersion': 'foo', + }; + cacheDir.childFile('flutter.version.json').writeAsStringSync(jsonEncode(versionJson)); + + processManager.addCommands([ + const FakeCommand( + command: [ + 'git', + '-c', + 'log.showSignature=false', + 'log', + '-n', + '1', + '--pretty=format:%ar', + ], + stdout: '1 second ago', + ), + const FakeCommand( + command: [ + 'git', + '-c', + 'log.showSignature=false', + 'log', + '-n', + '1', + '--pretty=format:%ar', + 'deadbeef', + ], + stdout: '1 second ago', + ), + ]); + + final FlutterVersion flutterVersion = FlutterVersion( + clock: _testClock, + fs: fs, + flutterRoot: flutterRoot.path, + ); + expect(flutterVersion.engineCommitDate, isNull); + expect(flutterVersion.toJson(), isNot(contains('engineCommitDate'))); + expect(flutterVersion.toString(), contains('Engine • revision deadbeef (1 second ago)\n')); + }, + overrides: {ProcessManager: () => processManager, Cache: () => cache}, + ); + testUsingContext( 'FlutterVersion() falls back to git if .version.json is malformed', () async { @@ -846,6 +952,23 @@ void main() { .ago(VersionFreshnessValidator.versionAgeConsideredUpToDate('stable') ~/ 2) .toString(), ), + FakeCommand( + command: const [ + 'git', + '-c', + 'log.showSignature=false', + 'log', + 'abcdefg', + '-n', + '1', + '--pretty=format:%ad', + '--date=iso', + ], + stdout: + _testClock + .ago(VersionFreshnessValidator.versionAgeConsideredUpToDate('stable') ~/ 2) + .toString(), + ), ]); // version file exists in a malformed state diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart index fc292adc07..2d6499e71b 100644 --- a/packages/flutter_tools/test/src/fakes.dart +++ b/packages/flutter_tools/test/src/fakes.dart @@ -366,6 +366,8 @@ class FakeFlutterVersion implements FlutterVersion { this.devToolsVersion = '2.8.0', this.engineRevision = 'abcdefghijklmnopqrstuvwxyz', this.engineRevisionShort = 'abcde', + this.engineAge = '0 hours ago', + this.engineCommitDate = '12/01/01', this.repositoryUrl = 'https://github.com/flutter/flutter.git', this.frameworkVersion = '0.0.0', this.frameworkRevision = '11111111111111111111', @@ -417,6 +419,12 @@ class FakeFlutterVersion implements FlutterVersion { @override final String engineRevisionShort; + @override + final String? engineCommitDate; + + @override + final String engineAge; + @override final String? repositoryUrl;