Skip to content

Commit 6eeb7a4

Browse files
authored
Merge branch 'main' into alestiago/include-hidden-check-command
2 parents 2c416f7 + 8124813 commit 6eeb7a4

File tree

13 files changed

+758
-6
lines changed

13 files changed

+758
-6
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Every request must be reviewed and accepted by:
22

3-
* @felangel @wolfenrain @renancaraujo @erickzanardo
3+
* @felangel @wolfenrain @renancaraujo @erickzanardo @alestiago

.github/workflows/bump_templates.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,4 @@ jobs:
3636
labels: bot
3737
author: VGV Bot <vgvbot@users.noreply.github.com>
3838
assignees: vgvbot
39-
reviewers: felangel
4039
committer: VGV Bot <vgvbot@users.noreply.github.com>

.github/workflows/spdx_license.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ jobs:
2323
defaults:
2424
run:
2525
working-directory: tool/spdx_license/hooks
26+
2627
runs-on: ubuntu-latest
28+
29+
# This job can be replaced by VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1,
30+
# once the following issue is resolved:
31+
# https://github.com/VeryGoodOpenSource/very_good_workflows/issues/150
2732
steps:
2833
- name: 📚 Git Checkout
2934
uses: actions/checkout@v4
@@ -56,7 +61,9 @@ jobs:
5661
defaults:
5762
run:
5863
working-directory: tool/spdx_license
64+
5965
runs-on: ubuntu-latest
66+
6067
steps:
6168
- name: 📚 Git Checkout
6269
uses: actions/checkout@v4
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: spdx_license_bot
2+
3+
on:
4+
# This should ideally trigger whenever there is a commit to the [SPDX License repository](https://github.com/spdx/license-list-data).
5+
# However, this is not yet possible see: https://github.com/orgs/community/discussions/26323
6+
schedule:
7+
# At 08:04 on every day-of-week from Monday through Friday.
8+
- cron: "4 8 * * 1-5"
9+
workflow_dispatch:
10+
11+
jobs:
12+
build:
13+
defaults:
14+
run:
15+
working-directory: tool/spdx_license
16+
17+
runs-on: ubuntu-latest
18+
19+
steps:
20+
- name: 📚 Git Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: 🎯 Setup Dart
24+
uses: dart-lang/setup-dart@v1
25+
26+
- name: 📦 Install Dependencies
27+
run: dart pub get
28+
29+
- name: 💻 Install Mason
30+
run: |
31+
dart pub global activate mason_cli
32+
mason get
33+
34+
- name: 🧱 Mason Make
35+
id: make
36+
run: if [[ $(mason make spdx_license -q --licenses "[]" -o test --on-conflict overwrite --set-exit-if-changed) =~ "0 files changed" ]]; then echo "did_change=false"; else echo "did_change=true"; fi >> $GITHUB_ENV
37+
38+
- name: 🔑 Config Git User
39+
if: ${{ env.did_change == 'true' }}
40+
run: |
41+
git config user.name VGV Bot
42+
git config user.email vgvbot@users.noreply.github.com
43+
44+
- name: 📝 Create Pull Request
45+
if: ${{ env.did_change == 'true' }}
46+
uses: peter-evans/create-pull-request@v5.0.2
47+
with:
48+
base: main
49+
branch: chore/update-spdx-license
50+
commit-message: "chore: update SPDX licenses"
51+
title: "chore: update SPDX licenses"
52+
body: Please squash and merge me!
53+
labels: bot
54+
author: VGV Bot <vgvbot@users.noreply.github.com>
55+
assignees: vgvbot
56+
committer: VGV Bot <vgvbot@users.noreply.github.com>

