blob: 39e0fb59f7281c9dc966549a086019f00d27e57f [file] [log] [blame]
Ian Hickson449f4a62019-11-27 15:04:02 -08001// Copyright 2014 The Flutter Authors. All rights reserved.
Adam Barth9662d492015-11-28 21:07:16 -08002// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
Yegor93126a82017-04-04 10:45:43 -07005import 'package:meta/meta.dart';
Yegor93126a82017-04-04 10:45:43 -07006
7import 'base/common.dart';
Ian Hickson9e42e4b2018-01-18 07:59:06 -08008import 'base/file_system.dart';
Todd Volkert016b5ab2017-01-09 08:37:00 -08009import 'base/io.dart';
Devon Carewbd564a02016-05-05 19:51:22 -070010import 'base/process.dart';
Jonah Williamsa871d592018-11-10 17:02:32 -080011import 'base/time.dart';
Jason Simmons32846de2016-05-12 15:54:35 -070012import 'cache.dart';
Jonah Williams91fd89e2019-01-25 16:16:26 -080013import 'convert.dart';
Jonah Williamsee7a37f2020-01-06 11:04:20 -080014import 'globals.dart' as globals;
Adam Barth9662d492015-11-28 21:07:16 -080015
includecmath71c42c92020-09-25 04:07:03 +080016/// The flutter GitHub repository.
17String get _flutterGit => globals.platform.environment['FLUTTER_GIT_URL'] ?? 'https://github.com/flutter/flutter.git';
18
Jonah Williamse2554a92020-02-13 11:56:45 -080019/// The names of each channel/branch in order of increasing stability.
20enum Channel {
21 master,
22 dev,
23 beta,
24 stable,
25}
26
includecmath71c42c92020-09-25 04:07:03 +080027// Beware: Keep order in accordance with stability
28const Set<String> kOfficialChannels = <String>{
29 'master',
30 'dev',
31 'beta',
32 'stable',
33};
Dan Fielde13e1702020-03-06 21:38:35 -080034
Jonah Williamse2554a92020-02-13 11:56:45 -080035/// Retrieve a human-readable name for a given [channel].
36///
includecmath71c42c92020-09-25 04:07:03 +080037/// Requires [kOfficialChannels] to be correctly ordered.
Jonah Williamse2554a92020-02-13 11:56:45 -080038String getNameForChannel(Channel channel) {
includecmath71c42c92020-09-25 04:07:03 +080039 return kOfficialChannels.elementAt(channel.index);
Jonah Williamse2554a92020-02-13 11:56:45 -080040}
41
42/// Retrieve the [Channel] representation for a string [name].
43///
44/// Returns `null` if [name] is not in the list of official channels, according
includecmath71c42c92020-09-25 04:07:03 +080045/// to [kOfficialChannels].
Jonah Williamse2554a92020-02-13 11:56:45 -080046Channel getChannelForName(String name) {
includecmath71c42c92020-09-25 04:07:03 +080047 if (kOfficialChannels.contains(name)) {
48 return Channel.values[kOfficialChannels.toList().indexOf(name)];
Jonah Williamse2554a92020-02-13 11:56:45 -080049 }
50 return null;
51}
52
Devon Carew25f332d2016-03-23 16:59:56 -070053class FlutterVersion {
Dan Fielde13e1702020-03-06 21:38:35 -080054 /// Parses the Flutter version from currently available tags in the local
55 /// repo.
56 ///
57 /// Call [fetchTagsAndUpdate] to update the version based on the latest tags
58 /// available upstream.
Jonah Williams0b882692020-11-12 15:29:03 -080059 FlutterVersion({
60 SystemClock clock = const SystemClock(),
61 String workingDirectory,
Anurag Roy05dadb02020-11-20 07:33:05 +053062 String frameworkRevision,
Jonah Williams0b882692020-11-12 15:29:03 -080063 }) : _clock = clock,
Anurag Roy05dadb02020-11-20 07:33:05 +053064 _workingDirectory = workingDirectory {
65 _frameworkRevision = frameworkRevision ?? _runGit(
Jonah Williamse2554a92020-02-13 11:56:45 -080066 gitLog(<String>['-n', '1', '--pretty=format:%H']).join(' '),
Jenn Magder537cf332020-10-14 13:12:46 -070067 globals.processUtils,
Jonah Williamse2554a92020-02-13 11:56:45 -080068 _workingDirectory,
69 );
Anurag Roy05dadb02020-11-20 07:33:05 +053070 _gitTagVersion = GitTagVersion.determine(globals.processUtils, workingDirectory: _workingDirectory, fetchTags: false, gitRef: _frameworkRevision);
Dan Fielde13e1702020-03-06 21:38:35 -080071 _frameworkVersion = gitTagVersion.frameworkVersionFor(_frameworkRevision);
72 }
73
Jonah Williams0b882692020-11-12 15:29:03 -080074 final SystemClock _clock;
75 final String _workingDirectory;
76
Jonah Williams08576cb2020-10-12 09:31:02 -070077 /// Fetches tags from the upstream Flutter repository and re-calculates the
Dan Fielde13e1702020-03-06 21:38:35 -080078 /// version.
79 ///
80 /// This carries a performance penalty, and should only be called when the
81 /// user explicitly wants to get the version, e.g. for `flutter --version` or
82 /// `flutter doctor`.
83 void fetchTagsAndUpdate() {
Jenn Magder537cf332020-10-14 13:12:46 -070084 _gitTagVersion = GitTagVersion.determine(globals.processUtils, workingDirectory: _workingDirectory, fetchTags: true);
Jenn Magder9861a1c2019-12-23 13:12:36 -080085 _frameworkVersion = gitTagVersion.frameworkVersionFor(_frameworkRevision);
Adam Barth9662d492015-11-28 21:07:16 -080086 }
Devon Carew864b7f42016-01-29 10:45:59 -080087
Devon Carew25f332d2016-03-23 16:59:56 -070088 String _repositoryUrl;
Jonah Williamsb1238462019-03-20 13:58:57 -070089 String get repositoryUrl {
90 final String _ = channel;
91 return _repositoryUrl;
92 }
Devon Carew25f332d2016-03-23 16:59:56 -070093
Devon Carew25f332d2016-03-23 16:59:56 -070094 String _channel;
Ian Hickson9e42e4b2018-01-18 07:59:06 -080095 /// The channel is the upstream branch.
Greg Spencer1b9cba42018-10-30 16:57:54 -070096 /// `master`, `dev`, `beta`, `stable`; or old ones, like `alpha`, `hackathon`, ...
Jonah Williamsb1238462019-03-20 13:58:57 -070097 String get channel {
98 if (_channel == null) {
Jonah Williamse2554a92020-02-13 11:56:45 -080099 final String channel = _runGit(
100 'git rev-parse --abbrev-ref --symbolic @{u}',
Jenn Magder537cf332020-10-14 13:12:46 -0700101 globals.processUtils,
Jonah Williamse2554a92020-02-13 11:56:45 -0800102 _workingDirectory,
103 );
Jonah Williamsb1238462019-03-20 13:58:57 -0700104 final int slash = channel.indexOf('/');
105 if (slash != -1) {
106 final String remote = channel.substring(0, slash);
Jonah Williamse2554a92020-02-13 11:56:45 -0800107 _repositoryUrl = _runGit(
108 'git ls-remote --get-url $remote',
Jenn Magder537cf332020-10-14 13:12:46 -0700109 globals.processUtils,
Jonah Williamse2554a92020-02-13 11:56:45 -0800110 _workingDirectory,
111 );
Jonah Williamsb1238462019-03-20 13:58:57 -0700112 _channel = channel.substring(slash + 1);
Alexandre Ardhuinb8731622019-09-24 21:03:37 +0200113 } else if (channel.isEmpty) {
Jonah Williamsb1238462019-03-20 13:58:57 -0700114 _channel = 'unknown';
115 } else {
116 _channel = channel;
117 }
118 }
119 return _channel;
120 }
Devon Carew25f332d2016-03-23 16:59:56 -0700121
Jenn Magder9861a1c2019-12-23 13:12:36 -0800122 GitTagVersion _gitTagVersion;
123 GitTagVersion get gitTagVersion => _gitTagVersion;
124
Ian Hickson9e42e4b2018-01-18 07:59:06 -0800125 /// The name of the local branch.
126 /// Use getBranchName() to read this.
Mehmet Fidanboylu96942bc2017-11-10 17:31:18 -0800127 String _branch;
128
Devon Carew25f332d2016-03-23 16:59:56 -0700129 String _frameworkRevision;
130 String get frameworkRevision => _frameworkRevision;
Devon Carewb0ebc712016-04-27 10:47:52 -0700131 String get frameworkRevisionShort => _shortGitRevision(frameworkRevision);
Devon Carew25f332d2016-03-23 16:59:56 -0700132
133 String _frameworkAge;
Jonah Williamsb1238462019-03-20 13:58:57 -0700134 String get frameworkAge {
Jonah Williamse2554a92020-02-13 11:56:45 -0800135 return _frameworkAge ??= _runGit(
136 gitLog(<String>['-n', '1', '--pretty=format:%ar']).join(' '),
Jenn Magder537cf332020-10-14 13:12:46 -0700137 globals.processUtils,
Jonah Williamse2554a92020-02-13 11:56:45 -0800138 _workingDirectory,
139 );
Jonah Williamsb1238462019-03-20 13:58:57 -0700140 }
Devon Carew25f332d2016-03-23 16:59:56 -0700141
Ian Hickson9e42e4b2018-01-18 07:59:06 -0800142 String _frameworkVersion;
143 String get frameworkVersion => _frameworkVersion;
144
Devon Carew7ab48f42016-08-16 08:38:19 -0700145 String get frameworkDate => frameworkCommitDate;
146
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800147 String get dartSdkVersion => globals.cache.dartSdkVersion;
Phil Quitslund803fbec2016-08-11 16:39:33 -0700148
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800149 String get engineRevision => globals.cache.engineRevision;
Devon Carewb0ebc712016-04-27 10:47:52 -0700150 String get engineRevisionShort => _shortGitRevision(engineRevision);
Devon Carew25f332d2016-03-23 16:59:56 -0700151
Jonah Williams0b882692020-11-12 15:29:03 -0800152 void ensureVersionFile() {
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800153 globals.fs.file(globals.fs.path.join(Cache.flutterRoot, 'version')).writeAsStringSync(_frameworkVersion);
Ian Hickson9e42e4b2018-01-18 07:59:06 -0800154 }
Devon Carew25f332d2016-03-23 16:59:56 -0700155
156 @override
157 String toString() {
Ian Hickson9e42e4b2018-01-18 07:59:06 -0800158 final String versionText = frameworkVersion == 'unknown' ? '' : ' $frameworkVersion';
Alexandre Ardhuin4fa32df2019-05-16 22:25:51 +0200159 final String flutterText = 'Flutter$versionText • channel $channel • ${repositoryUrl ?? 'unknown source'}';
Chris Bracken7a093162017-03-03 17:50:46 -0800160 final String frameworkText = 'Framework • revision $frameworkRevisionShort ($frameworkAge) • $frameworkCommitDate';
161 final String engineText = 'Engine • revision $engineRevisionShort';
162 final String toolsText = 'Tools • Dart $dartSdkVersion';
Devon Carew25f332d2016-03-23 16:59:56 -0700163
Greg Spencerdc2cc632018-10-26 13:21:36 -0700164 // Flutter 1.10.2-pre.69 • channel master • https://github.com/flutter/flutter.git
165 // Framework • revision 340c158f32 (84 minutes ago) • 2018-10-26 11:27:22 -0400
166 // Engine • revision 9c46333e14
167 // Tools • Dart 2.1.0 (build 2.1.0-dev.8.0 bf26f760b1)
Devon Carew7ab48f42016-08-16 08:38:19 -0700168
169 return '$flutterText\n$frameworkText\n$engineText\n$toolsText';
Phil Quitslundabeb5c72016-08-15 11:07:37 -0700170 }
171
Kevin Moore1b56cb72017-06-06 18:40:32 -0700172 Map<String, Object> toJson() => <String, Object>{
Alexandre Ardhuinf11c3412019-09-27 10:46:45 +0200173 'frameworkVersion': frameworkVersion ?? 'unknown',
174 'channel': channel,
175 'repositoryUrl': repositoryUrl ?? 'unknown source',
176 'frameworkRevision': frameworkRevision,
177 'frameworkCommitDate': frameworkCommitDate,
178 'engineRevision': engineRevision,
179 'dartSdkVersion': dartSdkVersion,
180 };
Kevin Moore1b56cb72017-06-06 18:40:32 -0700181
Phil Quitslundabeb5c72016-08-15 11:07:37 -0700182 /// A date String describing the last framework commit.
Zachary Andersone4b809b2019-11-27 10:18:43 -0800183 ///
184 /// If a git command fails, this will return a placeholder date.
185 String get frameworkCommitDate => _latestGitCommitDate(lenient: true);
Yegor93126a82017-04-04 10:45:43 -0700186
Zachary Andersone4b809b2019-11-27 10:18:43 -0800187 // The date of the latest commit on the given branch. If no branch is
188 // specified, then it is the current local branch.
189 //
190 // If lenient is true, and the git command fails, a placeholder date is
191 // returned. Otherwise, the VersionCheckError exception is propagated.
192 static String _latestGitCommitDate({
193 String branch,
194 bool lenient = false,
195 }) {
∂ω∂4277f362019-08-21 23:55:57 +0300196 final List<String> args = gitLog(<String>[
Alexandre Ardhuin758009b2019-07-02 21:11:56 +0200197 if (branch != null) branch,
198 '-n',
199 '1',
200 '--pretty=format:%ad',
201 '--date=iso',
∂ω∂4277f362019-08-21 23:55:57 +0300202 ]);
Zachary Andersone4b809b2019-11-27 10:18:43 -0800203 try {
204 // Don't plumb 'lenient' through directly so that we can print an error
205 // if something goes wrong.
206 return _runSync(args, lenient: false);
207 } on VersionCheckError catch (e) {
208 if (lenient) {
209 final DateTime dummyDate = DateTime.fromMillisecondsSinceEpoch(0);
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800210 globals.printError('Failed to find the latest git commit date: $e\n'
Zachary Andersone4b809b2019-11-27 10:18:43 -0800211 'Returning $dummyDate instead.');
212 // Return something that DateTime.parse() can parse.
213 return dummyDate.toString();
214 } else {
215 rethrow;
216 }
217 }
Devon Carew25f332d2016-03-23 16:59:56 -0700218 }
219
Yegor93126a82017-04-04 10:45:43 -0700220 /// The name of the temporary git remote used to check for the latest
221 /// available Flutter framework version.
222 ///
223 /// In the absence of bugs and crashes a Flutter developer should never see
224 /// this remote appear in their `git remote` list, but also if it happens to
225 /// persist we do the proper clean-up for extra robustness.
Greg Spencer3c5a7a32018-05-17 23:04:41 -0700226 static const String _versionCheckRemote = '__flutter_version_check__';
Yegor93126a82017-04-04 10:45:43 -0700227
228 /// The date of the latest framework commit in the remote repository.
229 ///
Zachary Andersone4b809b2019-11-27 10:18:43 -0800230 /// Throws [VersionCheckError] if a git command fails, for example, when the
231 /// remote git repository is not reachable due to a network issue.
Jason Simmons9455fb62017-04-14 09:58:31 -0700232 static Future<String> fetchRemoteFrameworkCommitDate(String branch) async {
Yegor93126a82017-04-04 10:45:43 -0700233 await _removeVersionCheckRemoteIfExists();
234 try {
235 await _run(<String>[
236 'git',
237 'remote',
238 'add',
Greg Spencer3c5a7a32018-05-17 23:04:41 -0700239 _versionCheckRemote,
Dan Fielde13e1702020-03-06 21:38:35 -0800240 _flutterGit,
Yegor93126a82017-04-04 10:45:43 -0700241 ]);
Greg Spencer3c5a7a32018-05-17 23:04:41 -0700242 await _run(<String>['git', 'fetch', _versionCheckRemote, branch]);
Zachary Andersone4b809b2019-11-27 10:18:43 -0800243 return _latestGitCommitDate(
244 branch: '$_versionCheckRemote/$branch',
245 lenient: false,
246 );
雷宇辰7779a142020-07-25 07:16:31 +0800247 } on VersionCheckError catch (error) {
248 if (globals.platform.environment.containsKey('FLUTTER_GIT_URL')) {
Jonah Williams08576cb2020-10-12 09:31:02 -0700249 globals.logger.printError('Warning: the Flutter git upstream was overridden '
雷宇辰7779a142020-07-25 07:16:31 +0800250 'by the environment variable FLUTTER_GIT_URL = $_flutterGit');
251 }
252 globals.logger.printError(error.toString());
253 rethrow;
Yegor93126a82017-04-04 10:45:43 -0700254 } finally {
255 await _removeVersionCheckRemoteIfExists();
256 }
257 }
258
Alexandre Ardhuin2d3ff102018-10-05 07:54:56 +0200259 static Future<void> _removeVersionCheckRemoteIfExists() async {
Yegor93126a82017-04-04 10:45:43 -0700260 final List<String> remotes = (await _run(<String>['git', 'remote']))
261 .split('\n')
Alexandre Ardhuinf62afdc2018-10-01 21:29:08 +0200262 .map<String>((String name) => name.trim()) // to account for OS-specific line-breaks
Yegor93126a82017-04-04 10:45:43 -0700263 .toList();
Zachary Andersone2340c62019-09-13 14:51:35 -0700264 if (remotes.contains(_versionCheckRemote)) {
Greg Spencer3c5a7a32018-05-17 23:04:41 -0700265 await _run(<String>['git', 'remote', 'remove', _versionCheckRemote]);
Zachary Andersone2340c62019-09-13 14:51:35 -0700266 }
Yegor93126a82017-04-04 10:45:43 -0700267 }
268
Ian Hickson9e42e4b2018-01-18 07:59:06 -0800269 /// Return a short string for the version (e.g. `master/0.0.59-pre.92`, `scroll_refactor/a76bc8e22b`).
Alexandre Ardhuin5169ab52019-02-21 09:27:07 +0100270 String getVersionString({ bool redactUnknownBranches = false }) {
Zachary Andersone2340c62019-09-13 14:51:35 -0700271 if (frameworkVersion != 'unknown') {
Ian Hickson9e42e4b2018-01-18 07:59:06 -0800272 return '${getBranchName(redactUnknownBranches: redactUnknownBranches)}/$frameworkVersion';
Zachary Andersone2340c62019-09-13 14:51:35 -0700273 }
Ian Hickson9e42e4b2018-01-18 07:59:06 -0800274 return '${getBranchName(redactUnknownBranches: redactUnknownBranches)}/$frameworkRevisionShort';
Seth Laddb471a9c2017-06-20 08:20:57 -0700275 }
276
277 /// Return the branch name.
278 ///
Ian Hickson9e42e4b2018-01-18 07:59:06 -0800279 /// If [redactUnknownBranches] is true and the branch is unknown,
280 /// the branch name will be returned as `'[user-branch]'`.
Alexandre Ardhuin09276be2018-06-05 08:50:40 +0200281 String getBranchName({ bool redactUnknownBranches = false }) {
Jonah Williamsb1238462019-03-20 13:58:57 -0700282 _branch ??= () {
Jenn Magder537cf332020-10-14 13:12:46 -0700283 final String branch = _runGit('git rev-parse --abbrev-ref HEAD', globals.processUtils);
Jonah Williamsb1238462019-03-20 13:58:57 -0700284 return branch == 'HEAD' ? channel : branch;
285 }();
Ian Hickson9e42e4b2018-01-18 07:59:06 -0800286 if (redactUnknownBranches || _branch.isEmpty) {
Devon Carewb0ebc712016-04-27 10:47:52 -0700287 // Only return the branch names we know about; arbitrary branch names might contain PII.
Jonah Williams0b882692020-11-12 15:29:03 -0800288 if (!kOfficialChannels.contains(_branch)) {
Ian Hickson9e42e4b2018-01-18 07:59:06 -0800289 return '[user-branch]';
Zachary Andersone2340c62019-09-13 14:51:35 -0700290 }
Devon Carewb0ebc712016-04-27 10:47:52 -0700291 }
Mehmet Fidanboylu96942bc2017-11-10 17:31:18 -0800292 return _branch;
Devon Carewadac9272016-04-26 16:25:11 -0700293 }
Yegor93126a82017-04-04 10:45:43 -0700294
xster9a0d4cf2017-11-14 18:50:15 -0800295 /// Returns true if `tentativeDescendantRevision` is a direct descendant to
296 /// the `tentativeAncestorRevision` revision on the Flutter framework repo
297 /// tree.
298 bool checkRevisionAncestry({
299 String tentativeDescendantRevision,
300 String tentativeAncestorRevision,
301 }) {
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800302 final ProcessResult result = globals.processManager.runSync(
Zachary Andersone4b809b2019-11-27 10:18:43 -0800303 <String>[
304 'git',
305 'merge-base',
306 '--is-ancestor',
307 tentativeAncestorRevision,
308 tentativeDescendantRevision
309 ],
xster9a0d4cf2017-11-14 18:50:15 -0800310 workingDirectory: Cache.flutterRoot,
311 );
312 return result.exitCode == 0;
313 }
314
Yegor93126a82017-04-04 10:45:43 -0700315 /// The amount of time we wait before pinging the server to check for the
316 /// availability of a newer version of Flutter.
317 @visibleForTesting
Greg Spencereb35f892018-11-14 16:51:12 -0800318 static const Duration checkAgeConsideredUpToDate = Duration(days: 3);
Yegor93126a82017-04-04 10:45:43 -0700319
320 /// We warn the user if the age of their Flutter installation is greater than
Greg Spencereb35f892018-11-14 16:51:12 -0800321 /// this duration. The durations are slightly longer than the expected release
322 /// cadence for each channel, to give the user a grace period before they get
323 /// notified.
Danny Tuppenye616c6c2018-06-27 12:50:40 +0100324 ///
Greg Spencereb35f892018-11-14 16:51:12 -0800325 /// For example, for the beta channel, this is set to five weeks because
326 /// beta releases happen approximately every month.
Yegor93126a82017-04-04 10:45:43 -0700327 @visibleForTesting
Greg Spencereb35f892018-11-14 16:51:12 -0800328 static Duration versionAgeConsideredUpToDate(String channel) {
329 switch (channel) {
330 case 'stable':
331 return const Duration(days: 365 ~/ 2); // Six months
332 case 'beta':
333 return const Duration(days: 7 * 8); // Eight weeks
334 case 'dev':
335 return const Duration(days: 7 * 4); // Four weeks
336 default:
337 return const Duration(days: 7 * 3); // Three weeks
338 }
339 }
Yegor93126a82017-04-04 10:45:43 -0700340
Yegor5efbe052017-04-10 13:21:02 -0700341 /// The amount of time we wait between issuing a warning.
342 ///
343 /// This is to avoid annoying users who are unable to upgrade right away.
Yegor93126a82017-04-04 10:45:43 -0700344 @visibleForTesting
Greg Spencereb35f892018-11-14 16:51:12 -0800345 static const Duration maxTimeSinceLastWarning = Duration(days: 1);
Yegor5efbe052017-04-10 13:21:02 -0700346
347 /// The amount of time we pause for to let the user read the message about
348 /// outdated Flutter installation.
349 ///
350 /// This can be customized in tests to speed them up.
351 @visibleForTesting
Danny Tuppenye616c6c2018-06-27 12:50:40 +0100352 static Duration timeToPauseToLetUserReadTheMessage = const Duration(seconds: 2);
Yegor93126a82017-04-04 10:45:43 -0700353
Greg Spencer7cebaac2018-08-29 21:53:39 -0700354 /// Reset the version freshness information by removing the stamp file.
355 ///
356 /// New version freshness information will be regenerated when
357 /// [checkFlutterVersionFreshness] is called after this. This is typically
358 /// used when switching channels so that stale information from another
359 /// channel doesn't linger.
Alexandre Ardhuin2d3ff102018-10-05 07:54:56 +0200360 static Future<void> resetFlutterVersionFreshnessCheck() async {
Greg Spencer7cebaac2018-08-29 21:53:39 -0700361 try {
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800362 await globals.cache.getStampFileFor(
Greg Spencereb35f892018-11-14 16:51:12 -0800363 VersionCheckStamp.flutterVersionCheckStampFile,
Greg Spencer7cebaac2018-08-29 21:53:39 -0700364 ).delete();
365 } on FileSystemException {
366 // Ignore, since we don't mind if the file didn't exist in the first place.
367 }
368 }
369
Yegor93126a82017-04-04 10:45:43 -0700370 /// Checks if the currently installed version of Flutter is up-to-date, and
371 /// warns the user if it isn't.
372 ///
373 /// This function must run while [Cache.lock] is acquired because it reads and
374 /// writes shared cache files.
Alexandre Ardhuin2d3ff102018-10-05 07:54:56 +0200375 Future<void> checkFlutterVersionFreshness() async {
Danny Tuppenye616c6c2018-06-27 12:50:40 +0100376 // Don't perform update checks if we're not on an official channel.
includecmath71c42c92020-09-25 04:07:03 +0800377 if (!kOfficialChannels.contains(channel)) {
Danny Tuppenye616c6c2018-06-27 12:50:40 +0100378 return;
379 }
380
Zachary Andersone4b809b2019-11-27 10:18:43 -0800381 DateTime localFrameworkCommitDate;
382 try {
383 localFrameworkCommitDate = DateTime.parse(_latestGitCommitDate(
384 lenient: false
385 ));
386 } on VersionCheckError {
Jenn Magderf6543462020-09-23 13:27:24 -0700387 // Don't perform the update check if the version check failed.
Zachary Andersone4b809b2019-11-27 10:18:43 -0800388 return;
389 }
390
Yegor93126a82017-04-04 10:45:43 -0700391 final Duration frameworkAge = _clock.now().difference(localFrameworkCommitDate);
Greg Spencereb35f892018-11-14 16:51:12 -0800392 final bool installationSeemsOutdated = frameworkAge > versionAgeConsideredUpToDate(channel);
Yegor93126a82017-04-04 10:45:43 -0700393
Danny Tuppenye616c6c2018-06-27 12:50:40 +0100394 // Get whether there's a newer version on the remote. This only goes
395 // to the server if we haven't checked recently so won't happen on every
396 // command.
397 final DateTime latestFlutterCommitDate = await _getLatestAvailableFlutterDate();
Zachary Andersone4b809b2019-11-27 10:18:43 -0800398 final VersionCheckResult remoteVersionStatus = latestFlutterCommitDate == null
399 ? VersionCheckResult.unknown
400 : latestFlutterCommitDate.isAfter(localFrameworkCommitDate)
401 ? VersionCheckResult.newVersionAvailable
402 : VersionCheckResult.versionIsCurrent;
Yegor93126a82017-04-04 10:45:43 -0700403
Danny Tuppenye616c6c2018-06-27 12:50:40 +0100404 // Do not load the stamp before the above server check as it may modify the stamp file.
Yegor5efbe052017-04-10 13:21:02 -0700405 final VersionCheckStamp stamp = await VersionCheckStamp.load();
Greg Spencereb35f892018-11-14 16:51:12 -0800406 final DateTime lastTimeWarningWasPrinted = stamp.lastTimeWarningWasPrinted ?? _clock.ago(maxTimeSinceLastWarning * 2);
407 final bool beenAWhileSinceWarningWasPrinted = _clock.now().difference(lastTimeWarningWasPrinted) > maxTimeSinceLastWarning;
Yegor5efbe052017-04-10 13:21:02 -0700408
Danny Tuppenye616c6c2018-06-27 12:50:40 +0100409 // We show a warning if either we know there is a new remote version, or we couldn't tell but the local
410 // version is outdated.
411 final bool canShowWarning =
Zachary Andersone4b809b2019-11-27 10:18:43 -0800412 remoteVersionStatus == VersionCheckResult.newVersionAvailable ||
413 (remoteVersionStatus == VersionCheckResult.unknown &&
414 installationSeemsOutdated);
Danny Tuppenye616c6c2018-06-27 12:50:40 +0100415
416 if (beenAWhileSinceWarningWasPrinted && canShowWarning) {
417 final String updateMessage =
Zachary Andersone4b809b2019-11-27 10:18:43 -0800418 remoteVersionStatus == VersionCheckResult.newVersionAvailable
419 ? newVersionAvailableMessage()
420 : versionOutOfDateMessage(frameworkAge);
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800421 globals.printStatus(updateMessage, emphasis: true);
Alexandre Ardhuin2d3ff102018-10-05 07:54:56 +0200422 await Future.wait<void>(<Future<void>>[
xster03393512017-06-16 14:22:10 -0700423 stamp.store(
424 newTimeWarningWasPrinted: _clock.now(),
425 ),
Alexandre Ardhuin2d3ff102018-10-05 07:54:56 +0200426 Future<void>.delayed(timeToPauseToLetUserReadTheMessage),
xster45446ae2017-06-15 18:49:19 -0700427 ]);
Yegor5efbe052017-04-10 13:21:02 -0700428 }
Yegor93126a82017-04-04 10:45:43 -0700429 }
430
∂ω∂4277f362019-08-21 23:55:57 +0300431 /// log.showSignature=false is a user setting and it will break things,
432 /// so we want to disable it for every git log call. This is a convenience
433 /// wrapper that does that.
434 @visibleForTesting
435 static List<String> gitLog(List<String> args) {
436 return <String>['git', '-c', 'log.showSignature=false', 'log'] + args;
437 }
438
Yegor93126a82017-04-04 10:45:43 -0700439 @visibleForTesting
440 static String versionOutOfDateMessage(Duration frameworkAge) {
441 String warning = 'WARNING: your installation of Flutter is ${frameworkAge.inDays} days old.';
442 // Append enough spaces to match the message box width.
443 warning += ' ' * (74 - warning.length);
444
445 return '''
446 ╔════════════════════════════════════════════════════════════════════════════╗
447 ║ $warning ║
448 ║ ║
449 ║ To update to the latest version, run "flutter upgrade". ║
450 ╚════════════════════════════════════════════════════════════════════════════╝
451''';
452 }
453
Danny Tuppenye616c6c2018-06-27 12:50:40 +0100454 @visibleForTesting
455 static String newVersionAvailableMessage() {
456 return '''
457 ╔════════════════════════════════════════════════════════════════════════════╗
458 ║ A new version of Flutter is available! ║
459 ║ ║
460 ║ To update to the latest version, run "flutter upgrade". ║
461 ╚════════════════════════════════════════════════════════════════════════════╝
462''';
463 }
464
Yegor93126a82017-04-04 10:45:43 -0700465 /// Gets the release date of the latest available Flutter version.
466 ///
467 /// This method sends a server request if it's been more than
Greg Spencereb35f892018-11-14 16:51:12 -0800468 /// [checkAgeConsideredUpToDate] since the last version check.
Yegor93126a82017-04-04 10:45:43 -0700469 ///
Ian Hickson0f1a7032017-06-08 17:13:03 -0700470 /// Returns null if the cached version is out-of-date or missing, and we are
Yegor93126a82017-04-04 10:45:43 -0700471 /// unable to reach the server to get the latest version.
Danny Tuppenye616c6c2018-06-27 12:50:40 +0100472 Future<DateTime> _getLatestAvailableFlutterDate() async {
Jonah Williams2fb53d82020-11-06 15:26:45 -0800473 globals.cache.checkLockAcquired();
Yegor5efbe052017-04-10 13:21:02 -0700474 final VersionCheckStamp versionCheckStamp = await VersionCheckStamp.load();
Yegor93126a82017-04-04 10:45:43 -0700475
Yegor5efbe052017-04-10 13:21:02 -0700476 if (versionCheckStamp.lastTimeVersionWasChecked != null) {
Zachary Andersone4b809b2019-11-27 10:18:43 -0800477 final Duration timeSinceLastCheck = _clock.now().difference(
478 versionCheckStamp.lastTimeVersionWasChecked,
479 );
Yegor93126a82017-04-04 10:45:43 -0700480
481 // Don't ping the server too often. Return cached value if it's fresh.
Zachary Andersone2340c62019-09-13 14:51:35 -0700482 if (timeSinceLastCheck < checkAgeConsideredUpToDate) {
Yegor5efbe052017-04-10 13:21:02 -0700483 return versionCheckStamp.lastKnownRemoteVersion;
Zachary Andersone2340c62019-09-13 14:51:35 -0700484 }
Yegor93126a82017-04-04 10:45:43 -0700485 }
486
487 // Cache is empty or it's been a while since the last server ping. Ping the server.
488 try {
Zachary Andersone4b809b2019-11-27 10:18:43 -0800489 final DateTime remoteFrameworkCommitDate = DateTime.parse(
490 await FlutterVersion.fetchRemoteFrameworkCommitDate(channel),
491 );
xster03393512017-06-16 14:22:10 -0700492 await versionCheckStamp.store(
Yegor5efbe052017-04-10 13:21:02 -0700493 newTimeVersionWasChecked: _clock.now(),
494 newKnownRemoteVersion: remoteFrameworkCommitDate,
495 );
Yegor93126a82017-04-04 10:45:43 -0700496 return remoteFrameworkCommitDate;
497 } on VersionCheckError catch (error) {
498 // This happens when any of the git commands fails, which can happen when
499 // there's no Internet connectivity. Remote version check is best effort
500 // only. We do not prevent the command from running when it fails.
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800501 globals.printTrace('Failed to check Flutter version in the remote repository: $error');
Danny Tuppenye616c6c2018-06-27 12:50:40 +0100502 // Still update the timestamp to avoid us hitting the server on every single
503 // command if for some reason we cannot connect (eg. we may be offline).
504 await versionCheckStamp.store(
505 newTimeVersionWasChecked: _clock.now(),
506 );
Yegor93126a82017-04-04 10:45:43 -0700507 return null;
508 }
509 }
Devon Carewadac9272016-04-26 16:25:11 -0700510}
511
Yegor5efbe052017-04-10 13:21:02 -0700512/// Contains data and load/save logic pertaining to Flutter version checks.
513@visibleForTesting
514class VersionCheckStamp {
Yegor5efbe052017-04-10 13:21:02 -0700515 const VersionCheckStamp({
516 this.lastTimeVersionWasChecked,
517 this.lastKnownRemoteVersion,
518 this.lastTimeWarningWasPrinted,
519 });
520
521 final DateTime lastTimeVersionWasChecked;
522 final DateTime lastKnownRemoteVersion;
523 final DateTime lastTimeWarningWasPrinted;
524
Alexandre Ardhuin2ea1d812018-10-04 07:28:07 +0200525 /// The prefix of the stamp file where we cache Flutter version check data.
526 @visibleForTesting
Greg Spencereb35f892018-11-14 16:51:12 -0800527 static const String flutterVersionCheckStampFile = 'flutter_version_check';
Alexandre Ardhuin2ea1d812018-10-04 07:28:07 +0200528
Yegor5efbe052017-04-10 13:21:02 -0700529 static Future<VersionCheckStamp> load() async {
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800530 final String versionCheckStamp = globals.cache.getStampFor(flutterVersionCheckStampFile);
Yegor5efbe052017-04-10 13:21:02 -0700531
532 if (versionCheckStamp != null) {
533 // Attempt to parse stamp JSON.
534 try {
Jason Simmons466d1542018-03-12 11:06:32 -0700535 final dynamic jsonObject = json.decode(versionCheckStamp);
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100536 if (jsonObject is Map<String, dynamic>) {
Jason Simmons466d1542018-03-12 11:06:32 -0700537 return fromJson(jsonObject);
Yegorfa47c342017-05-08 12:59:33 -0700538 } else {
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800539 globals.printTrace('Warning: expected version stamp to be a Map but found: $jsonObject');
Yegor5efbe052017-04-10 13:21:02 -0700540 }
Zachary Anderson6c408a02020-03-06 10:22:12 -0800541 } on Exception catch (error, stackTrace) {
Yegor5efbe052017-04-10 13:21:02 -0700542 // Do not crash if JSON is malformed.
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800543 globals.printTrace('${error.runtimeType}: $error\n$stackTrace');
Yegor5efbe052017-04-10 13:21:02 -0700544 }
545 }
546
547 // Stamp is missing or is malformed.
Yegora5593b12017-04-10 14:20:47 -0700548 return const VersionCheckStamp();
Yegor5efbe052017-04-10 13:21:02 -0700549 }
550
Devon Carew9d9836f2018-07-09 12:22:46 -0700551 static VersionCheckStamp fromJson(Map<String, dynamic> jsonObject) {
Yegor5efbe052017-04-10 13:21:02 -0700552 DateTime readDateTime(String property) {
Jason Simmons466d1542018-03-12 11:06:32 -0700553 return jsonObject.containsKey(property)
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100554 ? DateTime.parse(jsonObject[property] as String)
Danny Tuppenye616c6c2018-06-27 12:50:40 +0100555 : null;
Yegor5efbe052017-04-10 13:21:02 -0700556 }
557
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200558 return VersionCheckStamp(
Yegor5efbe052017-04-10 13:21:02 -0700559 lastTimeVersionWasChecked: readDateTime('lastTimeVersionWasChecked'),
560 lastKnownRemoteVersion: readDateTime('lastKnownRemoteVersion'),
561 lastTimeWarningWasPrinted: readDateTime('lastTimeWarningWasPrinted'),
562 );
563 }
564
Alexandre Ardhuin2d3ff102018-10-05 07:54:56 +0200565 Future<void> store({
Yegor5efbe052017-04-10 13:21:02 -0700566 DateTime newTimeVersionWasChecked,
567 DateTime newKnownRemoteVersion,
568 DateTime newTimeWarningWasPrinted,
569 }) async {
570 final Map<String, String> jsonData = toJson();
571
Zachary Andersone2340c62019-09-13 14:51:35 -0700572 if (newTimeVersionWasChecked != null) {
Yegor5efbe052017-04-10 13:21:02 -0700573 jsonData['lastTimeVersionWasChecked'] = '$newTimeVersionWasChecked';
Zachary Andersone2340c62019-09-13 14:51:35 -0700574 }
Yegor5efbe052017-04-10 13:21:02 -0700575
Zachary Andersone2340c62019-09-13 14:51:35 -0700576 if (newKnownRemoteVersion != null) {
Yegor5efbe052017-04-10 13:21:02 -0700577 jsonData['lastKnownRemoteVersion'] = '$newKnownRemoteVersion';
Zachary Andersone2340c62019-09-13 14:51:35 -0700578 }
Yegor5efbe052017-04-10 13:21:02 -0700579
Zachary Andersone2340c62019-09-13 14:51:35 -0700580 if (newTimeWarningWasPrinted != null) {
Yegor5efbe052017-04-10 13:21:02 -0700581 jsonData['lastTimeWarningWasPrinted'] = '$newTimeWarningWasPrinted';
Zachary Andersone2340c62019-09-13 14:51:35 -0700582 }
Yegor5efbe052017-04-10 13:21:02 -0700583
Greg Spencereb35f892018-11-14 16:51:12 -0800584 const JsonEncoder prettyJsonEncoder = JsonEncoder.withIndent(' ');
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800585 globals.cache.setStampFor(flutterVersionCheckStampFile, prettyJsonEncoder.convert(jsonData));
Yegor5efbe052017-04-10 13:21:02 -0700586 }
587
588 Map<String, String> toJson({
589 DateTime updateTimeVersionWasChecked,
590 DateTime updateKnownRemoteVersion,
591 DateTime updateTimeWarningWasPrinted,
592 }) {
593 updateTimeVersionWasChecked = updateTimeVersionWasChecked ?? lastTimeVersionWasChecked;
594 updateKnownRemoteVersion = updateKnownRemoteVersion ?? lastKnownRemoteVersion;
595 updateTimeWarningWasPrinted = updateTimeWarningWasPrinted ?? lastTimeWarningWasPrinted;
596
597 final Map<String, String> jsonData = <String, String>{};
598
Zachary Andersone2340c62019-09-13 14:51:35 -0700599 if (updateTimeVersionWasChecked != null) {
Yegor5efbe052017-04-10 13:21:02 -0700600 jsonData['lastTimeVersionWasChecked'] = '$updateTimeVersionWasChecked';
Zachary Andersone2340c62019-09-13 14:51:35 -0700601 }
Yegor5efbe052017-04-10 13:21:02 -0700602
Zachary Andersone2340c62019-09-13 14:51:35 -0700603 if (updateKnownRemoteVersion != null) {
Yegor5efbe052017-04-10 13:21:02 -0700604 jsonData['lastKnownRemoteVersion'] = '$updateKnownRemoteVersion';
Zachary Andersone2340c62019-09-13 14:51:35 -0700605 }
Yegor5efbe052017-04-10 13:21:02 -0700606
Zachary Andersone2340c62019-09-13 14:51:35 -0700607 if (updateTimeWarningWasPrinted != null) {
Yegor5efbe052017-04-10 13:21:02 -0700608 jsonData['lastTimeWarningWasPrinted'] = '$updateTimeWarningWasPrinted';
Zachary Andersone2340c62019-09-13 14:51:35 -0700609 }
Yegor5efbe052017-04-10 13:21:02 -0700610
611 return jsonData;
612 }
613}
614
Yegor93126a82017-04-04 10:45:43 -0700615/// Thrown when we fail to check Flutter version.
616///
617/// This can happen when we attempt to `git fetch` but there is no network, or
618/// when the installation is not git-based (e.g. a user clones the repo but
619/// then removes .git).
620class VersionCheckError implements Exception {
621
622 VersionCheckError(this.message);
623
624 final String message;
625
626 @override
627 String toString() => '$VersionCheckError: $message';
628}
629
630/// Runs [command] and returns the standard output as a string.
631///
Ian Hicksonefb45ea2017-09-27 16:13:48 -0700632/// If [lenient] is true and the command fails, returns an empty string.
Yegor93126a82017-04-04 10:45:43 -0700633/// Otherwise, throws a [ToolExit] exception.
Alexandre Ardhuin5169ab52019-02-21 09:27:07 +0100634String _runSync(List<String> command, { bool lenient = true }) {
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800635 final ProcessResult results = globals.processManager.runSync(
Zachary Andersone4b809b2019-11-27 10:18:43 -0800636 command,
637 workingDirectory: Cache.flutterRoot,
638 );
Yegor93126a82017-04-04 10:45:43 -0700639
Zachary Andersone2340c62019-09-13 14:51:35 -0700640 if (results.exitCode == 0) {
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100641 return (results.stdout as String).trim();
Zachary Andersone2340c62019-09-13 14:51:35 -0700642 }
Yegor93126a82017-04-04 10:45:43 -0700643
644 if (!lenient) {
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200645 throw VersionCheckError(
Yegor93126a82017-04-04 10:45:43 -0700646 'Command exited with code ${results.exitCode}: ${command.join(' ')}\n'
Zachary Andersone4b809b2019-11-27 10:18:43 -0800647 'Standard out: ${results.stdout}\n'
Yegor93126a82017-04-04 10:45:43 -0700648 'Standard error: ${results.stderr}'
649 );
650 }
651
652 return '';
653}
654
Jonah Williamse2554a92020-02-13 11:56:45 -0800655String _runGit(String command, ProcessUtils processUtils, [String workingDirectory]) {
Zachary Anderson73c10e82019-09-11 18:20:42 -0700656 return processUtils.runSync(
657 command.split(' '),
Jonah Williamse2554a92020-02-13 11:56:45 -0800658 workingDirectory: workingDirectory ?? Cache.flutterRoot,
Zachary Anderson73c10e82019-09-11 18:20:42 -0700659 ).stdout.trim();
Ian Hickson9e42e4b2018-01-18 07:59:06 -0800660}
661
Yegor93126a82017-04-04 10:45:43 -0700662/// Runs [command] in the root of the Flutter installation and returns the
663/// standard output as a string.
664///
665/// If the command fails, throws a [ToolExit] exception.
666Future<String> _run(List<String> command) async {
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800667 final ProcessResult results = await globals.processManager.run(command, workingDirectory: Cache.flutterRoot);
Yegor93126a82017-04-04 10:45:43 -0700668
Zachary Andersone2340c62019-09-13 14:51:35 -0700669 if (results.exitCode == 0) {
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100670 return (results.stdout as String).trim();
Zachary Andersone2340c62019-09-13 14:51:35 -0700671 }
Yegor93126a82017-04-04 10:45:43 -0700672
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200673 throw VersionCheckError(
Yegor93126a82017-04-04 10:45:43 -0700674 'Command exited with code ${results.exitCode}: ${command.join(' ')}\n'
675 'Standard error: ${results.stderr}'
676 );
Adam Barth9662d492015-11-28 21:07:16 -0800677}
Devon Carewb0ebc712016-04-27 10:47:52 -0700678
679String _shortGitRevision(String revision) {
Zachary Andersone2340c62019-09-13 14:51:35 -0700680 if (revision == null) {
Todd Volkertcedbfd42016-10-14 15:43:44 -0700681 return '';
Zachary Andersone2340c62019-09-13 14:51:35 -0700682 }
Devon Carewb0ebc712016-04-27 10:47:52 -0700683 return revision.length > 10 ? revision.substring(0, 10) : revision;
684}
Ian Hickson9e42e4b2018-01-18 07:59:06 -0800685
Nolan Scobie43c1b342020-08-06 19:18:52 -0400686/// Version of Flutter SDK parsed from Git.
Ian Hickson9e42e4b2018-01-18 07:59:06 -0800687class GitTagVersion {
Christopher Fujino23966162020-04-03 09:39:28 -0700688 const GitTagVersion({
689 this.x,
690 this.y,
691 this.z,
692 this.hotfix,
693 this.devVersion,
694 this.devPatch,
695 this.commits,
696 this.hash,
Christopher Fujino19c89482020-04-08 12:47:05 -0700697 this.gitTag,
Christopher Fujino23966162020-04-03 09:39:28 -0700698 });
Alexandre Ardhuinef276ff2019-01-29 21:47:16 +0100699 const GitTagVersion.unknown()
700 : x = null,
701 y = null,
702 z = null,
Ian Hicksona07c9a12019-03-08 19:26:34 -0800703 hotfix = null,
Alexandre Ardhuinef276ff2019-01-29 21:47:16 +0100704 commits = 0,
Christopher Fujino23966162020-04-03 09:39:28 -0700705 devVersion = null,
706 devPatch = null,
Christopher Fujino19c89482020-04-08 12:47:05 -0700707 hash = '',
708 gitTag = '';
Ian Hickson9e42e4b2018-01-18 07:59:06 -0800709
710 /// The X in vX.Y.Z.
711 final int x;
712
713 /// The Y in vX.Y.Z.
714 final int y;
715
716 /// The Z in vX.Y.Z.
717 final int z;
718
Nolan Scobie43c1b342020-08-06 19:18:52 -0400719 /// the F in vX.Y.Z+hotfix.F.
Ian Hicksona07c9a12019-03-08 19:26:34 -0800720 final int hotfix;
721
Ian Hickson9e42e4b2018-01-18 07:59:06 -0800722 /// Number of commits since the vX.Y.Z tag.
723 final int commits;
724
725 /// The git hash (or an abbreviation thereof) for this commit.
726 final String hash;
727
Nolan Scobie43c1b342020-08-06 19:18:52 -0400728 /// The N in X.Y.Z-dev.N.M.
Christopher Fujino23966162020-04-03 09:39:28 -0700729 final int devVersion;
730
Nolan Scobie43c1b342020-08-06 19:18:52 -0400731 /// The M in X.Y.Z-dev.N.M.
Christopher Fujino23966162020-04-03 09:39:28 -0700732 final int devPatch;
733
Christopher Fujino19c89482020-04-08 12:47:05 -0700734 /// The git tag that is this version's closest ancestor.
735 final String gitTag;
736
Anurag Roy05dadb02020-11-20 07:33:05 +0530737 static GitTagVersion determine(ProcessUtils processUtils, {String workingDirectory, bool fetchTags = false, String gitRef = 'HEAD'}) {
Dan Fielde13e1702020-03-06 21:38:35 -0800738 if (fetchTags) {
Dan Fieldc8efcb62020-03-27 22:31:01 -0700739 final String channel = _runGit('git rev-parse --abbrev-ref HEAD', processUtils, workingDirectory);
740 if (channel == 'dev' || channel == 'beta' || channel == 'stable') {
741 globals.printTrace('Skipping request to fetchTags - on well known channel $channel.');
742 } else {
Jonah Williamseae77802020-06-04 13:54:32 -0700743 _runGit('git fetch $_flutterGit --tags -f', processUtils, workingDirectory);
Dan Fieldc8efcb62020-03-27 22:31:01 -0700744 }
Dan Fielde13e1702020-03-06 21:38:35 -0800745 }
Christopher Fujino37ac9012020-04-23 15:19:27 -0700746 final List<String> tags = _runGit(
Anurag Roy05dadb02020-11-20 07:33:05 +0530747 'git tag --points-at $gitRef', processUtils, workingDirectory).trim().split('\n');
Ian Hicksona07c9a12019-03-08 19:26:34 -0800748
Christopher Fujino37ac9012020-04-23 15:19:27 -0700749 // Check first for a stable tag
750 final RegExp stableTagPattern = RegExp(r'^\d+\.\d+\.\d+$');
751 for (final String tag in tags) {
Christopher Fujino8dcb4c32020-08-11 17:29:35 -0700752 if (stableTagPattern.hasMatch(tag.trim())) {
753 return parse(tag);
Christopher Fujino37ac9012020-04-23 15:19:27 -0700754 }
Christopher Fujino23966162020-04-03 09:39:28 -0700755 }
Christopher Fujino37ac9012020-04-23 15:19:27 -0700756 // Next check for a dev tag
757 final RegExp devTagPattern = RegExp(r'^\d+\.\d+\.\d+-\d+\.\d+\.pre$');
758 for (final String tag in tags) {
Christopher Fujino8dcb4c32020-08-11 17:29:35 -0700759 if (devTagPattern.hasMatch(tag.trim())) {
760 return parse(tag);
Christopher Fujino37ac9012020-04-23 15:19:27 -0700761 }
Christopher Fujino23966162020-04-03 09:39:28 -0700762 }
Christopher Fujino37ac9012020-04-23 15:19:27 -0700763
764 // If we're not currently on a tag, use git describe to find the most
765 // recent tag and number of commits past.
766 return parse(
767 _runGit(
Anurag Roy05dadb02020-11-20 07:33:05 +0530768 'git describe --match *.*.* --long --tags $gitRef',
Christopher Fujino37ac9012020-04-23 15:19:27 -0700769 processUtils,
770 workingDirectory,
771 )
Christopher Fujino19c89482020-04-08 12:47:05 -0700772 );
773 }
774
Christopher Fujino37ac9012020-04-23 15:19:27 -0700775 /// Parse a version string.
776 ///
777 /// The version string can either be an exact release tag (e.g. '1.2.3' for
778 /// stable or 1.2.3-4.5.pre for a dev) or the output of `git describe` (e.g.
779 /// for commit abc123 that is 6 commits after tag 1.2.3-4.5.pre, git would
780 /// return '1.2.3-4.5.pre-6-gabc123').
Christopher Fujino19c89482020-04-08 12:47:05 -0700781 static GitTagVersion parseVersion(String version) {
782 final RegExp versionPattern = RegExp(
Christopher Fujino37ac9012020-04-23 15:19:27 -0700783 r'^(\d+)\.(\d+)\.(\d+)(-\d+\.\d+\.pre)?(?:-(\d+)-g([a-f0-9]+))?$');
784 final Match match = versionPattern.firstMatch(version.trim());
785 if (match == null) {
Christopher Fujino19c89482020-04-08 12:47:05 -0700786 return const GitTagVersion.unknown();
787 }
Christopher Fujino37ac9012020-04-23 15:19:27 -0700788
789 final List<String> matchGroups = match.groups(<int>[1, 2, 3, 4, 5, 6]);
790 final int x = matchGroups[0] == null ? null : int.tryParse(matchGroups[0]);
791 final int y = matchGroups[1] == null ? null : int.tryParse(matchGroups[1]);
792 final int z = matchGroups[2] == null ? null : int.tryParse(matchGroups[2]);
793 final String devString = matchGroups[3];
794 int devVersion, devPatch;
795 if (devString != null) {
796 final Match devMatch = RegExp(r'^-(\d+)\.(\d+)\.pre$')
797 .firstMatch(devString);
798 final List<String> devGroups = devMatch.groups(<int>[1, 2]);
799 devVersion = devGroups[0] == null ? null : int.tryParse(devGroups[0]);
800 devPatch = devGroups[1] == null ? null : int.tryParse(devGroups[1]);
Christopher Fujino19c89482020-04-08 12:47:05 -0700801 }
Christopher Fujino37ac9012020-04-23 15:19:27 -0700802 // count of commits past last tagged version
803 final int commits = matchGroups[4] == null ? 0 : int.tryParse(matchGroups[4]);
804 final String hash = matchGroups[5] ?? '';
805
Christopher Fujino19c89482020-04-08 12:47:05 -0700806 return GitTagVersion(
Christopher Fujino37ac9012020-04-23 15:19:27 -0700807 x: x,
808 y: y,
809 z: z,
810 devVersion: devVersion,
811 devPatch: devPatch,
812 commits: commits,
813 hash: hash,
814 gitTag: '$x.$y.$z${devString ?? ''}', // e.g. 1.2.3-4.5.pre
Christopher Fujino23966162020-04-03 09:39:28 -0700815 );
816 }
817
818 static GitTagVersion parse(String version) {
819 GitTagVersion gitTagVersion;
820
Christopher Fujino23966162020-04-03 09:39:28 -0700821 gitTagVersion = parseVersion(version);
822 if (gitTagVersion != const GitTagVersion.unknown()) {
823 return gitTagVersion;
824 }
825 globals.printTrace('Could not interpret results of "git describe": $version');
826 return const GitTagVersion.unknown();
Ian Hickson9e42e4b2018-01-18 07:59:06 -0800827 }
828
829 String frameworkVersionFor(String revision) {
Zachary Andersone2340c62019-09-13 14:51:35 -0700830 if (x == null || y == null || z == null || !revision.startsWith(hash)) {
Ian Hickson64e2e002018-01-26 16:59:56 -0800831 return '0.0.0-unknown';
Zachary Andersone2340c62019-09-13 14:51:35 -0700832 }
Ian Hicksona07c9a12019-03-08 19:26:34 -0800833 if (commits == 0) {
Christopher Fujino19c89482020-04-08 12:47:05 -0700834 return gitTag;
Ian Hicksona07c9a12019-03-08 19:26:34 -0800835 }
Zachary Andersone2340c62019-09-13 14:51:35 -0700836 if (hotfix != null) {
Christopher Fujino19c89482020-04-08 12:47:05 -0700837 // This is an unexpected state where untagged commits exist past a hotfix
838 return '$x.$y.$z+hotfix.${hotfix + 1}.pre.$commits';
Zachary Andersone2340c62019-09-13 14:51:35 -0700839 }
Christopher Fujino19c89482020-04-08 12:47:05 -0700840 if (devPatch != null && devVersion != null) {
841 return '$x.$y.$z-${devVersion + 1}.0.pre.$commits';
842 }
Chris Yangc9cd8252020-08-05 15:20:27 -0700843 return '$x.$y.${z + 1}-0.0.pre.$commits';
Ian Hickson9e42e4b2018-01-18 07:59:06 -0800844 }
845}
Danny Tuppenye616c6c2018-06-27 12:50:40 +0100846
847enum VersionCheckResult {
848 /// Unable to check whether a new version is available, possibly due to
849 /// a connectivity issue.
850 unknown,
851 /// The current version is up to date.
852 versionIsCurrent,
853 /// A newer version is available.
854 newVersionAvailable,
855}