feat: add flutter_version_resolver package (#3266)

Co-authored-by: Felix Angelov <felix@shorebird.dev>
This commit is contained in:
Bryan Oltman
2025-08-05 10:54:30 -04:00
committed by GitHub
parent b737a7a6b3
commit 9ff9a8883c
13 changed files with 390 additions and 2 deletions
+2 -1
View File
@@ -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`.
+19
View File
@@ -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
View File
@@ -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"
+1
View File
@@ -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