From 4c6014beb974c93ba760621084af60a3703db7c8 Mon Sep 17 00:00:00 2001 From: Michael Lamers Date: Wed, 27 Nov 2024 15:46:25 +0100 Subject: [PATCH] Adds CI utils with check for lower bounds (#194) * Adds CI utils with check for lower bounds * dart format * fix CI * Improve error message * dart format * adapt lower bounds of dependencies --- .github/workflows/ci.yml | 7 + pubspec.yaml | 8 +- scripts/ci_util/.gitignore | 3 + scripts/ci_util/CHANGELOG.md | 3 + scripts/ci_util/README.md | 1 + scripts/ci_util/analysis_options.yaml | 30 ++++ scripts/ci_util/bin/ci_util.dart | 12 ++ ...heck_lower_bound_dependencies_command.dart | 168 ++++++++++++++++++ scripts/ci_util/pubspec.yaml | 17 ++ 9 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 scripts/ci_util/.gitignore create mode 100644 scripts/ci_util/CHANGELOG.md create mode 100644 scripts/ci_util/README.md create mode 100644 scripts/ci_util/analysis_options.yaml create mode 100644 scripts/ci_util/bin/ci_util.dart create mode 100644 scripts/ci_util/lib/src/check_lower_bound_dependencies_command.dart create mode 100644 scripts/ci_util/pubspec.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc7c4ae..23aabfa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,6 +77,13 @@ jobs: - name: Analyze project source run: dart analyze --fatal-infos --fatal-warnings . + - name: Check lower bound of dependencies + run: | + pushd scripts/ci_util + dart pub get + popd + dart scripts/ci_util/bin/ci_util.dart check-lower-bound-dependencies + semver: needs: [get_last_released_version, get_flutter_version] uses: bmw-tech/dart_apitool/.github/workflows/check_version.yml@workflow/v1 diff --git a/pubspec.yaml b/pubspec.yaml index 4fcf53c..5310a6f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,16 +10,16 @@ environment: dependencies: analyzer: ^6.5.0 args: ^2.3.1 - collection: ^1.16.0 + collection: ^1.17.0 colorize: ^3.0.0 colorize_lumberdash: ^3.0.0 console: ^4.1.0 - freezed_annotation: ^2.2.0 + freezed_annotation: ^2.4.2 json_annotation: ^4.8.1 lumberdash: ^3.0.0 - path: ^1.8.2 + path: ^1.9.0 plist_parser: ^0.0.9 - pub_semver: ^2.1.1 + pub_semver: ^2.1.4 pubspec_parse: ^1.2.0 stack: ^0.2.1 tuple: ^2.0.0 diff --git a/scripts/ci_util/.gitignore b/scripts/ci_util/.gitignore new file mode 100644 index 0000000..3a85790 --- /dev/null +++ b/scripts/ci_util/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/scripts/ci_util/CHANGELOG.md b/scripts/ci_util/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/scripts/ci_util/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/scripts/ci_util/README.md b/scripts/ci_util/README.md new file mode 100644 index 0000000..cbe4780 --- /dev/null +++ b/scripts/ci_util/README.md @@ -0,0 +1 @@ +CI Utils for dart_apitool \ No newline at end of file diff --git a/scripts/ci_util/analysis_options.yaml b/scripts/ci_util/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/scripts/ci_util/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/scripts/ci_util/bin/ci_util.dart b/scripts/ci_util/bin/ci_util.dart new file mode 100644 index 0000000..d722fc6 --- /dev/null +++ b/scripts/ci_util/bin/ci_util.dart @@ -0,0 +1,12 @@ +import 'package:args/command_runner.dart'; +import 'package:ci_util/src/check_lower_bound_dependencies_command.dart'; + +CommandRunner buildCommandRunner() { + return CommandRunner('ci_util', 'CI utilities for dart_apitool') + ..addCommand(CheckLowerBoundDependenciesCommand()); +} + +void main(List arguments) async { + final runner = buildCommandRunner(); + await runner.run(arguments); +} diff --git a/scripts/ci_util/lib/src/check_lower_bound_dependencies_command.dart b/scripts/ci_util/lib/src/check_lower_bound_dependencies_command.dart new file mode 100644 index 0000000..d18585c --- /dev/null +++ b/scripts/ci_util/lib/src/check_lower_bound_dependencies_command.dart @@ -0,0 +1,168 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:path/path.dart' as path; +import 'package:pubspec_manager/pubspec_manager.dart'; + +class CheckLowerBoundDependenciesCommand extends Command { + @override + String get description => + 'checks if the package can compile with only the lower bound constraints of its dependencies.'; + + @override + String get name => 'check-lower-bound-dependencies'; + + @override + Future run() async { + // copy sources to temporary directory + final String apiToolRootPath = _getApiToolRootPath(); + + // get all dependencies + final pubspec = + PubSpec.loadFromPath(path.join(apiToolRootPath, 'pubspec.yaml')); + + final testFutures = pubspec.dependencies.list + .whereType() + .map((d) async { + try { + await _testWithFixedDependency((d as Dependency).name); + } catch (e) { + return LowerBoundCheckResult( + dependencyName: (d as Dependency).name, error: e.toString()); + } + return LowerBoundCheckResult(dependencyName: (d as Dependency).name); + }); + final failedDependencies = (await Future.wait(testFutures)) + .where((element) => element.error != null) + .toList(); + if (failedDependencies.isNotEmpty) { + final errorMessage = StringBuffer(); + errorMessage.writeln( + 'Following dependencies failed when locked to their lower bound:'); + errorMessage + .writeln(failedDependencies.map((e) => e.dependencyName).join('\n')); + errorMessage.writeln(); + errorMessage.writeln('See error messages for details:'); + errorMessage.writeln( + failedDependencies.map((e) => e.error!).join('\n-----------\n')); + throw Exception(errorMessage.toString()); + } + } + + Future _testWithFixedDependency(String dependencyName) async { + final String apiToolRootPath = _getApiToolRootPath(); + final tempDir = await Directory.systemTemp.createTemp(); + try { + await _copyPath(apiToolRootPath, tempDir.path); + await _fixDependency( + path.join(tempDir.path, 'pubspec.yaml'), dependencyName); + await _executePubGet(tempDir.path); + await _executeBuild(tempDir.path); + } finally { + await tempDir.delete(recursive: true); + } + } + + String _getApiToolRootPath() { + return path.normalize( + path.join( + path.dirname(path.absolute(Platform.script.path)), + '..', + '..', + '..', + ), + ); + } + + Future _executePubGet(String path) async { + await _executeDart(path, ['pub', 'get']); + } + + Future _executeBuild(String path) async { + await _executeDart(path, ['compile', 'exe', 'bin/main.dart']); + } + + Future _executeDart(String path, List arguments) async { + // check if we are in a fvm environment + final fvmPath = await Process.run('which', ['fvm']); + String executable = 'fvm'; + List additionalArguments = ['dart']; + if (fvmPath.exitCode != 0) { + print('fvm not found, using default dart.'); + executable = 'dart'; + additionalArguments = []; + } + final result = await Process.run( + executable, + [...additionalArguments, ...arguments], + workingDirectory: path, + ); + print(result.stdout); + print(result.stderr); + if (result.exitCode != 0) { + throw Exception('Error executing dart: ${result.stderr}'); + } + } + + Future _fixDependency(String pubspecPath, String dependencyName) async { + // load pubspec yaml + final pubspec = PubSpec.loadFromPath(pubspecPath); + // adapt dependency + bool adaptedOne = false; + for (final dependency in pubspec.dependencies.list) { + if (dependency.name == dependencyName && + dependency is DependencyVersioned) { + final castedDependency = dependency as DependencyVersioned; + castedDependency.versionConstraint = + castedDependency.versionConstraint.replaceFirst('^', ''); + adaptedOne = true; + } + } + if (!adaptedOne) { + throw Exception('Dependency $dependencyName not found in pubspec.yaml.'); + } + // write pubspec yaml + pubspec.saveTo(pubspecPath); + } + + Future _copyPath(String from, String to) async { + if (_doNothing(from, to)) { + return; + } + if (await Directory(to).exists()) { + await Directory(to).delete(); + } + await Directory(to).create(recursive: true); + await for (final file in Directory(from).list(recursive: true)) { + // ignore .git directory and its content + if (path.split(file.path).contains('.git')) { + continue; + } + final copyTo = path.join(to, path.relative(file.path, from: from)); + if (file is Directory) { + await Directory(copyTo).create(recursive: true); + } else if (file is File) { + await File(file.path).copy(copyTo); + } else if (file is Link) { + await Link(copyTo).create(await file.target(), recursive: true); + } + } + } + + bool _doNothing(String from, String to) { + if (path.canonicalize(from) == path.canonicalize(to)) { + return true; + } + if (path.isWithin(from, to)) { + throw ArgumentError('Cannot copy from $from to $to'); + } + return false; + } +} + +class LowerBoundCheckResult { + final String dependencyName; + final String? error; + + LowerBoundCheckResult({required this.dependencyName, this.error}); +} diff --git a/scripts/ci_util/pubspec.yaml b/scripts/ci_util/pubspec.yaml new file mode 100644 index 0000000..a5260c4 --- /dev/null +++ b/scripts/ci_util/pubspec.yaml @@ -0,0 +1,17 @@ +name: ci_util +description: A sample command-line application with basic argument parsing. +version: 0.0.1 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.3.3 + +# Add regular dependencies here. +dependencies: + args: ^2.4.2 + path: ^1.9.0 + pubspec_manager: ^1.0.0 + +dev_dependencies: + lints: ^3.0.0 + test: ^1.24.0