diff --git a/README.md b/README.md
index 8c8efa97..52df7180 100644
--- a/README.md
+++ b/README.md
@@ -9,10 +9,12 @@ Flutter Version Management: A simple cli to manage Flutter SDK versions.
**Features:**
-- Configure Flutter SDK version per project
+- Configure and use Flutter SDK version per project
- Ability to install and cache multiple Flutter SDK Versions
-- Easily switch between Flutter channels & versions
-- Per project Flutter SDK upgrade
+- Fast switch between Flutter channels & versions
+- Dynamic sdk paths for IDE debugging support.
+- Version FVM config with project for consistency across teams and CI environments.
+- Set global Flutter version across projects
## Version Management
@@ -83,6 +85,14 @@ List all the versions that are installed on your machine. This command will also
> fvm list
```
+### List Flutter Releases
+
+Displays all Flutter releases, including the current version for `dev`, `beta` and `stable` channels.
+
+```bash
+> fvm releases
+```
+
### Change FVM Cache Directory
There are some configurations that allows for added flexibility on FVM. If no `cache-path` is set, the default fvm path will be used.
diff --git a/coverage_badge.svg b/coverage_badge.svg
index f6e16c2f..3b4d6cb9 100644
--- a/coverage_badge.svg
+++ b/coverage_badge.svg
@@ -8,13 +8,13 @@
-
+
coverage
coverage
- 74%
- 74%
+ 72%
+ 72%
diff --git a/lib/commands/install.dart b/lib/commands/install.dart
index e7085886..67fc6623 100644
--- a/lib/commands/install.dart
+++ b/lib/commands/install.dart
@@ -30,11 +30,13 @@ class InstallCommand extends Command {
Guards.isGitInstalled();
String version;
+ var hasConfig = false;
if (argResults.arguments.isEmpty) {
final configVersion = getConfigFlutterVersion();
if (configVersion == null) {
throw ExceptionMissingChannelVersion();
}
+ hasConfig = true;
version = configVersion;
} else {
version = argResults.arguments[0].toLowerCase();
@@ -45,5 +47,8 @@ class InstallCommand extends Command {
final flutterVersion = await inferFlutterVersion(version);
await installFlutterVersion(flutterVersion, skipSetup: skipSetup);
+ if (hasConfig) {
+ setAsProjectVersion(version);
+ }
}
}
diff --git a/lib/commands/list.dart b/lib/commands/list.dart
index 45642a37..110a8abe 100644
--- a/lib/commands/list.dart
+++ b/lib/commands/list.dart
@@ -1,5 +1,6 @@
import 'dart:io';
+import 'package:console/console.dart';
import 'package:fvm/constants.dart';
import 'package:fvm/utils/helpers.dart';
import 'package:fvm/utils/print.dart';
@@ -35,7 +36,7 @@ class ListCommand extends Command {
void printVersions(String version) {
if (isCurrentVersion(version)) {
- version = '$version (current)';
+ version = '$version ${Icon.HEAVY_CHECKMARK}';
}
Print.info(version);
}
diff --git a/lib/commands/releases.dart b/lib/commands/releases.dart
new file mode 100644
index 00000000..df873efd
--- /dev/null
+++ b/lib/commands/releases.dart
@@ -0,0 +1,42 @@
+import 'package:args/command_runner.dart';
+import 'package:console/console.dart';
+import 'package:date_format/date_format.dart';
+import 'package:io/ansi.dart';
+
+import 'package:fvm/utils/releases_helper.dart';
+
+/// List installed SDK Versions
+class ReleasesCommand extends Command {
+ // The [name] and [description] properties must be defined by every
+ // subclass.
+ @override
+ final name = 'releases';
+
+ @override
+ final description = 'Lists Flutter SDK releases.';
+
+ /// Constructor
+ ReleasesCommand();
+
+ @override
+ void run() async {
+ final flutterReleases = await fetchReleases();
+ final channels = flutterReleases.currentRelease.toHashMap();
+ final releases = flutterReleases.releases.reversed;
+
+ releases.forEach((r) {
+ final channel = channels[r.version];
+ final channelOutput = green.wrap('$channel');
+ final version = yellow.wrap(r.version.padRight(17));
+ final pipe = Icon.PIPE_VERTICAL;
+ final friendlyDate =
+ formatDate(r.releaseDate, [M, ' ', d, ' ', yy]).padRight(10);
+ if (channel != null) {
+ print('----------$channelOutput----------');
+ print('$friendlyDate $pipe $version');
+ } else {
+ print('$friendlyDate $pipe $version');
+ }
+ });
+ }
+}
diff --git a/lib/exceptions.dart b/lib/exceptions.dart
index 81f1134b..470d716a 100644
--- a/lib/exceptions.dart
+++ b/lib/exceptions.dart
@@ -78,7 +78,20 @@ class ExceptionMissingChannelVersion implements Exception {
}
}
-/// Cannot find a config for the projec
+/// Could not fetch Flutter releases
+class ExceptionCouldNotFetchReleases implements Exception {
+ final message = 'Could not fetch Flutter releases.';
+
+ /// Constructor
+ ExceptionCouldNotFetchReleases();
+
+ @override
+ String toString() {
+ return message;
+ }
+}
+
+/// Cannot find a config for the project
class ExceptionProjectConfigNotFound implements Exception {
final message = 'No config found for this project.';
diff --git a/lib/fvm.dart b/lib/fvm.dart
index 0181e0a7..5ea433d9 100644
--- a/lib/fvm.dart
+++ b/lib/fvm.dart
@@ -3,6 +3,7 @@ import 'package:fvm/commands/config.dart';
import 'package:fvm/commands/flutter.dart';
import 'package:fvm/commands/install.dart';
import 'package:fvm/commands/list.dart';
+import 'package:fvm/commands/releases.dart';
import 'package:fvm/commands/remove.dart';
import 'package:fvm/commands/runner.dart';
import 'package:fvm/commands/use.dart';
@@ -22,6 +23,7 @@ Future fvmRunner(List args) async {
runner..addCommand(UseCommand());
runner..addCommand(ConfigCommand());
runner..addCommand(VersionCommand());
+ runner..addCommand(ReleasesCommand());
return await runner.run(args).catchError((exc, st) {
if (exc is String) {
diff --git a/lib/utils/helpers.dart b/lib/utils/helpers.dart
index 14176a18..64840bf7 100644
--- a/lib/utils/helpers.dart
+++ b/lib/utils/helpers.dart
@@ -1,3 +1,5 @@
+import 'dart:async';
+
import 'dart:io';
import 'package:fvm/constants.dart';
@@ -79,10 +81,7 @@ bool isCurrentVersion(String version) {
/// The Flutter SDK Path referenced on FVM
String getFlutterSdkPath({String version}) {
var sdkVersion = version;
- if (version == null) {
- final config = readProjectConfig();
- sdkVersion = config.flutterSdkVersion;
- }
+ sdkVersion ??= readProjectConfig().flutterSdkVersion;
return path.join(kVersionsDir.path, sdkVersion);
}
@@ -90,3 +89,18 @@ String getFlutterSdkExec({String version}) {
return path.join(getFlutterSdkPath(version: version), 'bin',
Platform.isWindows ? 'flutter.bat' : 'flutter');
}
+
+String camelCase(String subject) {
+ final _splittedString = subject.split('_');
+
+ if (_splittedString.isEmpty) return '';
+
+ final _firstWord = _splittedString[0].toLowerCase();
+ final _restWords = _splittedString.sublist(1).map(capitalize).toList();
+
+ return _firstWord + _restWords.join('');
+}
+
+String capitalize(String word) {
+ return '${word[0].toUpperCase()}${word.substring(1)}';
+}
diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart
index 8cc8c4c6..437c549a 100644
--- a/lib/utils/logger.dart
+++ b/lib/utils/logger.dart
@@ -2,8 +2,3 @@ import 'package:cli_util/cli_logging.dart';
/// Log
Logger logger = Logger.standard();
-
-/// Finishes progress
-void finishProgress(Progress progress) {
- progress.finish(showTiming: true);
-}
diff --git a/lib/utils/releases_helper.dart b/lib/utils/releases_helper.dart
new file mode 100644
index 00000000..a30c86ac
--- /dev/null
+++ b/lib/utils/releases_helper.dart
@@ -0,0 +1,157 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:fvm/exceptions.dart';
+import 'package:http/http.dart' as http;
+
+/// Gets platform specific release URL
+String getReleasesUrl({String platform}) {
+ platform ??= Platform.operatingSystem;
+ return 'https://storage.googleapis.com/flutter_infra/releases/releases_$platform.json';
+}
+
+/// Fetches Flutter SDK Releases
+Future fetchReleases() async {
+ try {
+ final response = await http.get(getReleasesUrl());
+ return flutterReleasesFromMap(response.body);
+ } on Exception {
+ throw ExceptionCouldNotFetchReleases();
+ }
+}
+
+Map filterCurrentReleases(Map json) {
+ final currentRelease = json['current_release'] as Map;
+ final releases = json['releases'] as List;
+ // Hashes of current releases
+ final hashMap = currentRelease.map((key, value) => MapEntry(value, key));
+
+ // Filter out channel/currentRelease versions
+ releases.forEach((r) {
+ // Check if release hash is in hashmap
+ final channel = hashMap[r['hash']];
+ if (channel != null) {
+ currentRelease[channel] = r['version'];
+ }
+ });
+
+ return currentRelease;
+}
+
+FlutterReleases flutterReleasesFromMap(String str) =>
+ FlutterReleases.fromMap(jsonDecode(str) as Map);
+
+String flutterReleasesToMap(FlutterReleases data) => json.encode(data.toMap());
+
+class FlutterReleases {
+ FlutterReleases({
+ this.baseUrl,
+ this.currentRelease,
+ this.releases,
+ });
+
+ final String baseUrl;
+ final CurrentRelease currentRelease;
+ final List releases;
+
+ factory FlutterReleases.fromMap(Map json) {
+ final currentRelease = filterCurrentReleases(json);
+ return FlutterReleases(
+ baseUrl: json['base_url'] as String,
+ currentRelease: CurrentRelease.fromMap(currentRelease),
+ releases: List.from(json['releases']
+ .map((x) => Release.fromMap(x as Map))
+ as Iterable),
+ );
+ }
+
+ Map toMap() => {
+ 'base_url': baseUrl,
+ 'current_release': currentRelease.toMap(),
+ 'releases': List.from(releases.map((x) => x.toMap())),
+ };
+}
+
+class CurrentRelease {
+ CurrentRelease({
+ this.beta,
+ this.dev,
+ this.stable,
+ });
+
+ final String beta;
+ final String dev;
+ final String stable;
+
+ factory CurrentRelease.fromMap(Map json) => CurrentRelease(
+ beta: json['beta'] as String,
+ dev: json['dev'] as String,
+ stable: json['stable'] as String,
+ );
+
+ Map toMap() => {
+ 'beta': beta,
+ 'dev': dev,
+ 'stable': stable,
+ };
+
+ Map toHashMap() => {
+ '$beta': 'beta',
+ '$dev': 'dev',
+ '$stable': 'stable',
+ };
+}
+
+class Release {
+ Release({
+ this.hash,
+ this.channel,
+ this.version,
+ this.releaseDate,
+ this.archive,
+ this.sha256,
+ });
+
+ final String hash;
+ final Channel channel;
+ final String version;
+ final DateTime releaseDate;
+ final String archive;
+ final String sha256;
+
+ factory Release.fromMap(Map json) => Release(
+ hash: json['hash'] as String,
+ channel: channelValues.map[json['channel']],
+ version: json['version'] as String,
+ releaseDate: DateTime.parse(json['release_date'] as String),
+ archive: json['archive'] as String,
+ sha256: json['sha256'] as String,
+ );
+
+ Map toMap() => {
+ 'hash': hash,
+ 'channel': channelValues.reverse[channel],
+ 'version': version,
+ 'release_date': releaseDate.toIso8601String(),
+ 'archive': archive,
+ 'sha256': sha256,
+ };
+}
+
+enum Channel { STABLE, DEV, BETA }
+
+final channelValues = EnumValues(
+ {'beta': Channel.BETA, 'dev': Channel.DEV, 'stable': Channel.STABLE});
+
+class EnumValues {
+ Map map;
+ Map reverseMap;
+
+ EnumValues(this.map);
+
+ Map get reverse {
+ reverseMap ??= map.map((k, v) => MapEntry(v, k));
+
+ return reverseMap;
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 107ba9d4..bf87a401 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -16,6 +16,8 @@ dependencies:
io: ^0.3.3
path: ^1.6.4
process_run: ^0.10.10+1
+ http: ^0.12.1
+ date_format: ^1.0.8
dev_dependencies:
pedantic: ^1.8.0
diff --git a/test/fvm_test.dart b/test/fvm_test.dart
index b21e6cb8..39f5119f 100644
--- a/test/fvm_test.dart
+++ b/test/fvm_test.dart
@@ -12,9 +12,6 @@ import 'test_helpers.dart';
final testPath = '$fvmHome/test_path';
-const channel = 'master';
-const release = '1.8.0';
-
void main() {
setUpAll(fvmSetUpAll);
tearDownAll(fvmTearDownAll);
diff --git a/test/test_helpers.dart b/test/test_helpers.dart
index 3ff7b6ab..6fc61d3c 100644
--- a/test/test_helpers.dart
+++ b/test/test_helpers.dart
@@ -3,6 +3,12 @@ import 'dart:io';
import 'package:fvm/constants.dart';
import 'package:fvm/utils/config_utils.dart';
+// git clone --mirror https://github.com/flutter/flutter.git ~/gitcaches/flutter.reference
+// git clone --reference ~/gitcaches/flutter.reference https://github.com/flutter/flutter.git
+
+String release = '1.8.0';
+String channel = 'stable';
+
void cleanup() {
final fvmHomeDir = Directory(fvmHome);
if (fvmHomeDir.existsSync()) {
diff --git a/test/utils/releases_test.dart b/test/utils/releases_test.dart
new file mode 100644
index 00000000..b9a21cff
--- /dev/null
+++ b/test/utils/releases_test.dart
@@ -0,0 +1,38 @@
+import 'package:fvm/fvm.dart';
+import 'package:fvm/utils/releases_helper.dart';
+@Timeout(Duration(minutes: 5))
+import 'package:test/test.dart';
+import 'package:http/http.dart' as http;
+
+void main() {
+ test('Can fetch releases for all platforms', () async {
+ try {
+ await http.get(getReleasesUrl(platform: 'macos'));
+ await http.get(getReleasesUrl(platform: 'linux'));
+ await http.get(getReleasesUrl(platform: 'windows'));
+ expect(true, true);
+ } on Exception {
+ fail('Could not resolve all platform releases');
+ }
+ });
+
+ test('Can run releases', () async {
+ try {
+ await fvmRunner(['releases']);
+
+ expect(true, true);
+ } on Exception {
+ rethrow;
+ }
+ });
+
+ test('Can download release', () async {
+ try {
+ final releases = await fetchReleases();
+ print(releases.toString());
+ expect(true, true);
+ } on Exception {
+ rethrow;
+ }
+ });
+}