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; + } + }); +}