diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index dc5d2cdccd0..4bc8c56d780 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.14.1 + +* Adds `min_dart` as an alternative to `min_flutter` in tool configuration. +* Makes .ci.yaml optional if batched release is not used. + ## 0.14.0 * Re-launches the published version of the tool for use in diff --git a/script/tool/lib/src/common/tool_config.dart b/script/tool/lib/src/common/tool_config.dart index f8dce1cb315..d9e9f52c97c 100644 --- a/script/tool/lib/src/common/tool_config.dart +++ b/script/tool/lib/src/common/tool_config.dart @@ -63,6 +63,19 @@ String? getMinFlutterVersion(Directory repoRoot) { return yaml; } +/// Returns the minimum Dart version allowed. +String? getMinDartVersion(Directory repoRoot) { + final Object? yaml = _getToolConfig(repoRoot)['min_dart']; + if (yaml == null) { + return null; + } + if (yaml is! String) { + printError('min_dart must be a full version string (e.g., "3.10.0").'); + throw ToolExit(exitInvalidArguments); + } + return yaml; +} + /// Returns the allowed dependencies, grouped by 'pinned' and 'unpinned'. ({List pinned, List unpinned}) getAllowedDependencies( Directory repoRoot, diff --git a/script/tool/lib/src/validate_command.dart b/script/tool/lib/src/validate_command.dart index 6794313ad3f..cb3fb7c0f52 100644 --- a/script/tool/lib/src/validate_command.dart +++ b/script/tool/lib/src/validate_command.dart @@ -5,6 +5,7 @@ import 'package:file/file.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; import 'common/core.dart'; import 'common/git_version_finder.dart'; @@ -19,6 +20,9 @@ import 'validators/readme_validator.dart'; import 'validators/repo_info_validator.dart'; import 'validators/version_and_changelog_validator.dart'; +const int _missingMinSdkVersionExitCode = 3; +const int _unknownVersionMappingExitCode = 4; + /// The set of possible validators. /// /// Exposed for testing so that unit tests can target a single validator's @@ -113,7 +117,10 @@ class ValidateCommand extends PackageLoopingCommand { ); /// The minimum version of Flutter that is allowed for any package. - late final String _minMinFlutterVersion; + Version? _minMinFlutterVersion; + + /// The minimum version of Dart that is allowed for any package. + Version? _minMinDartVersion; @override final String name = 'validate'; @@ -149,7 +156,20 @@ class ValidateCommand extends PackageLoopingCommand { } if (_shouldRun(Validator.pubspec)) { await _loadAllowedDependencies(); - _minMinFlutterVersion = _loadMinMinFlutterVersion(); + final (flutter: Version? minFlutter, dart: Version? minDart) = + _loadMinMinSdkVersions(); + _minMinFlutterVersion = minFlutter; + _minMinDartVersion = + minDart ?? + (minFlutter == null ? null : getDartSdkForFlutterSdk(minFlutter)); + if (_minMinDartVersion == null) { + printError( + 'Dart SDK version for Flutter SDK version $_minMinFlutterVersion is unknown. ' + 'Please update the map for getDartSdkForFlutterSdk with the ' + 'corresponding Dart version.', + ); + throw ToolExit(_unknownVersionMappingExitCode); + } } if (_shouldRun(Validator.dependabot)) { _dependabotCoverage = DependabotValidator.loadConfig(repoRoot: _repoRoot); @@ -220,6 +240,7 @@ class ValidateCommand extends PackageLoopingCommand { allowedPackages: _allowedPackages, repoRoot: rootDir, minMinFlutterVersion: _minMinFlutterVersion, + minMinDartVersion: _minMinDartVersion, ); return validator.validatePubspec(package); } @@ -323,13 +344,20 @@ class ValidateCommand extends PackageLoopingCommand { _allowedPackages.pinned.addAll(allowedDeps.pinned); } - String _loadMinMinFlutterVersion() { - final String? minVersion = getMinFlutterVersion(_repoRoot); - if (minVersion == null) { - printError('min_flutter is missing in $configFilename'); - return ''; + ({Version? flutter, Version? dart}) _loadMinMinSdkVersions() { + final String? minFlutter = getMinFlutterVersion(_repoRoot); + final String? minDart = getMinDartVersion(_repoRoot); + if (minFlutter == null && minDart == null) { + printError( + 'Either min_flutter or min_dart must be provided ' + 'in the repo tool configuration.', + ); + throw ToolExit(_missingMinSdkVersionExitCode); } - return minVersion.trim(); + return ( + flutter: minFlutter == null ? null : Version.parse(minFlutter), + dart: minDart == null ? null : Version.parse(minDart), + ); } Pubspec? _tryParsePubspec(String pubspecContents) { diff --git a/script/tool/lib/src/validators/pubspec_validator.dart b/script/tool/lib/src/validators/pubspec_validator.dart index 0b13b6240de..71e5d0bf4f8 100644 --- a/script/tool/lib/src/validators/pubspec_validator.dart +++ b/script/tool/lib/src/validators/pubspec_validator.dart @@ -65,20 +65,23 @@ class PubspecValidator { required AllowPackageLists allowedPackages, required void Function(String) warningLogger, required Directory repoRoot, - required String minMinFlutterVersion, + Version? minMinFlutterVersion, + Version? minMinDartVersion, }) : _path = path, _indentation = indentation, _allowedPackages = allowedPackages, _logWarning = warningLogger, _repoRoot = repoRoot, - _minMinFlutterVersion = minMinFlutterVersion; + _minMinFlutterVersion = minMinFlutterVersion, + _minMinDartVersion = minMinDartVersion; final path.Context _path; final String _indentation; final AllowPackageLists _allowedPackages; final void Function(String) _logWarning; final Directory _repoRoot; - final String _minMinFlutterVersion; + final Version? _minMinFlutterVersion; + final Version? _minMinDartVersion; /// Validates that the pubspec of a package follows repository conventions, /// returning a list of errors. @@ -115,9 +118,6 @@ class PubspecValidator { final String? minVersionError = _checkForMinimumVersionError( pubspec, package, - minMinFlutterVersion: _minMinFlutterVersion.isEmpty - ? null - : Version.parse(_minMinFlutterVersion), ); if (minVersionError != null) { printError('$_indentation$minVersionError'); @@ -423,24 +423,8 @@ class PubspecValidator { /// Returns an error string if validation fails. String? _checkForMinimumVersionError( Pubspec pubspec, - RepositoryPackage package, { - Version? minMinFlutterVersion, - }) { - String unknownDartVersionError(Version flutterVersion) { - return 'Dart SDK version for Flutter SDK version ' - '$flutterVersion is unknown. ' - 'Please update the map for getDartSdkForFlutterSdk with the ' - 'corresponding Dart version.'; - } - - Version? minMinDartVersion; - if (minMinFlutterVersion != null) { - minMinDartVersion = getDartSdkForFlutterSdk(minMinFlutterVersion); - if (minMinDartVersion == null) { - return unknownDartVersionError(minMinFlutterVersion); - } - } - + RepositoryPackage package, + ) { final Version? dartConstraintMin = _minimumForConstraint( pubspec.environment['sdk'], ); @@ -449,19 +433,19 @@ class PubspecValidator { ); // Validate the Flutter constraint, if any. - if (flutterConstraintMin != null && minMinFlutterVersion != null) { - if (flutterConstraintMin < minMinFlutterVersion) { + if (flutterConstraintMin != null && _minMinFlutterVersion != null) { + if (flutterConstraintMin < _minMinFlutterVersion) { return 'Minimum allowed Flutter version $flutterConstraintMin is less ' - 'than $minMinFlutterVersion'; + 'than $_minMinFlutterVersion'; } } // Validate the Dart constraint, if any. if (dartConstraintMin != null) { // Ensure that it satisfies the minimum. - if (minMinDartVersion != null) { - if (dartConstraintMin < minMinDartVersion) { - return 'Minimum allowed Dart version $dartConstraintMin is less than $minMinDartVersion'; + if (_minMinDartVersion != null) { + if (dartConstraintMin < _minMinDartVersion) { + return 'Minimum allowed Dart version $dartConstraintMin is less than $_minMinDartVersion'; } } @@ -471,7 +455,10 @@ class PubspecValidator { flutterConstraintMin, ); if (dartVersionForFlutterMinimum == null) { - return unknownDartVersionError(flutterConstraintMin); + return 'Dart SDK version for Flutter SDK version ' + '$flutterConstraintMin is unknown. ' + 'Please update the map for getDartSdkForFlutterSdk with the ' + 'corresponding Dart version.'; } if (dartVersionForFlutterMinimum != dartConstraintMin) { return 'The minimum Dart version is $dartConstraintMin, but the ' diff --git a/script/tool/lib/src/validators/repo_info_validator.dart b/script/tool/lib/src/validators/repo_info_validator.dart index 0662a1a17f9..cbb3449a556 100644 --- a/script/tool/lib/src/validators/repo_info_validator.dart +++ b/script/tool/lib/src/validators/repo_info_validator.dart @@ -428,6 +428,17 @@ class RepoInfoValidator { }) { final errors = []; final File ciYamlFile = repoRoot.childFile('.ci.yaml'); + if (!ciYamlFile.existsSync()) { + if (isBatchRelease) { + printError( + '${_indentation}Batched release is currently only supported for ' + 'repos using .ci.yaml', + ); + errors.add('Missing .ci.yaml'); + } + return errors; + } + final String content = ciYamlFile.readAsStringSync(); final yaml = loadYaml(content) as YamlMap; diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index b7938174941..1752a98250f 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity and CI utils for Flutter team package repositories. repository: https://github.com/flutter/packages/tree/main/script/tool -version: 0.14.0 +version: 0.14.1 dependencies: args: ^2.1.0 diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 8ff74ecbaa0..604fd391752 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -598,6 +598,7 @@ void setToolConfig( Directory repoRoot, { String repoName = 'flutter/packages', String? minFlutterVersion, + String? minDartVersion, List? pinnedDependencies, List? unpinnedDependencies, Map? packageLabels, @@ -606,6 +607,9 @@ void setToolConfig( if (minFlutterVersion != null) { editor.update(['min_flutter'], minFlutterVersion); } + if (minDartVersion != null) { + editor.update(['min_dart'], minDartVersion); + } if (pinnedDependencies != null || unpinnedDependencies != null) { const allowedDependenciesKey = 'allowed_dependencies'; const pinnedKey = 'pinned'; diff --git a/script/tool/test/validate_command_pubspec_test.dart b/script/tool/test/validate_command_pubspec_test.dart index 93765d462c1..03a6b9f4e5c 100644 --- a/script/tool/test/validate_command_pubspec_test.dart +++ b/script/tool/test/validate_command_pubspec_test.dart @@ -154,7 +154,7 @@ void main() { repoRoot = packagesDir.parent; // Set an basic config with just the required elements so that tests // that don't need test-specific settings still pass. - setToolConfig(repoRoot); + setToolConfig(repoRoot, minDartVersion: '1.0.0'); final command = ValidateCommand( packagesDir, @@ -490,7 +490,11 @@ ${_dependenciesSection()} ${_devDependenciesSection()} ${_topicsSection()} '''); - setToolConfig(repoRoot, repoName: 'flutter/core-packages'); + setToolConfig( + repoRoot, + repoName: 'flutter/core-packages', + minDartVersion: '3.0.0', + ); final List output = await runCapturingPrint(runner, [ 'validate', @@ -1668,6 +1672,104 @@ ${_topicsSection()} }, ); + test('fails when a non-Flutter package has a too-low minimum Dart version ' + 'for the min Flutter version', () async { + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + examples: [], + ); + + package.pubspecFile.writeAsStringSync(''' +${_headerSection('a_package')} +${_environmentSection(dartConstraint: '^3.1.0', flutterConstraint: null)} +${_dependenciesSection()} +${_topicsSection()} +'''); + setToolConfig(repoRoot, minFlutterVersion: '3.38.0'); + + Error? commandError; + final List output = await runCapturingPrint( + runner, + ['validate'], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Minimum allowed Dart version 3.1.0 is less than 3.10.0'), + ]), + ); + }); + + test( + 'passes when a non-Flutter package requires exactly the minimum Dart version ' + 'for the min Flutter version', + () async { + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + examples: [], + ); + + package.pubspecFile.writeAsStringSync(''' +${_headerSection('a_package')} +${_environmentSection(dartConstraint: '^3.8.0', flutterConstraint: null)} +${_dependenciesSection()} +${_topicsSection()} +'''); + setToolConfig(repoRoot, minFlutterVersion: '3.32.0'); + + final List output = await runCapturingPrint(runner, [ + 'validate', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package...'), + contains('No issues found!'), + ]), + ); + }, + ); + + test( + 'passes when a non-Flutter package requires a higher minimum Dart version ' + 'for the min Flutter version', + () async { + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + examples: [], + ); + + package.pubspecFile.writeAsStringSync(''' +${_headerSection('a_package')} +${_environmentSection(dartConstraint: '^3.8.0', flutterConstraint: null)} +${_dependenciesSection()} +${_topicsSection()} +'''); + setToolConfig(repoRoot, minFlutterVersion: '3.22.0'); + + final List output = await runCapturingPrint(runner, [ + 'validate', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package...'), + contains('No issues found!'), + ]), + ); + }, + ); + test( 'fails when a non-Flutter package has a too-low minimum Dart version', () async { @@ -1683,7 +1785,7 @@ ${_environmentSection(dartConstraint: '^3.1.0', flutterConstraint: null)} ${_dependenciesSection()} ${_topicsSection()} '''); - setToolConfig(repoRoot, minFlutterVersion: '3.38.0'); + setToolConfig(repoRoot, minDartVersion: '3.10.0'); Error? commandError; final List output = await runCapturingPrint( @@ -1710,7 +1812,6 @@ ${_topicsSection()} final RepositoryPackage package = createFakePackage( 'a_package', packagesDir, - isFlutter: true, examples: [], ); @@ -1720,7 +1821,7 @@ ${_environmentSection(dartConstraint: '^3.8.0', flutterConstraint: null)} ${_dependenciesSection()} ${_topicsSection()} '''); - setToolConfig(repoRoot, minFlutterVersion: '3.32.0'); + setToolConfig(repoRoot, minDartVersion: '3.8.0'); final List output = await runCapturingPrint(runner, [ 'validate', @@ -1742,7 +1843,6 @@ ${_topicsSection()} final RepositoryPackage package = createFakePackage( 'a_package', packagesDir, - isFlutter: true, examples: [], ); @@ -1752,7 +1852,7 @@ ${_environmentSection(dartConstraint: '^3.8.0', flutterConstraint: null)} ${_dependenciesSection()} ${_topicsSection()} '''); - setToolConfig(repoRoot, minFlutterVersion: '3.22.0'); + setToolConfig(repoRoot, minDartVersion: '3.6.0'); final List output = await runCapturingPrint(runner, [ 'validate', @@ -1967,6 +2067,7 @@ ${_topicsSection()} setToolConfig( packagesDir.parent, unpinnedDependencies: ['allowed'], + minDartVersion: '1.0.0', ); final List output = await runCapturingPrint(runner, [ @@ -1999,6 +2100,7 @@ ${_topicsSection()} setToolConfig( packagesDir.parent, pinnedDependencies: ['allow_pinned'], + minDartVersion: '1.0.0', ); final List output = await runCapturingPrint(runner, [ @@ -2032,6 +2134,7 @@ ${_topicsSection()} setToolConfig( packagesDir.parent, pinnedDependencies: ['allow_pinned'], + minDartVersion: '1.0.0', ); final List output = await runCapturingPrint(runner, [ @@ -2063,6 +2166,7 @@ ${_topicsSection()} setToolConfig( packagesDir.parent, pinnedDependencies: ['allow_pinned'], + minDartVersion: '1.0.0', ); Error? commandError; diff --git a/script/tool/test/validate_command_repo_info_test.dart b/script/tool/test/validate_command_repo_info_test.dart index c1d05ebb627..7632882350f 100644 --- a/script/tool/test/validate_command_repo_info_test.dart +++ b/script/tool/test/validate_command_repo_info_test.dart @@ -132,6 +132,32 @@ ${readmeTableEntry('a_package', repoName: repoName)} ); }); + test( + 'passes when no .ci.yaml is present if nothing uses batched release', + () async { + final packages = [ + createFakePackage('a_package', packagesDir), + ]; + + root.childFile('README.md').writeAsStringSync(''' +${readmeTableHeader()} +${readmeTableEntry('a_package')} +'''); + writeAutoLabelerYaml(packages); + + root.childFile('.ci.yaml').deleteSync(); + + final List output = await runCapturingPrint(runner, [ + 'validate', + ]); + + expect( + output, + containsAllInOrder([contains('Running for a_package')]), + ); + }, + ); + test( 'passes for federated plugins with only app-facing package listed', () async {