feat: add flutter_version_resolver package (#3266)
Co-authored-by: Felix Angelov <felix@shorebird.dev>
This commit is contained in:
@@ -22,8 +22,9 @@ This repository is a monorepo containing the following packages:
|
||||
| [shorebird_cli](packages/shorebird_cli/README.md) | Command-line which allows developers to interact with various Shorebird services |
|
||||
| [shorebird_code_push_client](packages/shorebird_code_push_client/README.md) | Dart library which allows Dart applications to interact with the Shorebird CodePush API |
|
||||
| [shorebird_code_push_protocol](packages/shorebird_code_push_protocol/README.md) | Dart library which contains common interfaces used by Shorebird CodePush |
|
||||
| [artifact_proxy](packages/artifact_proxy/README.md) | Dart server which supports intercepting and proxying Flutter artifact requests. |
|
||||
| [artifact_proxy](packages/artifact_proxy/README.md) | Dart server which supports intercepting and proxying Flutter artifact requests |
|
||||
| [discord_gcp_alerts](packages/discord_gcp_alerts/README.md) | Dart server which forwards GCP alerts to Discord |
|
||||
| [flutter_version_resolver](packages/flutter_version_resolver/README.md) | Command-line utility that determines which Flutter version should be used for a project |
|
||||
| [jwt](packages/jwt/README.md) | Dart library for verifying JSON Web Tokens |
|
||||
| [redis_client](packages/redis_client/README.md) | Dart library for interacting with Redis |
|
||||
| [scoped_deps](packages/scoped_deps/README.md) | A simple dependency injection library built on Zones |
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# https://dart.dev/guides/libraries/private-files
|
||||
# Created by `dart pub`
|
||||
.dart_tool/
|
||||
@@ -0,0 +1,11 @@
|
||||
# Contributing
|
||||
|
||||
We are happy to accept contributions!
|
||||
|
||||
## Developing
|
||||
|
||||
This library has 100% coverage and all PRs are expected to be tested.
|
||||
|
||||
### Running Tests
|
||||
|
||||
All you need to do is run `dart test`.
|
||||
@@ -0,0 +1,19 @@
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,4 @@
|
||||
# Flutter Version Resolver
|
||||
|
||||
Analyzes Flutter packages to determine the Flutter version that should be used
|
||||
to build and test.
|
||||
@@ -0,0 +1 @@
|
||||
include: ../../analysis_options.yaml
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_version_resolver/flutter_version_resolver.dart';
|
||||
import 'package:flutter_version_resolver/src/logger.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:scoped_deps/scoped_deps.dart';
|
||||
|
||||
/// Resolves the Flutter version for a package and optionally writes it to a
|
||||
/// file.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```sh
|
||||
/// dart run bin/flutter_version_resolver.dart <path-to-package> [<output-file>]
|
||||
/// ```
|
||||
Future<int> main(List<String> arguments) async {
|
||||
return await runScoped(() async {
|
||||
if (arguments.isEmpty || arguments.length > 2) {
|
||||
logger.err(
|
||||
'Usage: dart run bin/flutter_version_resolver.dart <path-to-package> [<output-file>]',
|
||||
);
|
||||
return ExitCode.usage.code;
|
||||
}
|
||||
|
||||
final packageDirectory = Directory(arguments[0]);
|
||||
if (!packageDirectory.existsSync()) {
|
||||
logger.err(
|
||||
'Package directory does not exist: ${packageDirectory.path}',
|
||||
);
|
||||
return ExitCode.usage.code;
|
||||
}
|
||||
|
||||
final flutterVersion = resolveFlutterVersion(
|
||||
packagePath: packageDirectory.path,
|
||||
);
|
||||
logger.info('Resolved Flutter version: $flutterVersion');
|
||||
|
||||
if (arguments.length > 1) {
|
||||
final outputFile = File(arguments[1])
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(flutterVersion);
|
||||
logger.info('Wrote flutter version to ${outputFile.path}');
|
||||
}
|
||||
|
||||
return ExitCode.success.code;
|
||||
}, values: {loggerRef});
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_version_resolver/src/logger.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
/// {@template version_constraint_exception}
|
||||
/// Thrown when a version constraint is found unexpectedly.
|
||||
/// {@endtemplate}
|
||||
class VersionConstraintException implements Exception {
|
||||
/// {@macro version_constraint_exception}
|
||||
VersionConstraintException({required this.versionConstraint});
|
||||
|
||||
/// The version constraint that was found.
|
||||
final String versionConstraint;
|
||||
}
|
||||
|
||||
/// Given a path to a Flutter package, attempts to determine the Flutter version
|
||||
/// that should be used to build the package.
|
||||
///
|
||||
/// This checks the following locations, in this order:
|
||||
///
|
||||
/// 1. The `environment` section of the pubspec.yaml file
|
||||
/// 2. TODO(bryanoltman): add fvm support
|
||||
///
|
||||
/// If no version is found, this returns the `stable` version.
|
||||
String resolveFlutterVersion({
|
||||
required String packagePath,
|
||||
}) {
|
||||
logger
|
||||
..info('Resolving Flutter version for $packagePath')
|
||||
..info('Checking pubspec.yaml environment section for flutter version');
|
||||
|
||||
try {
|
||||
final flutterVersion = flutterVersionFromPubspecEnvironment(
|
||||
packagePath: packagePath,
|
||||
);
|
||||
if (flutterVersion != null) {
|
||||
logger.info('Found flutter version in pubspec.yaml: $flutterVersion');
|
||||
return flutterVersion.toString();
|
||||
}
|
||||
} on VersionConstraintException catch (e) {
|
||||
logger.err(
|
||||
'''Found version constraint: ${e.versionConstraint}. Version constraints are not supported in pubspec.yaml. Please specify a specific version.''',
|
||||
);
|
||||
return 'stable';
|
||||
} on Exception catch (e) {
|
||||
logger
|
||||
..err('Error resolving Flutter version: $e')
|
||||
..info('Falling back to "stable" branch');
|
||||
return 'stable';
|
||||
}
|
||||
|
||||
logger.info('No flutter version found in pubspec.yaml, using stable');
|
||||
return 'stable';
|
||||
}
|
||||
|
||||
/// Returns the Flutter version specified in the `environment` section of the
|
||||
/// pubspec.yaml file, or `null` if no version is specified.
|
||||
///
|
||||
/// A pubspec.yaml with the following will return `Version(3, 20, 0)`:
|
||||
/// ```yaml
|
||||
/// environment:
|
||||
/// sdk: ^3.8.1
|
||||
/// flutter: 3.20.0
|
||||
/// ```
|
||||
Version? flutterVersionFromPubspecEnvironment({required String packagePath}) {
|
||||
final pubspecFile = File(p.join(packagePath, 'pubspec.yaml'));
|
||||
if (!pubspecFile.existsSync()) {
|
||||
throw Exception('pubspec.yaml not found at ${pubspecFile.path}');
|
||||
}
|
||||
|
||||
final pubspecYaml = loadYaml(pubspecFile.readAsStringSync());
|
||||
if (pubspecYaml is! YamlMap) {
|
||||
throw Exception('Failed to parse pubspec.yaml at ${pubspecFile.path}');
|
||||
}
|
||||
|
||||
final environment = pubspecYaml['environment'] as YamlMap?;
|
||||
final flutterVersionString = environment?['flutter'] as String?;
|
||||
if (flutterVersionString == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final version = VersionConstraint.parse(flutterVersionString);
|
||||
if (version is Version) {
|
||||
return version;
|
||||
}
|
||||
|
||||
// We were successfully able to parse the flutterVersionString, but it is a
|
||||
// version constraint, not a specific version.
|
||||
throw VersionConstraintException(versionConstraint: flutterVersionString);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:scoped_deps/scoped_deps.dart';
|
||||
|
||||
/// A reference to a [Logger] instance.
|
||||
final loggerRef = create(Logger.new);
|
||||
|
||||
/// The [Logger] instance available in the current zone.
|
||||
Logger get logger => read(loggerRef);
|
||||
@@ -0,0 +1,20 @@
|
||||
name: flutter_version_resolver
|
||||
description: A Dart tool that resolves a Flutter version for a given Flutter application
|
||||
version: 1.0.0
|
||||
publish_to: none
|
||||
resolution: workspace
|
||||
|
||||
environment:
|
||||
sdk: ^3.8.1
|
||||
|
||||
dependencies:
|
||||
mason_logger: ^0.3.3
|
||||
path: ^1.9.1
|
||||
pub_semver: ^2.2.0
|
||||
scoped_deps: ^0.1.0+2
|
||||
yaml: ^3.1.3
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^5.0.0
|
||||
mocktail: ^1.0.4
|
||||
test: ^1.24.0
|
||||
@@ -0,0 +1,173 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_version_resolver/flutter_version_resolver.dart';
|
||||
import 'package:flutter_version_resolver/src/logger.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
import 'package:scoped_deps/scoped_deps.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _MockLogger extends Mock implements Logger {}
|
||||
|
||||
void main() {
|
||||
late Logger logger;
|
||||
late Directory packageDirectory;
|
||||
late File pubspecFile;
|
||||
|
||||
R runWithOverrides<R>(R Function() body) {
|
||||
return runScoped(
|
||||
body,
|
||||
values: {loggerRef.overrideWith(() => logger)},
|
||||
);
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
logger = _MockLogger();
|
||||
|
||||
packageDirectory = Directory.systemTemp.createTempSync(
|
||||
'flutter_version_resolver_test',
|
||||
);
|
||||
pubspecFile = File(
|
||||
p.join(packageDirectory.path, 'pubspec.yaml'),
|
||||
)..writeAsStringSync('name: flutter_version_resolver_test');
|
||||
});
|
||||
|
||||
group('resolveFlutterVersion', () {
|
||||
group('when no flutter version is specified in the pubspec.yaml file', () {
|
||||
test('returns the stable version', () {
|
||||
runWithOverrides(() {
|
||||
expect(
|
||||
resolveFlutterVersion(packagePath: packageDirectory.path),
|
||||
equals('stable'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('when a flutter version is specified in the pubspec.yaml file', () {
|
||||
setUp(() {
|
||||
pubspecFile.writeAsStringSync('''
|
||||
environment:
|
||||
sdk: ^3.8.1
|
||||
flutter: 3.20.0
|
||||
''');
|
||||
});
|
||||
|
||||
test('returns the version', () {
|
||||
runWithOverrides(() {
|
||||
expect(
|
||||
resolveFlutterVersion(packagePath: packageDirectory.path),
|
||||
equals('3.20.0'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('when a version constraint is specified in the pubspec.yaml file', () {
|
||||
setUp(() {
|
||||
pubspecFile.writeAsStringSync('''
|
||||
environment:
|
||||
sdk: ^3.8.1
|
||||
flutter: "^3.8.0"
|
||||
''');
|
||||
});
|
||||
|
||||
test('prints an error message and returns the stable version', () {
|
||||
runWithOverrides(() {
|
||||
expect(
|
||||
resolveFlutterVersion(packagePath: packageDirectory.path),
|
||||
equals('stable'),
|
||||
);
|
||||
});
|
||||
verify(
|
||||
() => logger.err(
|
||||
'''Found version constraint: ^3.8.0. Version constraints are not supported in pubspec.yaml. Please specify a specific version.''',
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('flutterVersionFromPubspecEnvironment', () {
|
||||
group('when no pubspec.yaml is found', () {
|
||||
test('throws an exception', () {
|
||||
expect(
|
||||
() => flutterVersionFromPubspecEnvironment(
|
||||
packagePath: 'no/such/package',
|
||||
),
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when no flutter version is specified', () {
|
||||
test('returns null', () {
|
||||
expect(
|
||||
flutterVersionFromPubspecEnvironment(
|
||||
packagePath: packageDirectory.path,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when a flutter version range is specified', () {
|
||||
setUp(() {
|
||||
pubspecFile.writeAsStringSync('''
|
||||
environment:
|
||||
sdk: ^3.8.1
|
||||
flutter: ">=3.8.0 <4.0.0"
|
||||
''');
|
||||
});
|
||||
|
||||
test('throws a VersionConstraintException', () {
|
||||
expect(
|
||||
() => flutterVersionFromPubspecEnvironment(
|
||||
packagePath: packageDirectory.path,
|
||||
),
|
||||
throwsA(isA<VersionConstraintException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when a minimum version is specified', () {
|
||||
setUp(() {
|
||||
pubspecFile.writeAsStringSync('''
|
||||
environment:
|
||||
sdk: ^3.8.1
|
||||
flutter: "^3.8.0"
|
||||
''');
|
||||
});
|
||||
|
||||
test('throws a VersionConstraintException', () {
|
||||
expect(
|
||||
() => flutterVersionFromPubspecEnvironment(
|
||||
packagePath: packageDirectory.path,
|
||||
),
|
||||
throwsA(isA<VersionConstraintException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when a flutter version is specified', () {
|
||||
setUp(() {
|
||||
pubspecFile.writeAsStringSync('''
|
||||
environment:
|
||||
sdk: ^3.8.1
|
||||
flutter: 3.20.0
|
||||
''');
|
||||
});
|
||||
|
||||
test('returns the version', () {
|
||||
expect(
|
||||
flutterVersionFromPubspecEnvironment(
|
||||
packagePath: packageDirectory.path,
|
||||
),
|
||||
equals(Version(3, 20, 0)),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+9
-1
@@ -401,6 +401,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.10.0"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -882,4 +890,4 @@ packages:
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
sdks:
|
||||
dart: ">=3.8.0 <4.0.0"
|
||||
dart: ">=3.8.1 <4.0.0"
|
||||
|
||||
@@ -5,6 +5,7 @@ environment:
|
||||
workspace:
|
||||
- packages/artifact_proxy
|
||||
- packages/discord_gcp_alerts
|
||||
- packages/flutter_version_resolver
|
||||
- packages/jwt
|
||||
- packages/redis_client
|
||||
- packages/scoped_deps
|
||||
|
||||
Reference in New Issue
Block a user