Skip to content

Commit 1c24aee

Browse files
authored
feat!: detect licenses locally (#883)
1 parent aa58be6 commit 1c24aee

26 files changed

+687
-1004
lines changed

lib/src/command_runner.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import 'package:pub_updater/pub_updater.dart';
88
import 'package:universal_io/io.dart';
99
import 'package:very_good_cli/src/commands/commands.dart';
1010
import 'package:very_good_cli/src/logger_extension.dart';
11-
import 'package:very_good_cli/src/pub_license/pub_license.dart';
1211
import 'package:very_good_cli/src/version.dart';
1312

1413
/// The package name.
@@ -23,7 +22,6 @@ class VeryGoodCommandRunner extends CompletionCommandRunner<int> {
2322
Logger? logger,
2423
PubUpdater? pubUpdater,
2524
Map<String, String>? environment,
26-
@visibleForTesting PubLicense? pubLicense,
2725
}) : _logger = logger ?? Logger(),
2826
_pubUpdater = pubUpdater ?? PubUpdater(),
2927
_environment = environment ?? Platform.environment,
@@ -39,7 +37,7 @@ class VeryGoodCommandRunner extends CompletionCommandRunner<int> {
3937
help: 'Noisy logging, including all shell commands executed.',
4038
);
4139
addCommand(CreateCommand(logger: _logger));
42-
addCommand(PackagesCommand(logger: _logger, pubLicense: pubLicense));
40+
addCommand(PackagesCommand(logger: _logger));
4341
addCommand(TestCommand(logger: _logger));
4442
addCommand(UpdateCommand(logger: _logger, pubUpdater: pubUpdater));
4543
}

lib/src/commands/packages/commands/check/check.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import 'package:args/command_runner.dart';
22
import 'package:mason/mason.dart';
33
import 'package:very_good_cli/src/commands/packages/commands/check/commands/commands.dart';
4-
import 'package:very_good_cli/src/pub_license/pub_license.dart';
54