analysis_options.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ include: package:very_good_analysis/analysis_options.5.1.0.yaml
22
analyzer:
33
exclude:
44
- "**/version.dart"
5+
- "bricks/**/__brick__"
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/// Enables checking a package's license from pub.dev.
2+
///
3+
/// This library is intented to be used by Very Good CLI to help extracting
4+
/// license information. The existance of this library is likely to be
5+
/// ephemeral. It may be obsolete once [pub.dev](https://pub.dev/) exposes
6+
/// stable license information in their official API; you may track the
7+
/// progress [here](https://github.com/dart-lang/pub-dev/issues/4717).
8+
library pub_license;
9+
10+
import 'package:html/dom.dart' as html_dom;
11+
import 'package:html/parser.dart' as html_parser;
12+
import 'package:http/http.dart' as http;
13+
import 'package:meta/meta.dart';
14+
15+
/// The pub.dev [Uri] used to retrieve the license of a package.
16+
Uri _pubPackageLicenseUri(String packageName) =>
17+
Uri.parse('https://pub.dev/packages/$packageName/license');
18+
19+
/// {@template pub_license_exception}
20+
/// An exception thrown by [PubLicense].
21+
/// {@endtemplate}
22+
class PubLicenseException implements Exception {
23+
/// {@macro pub_license_exception}
24+
const PubLicenseException(String message)
25+
: message = '[pub_license] $message';
26+
27+
/// The exception message.
28+
final String message;
29+
}
30+
31+
/// The function signature for parsing HTML documents.
32+
@visibleForTesting
33+
typedef HtmlDocumentParse = html_dom.Document Function(
34+
dynamic input, {
35+
String? encoding,
36+
bool generateSpans,
37+
String? sourceUrl,
38+
});
39+
40+
/// {@template pub_license}
41+
/// Enables checking pub.dev's hosted packages license.
42+
/// {@endtemplate}
43+
class PubLicense {
44+
/// {@macro pub_license}
45+
PubLicense({
46+
@visibleForTesting http.Client? client,
47+
@visibleForTesting HtmlDocumentParse? parse,
48+
}) : _client = client ?? http.Client(),
49+
_parse = parse ?? html_parser.parse;
50+
51+
final http.Client _client;
52+
53+
final html_dom.Document Function(
54+
dynamic input, {
55+
String? encoding,
56+
bool generateSpans,
57+
String? sourceUrl,
58+
}) _parse;
59+
60+
/// Retrieves the license of a package.
61+
///
62+
/// Some packages may have multiple licenses, hence a [Set] is returned.
63+
///
64+
/// It may throw a [PubLicenseException] if:
65+
/// * The response from pub.dev is not successful.
66+
/// * The response body cannot be parsed.
67+
Future<Set<String>> getLicense(String packageName) async {
68+
final response = await _client.get(_pubPackageLicenseUri(packageName));
69+
70+
if (response.statusCode != 200) {
71+
throw PubLicenseException(
72+
'''Failed to retrieve the license of the package, received status code: ${response.statusCode}''',
73+
);
74+
}
75+
76+
late final html_dom.Document document;
77+
try {
78+
document = _parse(response.body);
79+
} on html_parser.ParseError catch (e) {
80+
throw PubLicenseException(
81+
'Failed to parse the response body, received error: $e',
82+
);
83+
} catch (e) {
84+
throw PubLicenseException(
85+
'''An unknown error occurred when trying to parse the response body, received error: $e''',
86+
);
87+
}
88+
89+
return _scrapeLicense(document);
90+
}
91+
}
92+
93+
/// Scrapes the license from the pub.dev's package license page.
94+
///
95+
/// The expected HTML structure is:
96+
/// ```html
97+
/// <aside class="detail-info-box">
98+
/// <h3> ... </h3>
99+
/// <p> ... </p>
100+
/// <h3 class="title">License</h3>
101+
/// <p>
102+
/// <img/>
103+
/// MIT (<a href="/packages/very_good_cli/license">LICENSE</a>)
104+
/// </p>
105+
/// </aside>
106+
/// ```
107+
///
108+
/// It may throw a [PubLicenseException] if:
109+
/// * The detail info box is not found.
110+
/// * The license header is not found.
111+
Set<String> _scrapeLicense(html_dom.Document document) {
112+
final detailInfoBox = document.querySelector('.detail-info-box');
113+
if (detailInfoBox == null) {
114+
throw const PubLicenseException(
115+
'''Failed to scrape license because `.detail-info-box` was not found.''',
116+
);
117+
}
118+
119+
String? rawLicenseText;
120+
for (var i = 0; i < detailInfoBox.children.length; i++) {
121+
final child = detailInfoBox.children[i];
122+
123+
final headerText = child.text.trim().toLowerCase();
124+
if (headerText == 'license') {
125+
rawLicenseText = detailInfoBox.children[i + 1].text.trim();
126+
break;
127+
}
128+
}
129+
if (rawLicenseText == null) {
130+
throw const PubLicenseException(
131+
'''Failed to scrape license because the license header was not found.''',
132+
);
133+
}
134+
135+
final licenseText = rawLicenseText.split('(').first.trim();
136+
return licenseText.split(',').map((e) => e.trim()).toSet();
137+
}

