Skip to content

Commit

Permalink
Adds CI utils with check for lower bounds (#194)
Browse files Browse the repository at this point in the history
* Adds CI utils with check for lower bounds

* dart format

* fix CI

* Improve error message

* dart format

* adapt lower bounds of dependencies
  • Loading branch information
devmil authored Nov 27, 2024
1 parent aa3a499 commit 4c6014b
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 4 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions scripts/ci_util/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
3 changes: 3 additions & 0 deletions scripts/ci_util/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 1.0.0

- Initial version.
1 change: 1 addition & 0 deletions scripts/ci_util/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CI Utils for dart_apitool
30 changes: 30 additions & 0 deletions scripts/ci_util/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions scripts/ci_util/bin/ci_util.dart
Original file line number Diff line number Diff line change
@@ -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<String> arguments) async {
final runner = buildCommandRunner();
await runner.run(arguments);
}
168 changes: 168 additions & 0 deletions scripts/ci_util/lib/src/check_lower_bound_dependencies_command.dart
Original file line number Diff line number Diff line change
@@ -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<DependencyVersioned>()
.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<String> arguments) async {
// check if we are in a fvm environment
final fvmPath = await Process.run('which', ['fvm']);
String executable = 'fvm';
List<String> 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<void> _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});
}
17 changes: 17 additions & 0 deletions scripts/ci_util/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 4c6014b

Please sign in to comment.