license_conflicts 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +6 -0
- data/CHANGELOG.md +16 -0
- data/README.md +29 -3
- data/lib/license_conflicts/conflicts_map.rb +113 -146
- data/lib/license_conflicts/declared_dependencies.rb +131 -0
- data/lib/license_conflicts/finder.rb +25 -2
- data/lib/license_conflicts/project_metadata.rb +50 -23
- data/lib/license_conflicts/version.rb +1 -1
- data/lib/license_conflicts.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6eb6707c39f32730f7a721952ab41d3995e5b75b33b69147dd8a6ccb403b4540
|
|
4
|
+
data.tar.gz: e555affa56259102318068dbdc4fd7cd27a832c2fdd682e6c55f06275e729265
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 02f04fd29206aaa303a33c40b6e65d72e4c34d5849fb1ccbde055450ca19a071501e7b4a15cb3ae02cf366b9872ff30d91a42d9bb0b9df014e4712c359973978
|
|
7
|
+
data.tar.gz: 1a7c1ff94d28ba7e41fec874177dc5d2f4740eb568acdacd16bcff36e05d2ca2ecf36685e6ef54e2cd826e111261dd8071490ab00de2f0ee00901f8481b8da36
|
data/.rubocop.yml
CHANGED
|
@@ -11,3 +11,9 @@ Style/StringLiteralsInInterpolation:
|
|
|
11
11
|
|
|
12
12
|
Layout/LineLength:
|
|
13
13
|
Max: 120
|
|
14
|
+
|
|
15
|
+
# RSpec specs use a single `describe` block per file, naturally above the default
|
|
16
|
+
# line limit; the limit does not apply to them.
|
|
17
|
+
Metrics/BlockLength:
|
|
18
|
+
Exclude:
|
|
19
|
+
- "spec/**/*"
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0](https://github.com/dennisurtubia/license_conflicts/compare/license_conflicts/v0.3.2...license_conflicts/v0.4.0) (2026-11-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* analyze only direct dependencies ([1851332](https://github.com/dennisurtubia/license_conflicts/commit/1851332351dd0cc2da1f98124a1ab31749e1dd70))
|
|
9
|
+
* analyze only direct dependencies ([a299cc2](https://github.com/dennisurtubia/license_conflicts/commit/a299cc21e4fd6105ebc03cc3d22ac14476d7fca5))
|
|
10
|
+
|
|
11
|
+
## [0.3.2](https://github.com/dennisurtubia/license_conflicts/compare/license_conflicts/v0.3.1...license_conflicts/v0.3.2) (2026-09-13)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* conflicts mapping ([afcab97](https://github.com/dennisurtubia/license_conflicts/commit/afcab97686ffaa1351054667e027a683c64b78be))
|
|
17
|
+
* conflicts mapping ([4e4cd3a](https://github.com/dennisurtubia/license_conflicts/commit/4e4cd3abc017181e4d45a35cbcad376d6e359af5))
|
|
18
|
+
|
|
3
19
|
## [0.3.1](https://github.com/dennisurtubia/license_conflicts/compare/license_conflicts/v0.3.0...license_conflicts/v0.3.1) (2026-03-21)
|
|
4
20
|
|
|
5
21
|
|
data/README.md
CHANGED
|
@@ -23,11 +23,35 @@
|
|
|
23
23
|
## How It Works
|
|
24
24
|
|
|
25
25
|
1. **Detects the project license** from its metadata file (`package.json`, `gemspec`, `pom.xml`, etc.)
|
|
26
|
-
2. **Scans
|
|
26
|
+
2. **Scans the dependencies** using LicenseFinder (see [Direct dependencies only](#direct-dependencies-only))
|
|
27
27
|
3. **Normalizes license names** — SPDX identifiers, common aliases, and variant spellings are all mapped to canonical names
|
|
28
28
|
4. **Checks for conflicts** using a built-in compatibility matrix
|
|
29
29
|
5. **Reports results** to stdout and exits with an appropriate code
|
|
30
30
|
|
|
31
|
+
### Direct dependencies only
|
|
32
|
+
|
|
33
|
+
Only the **direct** dependencies are analyzed: the ones the project declares in its own manifests
|
|
34
|
+
at the project root. Transitive dependencies that LicenseFinder also reports are left out.
|
|
35
|
+
|
|
36
|
+
| Manifest | Declarations read |
|
|
37
|
+
|--------------------------------|-------------------------------------------------------------------------|
|
|
38
|
+
| `package.json`, `bower.json` | `dependencies`, `devDependencies` |
|
|
39
|
+
| `Gemfile`, `*.gemspec` | `gem`, `add_dependency`, `add_runtime_dependency`, `add_development_dependency` |
|
|
40
|
+
| `requirements.txt` | every requirement line (`-r`/`-e` options and bare URLs are skipped) |
|
|
41
|
+
| `setup.cfg` | `[options] install_requires` |
|
|
42
|
+
| `pyproject.toml` | `[project] dependencies`; Poetry dependency tables (`python` excluded) |
|
|
43
|
+
| `go.mod` | `require` directives not marked `// indirect` |
|
|
44
|
+
| `pom.xml` | `<dependencies>` (by `artifactId` and `groupId:artifactId`) |
|
|
45
|
+
|
|
46
|
+
Every manifest present contributes. Names are compared ignoring case and the `-`, `_` and `.`
|
|
47
|
+
separators. When a package manager reports a declared name in several versions, the one
|
|
48
|
+
matching the copy at its install path is checked. Declared dependencies that were not resolved
|
|
49
|
+
are not counted. If no manifest above is present (for example, a project that only has
|
|
50
|
+
`Godeps/Godeps.json`, which pins every vendored package without telling direct from transitive
|
|
51
|
+
ones), every dependency LicenseFinder reports is analyzed.
|
|
52
|
+
|
|
53
|
+
Transitive dependencies are a known limitation, not yet covered.
|
|
54
|
+
|
|
31
55
|
---
|
|
32
56
|
|
|
33
57
|
## Supported Languages
|
|
@@ -47,7 +71,9 @@
|
|
|
47
71
|
## Requirements
|
|
48
72
|
|
|
49
73
|
- Ruby >= 2.6.0
|
|
50
|
-
- The package manager for your project type must be installed (e.g. `npm`, `bundler`, `mvn`)
|
|
74
|
+
- The package manager for your project type must be installed (e.g. `npm`, `bundler`, `mvn`).
|
|
75
|
+
LicenseFinder activates every package manager whose manifest it finds at the project root, so
|
|
76
|
+
a repository that also ships, say, a `composer.json` needs `composer` too (otherwise exit `2`).
|
|
51
77
|
|
|
52
78
|
---
|
|
53
79
|
|
|
@@ -97,7 +123,7 @@ Results are printed to **stdout** as a single comma-separated line:
|
|
|
97
123
|
|
|
98
124
|
| Field | Description |
|
|
99
125
|
|-----------------------|----------------------------------------------------------------------------------|
|
|
100
|
-
| `dependency_count` | Number of
|
|
126
|
+
| `dependency_count` | Number of analyzed (direct) dependencies |
|
|
101
127
|
| `project_license` | Detected license of the analyzed project |
|
|
102
128
|
| `conflicting_licenses`| Semicolon-separated list of incompatible licenses found (empty if none) |
|
|
103
129
|
| `report` | Full dependency report (only present when `--format` is specified) |
|
|
@@ -1,52 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
|
|
3
|
+
# Key: declared license of the EXAMINED PROJECT.
|
|
4
|
+
# Value: DEPENDENCY licenses that conflict with it, i.e. licenses that
|
|
5
|
+
# CANNOT be relicensed/combined under the project's license.
|
|
6
|
+
#
|
|
7
|
+
# Directional semantics (Kapitsaki et al. 2017, Definition 1 and Fig. 2):
|
|
8
|
+
# conflict(project P, dependency D) <=> there is no path D -> P in the
|
|
9
|
+
# article's compatibility graph.
|
|
10
|
+
#
|
|
11
|
+
# Documented assumptions/limitations:
|
|
12
|
+
# - Single-licensed projects only (OR/AND multi-licensing is out of scope).
|
|
13
|
+
# - Flattened names treated as the exact version ("only"): "GPLv2" = GPL-2.0-only, etc.
|
|
14
|
+
# - Canonical names as returned by LicenseFinder, normalized via LicenseNormalizer.
|
|
15
|
+
#
|
|
16
|
+
# rubocop:disable Metrics/ModuleLength -- hardcoded data map, long by nature.
|
|
8
17
|
module LicenseConflicts
|
|
9
|
-
|
|
10
|
-
"MIT",
|
|
11
|
-
"New BSD",
|
|
12
|
-
"Simplified BSD",
|
|
13
|
-
"Zlib",
|
|
14
|
-
"MPL 1.1",
|
|
15
|
-
"CDDL 1.0",
|
|
16
|
-
"AGPL 1.0"
|
|
17
|
-
].freeze
|
|
18
|
-
|
|
19
|
-
NEW_BSD_CONFLICTS = [
|
|
20
|
-
"MIT",
|
|
21
|
-
"Simplified BSD",
|
|
22
|
-
"Zlib",
|
|
23
|
-
"MPL 1.1",
|
|
24
|
-
"CDDL 1.0",
|
|
25
|
-
"AGPL 1.0"
|
|
26
|
-
].freeze
|
|
27
|
-
|
|
28
|
-
GPL2_CONFLICTS = [
|
|
18
|
+
ALL_LICENSES = [
|
|
29
19
|
"MIT",
|
|
30
20
|
"Simplified BSD",
|
|
31
21
|
"New BSD",
|
|
32
|
-
"Apache 2.0",
|
|
33
22
|
"Zlib",
|
|
34
|
-
"AFL 3.0",
|
|
35
|
-
"MPL 1.1",
|
|
36
|
-
"MPL 2.0",
|
|
37
|
-
"CDDL 1.0",
|
|
38
|
-
"LGPL 2.1",
|
|
39
|
-
"LGPL 3.0",
|
|
40
|
-
"OSL 3.0",
|
|
41
|
-
"AGPL 1.0"
|
|
42
|
-
].freeze
|
|
43
|
-
|
|
44
|
-
GPL3_CONFLICTS = [
|
|
45
|
-
"MIT",
|
|
46
|
-
"Simplified BSD",
|
|
47
|
-
"New BSD",
|
|
48
23
|
"Apache 2.0",
|
|
49
|
-
"Zlib",
|
|
50
24
|
"AFL 3.0",
|
|
51
25
|
"MPL 1.1",
|
|
52
26
|
"MPL 2.0",
|
|
@@ -55,114 +29,101 @@ module LicenseConflicts
|
|
|
55
29
|
"LGPL 3.0",
|
|
56
30
|
"OSL 3.0",
|
|
57
31
|
"GPLv2",
|
|
58
|
-
"
|
|
32
|
+
"GPLv3",
|
|
33
|
+
"AGPL 1.0",
|
|
34
|
+
"AGPL 3"
|
|
59
35
|
].freeze
|
|
60
36
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
"New BSD",
|
|
65
|
-
"Apache 2.0",
|
|
66
|
-
"Zlib",
|
|
67
|
-
"AFL 3.0",
|
|
68
|
-
"MPL 1.1",
|
|
69
|
-
"CDDL 1.0",
|
|
70
|
-
"LGPL 3.0",
|
|
71
|
-
"OSL 3.0",
|
|
72
|
-
"AGPL 1.0"
|
|
73
|
-
].freeze
|
|
37
|
+
CONFLICTS_MAP = {
|
|
38
|
+
# Only MIT code (and public domain) can be relicensed as MIT.
|
|
39
|
+
"MIT" => (ALL_LICENSES - ["MIT"]).freeze,
|
|
74
40
|
|
|
75
|
-
|
|
76
|
-
"MIT",
|
|
77
|
-
"Zlib",
|
|
78
|
-
"MPL 1.1",
|
|
79
|
-
"CDDL 1.0",
|
|
80
|
-
"AGPL 1.0"
|
|
81
|
-
].freeze
|
|
41
|
+
"Simplified BSD" => (ALL_LICENSES - ["MIT", "Simplified BSD"]).freeze,
|
|
82
42
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
"New BSD" => NEW_BSD_CONFLICTS,
|
|
92
|
-
"Apache 2.0" => APACHE2_CONFLICTS,
|
|
93
|
-
"Zlib" => [
|
|
94
|
-
"MIT",
|
|
95
|
-
"New BSD",
|
|
96
|
-
"Simplified BSD",
|
|
43
|
+
"New BSD" => (ALL_LICENSES - ["MIT", "Simplified BSD", "New BSD"]).freeze,
|
|
44
|
+
|
|
45
|
+
# No other license flows into Zlib in the article's graph.
|
|
46
|
+
"Zlib" => (ALL_LICENSES - ["Zlib"]).freeze,
|
|
47
|
+
|
|
48
|
+
# Permissive licenses (MIT, BSDs, Zlib) flow into Apache-2.0; everything else conflicts.
|
|
49
|
+
"Apache 2.0" => [
|
|
50
|
+
"AFL 3.0",
|
|
97
51
|
"MPL 1.1",
|
|
52
|
+
"MPL 2.0",
|
|
98
53
|
"CDDL 1.0",
|
|
99
|
-
"
|
|
100
|
-
|
|
54
|
+
"LGPL 2.1",
|
|
55
|
+
"LGPL 3.0",
|
|
56
|
+
"OSL 3.0",
|
|
57
|
+
"GPLv2",
|
|
58
|
+
"GPLv3",
|
|
59
|
+
"AGPL 1.0",
|
|
60
|
+
"AGPL 3"
|
|
61
|
+
].freeze,
|
|
62
|
+
|
|
101
63
|
"AFL 3.0" => [
|
|
102
|
-
"MIT",
|
|
103
|
-
"Simplified BSD",
|
|
104
|
-
"New BSD",
|
|
105
|
-
"Apache 2.0",
|
|
106
64
|
"MPL 1.1",
|
|
107
65
|
"MPL 2.0",
|
|
108
66
|
"CDDL 1.0",
|
|
109
67
|
"LGPL 2.1",
|
|
110
68
|
"LGPL 3.0",
|
|
69
|
+
"OSL 3.0",
|
|
111
70
|
"GPLv2",
|
|
112
71
|
"GPLv3",
|
|
113
|
-
"AGPL
|
|
114
|
-
"
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
72
|
+
"AGPL 1.0",
|
|
73
|
+
"AGPL 3"
|
|
74
|
+
].freeze,
|
|
75
|
+
|
|
76
|
+
# No other license flows into MPL-1.1.
|
|
77
|
+
"MPL 1.1" => (ALL_LICENSES - ["MPL 1.1"]).freeze,
|
|
78
|
+
|
|
79
|
+
# MIT/BSDs (solid edge), Zlib/Apache-2.0 and MPL-1.1 (non-transitive edges as the
|
|
80
|
+
# last edge of the path) flow into MPL-2.0. Article's py2exe case: MIT + MPL-1.1
|
|
81
|
+
# are combinable under MPL-2.0.
|
|
82
|
+
"MPL 2.0" => [
|
|
123
83
|
"AFL 3.0",
|
|
84
|
+
"CDDL 1.0",
|
|
85
|
+
"LGPL 2.1",
|
|
124
86
|
"LGPL 3.0",
|
|
125
87
|
"OSL 3.0",
|
|
126
|
-
"
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
88
|
+
"GPLv2",
|
|
89
|
+
"GPLv3",
|
|
90
|
+
"AGPL 1.0",
|
|
91
|
+
"AGPL 3"
|
|
92
|
+
].freeze,
|
|
93
|
+
|
|
94
|
+
# Only MPL-1.1 flows into CDDL-1.0 (CDDL is derived from MPL-1.1).
|
|
95
|
+
"CDDL 1.0" => (ALL_LICENSES - ["MPL 1.1", "CDDL 1.0"]).freeze,
|
|
96
|
+
|
|
97
|
+
# Zlib and Apache-2.0 do NOT flow into LGPL-2.1 (explicit incompatibility in the
|
|
98
|
+
# article -- Shopware case). GPL cannot flow "back" into LGPL.
|
|
99
|
+
"LGPL 2.1" => [
|
|
134
100
|
"Zlib",
|
|
101
|
+
"Apache 2.0",
|
|
135
102
|
"AFL 3.0",
|
|
136
103
|
"MPL 1.1",
|
|
137
|
-
"
|
|
138
|
-
"LGPL 2.1",
|
|
104
|
+
"CDDL 1.0",
|
|
139
105
|
"LGPL 3.0",
|
|
140
106
|
"OSL 3.0",
|
|
141
107
|
"GPLv2",
|
|
142
108
|
"GPLv3",
|
|
143
|
-
"AGPL
|
|
144
|
-
"AGPL
|
|
145
|
-
],
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
"New BSD",
|
|
150
|
-
"Apache 2.0",
|
|
151
|
-
"Zlib",
|
|
109
|
+
"AGPL 1.0",
|
|
110
|
+
"AGPL 3"
|
|
111
|
+
].freeze,
|
|
112
|
+
|
|
113
|
+
# Apache-2.0 and Zlib flow into LGPL-3.0 (unlike LGPL-2.1).
|
|
114
|
+
"LGPL 3.0" => [
|
|
152
115
|
"AFL 3.0",
|
|
153
116
|
"MPL 1.1",
|
|
154
|
-
"MPL 2.0",
|
|
155
117
|
"CDDL 1.0",
|
|
118
|
+
"LGPL 2.1",
|
|
156
119
|
"OSL 3.0",
|
|
157
|
-
"
|
|
158
|
-
|
|
120
|
+
"GPLv2",
|
|
121
|
+
"GPLv3",
|
|
122
|
+
"AGPL 1.0",
|
|
123
|
+
"AGPL 3"
|
|
124
|
+
].freeze,
|
|
125
|
+
|
|
159
126
|
"OSL 3.0" => [
|
|
160
|
-
"MIT",
|
|
161
|
-
"Simplified BSD",
|
|
162
|
-
"New BSD",
|
|
163
|
-
"Apache 2.0",
|
|
164
|
-
"Zlib",
|
|
165
|
-
"AFL 3.0",
|
|
166
127
|
"MPL 1.1",
|
|
167
128
|
"MPL 2.0",
|
|
168
129
|
"CDDL 1.0",
|
|
@@ -170,43 +131,49 @@ module LicenseConflicts
|
|
|
170
131
|
"LGPL 3.0",
|
|
171
132
|
"GPLv2",
|
|
172
133
|
"GPLv3",
|
|
173
|
-
"AGPL
|
|
174
|
-
"AGPL
|
|
175
|
-
],
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
"Simplified BSD",
|
|
181
|
-
"New BSD",
|
|
182
|
-
"Apache 2.0",
|
|
134
|
+
"AGPL 1.0",
|
|
135
|
+
"AGPL 3"
|
|
136
|
+
].freeze,
|
|
137
|
+
|
|
138
|
+
# Apache-2.0 and Zlib are incompatible with GPL-2.0 (explicit examples in the
|
|
139
|
+
# article, Section 4.1 and Table 4). GPLv3 incompatible with GPLv2 "and vice versa".
|
|
140
|
+
"GPLv2" => [
|
|
183
141
|
"Zlib",
|
|
142
|
+
"Apache 2.0",
|
|
184
143
|
"AFL 3.0",
|
|
185
144
|
"MPL 1.1",
|
|
186
|
-
"MPL 2.0",
|
|
187
145
|
"CDDL 1.0",
|
|
188
|
-
"LGPL 2.1",
|
|
189
146
|
"LGPL 3.0",
|
|
190
147
|
"OSL 3.0",
|
|
191
|
-
"GPLv2",
|
|
192
148
|
"GPLv3",
|
|
193
|
-
"AGPL 1.0"
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
"Apache 2.0",
|
|
200
|
-
"Zlib",
|
|
149
|
+
"AGPL 1.0",
|
|
150
|
+
"AGPL 3"
|
|
151
|
+
].freeze,
|
|
152
|
+
|
|
153
|
+
# Apache-2.0, Zlib, MPL-2.0 and the LGPLs flow into GPL-3.0.
|
|
154
|
+
"GPLv3" => [
|
|
201
155
|
"AFL 3.0",
|
|
202
156
|
"MPL 1.1",
|
|
203
|
-
"MPL 2.0",
|
|
204
157
|
"CDDL 1.0",
|
|
205
|
-
"LGPL 2.1",
|
|
206
|
-
"LGPL 3.0",
|
|
207
158
|
"OSL 3.0",
|
|
208
159
|
"GPLv2",
|
|
209
|
-
"
|
|
210
|
-
|
|
160
|
+
"AGPL 1.0",
|
|
161
|
+
"AGPL 3"
|
|
162
|
+
].freeze,
|
|
163
|
+
|
|
164
|
+
# Exact-only assumption: nothing flows into AGPL-1.0 (in the article's graph the
|
|
165
|
+
# node is AGPL-1.0+, with an outgoing edge only to AGPL-3.0).
|
|
166
|
+
"AGPL 1.0" => (ALL_LICENSES - ["AGPL 1.0"]).freeze,
|
|
167
|
+
|
|
168
|
+
# GPL-3.0 flows into AGPL-3.0 (GPLv3 clause 13); GPL-2.0 does not (Odoo case).
|
|
169
|
+
"AGPL 3" => [
|
|
170
|
+
"AFL 3.0",
|
|
171
|
+
"MPL 1.1",
|
|
172
|
+
"CDDL 1.0",
|
|
173
|
+
"OSL 3.0",
|
|
174
|
+
"GPLv2",
|
|
175
|
+
"AGPL 1.0"
|
|
176
|
+
].freeze
|
|
211
177
|
}.freeze
|
|
212
178
|
end
|
|
179
|
+
# rubocop:enable Metrics/ModuleLength
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
module LicenseConflicts
|
|
7
|
+
# Names of the dependencies a project declares in its own manifests at the project root,
|
|
8
|
+
# i.e. its direct dependencies. Every supported manifest present contributes to the set.
|
|
9
|
+
class DeclaredDependencies
|
|
10
|
+
# Manifest (glob relative to the project root) => reader returning the declared names, or
|
|
11
|
+
# nil when the file cannot be parsed. Godeps/Godeps.json is not listed: it pins every
|
|
12
|
+
# vendored package without telling direct from transitive ones.
|
|
13
|
+
READERS = {
|
|
14
|
+
"package.json" => :json_manifest_names,
|
|
15
|
+
"bower.json" => :json_manifest_names,
|
|
16
|
+
"*.gemspec" => :gemspec_names,
|
|
17
|
+
"Gemfile" => :gemfile_names,
|
|
18
|
+
"requirements.txt" => :requirements_names,
|
|
19
|
+
"setup.cfg" => :setup_cfg_names,
|
|
20
|
+
"pyproject.toml" => :pyproject_names,
|
|
21
|
+
"go.mod" => :go_mod_names,
|
|
22
|
+
"pom.xml" => :pom_names
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
# Some registries treat names differing only in case or in `-`, `_` and `.` as the same
|
|
26
|
+
# package, and package managers may report a name normalized differently from how the
|
|
27
|
+
# manifest spells it; names are compared by this key.
|
|
28
|
+
def self.key(name)
|
|
29
|
+
name.to_s.downcase.gsub(/[-_.]+/, "-")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(root = ".")
|
|
33
|
+
@root = root
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Set of normalized names, or nil when no readable manifest is present (direct and
|
|
37
|
+
# transitive dependencies cannot be told apart).
|
|
38
|
+
def names
|
|
39
|
+
return @names if defined?(@names)
|
|
40
|
+
|
|
41
|
+
declared = READERS.flat_map do |pattern, reader|
|
|
42
|
+
Dir.glob(File.join(@root, pattern)).map { |file| send(reader, File.read(file)) }
|
|
43
|
+
end.compact
|
|
44
|
+
@names = declared.empty? ? nil : declared.flatten.to_set { |name| self.class.key(name) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def include?(name)
|
|
48
|
+
names&.include?(self.class.key(name))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Requirement specifiers such as `requests[security]>=2.0; python_version > "3"`.
|
|
54
|
+
def requirement_name(specifier)
|
|
55
|
+
specifier[/\A\s*([A-Za-z0-9][A-Za-z0-9._-]*)/, 1]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def json_manifest_names(content)
|
|
59
|
+
data = JSON.parse(content)
|
|
60
|
+
return [] unless data.is_a?(Hash)
|
|
61
|
+
|
|
62
|
+
%w[dependencies devDependencies].flat_map { |field| data[field].is_a?(Hash) ? data[field].keys : [] }
|
|
63
|
+
rescue JSON::ParserError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def gemspec_names(content)
|
|
68
|
+
content.scan(/\.add_(?:runtime_|development_)?dependency\s*\(?\s*["']([^"']+)["']/).flatten
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def gemfile_names(content)
|
|
72
|
+
content.scan(/^\s*gem\s*\(?\s*["']([^"']+)["']/).flatten
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def requirements_names(content)
|
|
76
|
+
content.each_line.filter_map do |line|
|
|
77
|
+
line = line.sub(/(?:\A|\s)#(?!egg=).*/, "").strip
|
|
78
|
+
next line[/#egg=([A-Za-z0-9._-]+)/, 1] if line.include?("://")
|
|
79
|
+
next if line.empty? || line.start_with?("-")
|
|
80
|
+
|
|
81
|
+
requirement_name(line)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def setup_cfg_names(content)
|
|
86
|
+
value = content[/^install_requires\s*=(.*(?:\n[ \t]+.*)*)/, 1]
|
|
87
|
+
return [] unless value
|
|
88
|
+
|
|
89
|
+
value.split("\n").filter_map { |line| requirement_name(line) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def pyproject_names(content)
|
|
93
|
+
require "tomlrb"
|
|
94
|
+
data = Tomlrb.parse(content)
|
|
95
|
+
pep621 = Array(data.dig("project", "dependencies")).filter_map { |specifier| requirement_name(specifier) }
|
|
96
|
+
pep621 + poetry_names(data.dig("tool", "poetry") || {})
|
|
97
|
+
rescue LoadError, Tomlrb::ParseError
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def poetry_names(poetry)
|
|
102
|
+
groups = (poetry["group"] || {}).values.map { |group| group["dependencies"] }
|
|
103
|
+
tables = [poetry["dependencies"], poetry["dev-dependencies"], *groups]
|
|
104
|
+
tables.flat_map { |table| table.is_a?(Hash) ? table.keys : [] } - ["python"]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# `require` directives, single or in a block, except the ones marked `// indirect`.
|
|
108
|
+
def go_mod_names(content)
|
|
109
|
+
block_lines = content.scan(/^require\s*\(\s*\n(.*?)^\)/m).flatten.flat_map(&:lines)
|
|
110
|
+
single_lines = content.scan(/^require\s+([^\s(].*)$/).flatten
|
|
111
|
+
(block_lines + single_lines).reject { |line| line.include?("// indirect") }
|
|
112
|
+
.filter_map { |line| line.strip.split.first unless line.strip.start_with?("//") }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def pom_names(content)
|
|
116
|
+
require "rexml/document"
|
|
117
|
+
path = %w[project dependencies dependency].map { |tag| "*[local-name()='#{tag}']" }.join("/")
|
|
118
|
+
REXML::XPath.match(REXML::Document.new(content), "/#{path}").flat_map { |node| pom_coordinates(node) }
|
|
119
|
+
rescue REXML::ParseException
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Both `artifactId` and `groupId:artifactId`, since the package manager may report either.
|
|
124
|
+
def pom_coordinates(dependency)
|
|
125
|
+
artifact, group = %w[artifactId groupId].map { |tag| REXML::XPath.first(dependency, "*[local-name()='#{tag}']")&.text }
|
|
126
|
+
return [] unless artifact
|
|
127
|
+
|
|
128
|
+
group ? [artifact, "#{group}:#{artifact}"] : [artifact]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'license_finder'
|
|
4
4
|
require 'license_conflicts/conflicts_map'
|
|
5
|
+
require "license_conflicts/declared_dependencies"
|
|
5
6
|
require 'license_conflicts/license_normalizer'
|
|
6
7
|
require 'license_conflicts/project_metadata'
|
|
7
8
|
|
|
@@ -23,7 +24,7 @@ module LicenseConflicts
|
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def dependencies_count
|
|
26
|
-
|
|
27
|
+
dependencies.count
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def project_license
|
|
@@ -56,8 +57,30 @@ module LicenseConflicts
|
|
|
56
57
|
@unapproved ||= finder.unapproved
|
|
57
58
|
end
|
|
58
59
|
|
|
60
|
+
# Only the direct dependencies, the ones the project declares in its own manifests, are
|
|
61
|
+
# analysed; transitive dependencies that LicenseFinder also reports are left out. When no
|
|
62
|
+
# supported manifest is found, every dependency LicenseFinder reports is kept.
|
|
63
|
+
def dependencies
|
|
64
|
+
@dependencies ||= begin
|
|
65
|
+
others = unapproved.reject { |d| d.name == project_name }
|
|
66
|
+
declared = DeclaredDependencies.new
|
|
67
|
+
declared.names ? one_per_name(others.select { |d| declared.include?(d.name) }) : others
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# A package manager may report several versions of the same name (the direct dependency and
|
|
72
|
+
# transitive copies); keep the one whose version matches the copy at its install path.
|
|
73
|
+
def one_per_name(packages)
|
|
74
|
+
packages.group_by { |d| DeclaredDependencies.key(d.name) }.map do |_key, versions|
|
|
75
|
+
next versions.first if versions.one?
|
|
76
|
+
|
|
77
|
+
versions.find { |d| d.install_path && ProjectMetadata.new(d.install_path.to_s).version == d.version } ||
|
|
78
|
+
versions.first
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
59
82
|
def check_conflicts
|
|
60
|
-
|
|
83
|
+
dependencies.filter { |dependency| has_conflict?(dependency.licenses.first&.name) }
|
|
61
84
|
end
|
|
62
85
|
|
|
63
86
|
def has_conflict?(dependency_license)
|
|
@@ -4,7 +4,12 @@ require "json"
|
|
|
4
4
|
require "pathname"
|
|
5
5
|
|
|
6
6
|
module LicenseConflicts
|
|
7
|
+
# rubocop:disable Metrics/ClassLength -- one small reader per supported manifest format.
|
|
7
8
|
class ProjectMetadata
|
|
9
|
+
def initialize(root = ".")
|
|
10
|
+
@root = root
|
|
11
|
+
end
|
|
12
|
+
|
|
8
13
|
def name
|
|
9
14
|
@name ||= read_name
|
|
10
15
|
end
|
|
@@ -13,6 +18,12 @@ module LicenseConflicts
|
|
|
13
18
|
@license ||= read_license
|
|
14
19
|
end
|
|
15
20
|
|
|
21
|
+
# Version declared by the manifest, used to identify which of several reported versions of a
|
|
22
|
+
# package is the one installed at its install path.
|
|
23
|
+
def version
|
|
24
|
+
@version ||= read_version
|
|
25
|
+
end
|
|
26
|
+
|
|
16
27
|
private
|
|
17
28
|
|
|
18
29
|
def read_name
|
|
@@ -24,7 +35,7 @@ module LicenseConflicts
|
|
|
24
35
|
go_mod_data&.dig(:name) ||
|
|
25
36
|
godeps_data&.dig(:name) ||
|
|
26
37
|
pom_xml_data&.dig(:name) ||
|
|
27
|
-
Pathname.
|
|
38
|
+
Pathname(@root).expand_path.basename.to_s
|
|
28
39
|
end
|
|
29
40
|
|
|
30
41
|
def read_license
|
|
@@ -36,74 +47,86 @@ module LicenseConflicts
|
|
|
36
47
|
pom_xml_data&.dig(:license)
|
|
37
48
|
end
|
|
38
49
|
|
|
50
|
+
def read_version
|
|
51
|
+
[package_json_data, bower_json_data].filter_map { |data| data&.dig("version") }.first ||
|
|
52
|
+
[gemspec_data, setup_cfg_data, pyproject_toml_data, pom_xml_data]
|
|
53
|
+
.filter_map { |data| data&.dig(:version) }.first
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def path(file)
|
|
57
|
+
File.join(@root, file)
|
|
58
|
+
end
|
|
59
|
+
|
|
39
60
|
def package_json_data
|
|
40
|
-
return unless File.exist?("
|
|
61
|
+
return unless File.exist?(path("package.json"))
|
|
41
62
|
|
|
42
|
-
@package_json_data ||= JSON.parse(File.read("
|
|
63
|
+
@package_json_data ||= JSON.parse(File.read(path("package.json")))
|
|
43
64
|
rescue JSON::ParserError
|
|
44
65
|
nil
|
|
45
66
|
end
|
|
46
67
|
|
|
47
68
|
def bower_json_data
|
|
48
|
-
return unless File.exist?("
|
|
69
|
+
return unless File.exist?(path("bower.json"))
|
|
49
70
|
|
|
50
|
-
@bower_json_data ||= JSON.parse(File.read("
|
|
71
|
+
@bower_json_data ||= JSON.parse(File.read(path("bower.json")))
|
|
51
72
|
rescue JSON::ParserError
|
|
52
73
|
nil
|
|
53
74
|
end
|
|
54
75
|
|
|
55
76
|
def gemspec_data
|
|
56
|
-
gemspec_file = Dir.glob("
|
|
77
|
+
gemspec_file = Dir.glob(path("*.gemspec")).first
|
|
57
78
|
return unless gemspec_file
|
|
58
79
|
|
|
59
80
|
@gemspec_data ||= begin
|
|
60
81
|
content = File.read(gemspec_file)
|
|
61
82
|
name = content[/\.name\s*=\s*["']([^"']+)["']/, 1]
|
|
62
83
|
license = content[/\.license\s*=\s*["']([^"']+)["']/, 1]
|
|
63
|
-
|
|
84
|
+
version = content[/\.version\s*=\s*["']([^"']+)["']/, 1]
|
|
85
|
+
{ name: name, license: license, version: version }
|
|
64
86
|
end
|
|
65
87
|
end
|
|
66
88
|
|
|
67
89
|
def setup_cfg_data
|
|
68
|
-
return unless File.exist?("
|
|
90
|
+
return unless File.exist?(path("setup.cfg"))
|
|
69
91
|
|
|
70
92
|
@setup_cfg_data ||= begin
|
|
71
|
-
content = File.read("
|
|
93
|
+
content = File.read(path("setup.cfg"))
|
|
72
94
|
name = content[/^name\s*=\s*(.+)/, 1]&.strip
|
|
73
95
|
license = content[/^license\s*=\s*(.+)/, 1]&.strip
|
|
74
|
-
|
|
96
|
+
version = content[/^version\s*=\s*(.+)/, 1]&.strip
|
|
97
|
+
{ name: name, license: license, version: version }
|
|
75
98
|
end
|
|
76
99
|
end
|
|
77
100
|
|
|
78
101
|
def pyproject_toml_data
|
|
79
|
-
return unless File.exist?("
|
|
102
|
+
return unless File.exist?(path("pyproject.toml"))
|
|
80
103
|
|
|
81
104
|
@pyproject_toml_data ||= begin
|
|
82
105
|
require "tomlrb"
|
|
83
|
-
data = Tomlrb.load_file("
|
|
106
|
+
data = Tomlrb.load_file(path("pyproject.toml"))
|
|
84
107
|
project = data["project"] || data.dig("tool", "poetry") || {}
|
|
85
108
|
license = project["license"]
|
|
86
109
|
license = license["text"] if license.is_a?(Hash)
|
|
87
|
-
{ name: project["name"], license: license }
|
|
110
|
+
{ name: project["name"], license: license, version: project["version"] }
|
|
88
111
|
rescue LoadError
|
|
89
112
|
nil
|
|
90
113
|
end
|
|
91
114
|
end
|
|
92
115
|
|
|
93
116
|
def go_mod_data
|
|
94
|
-
return unless File.exist?("
|
|
117
|
+
return unless File.exist?(path("go.mod"))
|
|
95
118
|
|
|
96
119
|
@go_mod_data ||= begin
|
|
97
|
-
name = File.read("
|
|
120
|
+
name = File.read(path("go.mod"))[/^module\s+(\S+)/, 1]
|
|
98
121
|
{ name: name }
|
|
99
122
|
end
|
|
100
123
|
end
|
|
101
124
|
|
|
102
125
|
def godeps_data
|
|
103
|
-
return unless File.exist?("
|
|
126
|
+
return unless File.exist?(path("Godeps/Godeps.json"))
|
|
104
127
|
|
|
105
128
|
@godeps_data ||= begin
|
|
106
|
-
data = JSON.parse(File.read("
|
|
129
|
+
data = JSON.parse(File.read(path("Godeps/Godeps.json")))
|
|
107
130
|
{ name: data["ImportPath"] }
|
|
108
131
|
rescue JSON::ParserError
|
|
109
132
|
nil
|
|
@@ -111,18 +134,22 @@ module LicenseConflicts
|
|
|
111
134
|
end
|
|
112
135
|
|
|
113
136
|
def pom_xml_data
|
|
114
|
-
return unless File.exist?("
|
|
137
|
+
return unless File.exist?(path("pom.xml"))
|
|
115
138
|
|
|
116
139
|
@pom_xml_data ||= begin
|
|
117
140
|
require "rexml/document"
|
|
118
|
-
doc
|
|
119
|
-
name
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
{ name: name, license: license }
|
|
141
|
+
doc = REXML::Document.new(File.read(path("pom.xml")))
|
|
142
|
+
name = xml_text(doc, "//project/name") || xml_text(doc, "//project/artifactId")
|
|
143
|
+
{ name: name, license: xml_text(doc, "//project/licenses/license/name"),
|
|
144
|
+
version: xml_text(doc, "//project/version") }
|
|
123
145
|
rescue REXML::ParseException
|
|
124
146
|
nil
|
|
125
147
|
end
|
|
126
148
|
end
|
|
149
|
+
|
|
150
|
+
def xml_text(doc, xpath)
|
|
151
|
+
REXML::XPath.first(doc, xpath)&.text
|
|
152
|
+
end
|
|
127
153
|
end
|
|
154
|
+
# rubocop:enable Metrics/ClassLength
|
|
128
155
|
end
|
data/lib/license_conflicts.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative 'license_conflicts/finder'
|
|
|
4
4
|
require_relative 'license_conflicts/version'
|
|
5
5
|
require_relative 'license_conflicts/report'
|
|
6
6
|
require_relative 'license_conflicts/project_metadata'
|
|
7
|
+
require_relative "license_conflicts/declared_dependencies"
|
|
7
8
|
require_relative 'license_conflicts/license_normalizer'
|
|
8
9
|
|
|
9
10
|
module LicenseConflicts
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: license_conflicts
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dennis Urtubia
|
|
@@ -132,6 +132,7 @@ files:
|
|
|
132
132
|
- lib/license_conflicts.rb
|
|
133
133
|
- lib/license_conflicts/cli.rb
|
|
134
134
|
- lib/license_conflicts/conflicts_map.rb
|
|
135
|
+
- lib/license_conflicts/declared_dependencies.rb
|
|
135
136
|
- lib/license_conflicts/finder.rb
|
|
136
137
|
- lib/license_conflicts/license_normalizer.rb
|
|
137
138
|
- lib/license_conflicts/project_metadata.rb
|