@@ -2,13 +2,36 @@ import 'dart:io';
22
33import 'package:args/args.dart' ;
44import 'package:args/command_runner.dart' ;
5+ import 'package:collection/collection.dart' ;
56import 'package:mason/mason.dart' ;
67import '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;
721import 'package:path/path.dart' as path;
822import 'package:pubspec_lock/pubspec_lock.dart' ;
9- import 'package:very_good_cli/src/pub_license/pub_license.dart' ;
1023import '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
1437const 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:
0 commit comments