65
/// {@template packages_check_command}
76
/// `very_good packages check` command for performing checks in a Dart or
@@ -11,10 +10,9 @@ class PackagesCheckCommand extends Command<int> {
1110
/// {@macro packages_check_command}
1211
PackagesCheckCommand({
1312
Logger? logger,
14-
PubLicense? pubLicense,
1513
}) {
1614
addSubcommand(
17-
PackagesCheckLicensesCommand(logger: logger, pubLicense: pubLicense),
15+
PackagesCheckLicensesCommand(logger: logger),
1816
);
1917
}
2018

lib/src/commands/packages/commands/check/commands/licenses.dart

Lines changed: 116 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,36 @@ import 'dart:io';
22

33
import 'package:args/args.dart';
44
import 'package:args/command_runner.dart';
5+
import 'package:collection/collection.dart';
56
import 'package:mason/mason.dart';
67
import 'package:meta/meta.dart';
8+
import 'package:package_config/package_config.dart' as package_config;
9+
10+
// We rely on PANA's license detection algorithm to retrieve licenses from
11+
// packages.
12+
//
13+
// This license detection algorithm is not exposed as a public API, so we have
14+
// to import it directly.
15+
//
16+
// See also:
17+
//
18+
// * [PANA's faster license detection GitHub issue](https://github.com/dart-lang/pana/issues/1277)
19+
// ignore: implementation_imports
20+
import 'package:pana/src/license_detection/license_detector.dart' as detector;
721
import 'package:path/path.dart' as path;
822
import 'package:pubspec_lock/pubspec_lock.dart';
9-
import 'package:very_good_cli/src/pub_license/pub_license.dart';
1023
import 'package:very_good_cli/src/pub_license/spdx_license.gen.dart';
1124

25+
/// Overrides the [package_config.findPackageConfig] function for testing.
26+
@visibleForTesting
27+
Future<package_config.PackageConfig?> Function(
28+
Directory directory,
29+
)? findPackageConfigOverride;
30+
31+
/// Overrides the [detector.detectLicense] function for testing.
32+
@visibleForTesting
33+
Future<detector.Result> Function(String, double)? detectLicenseOverride;
34+
1235
/// The basename of the pubspec lock file.
1336
@visibleForTesting
1437
const pubspecLockBasename = 'pubspec.lock';
@@ -24,6 +47,16 @@ final licenseDocumentationUri = Uri.parse(
2447
'https://cli.vgv.dev/docs/commands/check_licenses',
2548
);
2649

50+
/// The detection threshold used by [detector.detectLicense].
51+
///
52+
/// This value is used to determine the confidence threshold for detecting
53+
/// licenses. The value should match the default value used by PANA.
54+
///
55+
/// See also:
56+
///
57+
/// * [PANA's default threshold value](https://github.com/dart-lang/pana/blob/b598d45051ba4e028e9021c2aeb9c04e4335de76/lib/src/license.dart#L48)
58+
const _defaultDetectionThreshold = 0.95;
59+
2760
/// Defines a [Map] with dependencies as keys and their licenses as values.
2861
///
2962
/// If a dependency's license failed to be retrieved its license will be `null`.
@@ -40,9 +73,7 @@ class PackagesCheckLicensesCommand extends Command<int> {
4073
/// {@macro packages_check_licenses_command}
4174
PackagesCheckLicensesCommand({
4275
Logger? logger,
43-
PubLicense? pubLicense,
44-
}) : _logger = logger ?? Logger(),
45-
_pubLicense = pubLicense ?? PubLicense() {
76+
}) : _logger = logger ?? Logger() {
4677
argParser
4778
..addFlag(
4879
'ignore-retrieval-failures',
@@ -80,8 +111,6 @@ class PackagesCheckLicensesCommand extends Command<int> {
80111

81112
final Logger _logger;
82113

83-
final PubLicense _pubLicense;
84-
85114
@override
86115
String get description =>
87116
"Check packages' licenses in a Dart or Flutter project.";
@@ -128,6 +157,13 @@ class PackagesCheckLicensesCommand extends Command<int> {
128157

129158
final target = _argResults.rest.length == 1 ? _argResults.rest[0] : '.';
130159
final targetPath = path.normalize(Directory(target).absolute.path);
160+
final targetDirectory = Directory(targetPath);
161+
if (!targetDirectory.existsSync()) {
162+
_logger.err(
163+
'''Could not find directory at $targetPath. Specify a valid path to a Dart or Flutter project.''',
164+
);
165+
return ExitCode.noInput.code;
166+
}
131167

132168
final progress = _logger.progress('Checking licenses on $targetPath');
133169

@@ -169,38 +205,86 @@ class PackagesCheckLicensesCommand extends Command<int> {
169205
return ExitCode.usage.code;
170206
}
171207

208+
final packageConfig = await _tryFindPackageConfig(targetDirectory);
209+
if (packageConfig == null) {
210+
progress.cancel();
211+
_logger.err(
212+
'''Could not find a valid package config in $targetPath. Run `dart pub get` or `flutter pub get` to generate one.''',
213+
);
214+
return ExitCode.noInput.code;
215+
}
216+
172217
final licenses = <String, Set<String>?>{};
218+
final detectLicense = detectLicenseOverride ?? detector.detectLicense;
173219
for (final dependency in filteredDependencies) {
174220
progress.update(
175221
'''Collecting licenses from ${licenses.length + 1} out of ${filteredDependencies.length} ${filteredDependencies.length == 1 ? 'package' : 'packages'}''',
176222
);
177223

178224
final dependencyName = dependency.package();
179-
Set<String>? rawLicense;
180-
try {
181-
rawLicense = await _pubLicense.getLicense(dependencyName);
182-
} on PubLicenseException catch (e) {
183-
final errorMessage = '[$dependencyName] ${e.message}';
225+
final cachePackageEntry = packageConfig.packages
226+
.firstWhereOrNull((package) => package.name == dependencyName);
227+
if (cachePackageEntry == null) {
228+
final errorMessage =
229+
'''[$dependencyName] Could not find cached package path. Consider running `dart pub get` or `flutter pub get` to generate a new `package_config.json`.''';
184230
if (!ignoreFailures) {
185231
progress.cancel();
186232
_logger.err(errorMessage);
187-
return ExitCode.unavailable.code;
233+
return ExitCode.noInput.code;
188234
}
189235

190236
_logger.err('\n$errorMessage');
237+
licenses[dependencyName] = {SpdxLicense.$unknown.value};
238+
continue;
239+
}
240+
241+
final packagePath = path.normalize(cachePackageEntry.root.path);
242+
final packageDirectory = Directory(packagePath);
243+
if (!packageDirectory.existsSync()) {
244+
final errorMessage =
245+
'''[$dependencyName] Could not find package directory at $packagePath.''';
246+
if (!ignoreFailures) {
247+
progress.cancel();
248+
_logger.err(errorMessage);
249+
return ExitCode.noInput.code;
250+
}
251+
252+
_logger.err('\n$errorMessage');
253+
licenses[dependencyName] = {SpdxLicense.$unknown.value};
254+
continue;
255+
}
256+
257+
final licenseFile = File(path.join(packagePath, 'LICENSE'));
258+
if (!licenseFile.existsSync()) {
259+
licenses[dependencyName] = {SpdxLicense.$unknown.value};
260+
continue;
261+
}
262+
263+
final licenseFileContent = licenseFile.readAsStringSync();
264+
265+
late final detector.Result detectorResult;
266+
try {
267+
detectorResult =
268+
await detectLicense(licenseFileContent, _defaultDetectionThreshold);
191269
} catch (e) {
192270
final errorMessage =
193-
'[$dependencyName] Unexpected failure with error: $e';
271+
'''[$dependencyName] Failed to detect license from $packagePath: $e''';
194272
if (!ignoreFailures) {
195273
progress.cancel();
196274
_logger.err(errorMessage);
197275
return ExitCode.software.code;
198276
}
199277

200278
_logger.err('\n$errorMessage');
201-
} finally {
202-
licenses[dependencyName] = rawLicense;
279+
licenses[dependencyName] = {SpdxLicense.$unknown.value};
280+
continue;
203281
}
282+
283+
final rawLicense = detectorResult.matches
284+
// ignore: invalid_use_of_visible_for_testing_member
285+
.map((match) => match.license.identifier)
286+
.toSet();
287+
licenses[dependencyName] = rawLicense;
204288
}
205289

206290
late final _BannedDependencyLicenseMap? bannedDependencies;
@@ -246,6 +330,23 @@ PubspecLock? _tryParsePubspecLock(File pubspecLockFile) {
246330
}
247331
}
248332

333+
/// Attempts to find a [package_config.PackageConfig] using
334+
/// [package_config.findPackageConfig].
335+
///
336+
/// If [package_config.findPackageConfig] fails to find a package config `null`
337+
/// is returned.
338+
Future<package_config.PackageConfig?> _tryFindPackageConfig(
339+
Directory directory,
340+
) async {
341+
try {
342+
final findPackageConfig =
343+
findPackageConfigOverride ?? package_config.findPackageConfig;
344+
return await findPackageConfig(directory);
345+
} catch (error) {
346+
return null;
347+
}
348+
}
349+
249350
/// Verifies that all [licenses] are valid license inputs.
250351
///
251352
/// Valid license inputs are:

lib/src/commands/packages/packages.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@ import 'package:args/command_runner.dart';
22
import 'package:mason/mason.dart';
33
import 'package:very_good_cli/src/commands/packages/commands/check/check.dart';
44
import 'package:very_good_cli/src/commands/packages/commands/commands.dart';
5-
import 'package:very_good_cli/src/pub_license/pub_license.dart';
65

76
/// {@template packages_command}
87
/// `very_good packages` command for managing packages.
98
/// {@endtemplate}
109
class PackagesCommand extends Command<int> {
1110
/// {@macro packages_command}
12-
PackagesCommand({Logger? logger, PubLicense? pubLicense}) {
11+
PackagesCommand({Logger? logger}) {
1312
addSubcommand(PackagesGetCommand(logger: logger));
14-
addSubcommand(PackagesCheckCommand(logger: logger, pubLicense: pubLicense));
13+
addSubcommand(PackagesCheckCommand(logger: logger));
1514
}
1615

1716
@override

0 commit comments

Comments
 (0)