pubspec.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ dependencies:
1515
cli_completion: ^0.4.0
1616
collection: ^1.17.1
1717
glob: ^2.0.2
18+
html: ^0.15.4 # This dependency is temporary and should be removed once pub_license is obsolete.
19+
http: ^1.1.0 # This dependency is temporary and should be removed once pub_license is obsolete.
1820
lcov_parser: ^0.1.2
1921
mason: 0.1.0-dev.51
2022
mason_logger: ^0.2.2
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/// A small script used to generate the fixture for the pub_license test.
2+
///
3+
/// Fixtures are simply a temporary snapshot of an HTML response from pub.dev.
4+
/// The generated fixtures allow testing pub_license scraping logic without
5+
/// making a request to pub.dev every time the test is run.
6+
///
7+
/// To run this script, use the following command:
8+
/// ```bash
9+
/// dart test/src/pub_license/fixtures/generate_pub_license_fixtures.dart
10+
/// ```
11+
///
12+
/// Or simply use the "Run" CodeLens from VSCode's Dart extension if running
13+
/// from VSCode.
14+
library generate_pub_license_fixtures;
15+
16+
// ignore_for_file: avoid_print
17+
18+
import 'dart:io';
19+
20+
import 'package:http/http.dart' as http;
21+
import 'package:path/path.dart' as path;
22+
23+
/// [Uri] used to test the case where a package has a single license.
24+
final _singleLicenseUri = Uri.parse(
25+
'https://pub.dev/packages/very_good_cli/license',
26+
);
27+
28+
/// [Uri] used to test the case where a package has multiple licenses.
29+
final _multipleLicenseUri = Uri.parse(
30+
'https://pub.dev/packages/just_audio/license',
31+
);
32+
33+
/// [Uri] used to test the case where a package has no license.
34+
final _noLicenseUri = Uri.parse(
35+
'https://pub.dev/packages/music_control_notification/license',
36+
);
37+
38+
Future<void> main() async {
39+
final fixtureUris = <String, Uri>{
40+
'singleLicense': _singleLicenseUri,
41+
'multipleLicense': _multipleLicenseUri,
42+
'noLicense': _noLicenseUri,
43+
};
44+
45+
final httpClient = http.Client();
46+
47+
for (final entry in fixtureUris.entries) {
48+
final name = entry.key;
49+
final uri = entry.value;
50+
51+
final response = await httpClient.get(uri);
52+
53+
if (response.statusCode != 200) {
54+
print(
55+
'''Failed to generate a fixture for $name, received status code: ${response.statusCode}''',
56+
);
57+
continue;
58+
}
59+
60+
final fixturePath = path.joinAll([
61+
Directory.current.path,
62+
'test',
63+
'src',
64+
'pub_license',
65+
'fixtures',
66+
'$name.html',
67+
]);
68+
File(fixturePath)
69+
..createSync(recursive: true)
70+
..writeAsStringSync(response.body);
71+
72+
print('Fixture generated at $fixturePath');
73+
}
74+
}

0 commit comments

Comments
 (0)