licomp-toolkit 0.6.3__py3-none-any.whl
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.
- licomp_toolkit/__init__.py +0 -0
- licomp_toolkit/__main__.py +322 -0
- licomp_toolkit/config.py +40 -0
- licomp_toolkit/data/reply_schema.json +305 -0
- licomp_toolkit/display_compatibility.py +21 -0
- licomp_toolkit/exception.py +11 -0
- licomp_toolkit/expr_parser.py +139 -0
- licomp_toolkit/format.py +362 -0
- licomp_toolkit/lic_expr.py +325 -0
- licomp_toolkit/license_policy.py +564 -0
- licomp_toolkit/return_codes.py +12 -0
- licomp_toolkit/schema_checker.py +67 -0
- licomp_toolkit/suggester.py +162 -0
- licomp_toolkit/toolkit.py +504 -0
- licomp_toolkit/utils.py +81 -0
- licomp_toolkit-0.6.3.dist-info/METADATA +164 -0
- licomp_toolkit-0.6.3.dist-info/RECORD +21 -0
- licomp_toolkit-0.6.3.dist-info/WHEEL +5 -0
- licomp_toolkit-0.6.3.dist-info/entry_points.txt +2 -0
- licomp_toolkit-0.6.3.dist-info/licenses/LICENSES/GPL-3.0-or-later.txt +232 -0
- licomp_toolkit-0.6.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Henrik Sandklef
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
import traceback
|
|
8
|
+
|
|
9
|
+
from licomp_toolkit.toolkit import LicompToolkit
|
|
10
|
+
from licomp_toolkit.toolkit import ExpressionExpressionChecker
|
|
11
|
+
from licomp.interface import Provisioning
|
|
12
|
+
from licomp.interface import UseCase
|
|
13
|
+
|
|
14
|
+
from flame.license_db import FossLicenses
|
|
15
|
+
|
|
16
|
+
class OutboundSuggester:
|
|
17
|
+
"""
|
|
18
|
+
OutboundSuggester suggests outbound candidate licenses from a
|
|
19
|
+
license expression
|
|
20
|
+
|
|
21
|
+
If you have a license expression like this "BSD-3-Clause AND MIT
|
|
22
|
+
AND GPL-2.0-only", typically the result of your dependencies'
|
|
23
|
+
licenses, and want to know which outbound license you can chose.
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self._compatibility_rankings = {}
|
|
28
|
+
self.lt = LicompToolkit()
|
|
29
|
+
self.le = ExpressionExpressionChecker()
|
|
30
|
+
self.flame = FossLicenses()
|
|
31
|
+
|
|
32
|
+
def compatibility_rankings(self, usecase, provisioning):
|
|
33
|
+
""" Returns a dict with ranked licenses based on your usecase/provisioning, e.g.
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
'GPL3': {'count': 2, 'valids': '1', 'index': 69},
|
|
37
|
+
'MIT': {'count': 108, 'valids': '1', 'index': 169}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
count - how many other licenses the license is compatible with (primary sort)
|
|
41
|
+
valids - how many valid resources provided a 'yes' answer (secondary sort)
|
|
42
|
+
index - index for the licenses, the higher index the more compatible (with other licenses)
|
|
43
|
+
"""
|
|
44
|
+
if usecase in self._compatibility_rankings:
|
|
45
|
+
if provisioning in self._compatibility_rankings[usecase]:
|
|
46
|
+
return self._compatibility_rankings[usecase][provisioning]
|
|
47
|
+
else:
|
|
48
|
+
self._compatibility_rankings[usecase] = {}
|
|
49
|
+
|
|
50
|
+
#
|
|
51
|
+
# identify compatiblities
|
|
52
|
+
lic_map = {}
|
|
53
|
+
# loop through every license (against themselves)
|
|
54
|
+
for in_lic in self.lt.supported_licenses():
|
|
55
|
+
for out_lic in self.lt.supported_licenses():
|
|
56
|
+
# get compat
|
|
57
|
+
ret = self.lt.outbound_inbound_compatibility(out_lic,
|
|
58
|
+
in_lic,
|
|
59
|
+
UseCase.string_to_usecase(usecase),
|
|
60
|
+
Provisioning.string_to_provisioning(provisioning))
|
|
61
|
+
results = ret['summary']['results']
|
|
62
|
+
valid_results = results['nr_valid']
|
|
63
|
+
# only increase the 'count' if the answers from the resources are only 'yes'
|
|
64
|
+
yes = results.get('yes', {'count': 0, 'percent': 0.0})
|
|
65
|
+
inc = 0
|
|
66
|
+
if yes['percent'] == 100:
|
|
67
|
+
inc = 1
|
|
68
|
+
|
|
69
|
+
lic_map[in_lic] = {
|
|
70
|
+
'count': lic_map.get(in_lic, {'count': 0})['count'] + inc,
|
|
71
|
+
'valids': valid_results,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# decorate (to sort list)
|
|
75
|
+
decorated = [(lic_map[lic]['count'], lic_map[lic]['valids'], i, lic) for i, lic in enumerate(lic_map)]
|
|
76
|
+
# sort after count, valids (in tuple)
|
|
77
|
+
decorated.sort()
|
|
78
|
+
# undecorate
|
|
79
|
+
ordered_lic = [lic for count, valids, i, lic in decorated]
|
|
80
|
+
|
|
81
|
+
# create new dict - use license name as key
|
|
82
|
+
ordered_lic_map = {}
|
|
83
|
+
for i, lic in enumerate(ordered_lic):
|
|
84
|
+
ordered_lic_map[lic] = lic_map[lic]
|
|
85
|
+
ordered_lic_map[lic]['index'] = i
|
|
86
|
+
|
|
87
|
+
# store foar later (re)use
|
|
88
|
+
self._compatibility_rankings[usecase][provisioning] = ordered_lic_map
|
|
89
|
+
|
|
90
|
+
return self._compatibility_rankings[usecase][provisioning]
|
|
91
|
+
|
|
92
|
+
def licenses(self, license_expr):
|
|
93
|
+
"""Returns a list of licenses in a license expression.
|
|
94
|
+
|
|
95
|
+
If you have a license expression like this:
|
|
96
|
+
"(MIT OR Apache-2.0) AND MIT AND GPL-2.0-only WITH Classpath-exception-2.0"
|
|
97
|
+
|
|
98
|
+
you get a list like this:
|
|
99
|
+
["MIT", "Apache-2.0", "GPL-2.0-only WITH Classpath-exception-2.0"]
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
normalized = self.flame.expression_license(license_expr, update_dual=False)['identified_license']
|
|
103
|
+
|
|
104
|
+
with_fixed = normalized.replace(' WITH ', '-WITH-')
|
|
105
|
+
|
|
106
|
+
splits = [x.replace('-WITH-', ' WITH ') for x in re.split(r' AND | OR |\(|\)', with_fixed) if len(x) > 1]
|
|
107
|
+
|
|
108
|
+
return splits
|
|
109
|
+
|
|
110
|
+
def compat_licenses(self, license_expr, usecase, provisioning, licenses_to_check=None, resources=None):
|
|
111
|
+
"""Returns a list of licenses that are compatible with a license expression
|
|
112
|
+
|
|
113
|
+
If you have a license expression like this:
|
|
114
|
+
"(MIT OR Apache-2.0) AND MIT AND GPL-2.0-only"
|
|
115
|
+
|
|
116
|
+
you get a list like this of licenses that are compatible with
|
|
117
|
+
the expression listed in order of ranked compatibility. If you
|
|
118
|
+
don't provide any licenses to check the functions defaults to
|
|
119
|
+
looking at the licenses in the expressions itself. This you will get a list like this:
|
|
120
|
+
|
|
121
|
+
["GPL-2.0-only"]
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
compats = []
|
|
125
|
+
if not licenses_to_check:
|
|
126
|
+
licenses_to_check = self.licenses(license_expr)
|
|
127
|
+
|
|
128
|
+
license_expression_parsed = self.flame.expression_license(license_expr, update_dual=False)['identified_license']
|
|
129
|
+
for lic in licenses_to_check:
|
|
130
|
+
logging.debug(f' check licenses: {lic}')
|
|
131
|
+
try:
|
|
132
|
+
ret = self.le.check_compatibility(lic,
|
|
133
|
+
license_expression_parsed,
|
|
134
|
+
usecase,
|
|
135
|
+
provisioning,
|
|
136
|
+
resources)
|
|
137
|
+
if ret['compatibility'] == 'yes':
|
|
138
|
+
compats.append(lic)
|
|
139
|
+
logging.debug(f' appending: {lic}')
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logging.debug(f'Exception caught: {e}')
|
|
142
|
+
logging.debug(traceback.format_exc())
|
|
143
|
+
|
|
144
|
+
# decorate compat list with indices (from rankings)
|
|
145
|
+
decorated = [(self.__compat_index(lic, usecase, provisioning), lic) for lic in compats]
|
|
146
|
+
|
|
147
|
+
# sort and reverse (to get the most compatible first)
|
|
148
|
+
decorated.sort()
|
|
149
|
+
decorated.reverse()
|
|
150
|
+
|
|
151
|
+
# undecorate
|
|
152
|
+
sorted_compats = [lic for index, lic in decorated]
|
|
153
|
+
|
|
154
|
+
return sorted_compats
|
|
155
|
+
|
|
156
|
+
def __compat_index(self, lic, usecase, provisioning):
|
|
157
|
+
# returns the ranking index for a license,
|
|
158
|
+
# given usecase and provisioning
|
|
159
|
+
|
|
160
|
+
# initialize the ranking
|
|
161
|
+
self.compatibility_rankings(usecase, provisioning)
|
|
162
|
+
return self._compatibility_rankings[usecase][provisioning][lic]['index']
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Henrik Sandklef
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from license_expression import Licensing
|
|
7
|
+
|
|
8
|
+
from licomp.interface import Licomp
|
|
9
|
+
from licomp.interface import UseCase
|
|
10
|
+
from licomp.interface import Provisioning
|
|
11
|
+
from licomp.interface import LicompException
|
|
12
|
+
from licomp.return_codes import ReturnCodes
|
|
13
|
+
from licomp.interface import CompatibilityStatus
|
|
14
|
+
|
|
15
|
+
from licomp_osadl.osadl import LicompOsadl
|
|
16
|
+
from licomp_reclicense.reclicense import LicompReclicense
|
|
17
|
+
from licomp_proprietary.proprietary import LicompProprietary
|
|
18
|
+
from licomp_dwheeler.dwheeler import LicompDw
|
|
19
|
+
from licomp_hermione.hermione import LicompHermione
|
|
20
|
+
from licomp_gnuguide.gnuguide import GnuQuickGuideLicense
|
|
21
|
+
from licomp_oslc_handbook.oslc_handbook import LicompOslcHandbook
|
|
22
|
+
from licomp_doubleopen.doubleopen import LicompDoubleOpen
|
|
23
|
+
|
|
24
|
+
from licomp_toolkit.config import disclaimer
|
|
25
|
+
from licomp_toolkit.config import licomp_toolkit_version
|
|
26
|
+
from licomp_toolkit.config import licomp_toolkit_file_version
|
|
27
|
+
from licomp_toolkit.config import cli_name
|
|
28
|
+
from licomp_toolkit.config import module_name
|
|
29
|
+
|
|
30
|
+
from licomp_toolkit.expr_parser import LicenseExpressionParser
|
|
31
|
+
from licomp_toolkit.expr_parser import COMPATIBILITY_TYPE
|
|
32
|
+
from licomp_toolkit.expr_parser import AND
|
|
33
|
+
from licomp_toolkit.expr_parser import OR
|
|
34
|
+
|
|
35
|
+
from licomp_toolkit.config import my_supported_api_version
|
|
36
|
+
|
|
37
|
+
class LicompToolkit(Licomp):
|
|
38
|
+
"""A class implementing Licomp, but for a misc Licomp resources
|
|
39
|
+
and packaging the responses into a new reply
|
|
40
|
+
(licomp_toolkit/reply_schema.json).
|
|
41
|
+
|
|
42
|
+
LicompToolkit can check a single license agaisnt another for
|
|
43
|
+
compatibility, but not license expressions.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self):
|
|
47
|
+
Licomp.__init__(self)
|
|
48
|
+
self._licomp_resources = {}
|
|
49
|
+
for licomp in [LicompReclicense, LicompOsadl]:
|
|
50
|
+
licomp_instance = licomp()
|
|
51
|
+
self._licomp_resources[licomp_instance.name()] = licomp_instance
|
|
52
|
+
|
|
53
|
+
self._licomp_resources_optional = {}
|
|
54
|
+
for licomp in [LicompHermione, LicompProprietary, LicompDw, GnuQuickGuideLicense, LicompOslcHandbook, LicompDoubleOpen]:
|
|
55
|
+
licomp_instance = licomp()
|
|
56
|
+
self._licomp_resources_optional[licomp_instance.name()] = licomp_instance
|
|
57
|
+
|
|
58
|
+
def supported_api_version(self):
|
|
59
|
+
return my_supported_api_version
|
|
60
|
+
|
|
61
|
+
def __add_to_list(self, store, data, name):
|
|
62
|
+
if not data:
|
|
63
|
+
return
|
|
64
|
+
if data not in store:
|
|
65
|
+
store[data] = []
|
|
66
|
+
store[data].append(name)
|
|
67
|
+
|
|
68
|
+
def __add_meta(self, compatibilities):
|
|
69
|
+
compatibilities["meta"] = {}
|
|
70
|
+
compatibilities["meta"]['disclaimer'] = disclaimer
|
|
71
|
+
|
|
72
|
+
def licomp_resources(self):
|
|
73
|
+
return self._licomp_resources | self._licomp_resources_optional
|
|
74
|
+
|
|
75
|
+
def licomp_standard_resources(self):
|
|
76
|
+
return self._licomp_resources
|
|
77
|
+
|
|
78
|
+
def licomp_optional_resources(self):
|
|
79
|
+
return self._licomp_resources_optional
|
|
80
|
+
|
|
81
|
+
def licomp_resource_long(self, resource):
|
|
82
|
+
return {
|
|
83
|
+
'name': resource.name(),
|
|
84
|
+
'version': resource.version(),
|
|
85
|
+
'usecases': [UseCase.usecase_to_string(x) for x in resource.supported_usecases()],
|
|
86
|
+
'provisionings': [Provisioning.provisioning_to_string(x) for x in resource.supported_provisionings()],
|
|
87
|
+
'licenses': resource.supported_licenses(),
|
|
88
|
+
'type': self._resource_type(resource),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
def licomp_resources_long(self):
|
|
92
|
+
_resources = []
|
|
93
|
+
for resource in self.licomp_resources().values():
|
|
94
|
+
_resources.append(self.licomp_resource_long(resource))
|
|
95
|
+
return _resources
|
|
96
|
+
|
|
97
|
+
def licomp_resources_short(self):
|
|
98
|
+
_resources = []
|
|
99
|
+
for resource in self.licomp_resources().values():
|
|
100
|
+
_resources.append(f'{resource.name()}:{resource.version()}')
|
|
101
|
+
return _resources
|
|
102
|
+
|
|
103
|
+
def _resource_type(self, resource):
|
|
104
|
+
if self._resource_is_standard(resource):
|
|
105
|
+
return 'standard'
|
|
106
|
+
return 'optional'
|
|
107
|
+
|
|
108
|
+
def _resource_is_optional(self, resource):
|
|
109
|
+
return resource.name() in self._licomp_resources_optional
|
|
110
|
+
|
|
111
|
+
def _resource_is_standard(self, resource):
|
|
112
|
+
return not self._resource_is_optional(resource)
|
|
113
|
+
|
|
114
|
+
def __summarize_compatibility(self, compatibilities, outbound, inbound, usecase, provisioning, resources):
|
|
115
|
+
compatibilities["summary"] = {}
|
|
116
|
+
statuses = {}
|
|
117
|
+
compats = {}
|
|
118
|
+
compatibilities['nr_licomp'] = len(resources)
|
|
119
|
+
# for resource_name in self._licomp_resources():
|
|
120
|
+
for compat in compatibilities["compatibilities"]:
|
|
121
|
+
logging.debug(f': {compat}')
|
|
122
|
+
logging.debug(f': {compat["resource_name"]}')
|
|
123
|
+
self.__add_to_list(statuses, compat['status'], compat)
|
|
124
|
+
self.__add_to_list(compats, compat['compatibility_status'], compat)
|
|
125
|
+
compatibilities["summary"]["resources"] = self.licomp_resources_short()
|
|
126
|
+
compatibilities["summary"]["outbound"] = outbound
|
|
127
|
+
compatibilities["summary"]["inbound"] = inbound
|
|
128
|
+
compatibilities["summary"]["usecase"] = UseCase.usecase_to_string(usecase)
|
|
129
|
+
compatibilities["summary"]["provisioning"] = Provisioning.provisioning_to_string(provisioning)
|
|
130
|
+
compatibilities["summary"]["statuses"] = statuses
|
|
131
|
+
compatibilities["summary"]["compatibility_statuses"] = compats
|
|
132
|
+
|
|
133
|
+
compat_number = len(compatibilities["summary"]["statuses"].get("success", []))
|
|
134
|
+
logging.debug(f': {compatibilities["summary"]["statuses"]}')
|
|
135
|
+
results = {}
|
|
136
|
+
results['nr_valid'] = f'{compat_number}'
|
|
137
|
+
for key, value in compatibilities["summary"]["compatibility_statuses"].items():
|
|
138
|
+
logging.debug(f': {len(value)}/{compat_number}')
|
|
139
|
+
if compat_number == 0:
|
|
140
|
+
continue
|
|
141
|
+
else:
|
|
142
|
+
if key == 'unsupported':
|
|
143
|
+
continue
|
|
144
|
+
else:
|
|
145
|
+
count = len(value)
|
|
146
|
+
perc = len(value) / compat_number * 100
|
|
147
|
+
results[key] = {
|
|
148
|
+
'count': count,
|
|
149
|
+
'percent': perc,
|
|
150
|
+
}
|
|
151
|
+
compatibilities['summary']['results'] = results
|
|
152
|
+
|
|
153
|
+
# override top class
|
|
154
|
+
def outbound_inbound_compatibility(self, outbound, inbound, usecase, provisioning, resources=None):
|
|
155
|
+
logging.debug(f'{inbound} {outbound} ')
|
|
156
|
+
|
|
157
|
+
compatibilities = {}
|
|
158
|
+
compatibilities['compatibilities'] = []
|
|
159
|
+
|
|
160
|
+
if not resources:
|
|
161
|
+
resources = self.licomp_resources().keys()
|
|
162
|
+
|
|
163
|
+
for resource_name in resources:
|
|
164
|
+
resource = self.licomp_resources()[resource_name]
|
|
165
|
+
logging.debug(f'-- resource: {resource.name()}')
|
|
166
|
+
|
|
167
|
+
compat = resource.outbound_inbound_compatibility(outbound, inbound, usecase, provisioning=provisioning)
|
|
168
|
+
compatibilities['compatibilities'].append(compat)
|
|
169
|
+
|
|
170
|
+
self.__summarize_compatibility(compatibilities, outbound, inbound, usecase, provisioning, resources)
|
|
171
|
+
self.__add_meta(compatibilities)
|
|
172
|
+
|
|
173
|
+
return compatibilities
|
|
174
|
+
|
|
175
|
+
def simplify(self, lic):
|
|
176
|
+
return str(Licensing([]).parse(lic).simplify())
|
|
177
|
+
|
|
178
|
+
def supported_licenses(self):
|
|
179
|
+
licenses = set()
|
|
180
|
+
for resource in self.licomp_resources().values():
|
|
181
|
+
licenses.update(set(resource.supported_licenses()))
|
|
182
|
+
licenses = list(licenses)
|
|
183
|
+
licenses.sort()
|
|
184
|
+
return licenses
|
|
185
|
+
|
|
186
|
+
def supported_provisionings(self):
|
|
187
|
+
provisionings = set()
|
|
188
|
+
for resource in self.licomp_resources().values():
|
|
189
|
+
provisionings.update(set(resource.supported_provisionings()))
|
|
190
|
+
return list(provisionings)
|
|
191
|
+
|
|
192
|
+
def supported_usecases(self):
|
|
193
|
+
usecases = set()
|
|
194
|
+
for resource in self.licomp_resources().values():
|
|
195
|
+
usecases.update(set(resource.supported_usecases()))
|
|
196
|
+
return list(usecases)
|
|
197
|
+
|
|
198
|
+
def disclaimer(self):
|
|
199
|
+
return disclaimer
|
|
200
|
+
|
|
201
|
+
def version(self, verbose=False):
|
|
202
|
+
return licomp_toolkit_version
|
|
203
|
+
|
|
204
|
+
def versions(self, verbose=False):
|
|
205
|
+
resources = {}
|
|
206
|
+
for resource in self.licomp_resources().values():
|
|
207
|
+
resources[resource.name()] = resource.version()
|
|
208
|
+
return {
|
|
209
|
+
self.name(): self.version(),
|
|
210
|
+
'licomp-resources': resources,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
def name(self):
|
|
214
|
+
return cli_name
|
|
215
|
+
|
|
216
|
+
class LicenseExpressionChecker():
|
|
217
|
+
"""This class can check compatibility between a single outbound
|
|
218
|
+
license (e.g GPL-2.0-only) against an inbound license expression
|
|
219
|
+
(e.g. MIT OR X11)
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
def __init__(self):
|
|
223
|
+
self.le_parser = LicenseExpressionParser()
|
|
224
|
+
self.licomp_toolkit = LicompToolkit()
|
|
225
|
+
|
|
226
|
+
def __compatibility_status(self, compatibility):
|
|
227
|
+
status = compatibility['summary']['results']
|
|
228
|
+
rets = []
|
|
229
|
+
for ret in status:
|
|
230
|
+
if ret == 'nr_valid':
|
|
231
|
+
continue
|
|
232
|
+
elif not ret:
|
|
233
|
+
pass
|
|
234
|
+
elif ret == CompatibilityStatus.compat_status_to_string(CompatibilityStatus.UNSUPPORTED):
|
|
235
|
+
pass
|
|
236
|
+
else:
|
|
237
|
+
rets.append(ret)
|
|
238
|
+
|
|
239
|
+
if len(rets) == 0:
|
|
240
|
+
return CompatibilityStatus.compat_status_to_string(CompatibilityStatus.UNSUPPORTED)
|
|
241
|
+
|
|
242
|
+
if len(rets) == 1:
|
|
243
|
+
return rets[0]
|
|
244
|
+
|
|
245
|
+
return CompatibilityStatus.compat_status_to_string(CompatibilityStatus.MIXED)
|
|
246
|
+
|
|
247
|
+
def check_compatibility(self,
|
|
248
|
+
outbound,
|
|
249
|
+
parsed_expression,
|
|
250
|
+
usecase,
|
|
251
|
+
provisioning,
|
|
252
|
+
resources,
|
|
253
|
+
detailed_report=True):
|
|
254
|
+
|
|
255
|
+
compat_object = {
|
|
256
|
+
'compatibility_check': f'outbound-{self.le_parser.parse_license_expression(outbound)["compatibility_type"]} -> inbound-{parsed_expression["compatibility_type"]}',
|
|
257
|
+
'check_class': __class__.__name__,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if parsed_expression[COMPATIBILITY_TYPE] == 'license':
|
|
261
|
+
compat_object['compatibility_check'] = 'outbound-license -> inbound-license'
|
|
262
|
+
lic = parsed_expression['license']
|
|
263
|
+
compat = self.licomp_toolkit.outbound_inbound_compatibility(outbound,
|
|
264
|
+
lic,
|
|
265
|
+
usecase,
|
|
266
|
+
provisioning,
|
|
267
|
+
resources)
|
|
268
|
+
compat_object['compatibility'] = self.__compatibility_status(compat)
|
|
269
|
+
if detailed_report:
|
|
270
|
+
compat_object['compatibility_details'] = compat
|
|
271
|
+
else:
|
|
272
|
+
compat_object['compatibility_details'] = None
|
|
273
|
+
compat_object['inbound_license'] = lic
|
|
274
|
+
compat_object['outbound_license'] = outbound
|
|
275
|
+
compat_object['compatibility_object'] = {}
|
|
276
|
+
|
|
277
|
+
else:
|
|
278
|
+
operator = parsed_expression['operator']
|
|
279
|
+
operands = parsed_expression['operands']
|
|
280
|
+
compat_object['operator'] = operator
|
|
281
|
+
|
|
282
|
+
compat_object['inbound_license'] = self.le_parser.to_string(parsed_expression)
|
|
283
|
+
compat_object['outbound_license'] = outbound
|
|
284
|
+
compat_object['compatibility_details'] = None
|
|
285
|
+
operands_object = []
|
|
286
|
+
for operand in operands:
|
|
287
|
+
operand_compat = self.check_compatibility(outbound, operand, usecase, provisioning, resources, detailed_report=detailed_report)
|
|
288
|
+
operand_object = {
|
|
289
|
+
'compatibility_object': operand_compat,
|
|
290
|
+
'compatibility': operand_compat['compatibility'],
|
|
291
|
+
}
|
|
292
|
+
operands_object.append(operand_object)
|
|
293
|
+
|
|
294
|
+
compat_object['compatibility'] = self.summarise_compatibilities(operator, operands_object)
|
|
295
|
+
compat_object['operands'] = operands_object
|
|
296
|
+
|
|
297
|
+
return compat_object
|
|
298
|
+
|
|
299
|
+
def __init_summary(self, operands):
|
|
300
|
+
summary = {
|
|
301
|
+
"yes": 0,
|
|
302
|
+
"no": 0,
|
|
303
|
+
"depends": 0,
|
|
304
|
+
"unknown": 0,
|
|
305
|
+
"unsupported": 0,
|
|
306
|
+
"mixed": 0,
|
|
307
|
+
}
|
|
308
|
+
for operand in operands:
|
|
309
|
+
compat = operand['compatibility']
|
|
310
|
+
summary[compat] = summary[compat] + 1
|
|
311
|
+
return summary
|
|
312
|
+
|
|
313
|
+
def __summarise_compatibilities_and(self, operands):
|
|
314
|
+
nr_operands = len(operands)
|
|
315
|
+
summary = self.__init_summary(operands)
|
|
316
|
+
|
|
317
|
+
if summary['no'] != 0:
|
|
318
|
+
return 'no'
|
|
319
|
+
|
|
320
|
+
if summary['yes'] == nr_operands:
|
|
321
|
+
return "yes"
|
|
322
|
+
|
|
323
|
+
return "no"
|
|
324
|
+
|
|
325
|
+
def __summarise_compatibilities_or(self, operands):
|
|
326
|
+
summary = self.__init_summary(operands)
|
|
327
|
+
|
|
328
|
+
if summary['yes'] != 0:
|
|
329
|
+
return 'yes'
|
|
330
|
+
|
|
331
|
+
return "no"
|
|
332
|
+
|
|
333
|
+
def summarise_compatibilities(self, operator, operands):
|
|
334
|
+
return {
|
|
335
|
+
AND: self.__summarise_compatibilities_and,
|
|
336
|
+
OR: self.__summarise_compatibilities_or,
|
|
337
|
+
}[operator](operands)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class ExpressionExpressionChecker():
|
|
341
|
+
"""
|
|
342
|
+
This class can check, for compatibility;
|
|
343
|
+
* inbound license expression (e.g. MIT OR Apache-2.0)
|
|
344
|
+
* against outbound license expression (e.g. GPL-2.0-only OR BSD-2-Clause)
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
def __init__(self):
|
|
348
|
+
self.le_checker = LicenseExpressionChecker()
|
|
349
|
+
self.le_parser = LicenseExpressionParser()
|
|
350
|
+
self.licomp_toolkit = LicompToolkit()
|
|
351
|
+
|
|
352
|
+
def __parsed_expression_to_name(self, parsed_expression):
|
|
353
|
+
return parsed_expression[parsed_expression[COMPATIBILITY_TYPE]]
|
|
354
|
+
|
|
355
|
+
def meta_information(self):
|
|
356
|
+
return {
|
|
357
|
+
'tool': module_name,
|
|
358
|
+
'file': 'verification',
|
|
359
|
+
'file_version': licomp_toolkit_file_version,
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
def check_compatibility(self, outbound, inbound, usecase, provisioning, resources=None, detailed_report=True):
|
|
363
|
+
|
|
364
|
+
# Check usecase
|
|
365
|
+
try:
|
|
366
|
+
usecase_obj = UseCase.string_to_usecase(usecase)
|
|
367
|
+
except KeyError:
|
|
368
|
+
raise LicompException(f'Usecase {usecase} not supported.', ReturnCodes.LICOMP_UNSUPPORTED_USECASE)
|
|
369
|
+
|
|
370
|
+
# Check provisioning
|
|
371
|
+
try:
|
|
372
|
+
provisioning_obj = Provisioning.string_to_provisioning(provisioning)
|
|
373
|
+
except KeyError:
|
|
374
|
+
raise LicompException(f'Provisioning {provisioning} not supported.', ReturnCodes.LICOMP_UNSUPPORTED_PROVISIONING)
|
|
375
|
+
|
|
376
|
+
licomp_resources = list(self.licomp_toolkit.licomp_standard_resources().keys())
|
|
377
|
+
if not resources:
|
|
378
|
+
resources = licomp_resources
|
|
379
|
+
else:
|
|
380
|
+
resources = resources
|
|
381
|
+
|
|
382
|
+
unavailable_resources = []
|
|
383
|
+
|
|
384
|
+
for resource in resources:
|
|
385
|
+
resource_object = self.licomp_toolkit.licomp_resources()[resource]
|
|
386
|
+
unavailable_reasons = []
|
|
387
|
+
|
|
388
|
+
# is usecase supported by resource
|
|
389
|
+
if not resource_object.usecase_supported(UseCase.string_to_usecase(usecase)):
|
|
390
|
+
unavailable_reasons.append(f'Usecase "{usecase}" not supported')
|
|
391
|
+
|
|
392
|
+
# is prov case supported by resource
|
|
393
|
+
if not resource_object.provisioning_supported(Provisioning.string_to_provisioning(provisioning)):
|
|
394
|
+
unavailable_reasons.append(f'Provisioning case "{provisioning}" not supported')
|
|
395
|
+
|
|
396
|
+
if unavailable_reasons:
|
|
397
|
+
unavailable_resources.append({
|
|
398
|
+
"resource": resource,
|
|
399
|
+
'reasons': ", ".join(unavailable_reasons),
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
unavailable_resource_keys = [resource['resource'] for resource in unavailable_resources]
|
|
403
|
+
available_resources = [resource for resource in resources if resource not in unavailable_resource_keys]
|
|
404
|
+
|
|
405
|
+
inbound_parsed = self.le_parser.parse_license_expression(inbound)
|
|
406
|
+
outbound_parsed = self.le_parser.parse_license_expression(outbound)
|
|
407
|
+
compatibility_object = self.__check_compatibility(outbound_parsed,
|
|
408
|
+
inbound_parsed,
|
|
409
|
+
usecase_obj,
|
|
410
|
+
provisioning_obj,
|
|
411
|
+
resources,
|
|
412
|
+
detailed_report)
|
|
413
|
+
return {
|
|
414
|
+
'meta': self.meta_information(),
|
|
415
|
+
'inbound': str(inbound),
|
|
416
|
+
'outbound': str(outbound),
|
|
417
|
+
'usecase': usecase,
|
|
418
|
+
'resources': resources,
|
|
419
|
+
'provisioning': provisioning,
|
|
420
|
+
'compatibility': compatibility_object['compatibility'],
|
|
421
|
+
'compatibility_report': compatibility_object,
|
|
422
|
+
'unavailable_resources': unavailable_resources,
|
|
423
|
+
'available_resources': available_resources,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
def __check_compatibility(self,
|
|
427
|
+
outbound_parsed,
|
|
428
|
+
inbound_parsed,
|
|
429
|
+
usecase,
|
|
430
|
+
provisioning,
|
|
431
|
+
resources,
|
|
432
|
+
detailed_report=True):
|
|
433
|
+
|
|
434
|
+
outbound_type = outbound_parsed[COMPATIBILITY_TYPE]
|
|
435
|
+
compat_object = {
|
|
436
|
+
'inbound_license': self.le_parser.to_string(inbound_parsed),
|
|
437
|
+
'outbound_license': self.le_parser.to_string(outbound_parsed),
|
|
438
|
+
'check_class': __class__.__name__,
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if outbound_type == 'license':
|
|
442
|
+
if False:
|
|
443
|
+
compat_object['compatibility_check'] = f'outbound-license -> inbound-{inbound_parsed["compatibility_type"]} HERE?'
|
|
444
|
+
outbound_parsed_license = outbound_parsed['license']
|
|
445
|
+
|
|
446
|
+
compat = self.le_checker.check_compatibility(outbound_parsed_license,
|
|
447
|
+
inbound_parsed,
|
|
448
|
+
usecase,
|
|
449
|
+
provisioning,
|
|
450
|
+
resources,
|
|
451
|
+
detailed_report)
|
|
452
|
+
compat_object['compatibility'] = compat['compatibility']
|
|
453
|
+
compat_object['compatibility_object'] = compat
|
|
454
|
+
compat_object['compatibility_details'] = None
|
|
455
|
+
|
|
456
|
+
outbound_parsed_license = outbound_parsed['license']
|
|
457
|
+
compat = self.le_checker.check_compatibility(outbound_parsed_license,
|
|
458
|
+
inbound_parsed,
|
|
459
|
+
usecase,
|
|
460
|
+
provisioning,
|
|
461
|
+
resources,
|
|
462
|
+
detailed_report)
|
|
463
|
+
compat_object = compat
|
|
464
|
+
|
|
465
|
+
elif outbound_type == 'expression':
|
|
466
|
+
compat_object['compatibility_details'] = None
|
|
467
|
+
compat_object['compatibility_check'] = f'outbound-expression -> inbound-{inbound_parsed["compatibility_type"]}'
|
|
468
|
+
operator = outbound_parsed['operator']
|
|
469
|
+
operands = outbound_parsed['operands']
|
|
470
|
+
|
|
471
|
+
compat_object['operator'] = operator
|
|
472
|
+
|
|
473
|
+
operands_object = []
|
|
474
|
+
for operand in operands:
|
|
475
|
+
# Check if:
|
|
476
|
+
# operand from outbound license
|
|
477
|
+
# is compatible with
|
|
478
|
+
# inbound license
|
|
479
|
+
operand_compat = self.__check_compatibility(operand,
|
|
480
|
+
inbound_parsed,
|
|
481
|
+
usecase,
|
|
482
|
+
provisioning,
|
|
483
|
+
resources,
|
|
484
|
+
detailed_report)
|
|
485
|
+
operand_object = {
|
|
486
|
+
'compatibility_object': operand_compat,
|
|
487
|
+
'compatibility': operand_compat['compatibility'],
|
|
488
|
+
}
|
|
489
|
+
operands_object.append(operand_object)
|
|
490
|
+
|
|
491
|
+
compat_object['compatibility'] = self.le_checker.summarise_compatibilities(operator, operands_object)
|
|
492
|
+
compat_object['operands'] = operands_object
|
|
493
|
+
|
|
494
|
+
return compat_object
|
|
495
|
+
|
|
496
|
+
def simplify(self, compat):
|
|
497
|
+
return {
|
|
498
|
+
'outbound': compat['outbound'],
|
|
499
|
+
'inbound': compat['inbound'],
|
|
500
|
+
'usecase': compat['usecase'],
|
|
501
|
+
'provisioning': compat['provisioning'],
|
|
502
|
+
'compatibility': compat['compatibility'],
|
|
503
|
+
'resources': compat['available_resources'],
|
|
504
|
+
}
|