From 0f819e49216e83e9b47904514404d885a167aa4b Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Thu, 29 May 2025 10:19:35 +0200 Subject: [PATCH 01/31] add test of expr->expr checks --- tests/python/test_expr_expr.py | 106 +++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/python/test_expr_expr.py diff --git a/tests/python/test_expr_expr.py b/tests/python/test_expr_expr.py new file mode 100644 index 0000000..1b74239 --- /dev/null +++ b/tests/python/test_expr_expr.py @@ -0,0 +1,106 @@ +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import json +import pytest +import logging +import sys + +from licomp.interface import Licomp +from licomp.interface import Provisioning +from licomp.interface import UseCase +from licomp.interface import CompatibilityStatus + +from licomp_toolkit.lic_expr import ExpressionExpressionChecker + + +eec = ExpressionExpressionChecker() + +def _compat_status(report): + return report['compatibility_report']['compatibility'] + +def _compat_type(report): + return report['compatibility_report']['compatibility_type'] + +# +# license compat with license +# + +# GPL-2.0-only -> MIT are compatible +def test_lic_lic_compat(): + compat_report = eec.check_compatibility('GPL-2.0-only', 'MIT', + usecase=UseCase.LIBRARY, + provisioning=Provisioning.BIN_DIST) + assert _compat_status(compat_report) == 'yes' + assert _compat_type(compat_report) == 'license' + +# MIT -> GPL-2.0-only -> are NOT compatible +def test_lic_lic_incompat(): + compat_report = eec.check_compatibility('MIT', 'GPL-2.0-only', + usecase=UseCase.LIBRARY, + provisioning=Provisioning.BIN_DIST) + assert _compat_status(compat_report) == 'no' + assert _compat_type(compat_report) == 'license' + +# +# license compat with expression +# + +# GPL-2.0-only -> MIT OR Apache-2.0 are compatible +def test_lic_expr_compat(): + compat_report = eec.check_compatibility('GPL-2.0-only', 'MIT OR Apache-2.0', + usecase=UseCase.LIBRARY, + provisioning=Provisioning.BIN_DIST) + assert _compat_status(compat_report) == 'yes' + assert _compat_type(compat_report) == 'license' + +# GPL-2.0-only -> MIT AND Apache-2.0 are NOT compatible +def test_lic_expr_incompat(): + compat_report = eec.check_compatibility('GPL-2.0-only', 'MIT AND Apache-2.0', + usecase=UseCase.LIBRARY, + provisioning=Provisioning.BIN_DIST) + assert _compat_status(compat_report) == 'no' + assert _compat_type(compat_report) == 'license' + +# +# expression compat with license +# +# GPL-2.0-only AND BSD-3-Clause -> MIT are compatible +def test_expr_lic_compat(): + compat_report = eec.check_compatibility('GPL-2.0-only AND BSD-3-Clause', 'MIT', + usecase=UseCase.LIBRARY, + provisioning=Provisioning.BIN_DIST) + assert _compat_status(compat_report) == 'yes' + assert _compat_type(compat_report) == 'operator' + +# GPL-2.0-only AND BSD-3-Clause -> Apache-2.0 are NOT compatible +def test_expr_lic_incompat(): + compat_report = eec.check_compatibility('GPL-2.0-only AND BSD-3-Clause', 'Apache-2.0', + usecase=UseCase.LIBRARY, + provisioning=Provisioning.BIN_DIST) + assert _compat_status(compat_report) == 'no' + assert _compat_type(compat_report) == 'operator' + +# +# expression compat with expression +# + +# GPL-2.0-only AND BSD-3-Clause -> MIT AND X11 are compatible +def test_expr_expr_compat(): + compat_report = eec.check_compatibility('GPL-2.0-only AND BSD-3-Clause', 'MIT AND X11', + usecase=UseCase.LIBRARY, + provisioning=Provisioning.BIN_DIST) + assert _compat_status(compat_report) == 'yes' + assert _compat_type(compat_report) == 'operator' + +# GPL-2.0-only AND BSD-3-Clause -> Apache-2.0 AND ISC are NOT compatible +def test_expr_expr_incompat(): + compat_report = eec.check_compatibility('GPL-2.0-only AND BSD-3-Clause', 'Apache-2.0 AND ISC', + usecase=UseCase.LIBRARY, + provisioning=Provisioning.BIN_DIST) + assert _compat_status(compat_report) == 'no' + assert _compat_type(compat_report) == 'operator' + + + From 609fd75038b7698c587fd7796554390a09db45b5 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Thu, 29 May 2025 10:20:05 +0200 Subject: [PATCH 02/31] make usable outside dir --- devel/licomp-toolkit | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/devel/licomp-toolkit b/devel/licomp-toolkit index da20856..a8fd40f 100755 --- a/devel/licomp-toolkit +++ b/devel/licomp-toolkit @@ -4,14 +4,16 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +SCRIPT_DIR=$(dirname ${BASH_SOURCE[0]})/../ + if [ "$1" = "--local" ] then shift - EXTRA_PYTHONPATH=:../licomp:../licomp-osadl:../licomp-reclicense:../licomp-proprietary:../licomp-hermione:../licomp-dwheeler + EXTRA_PYTHONPATH=:${SCRIPT_DIR}/../licomp:${SCRIPT_DIR}/../licomp-osadl:${SCRIPT_DIR}/../licomp-reclicense:${SCRIPT_DIR}/../licomp-proprietary:${SCRIPT_DIR}/../licomp-hermione:${SCRIPT_DIR}/../licomp-dwheeler fi if [ "$1" = "" ] then ARGS="verify -il MIT -ol MIT" fi -PYTHONPATH=${EXTRA_PYTHONPATH}:${PYTHONPATH} ./licomp_toolkit/__main__.py $* $ARGS +PYTHONPATH=${EXTRA_PYTHONPATH}:${PYTHONPATH} ${SCRIPT_DIR}/licomp_toolkit/__main__.py $* $ARGS From 251fb756afb98ff35b46558d3a8f316dad09684e Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Thu, 29 May 2025 10:23:00 +0200 Subject: [PATCH 03/31] fix formatters for licenses, usecases, resources --- licomp_toolkit/__main__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/licomp_toolkit/__main__.py b/licomp_toolkit/__main__.py index ffe243c..da2d83f 100755 --- a/licomp_toolkit/__main__.py +++ b/licomp_toolkit/__main__.py @@ -50,7 +50,8 @@ def verify(self, args): def supported_licenses(self, args): licenses = self.licomp_toolkit.supported_licenses() - return licenses, ReturnCodes.LICOMP_OK.value, None + formatter = LicompToolkitFormatter.formatter(args.output_format) + return formatter.format_licomp_resources(licenses), ReturnCodes.LICOMP_OK.value, None def supported_usecases(self, args): usecases = self.licomp_toolkit.supported_usecases() @@ -65,7 +66,8 @@ def supported_provisionings(self, args): return provisioning_names, ReturnCodes.LICOMP_OK.value, None def supported_resources(self, args): - return [f'{x.name()}:{x.version()}' for x in self.licomp_toolkit.licomp_resources().values()], ReturnCodes.LICOMP_OK, False + formatter = LicompToolkitFormatter.formatter(args.output_format) + return formatter.format_licomp_resources([f'{x.name()}:{x.version()}' for x in self.licomp_toolkit.licomp_resources().values()]), ReturnCodes.LICOMP_OK.value, False def supports_license(self, args): lic = args.license From 7c956cff966a11b75aedc43ce6d8bfb113509a10 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Thu, 29 May 2025 10:24:26 +0200 Subject: [PATCH 04/31] create object, instead of manipulating input objects --- licomp_toolkit/lic_expr.py | 195 +++++++++++++++++++++---------------- 1 file changed, 113 insertions(+), 82 deletions(-) diff --git a/licomp_toolkit/lic_expr.py b/licomp_toolkit/lic_expr.py index fe37958..9bb21df 100644 --- a/licomp_toolkit/lic_expr.py +++ b/licomp_toolkit/lic_expr.py @@ -4,15 +4,21 @@ import json -# import logging +import logging from license_expression import get_spdx_licensing from licomp_toolkit.toolkit import LicompToolkit +from licomp.interface import LicompException + # from licomp.interface import UseCase # from licomp.interface import Provisioning AND = "AND" OR = "OR" +COMPATIBILITY_TYPE = 'compatibility_type' +COMPATIBILITY_OUTBOUND_LICENSE = 'outbound_license' +COMPATIBILITY_INBOUND_LICENSE = 'inbound_license' + class LicenseExpressionParser(): def __init__(self): @@ -23,9 +29,9 @@ def __init__(self): self.LICENSE_WITH_SYMBOL = "LicenseWithExceptionSymbol" def parse_license_expression(self, expression): - print(" ---------------------- " + expression + "-------------------------") + logging.debug(" ---------------------- " + expression + "-------------------------") p = self.__parse_expression(self.licensing.parse(expression).pretty().replace('\n', ' ')) - print(" ---------------------- " + expression + "------------------------->> \n" + json.dumps(p, indent=4)) + logging.debug(" ---------------------- " + expression + "------------------------->> \n" + json.dumps(p, indent=4)) return p def __is_license_with_exception(self, expression): @@ -54,9 +60,7 @@ def __get_operands_string(self, expression): # nr characters until closing (operator) parenthesis left_parens = 1 operand_size = 1 - print("identify idx: " + expression[op_size:]) for c in expression[op_size:]: - # print("at " + c + ": " + str(operand_size)) operand_size += 1 if c == '(': left_parens += 1 @@ -68,9 +72,6 @@ def __get_operands_string(self, expression): rest = expression[op_size:operand_size + 1] remains = expression[operand_size - 1] - print("expression : " + expression) - print("operands : " + rest) - print("remains : " + remains) return rest, remains def is_close(self, expression): @@ -78,58 +79,46 @@ def is_close(self, expression): def __cleanup_license(self, operand): stripped_operand = operand.strip() - print("OPERself.AND: " + operand) if self.__is_license_with_exception(operand): trimmed_operand = stripped_operand.replace(f"{self.LICENSE_WITH_SYMBOL}('", '', 1) else: trimmed_operand = stripped_operand.replace(f"{self.LICENSE_SYMBOL}('", '', 1) - # print("TRIMMED: " + trimmed_operand) closing_paren_index = trimmed_operand.find(")") - # print(" CLEANEDUP to: >" + trimmed_operand + "<") - # print(" CLEANEDUP idx: " + str(closing_paren_index)) op = trimmed_operand[:closing_paren_index - 1] remains = trimmed_operand[closing_paren_index + 1:].strip() - print(" CLEANEDUP rem:>" + remains) if remains.startswith(","): remains = remains[1:] - # print(" CLEANEDUP op: " + op) - # print(" CLEANEDUP rem:>" + remains + "<") return op, remains.strip() def __parse_expression(self, expression): - print("pe: " + expression) + logging.debug("__parse_expression:" + expression) if self.__is_operator(expression): operator = self.__get_operator(expression) operands = [] - print("GETTING OPERself.ANDS FROM: " + expression) ops, remains = self.__get_operands_string(expression) while ops != "": - print("PARSE: " + ops) - print("PARSE: " + ops.strip()) if self.__is_license(ops.strip(), with_exception=True): operand, rem = self.__cleanup_license(ops) operands.append({ - "type": "license", - "license": operand, + COMPATIBILITY_TYPE: "license", + 'license': operand, }) ops = rem elif self.__is_operator(ops.strip()): operand = self.__parse_expression(ops.strip()) operands.append(operand) - print("OP ADD: " + str(operand)) ops = "" else: print("uh oh ... " + str(ops)) import sys sys.exit(1) - print("OP RET: " + str(operand)) return { - "type": "operator", + COMPATIBILITY_TYPE: "operator", "operator": operator, "operands": operands, } @@ -139,22 +128,31 @@ def __parse_expression(self, expression): cleaned_up, rem = self.__cleanup_license(expression.strip()) return { - "type": "license", - "license": cleaned_up, + COMPATIBILITY_TYPE: "license", + 'license': cleaned_up, + } elif self.__is_close(expression): - print("op <--- CLOSE") return "" raise Exception("Bottom reached") + def to_string(self, parsed_license): + license_type = parsed_license['compatibility_type'] + if license_type == 'license': + return parsed_license['license'] + + operator = parsed_license['operator'] + license_expression = [] + for operand in parsed_license['operands']: + license_expression.append(f' ( {self.to_string(operand)} ) ') + return str(self.licensing.parse(operator.join(license_expression))) + class LicenseExpressionChecker(): def outbound_inbound_compatibility(self, outbound, lic): licomp = LicompToolkit() - print("out: " + str(outbound)) - print("in: " + str(lic)) return licomp.outbound_inbound_compatibility(outbound, lic, usecase="library", @@ -170,47 +168,55 @@ def __compatibility_status(self, compatibility): continue rets.append(ret) - print("________________________________________________status: " + str(status)) - print("________________________________________________status: " + str(rets)) - - # status: {'nr_valid': '5', 'yes': {'count': 5, 'percent': 100.0}} - if len(rets) == 1: - print("RETURN: " + str(rets[0])) return rets[0] - print("RETURN: " + str(rets)) - print("RETURN: " + str(compatibility)) return "yes" def check_compatibility(self, outbound, parsed_expression, detailed_report=False): - if parsed_expression['type'] == 'license': - print(f' license: {parsed_expression}') + compat_object = { + #COMPATIBILITY_OUTBOUND_LICENSE: outbound, + #COMPATIBILITY_INBOUND_LICENSE: parsed_expression, + COMPATIBILITY_TYPE: parsed_expression[COMPATIBILITY_TYPE], + 'compatiblity_check': 'outbound-operator -> inbound-license' + } + + if parsed_expression[COMPATIBILITY_TYPE] == 'license': + compat_object['compatiblity_check'] = 'outbound-license -> inbound-license' lic = parsed_expression['license'] compat = self.outbound_inbound_compatibility(outbound, lic) - parsed_expression['compatibility'] = self.__compatibility_status(compat) + compat_object['compatibility'] = self.__compatibility_status(compat) + #parsed_expression['compatibility'] = self.__compatibility_status(compat) if detailed_report: - parsed_expression['compatibility_details'] = compat - parsed_expression['outbound'] = outbound - print("Added compat: " + str(compat)) + compat_object['compatibility_details'] = compat - return parsed_expression + compat_object['inbound_license'] = lic + compat_object['outbound_license'] = outbound + else: operator = parsed_expression['operator'] operands = parsed_expression['operands'] + compat_object['compatibility_object'] = { + 'operator': operator, + 'operands': [] + } + operands_object = [] for operand in operands: - print(f'hi yall {operator}: {operand}') - self.check_compatibility(outbound, operand, detailed_report=detailed_report) - # compat = summarise_compatibilities(operator, operand) - # operand['compatibility'] = "compat" - # print("Added compat: " + str(compat)) - operand['outbound'] = outbound - - parsed_expression["outbound"] = outbound - parsed_expression["compatibility"] = self.summarise_compatibilities(operator, operands) - return parsed_expression - + operand_compat = self.check_compatibility(outbound, operand, detailed_report=detailed_report) + operand_object = { + #'operand': operand, + 'compatibility_object': operand_compat, + 'compatibility': operand_compat['compatibility'] + } + operands_object.append(operand_object) + + compat_object['compatibility'] = self.summarise_compatibilities(operator, operands_object) + compat_object['compatibility_object']['operands'] = operands_object +# compat_object['HESA---------------'] = self.summarise_compatibilities(operator, operands_object) + + return compat_object + def __init_summary(self, operands): summary = { "yes": 0, @@ -219,17 +225,13 @@ def __init_summary(self, operands): "unknown": 0, } for operand in operands: - print("_init_summary: " + str(operand)) compat = operand['compatibility'] summary[compat] = summary[compat] + 1 return summary def __summarise_compatibilities_and(self, operands): nr_operands = len(operands) - for operand in operands: - print("OP: " + str(operand)) summary = self.__init_summary(operands) - print("len: " + str(nr_operands)) if summary['no'] != 0: return 'no' @@ -261,28 +263,43 @@ def __init__(self): self.le_parser = LicenseExpressionParser() def __parsed_expression_to_name(self, parsed_expression): - return parsed_expression[parsed_expression['type']] + return parsed_expression[parsed_expression[COMPATIBILITY_TYPE]] + + def check_compatibility(self, outbound, inbound, usecase, provisioning, detailed_report=False): - def check_compatibility(self, outbound, inbound, detailed_report=False): inbound_parsed = self.le_parser.parse_license_expression(inbound) outbound_parsed = self.le_parser.parse_license_expression(outbound) compatibility_report = self.__check_compatibility(outbound_parsed, inbound_parsed, + # TODO: add context detailed_report) return { 'inbound': inbound, 'outbound': outbound, - 'compatibility_report': compatibility_report + 'usecase': UseCase.usecase_to_string(usecase), + 'provisioning': Provisioning.provisioning_to_string(provisioning), + 'compatibility': compatibility_report['compatibility'], + 'compatibility_type': compatibility_report['compatibility_type'], + 'compatibility_check': f'outbound-{compatibility_report["compatibility_type"]} -> inbound-{inbound_parsed["compatibility_type"]}', + 'compatibility_report': compatibility_report, } def __check_compatibility(self, outbound_parsed, inbound_parsed, detailed_report=False): - outbound_type = outbound_parsed['type'] + outbound_type = outbound_parsed[COMPATIBILITY_TYPE] + compat_object = { + #COMPATIBILITY_OUTBOUND_LICENSE: outbound_parsed, + #COMPATIBILITY_INBOUND_LICENSE: inbound_parsed, + COMPATIBILITY_TYPE: outbound_type, + 'inbound_license': self.le_parser.to_string(inbound_parsed), + 'outbound_license': self.le_parser.to_string(outbound_parsed) + } + if outbound_type == 'license': - print(f' license: {outbound_parsed}') + compat_object['compatiblity_check']: f'outbound-license -> inbound->{inbound_parsed["compatibility_type"]}' outbound_parsed_license = outbound_parsed['license'] # Check if: # outbound license @@ -291,53 +308,67 @@ def __check_compatibility(self, outbound_parsed, inbound_parsed, detailed_report compat = self.le_checker.check_compatibility(outbound_parsed_license, inbound_parsed, detailed_report) + compat_object['compatibility'] = compat['compatibility'] + compat_object['compatibility_details'] = compat - outbound_parsed['compatibility'] = compat['compatibility'] # TODO: bring back details - # outbound_parsed['compatibility_details'] = compat + # compat_object['compatibility_details'] = compat - return compat - - if outbound_type == 'operator': + elif outbound_type == 'operator': + compat_object['compatiblity_check']: f'outbound-operator -> inbound->{inbound_parsed["compatibility_type"]}' outbound_parsed_operator = outbound_parsed['operator'] - print(f' operator: {outbound_parsed_operator}') operator = outbound_parsed['operator'] operands = outbound_parsed['operands'] + compat_object['compatibility_object'] = { + 'operator': operator, + 'operands': [] + } + + operands_object = [] for operand in operands: # Check if: # operand from outbound license # is compatible with # inbound license - inbound_compat = self.__check_compatibility(operand, + operand_compat = self.__check_compatibility(operand, inbound_parsed, detailed_report) + operand_object = { + #'operand': operand, + 'compatibility_object': operand_compat, + 'compatibility': operand_compat['compatibility'] + } # operand['compatibility_details'] = inbound_compat - operand['inbound_compatibility'] = inbound_parsed - operand['compatibility'] = inbound_compat['compatibility'] + #operand['compatibility_object'] = inbound_parsed + #operand['outbound_license_SANDKLEF'] = "whattt" + #operand['compatibility'] = inbound_compat['compatibility'] + operands_object.append(operand_object) - outbound_parsed['compatibility'] = self.le_checker.summarise_compatibilities(operator, operands) + compat_object['compatibility'] = self.le_checker.summarise_compatibilities(operator, operands_object) + compat_object['compatibility_object']['operands'] = operands_object # TODO: bring back details - return outbound_parsed - - return " WOOPS" + return compat_object # parser = LicenseExpressionParser() expr_checker = ExpressionExpressionChecker() -inbound = "MIT OR Apache-2.0" +inbound = "Apache-2.0" +outbound = "GPL-2.0-only" +inbound = "MIT OR Apache-2.0 AND X11" outbound = "GPL-2.0-only AND BSD-2-Clause" +from licomp.interface import UseCase +from licomp.interface import Provisioning checked = expr_checker.check_compatibility(outbound, inbound, + usecase=UseCase.LIBRARY, + provisioning=Provisioning.BIN_DIST, detailed_report=False) -print("------------------------------------------") -print() -print() -print() +#print(str(checked)) print(json.dumps(checked, indent=4)) From bdc32a4132dc4c0d7b1153c4237e92d9cc248163 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Thu, 29 May 2025 10:25:01 +0200 Subject: [PATCH 05/31] add formatters for licenses --- licomp_toolkit/toolkit.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/licomp_toolkit/toolkit.py b/licomp_toolkit/toolkit.py index e9f6c44..cbcd842 100644 --- a/licomp_toolkit/toolkit.py +++ b/licomp_toolkit/toolkit.py @@ -38,6 +38,9 @@ def format_compatibilities(self, compat): def format_licomp_resources(self, licomp_resources): return None + def format_licomp_licenses(self, licomp_licenses): + return None + def format_licomp_versions(self, licomp_versions): return None @@ -49,7 +52,10 @@ def format_compatibilities(self, compat): def format_licomp_resources(self, licomp_resources): return json.dumps(licomp_resources, indent=4) - def format_licomp_versions(self, licomp_versions): + def format_licomp_licenses(self, licomp_resources): + return json.dumps(licomp_licenses, indent=4) + + def format_licomp_versions(self, liczomp_versions): return json.dumps(licomp_versions, indent=4) class TextLicompToolkitFormatter(): @@ -57,6 +63,9 @@ class TextLicompToolkitFormatter(): def format_licomp_resources(self, licomp_resources): return "\n".join(licomp_resources) + def format_licomp_licenses(self, licomp_licenses): + return "\n".join(licomp_licenses) + def format_compatibilities(self, compat): summary = compat['summary'] output = [] From f343d683dd9fc70818c9455a3c7818e44af535a0 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Thu, 29 May 2025 18:05:37 +0200 Subject: [PATCH 06/31] add schema for compatibility reply --- licomp_toolkit/data/reply_schema.json | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 licomp_toolkit/data/reply_schema.json diff --git a/licomp_toolkit/data/reply_schema.json b/licomp_toolkit/data/reply_schema.json new file mode 100644 index 0000000..bec08c2 --- /dev/null +++ b/licomp_toolkit/data/reply_schema.json @@ -0,0 +1,71 @@ +{ + "$schema" : "http://json-schema.org/draft-07/schema#", + "$id" : "http://spdx.org/rdf/terms", + "title" : "SPDX 2.2", + "type" : "object", + "properties" : { + "compatibility_report": { + "type" : "object", + "$ref": "#/$defs/compatibility_object" + }, + "outbound" : {"type" : "string", "description" : "" }, + "inbound" : { "type" : "string", "description" : "" }, + "usecase" : { "type" : "string", "description" : "" }, + "provisioning" : { "type" : "string", "description" : "" }, + "modification" : { "type" : "string", "description" : "" }, + "compatibility" : { "type" : "string", "description" : "" } + }, + "required" : [ "compatibility_report", "compatibility", "outbound", "inbound" , "usecase" ], + "additionalProperties" : false, + "$defs": { + "compatibility_object": { + "type" : "object", + "oneOf": [ + { + "properties": { + "compatibility": { "type": "string" }, + "compatibility_type": { "enum": [ "license" ] }, + "compatibility_check": { "type": "string" }, + "inbound_license": { "type": "string" }, + "outbound_license": { "type": "string" }, + "compatibility_object": { + "type" : "object", + "anyOf": [ + { + "$ref": "#/$defs/compatibility_object" + }, + { + "properties": {} + } + ] + } + }, + "required": [ "compatibility", "compatibility_type", "compatibility_check", "inbound_license" , "outbound_license", "compatibility_object"], + "additionalProperties" : false + }, + { + "properties": { + "compatibility": { "type": "string" }, + "compatibility_type": { "enum": [ "expression" ] }, + "compatibility_check": { "type": "string" }, + "inbound_license": { "type": "string" }, + "outbound_license": { "type": "string" }, + "operator": { "enum": [ "AND", "OR" ] }, + "operands": { + "type": "array", + "items": { + "compatibility_object": { + "type" : "object", + "$ref": "#/$defs/compatibility_object" + }, + "compatibility": { "type": "string" } + } + } + }, + "required": [ "compatibility", "compatibility_type", "compatibility_check", "inbound_license" , "outbound_license" , "operator", "operands" ], + "additionalProperties" : false + } + ] + } + } +} From 7ec84e9f3c67624f343a1818b1047006e5d5e4c1 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Fri, 30 May 2025 13:15:47 +0200 Subject: [PATCH 07/31] add validator tests --- tests/python/test_validator.py | 82 ++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tests/python/test_validator.py diff --git a/tests/python/test_validator.py b/tests/python/test_validator.py new file mode 100644 index 0000000..c9d43ae --- /dev/null +++ b/tests/python/test_validator.py @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import json +import pytest +import logging +import sys + +from licomp.interface import Licomp +from licomp.interface import Provisioning +from licomp.interface import UseCase +from licomp.interface import CompatibilityStatus + +from licomp_toolkit.toolkit import ExpressionExpressionChecker +from licomp_toolkit.schema_checker import LicompToolkitSchemaChecker + +eec = ExpressionExpressionChecker() +checker = LicompToolkitSchemaChecker() + +def test_lic_lic(): + compat_report = eec.check_compatibility("MIT", "BSD-3-Clause", + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST), + detailed_report=True) + + ret = checker.validate(compat_report) + print("Validating simple expression (non deep): " + str(ret)) + + ret = checker.validate(compat_report, deep=True) + print("Validating simple expression (deep): " + str(ret)) + +def test_lic_expr(): + compat_report = eec.check_compatibility("MIT", "BSD-3-Clause OR BSD-2-Clause", + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST), + detailed_report=True) + + ret = checker.validate(compat_report) + print("Validating license->expression (non deep): " + str(ret)) + + ret = checker.validate(compat_report, deep=True) + print("Validating license->expression (deep): " + str(ret)) + + +def test_expr_lic(): + compat_report = eec.check_compatibility("MIT OR X11", "BSD-3-Clause", + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST), + detailed_report=True) + + ret = checker.validate(compat_report) + print("Validating expression->license (non deep): " + str(ret)) + + ret = checker.validate(compat_report, deep=True) + print("Validating expression->license (deep): " + str(ret)) + + +def test_expr_expr(): + compat_report = eec.check_compatibility("MIT OR X11", "BSD-3-Clause OR BSD-2-Clause", + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST), + detailed_report=True) + + ret = checker.validate(compat_report) + print("Validating expression->expression (non deep): " + str(ret)) + + ret = checker.validate(compat_report, deep=True) + print("Validating expression->expression (deep): " + str(ret)) + +def test_expr_expr_many(): + compat_report = eec.check_compatibility("MIT OR X11 OR ISC AND MIT-0", "BSD-3-Clause OR BSD-2-Clause OR 0BSD AND Apache-2.0 AND 0BSD", + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST), + detailed_report=True) + + ret = checker.validate(compat_report) + print("Validating expression->expression (non deep): " + str(ret)) + + ret = checker.validate(compat_report, deep=True) + print("Validating expression->expression (deep): " + str(ret)) + From df80ad085dcb587897cb9c71e9caef13c27b5a5e Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sat, 31 May 2025 13:59:52 +0200 Subject: [PATCH 08/31] add schema --- .reuse/dep5 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.reuse/dep5 b/.reuse/dep5 index b110a4c..ec76bbb 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -1,8 +1,11 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: licomp_osadl +Upstream-Name: licomp_toolkit Upstream-Contact: Henrik Sandklef -Source: https://github.com/hesa/licomp_osadl +Source: https://github.com/hesa/licomp_toolkit +Files: licomp_toolkit/data/reply_schema.json +Copyright: Henrik Sandklef +License: GPL-3.0-or-later Files: .github/** Copyright: Henrik Sandklef From 81238ef1f756790b4fa1fe2c4673e09c6ea47457 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sat, 31 May 2025 14:03:30 +0200 Subject: [PATCH 09/31] parse expressions rather than licenses --- licomp_toolkit/expr_parser.py | 142 ++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 licomp_toolkit/expr_parser.py diff --git a/licomp_toolkit/expr_parser.py b/licomp_toolkit/expr_parser.py new file mode 100644 index 0000000..df09af5 --- /dev/null +++ b/licomp_toolkit/expr_parser.py @@ -0,0 +1,142 @@ +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import logging + +from license_expression import get_spdx_licensing +from licomp.interface import LicompException +from licomp.return_codes import ReturnCodes + +AND = "AND" +OR = "OR" + +COMPATIBILITY_TYPE = 'compatibility_type' +COMPATIBILITY_OUTBOUND_LICENSE = 'outbound_license' +COMPATIBILITY_INBOUND_LICENSE = 'inbound_license' + +class LicenseExpressionParser(): + + def __init__(self): + self.licensing = get_spdx_licensing() + + self.CLOSE_PARENTHESIS = ")" + self.LICENSE_SYMBOL = "LicenseSymbol" + self.LICENSE_WITH_SYMBOL = "LicenseWithExceptionSymbol" + + def parse_license_expression(self, expression): + if not expression: + raise LicompException("No license provided: " + str(expression), ReturnCodes.LICOMP_PARSE_ERROR) + + p = self.__parse_expression(self.licensing.parse(expression).pretty().replace('\n', ' ')) + return p + + def __is_license_with_exception(self, expression): + return expression.strip().startswith(self.LICENSE_WITH_SYMBOL) + + def __is_license(self, expression, with_exception=False): + if with_exception: + return expression.strip().startswith(self.LICENSE_SYMBOL) or expression.strip().startswith(self.LICENSE_WITH_SYMBOL) + return expression.strip().startswith(self.LICENSE_SYMBOL) + + def __is_operator(self, expression): + return expression.startswith(AND) or expression.startswith(OR) + + def __get_operator(self, expression): + if expression.startswith(AND): + return AND + if expression.startswith(OR): + return OR + raise Exception("BAD EXPRESSION----") + + def __get_operands_string(self, expression): + # length of the operator and parenthesis + op = self.__get_operator(expression) + op_size = len(op) + 1 + + # nr characters until closing (operator) parenthesis + left_parens = 1 + operand_size = 1 + for c in expression[op_size:]: + operand_size += 1 + if c == '(': + left_parens += 1 + elif c == ')': + left_parens -= 1 + + if left_parens == 0: + break + + rest = expression[op_size:operand_size + 1] + remains = expression[operand_size + 4:] + return rest, remains + + def is_close(self, expression): + return expression.startswith(self.CLOSE_PARENTHESIS) + + def __cleanup_license(self, operand): + stripped_operand = operand.strip() + + if self.__is_license_with_exception(operand): + trimmed_operand = stripped_operand.replace(f"{self.LICENSE_WITH_SYMBOL}('", '', 1) + else: + trimmed_operand = stripped_operand.replace(f"{self.LICENSE_SYMBOL}('", '', 1) + closing_paren_index = trimmed_operand.find(")") + op = trimmed_operand[:closing_paren_index - 1] + remains = trimmed_operand[closing_paren_index + 1:].strip() + if remains.startswith(","): + remains = remains[1:] + return op, remains.strip() + + def __parse_expression(self, expression): + logging.debug(f'__parse_expression: {expression}') + + if self.__is_operator(expression): + operator = self.__get_operator(expression) + operands = [] + ops, remains = self.__get_operands_string(expression) + while ops != "": + if self.__is_license(ops.strip(), with_exception=True): + operand, rem = self.__cleanup_license(ops) + operands.append({ + COMPATIBILITY_TYPE: "license", + 'license': operand, + }) + ops = rem + + elif self.__is_operator(ops.strip()): + operand = self.__parse_expression(ops.strip()) + _ops, _remains = self.__get_operands_string(ops.strip()) + operands.append(operand) + ops = _remains + + else: + raise LicompException(f'Failed parsing expression "{ops}". Complete expression "{expression}"', ReturnCodes.LICOMP_PARSE_ERROR) + return { + COMPATIBILITY_TYPE: 'expression', + "operator": operator, + "operands": operands, + } + + elif self.__is_license(expression, with_exception=True): + cleaned_up, rem = self.__cleanup_license(expression.strip()) + return { + COMPATIBILITY_TYPE: "license", + 'license': cleaned_up, + } + + elif self.__is_close(expression): + return "" + + raise Exception("Bottom reached") + + def to_string(self, parsed_license): + license_type = parsed_license['compatibility_type'] + if license_type == 'license': + return parsed_license['license'] + + operator = parsed_license['operator'] + license_expression = [] + for operand in parsed_license['operands']: + license_expression.append(f' ( {self.to_string(operand)} ) ') + return str(self.licensing.parse(operator.join(license_expression))) From d400add14a16457228ff74fea40e62f19cccb77a Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sat, 31 May 2025 14:03:49 +0200 Subject: [PATCH 10/31] add reuse info --- devel/licomp-all.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100755 devel/licomp-all.sh diff --git a/devel/licomp-all.sh b/devel/licomp-all.sh new file mode 100755 index 0000000..8913567 --- /dev/null +++ b/devel/licomp-all.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + + + +RESOURCES=$(./devel/licomp-toolkit supported-resources | jq -r .[] | cut -d : -f 1 | sed 's,_,-,g') + +for RESOURCE in $RESOURCES +do + echo "# $RESOURCE" + $RESOURCE $* +done From 2d46b5c1fe3390a7fd642870e9bc4c97623ce48a Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sat, 31 May 2025 14:04:08 +0200 Subject: [PATCH 11/31] add better default example --- devel/licomp-toolkit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devel/licomp-toolkit b/devel/licomp-toolkit index a8fd40f..c9ad5d1 100755 --- a/devel/licomp-toolkit +++ b/devel/licomp-toolkit @@ -13,7 +13,7 @@ then fi if [ "$1" = "" ] then - ARGS="verify -il MIT -ol MIT" + ARGS="verify -il MIT -ol \"MIT OR X11\"" fi PYTHONPATH=${EXTRA_PYTHONPATH}:${PYTHONPATH} ${SCRIPT_DIR}/licomp_toolkit/__main__.py $* $ARGS From 9bad6047e064265e7e3d7bd1e12f05be9a37ea5e Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sun, 1 Jun 2025 23:32:47 +0200 Subject: [PATCH 12/31] separate formating to file --- licomp_toolkit/format.py | 67 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 licomp_toolkit/format.py diff --git a/licomp_toolkit/format.py b/licomp_toolkit/format.py new file mode 100644 index 0000000..7b7b0f6 --- /dev/null +++ b/licomp_toolkit/format.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import json + +class LicompToolkitFormatter(): + + @staticmethod + def formatter(fmt): + if fmt.lower() == 'json': + return JsonLicompToolkitFormatter() + if fmt.lower() == 'text': + return TextLicompToolkitFormatter() + + def format_compatibilities(self, compat): + return None + + def format_licomp_resources(self, licomp_resources): + return None + + def format_licomp_licenses(self, licomp_licenses): + return None + + def format_licomp_versions(self, licomp_versions): + return None + +class JsonLicompToolkitFormatter(): + + def format_compatibilities(self, compat): + return json.dumps(compat, indent=4) + + def format_licomp_resources(self, licomp_resources): + return json.dumps(licomp_resources, indent=4) + + def format_licomp_licenses(self, licomp_licenses): + return json.dumps(licomp_licenses, indent=4) + + def format_licomp_versions(self, licomp_versions): + return json.dumps(licomp_versions, indent=4) + +class TextLicompToolkitFormatter(): + + def format_licomp_resources(self, licomp_resources): + return "\n".join(licomp_resources) + + def format_licomp_licenses(self, licomp_licenses): + return "\n".join(licomp_licenses) + + def format_compatibilities(self, compat): + summary = compat['summary'] + output = [] + nr_valid = summary['results']['nr_valid'] + output.append(f'{nr_valid} succesfull response(s)') + if int(nr_valid) > 0: + output.append('Results:') + statuses = summary['compatibility_statuses'] + for status in statuses.keys(): + output.append(f' {status}: {", ".join(statuses[status])}') + return "\n".join(output) + + def format_licomp_versions(self, licomp_versions): + lt = 'licomp-toolkit' + res = [f'{lt}: {licomp_versions[lt]}'] + for k, v in licomp_versions['licomp-resources'].items(): + res.append(f'{k}: {v}') + return '\n'.join(res) From 5af5a3a28014ad15a3e9b4b81917a98c7767ccf4 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sun, 1 Jun 2025 23:33:19 +0200 Subject: [PATCH 13/31] code to check returned data against a schema --- licomp_toolkit/schema_checker.py | 66 ++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 licomp_toolkit/schema_checker.py diff --git a/licomp_toolkit/schema_checker.py b/licomp_toolkit/schema_checker.py new file mode 100644 index 0000000..8ef2abd --- /dev/null +++ b/licomp_toolkit/schema_checker.py @@ -0,0 +1,66 @@ +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import json +import jsonschema +import logging +import os + +from licomp_toolkit.toolkit import LicompToolkit +from licomp.interface import LicompException + +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +DATA_DIR = os.path.realpath(os.path.join(SCRIPT_DIR, "data")) +SCHEMA_FILE = os.path.realpath(os.path.join(DATA_DIR, "reply_schema.json")) + +class LicompToolkitSchemaChecker: + + def __init__(self): + with open(SCHEMA_FILE) as fp: + self.expr_expr_schema = json.load(fp) + + def __validate_deeply(self, compat): + validations = 0 + lt = LicompToolkit() + + compat_check = compat['compatibility_check'] + if compat_check == 'outbound-license -> inbound-license': + compat_object = compat['compatibility_object'] + if not compat_object: + details = compat['compatibility_details'] + else: + details = compat_object['compatibility_details'] + compatibilities = details['compatibilities'] + for compatibility_object in compatibilities.values(): + inner_validations = lt.validate(compatibility_object) + validations += 1 + logging.debug('Validation OK') + return validations + else: + + if compat_check == 'outbound-expression -> inbound-license' or compat_check == 'outbound-expression -> inbound-expression': + compat_object = compat + elif compat['compatibility_check'] == 'outbound-license -> inbound-expression': + compat_object = compat['compatibility_object'] + else: + raise LicompException("Validation failed. Invalid state: " + compat_check) + + for operand in compat_object['operands']: + operand_compat_object = operand['compatibility_object'] + inner_validations = self.__validate_deeply(operand_compat_object) + validations += inner_validations + return validations + + def validate(self, content, deep=False): + jsonschema.validate(instance=content, + schema=self.expr_expr_schema) + validations = 1 + if deep: + report = content['compatibility_report'] + validations = self.__validate_deeply(report) + return validations + + def validate_file(self, filename, deep=False): + with open(filename) as fp: + return self.validate(json.load(fp), deep) From 6591026a205a0e5c25ce107d7c04509c5b020e10 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sun, 1 Jun 2025 23:33:39 +0200 Subject: [PATCH 14/31] add tests for the expression parser --- tests/python/test_expr_parser.py | 119 +++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 tests/python/test_expr_parser.py diff --git a/tests/python/test_expr_parser.py b/tests/python/test_expr_parser.py new file mode 100644 index 0000000..3171dd9 --- /dev/null +++ b/tests/python/test_expr_parser.py @@ -0,0 +1,119 @@ +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import pytest + +from licomp_toolkit.expr_parser import LicenseExpressionParser +from licomp.interface import LicompException + +parser = LicenseExpressionParser() + +def _type(expr): + return expr['compatibility_type'] + +def _is_expression(expr): + return expr['compatibility_type'] == 'expression' + +def _is_license(expr): + return expr['compatibility_type'] == 'license' + +def _license(expr): + return expr['license'] + +def _operator(expr): + return expr['operator'] + +def _operands(expr): + return expr['operands'] + +def test_none(): + with pytest.raises(LicompException): + expression = parser.parse_license_expression(None) + +def test_empty(): + with pytest.raises(LicompException): + expression = parser.parse_license_expression('') + +def test_single(): + expression = parser.parse_license_expression('MIT') + assert _is_license(expression) + assert _license(expression) == 'MIT' + +def test_single_with(): + expression = parser.parse_license_expression('GPL-3.0-only WITH GCC-exception-3.1') + assert _is_license(expression) + assert _license(expression) == 'GPL-3.0-only WITH GCC-exception-3.1' + +def test_simple_or(): + expression = parser.parse_license_expression('MIT OR X11') + operands = [x['license'] for x in _operands(expression)] + operands.sort() + assert operands == [ 'MIT', 'X11' ] + assert _is_expression(expression) + assert _operator(expression) == 'OR' + +def test_simple_or_with(): + expression = parser.parse_license_expression('MIT OR GPL-3.0-only WITH GCC-exception-3.1') + operands = [x['license'] for x in _operands(expression)] + operands.sort() + assert operands == [ 'GPL-3.0-only WITH GCC-exception-3.1', 'MIT' ] + assert _is_expression(expression) + assert _operator(expression) == 'OR' + +def test_simple_or_with2(): + expression = parser.parse_license_expression('GPL-3.0-only WITH GCC-exception-3.1 OR MIT') + operands = [x['license'] for x in _operands(expression)] + operands.sort() + assert operands == [ 'GPL-3.0-only WITH GCC-exception-3.1', 'MIT' ] + assert _is_expression(expression) + assert _operator(expression) == 'OR' + +def test_simple_and(): + expression = parser.parse_license_expression('MIT AND X11') + operands = [x['license'] for x in _operands(expression)] + operands.sort() + assert operands == [ 'MIT', 'X11' ] + assert _is_expression(expression) + assert _operator(expression) == 'AND' + +def test_simple_and_with(): + expression = parser.parse_license_expression('MIT AND GPL-3.0-only WITH GCC-exception-3.1') + operands = [x['license'] for x in _operands(expression)] + operands.sort() + assert operands == [ 'GPL-3.0-only WITH GCC-exception-3.1', 'MIT' ] + assert _is_expression(expression) + assert _operator(expression) == 'AND' + +def test_many_ors(): + expression = parser.parse_license_expression('MIT OR X11 OR BSD-3-Clause OR ISC') + operands = [x['license'] for x in _operands(expression)] + operands.sort() + assert operands == [ 'BSD-3-Clause', 'ISC', 'MIT', 'X11' ] + assert _is_expression(expression) + assert _operator(expression) == 'OR' + +def test_many_ands(): + expression = parser.parse_license_expression('MIT AND X11 AND BSD-3-Clause AND ISC AND GPL-3.0-only WITH GCC-exception-3.1') + operands = [x['license'] for x in _operands(expression)] + operands.sort() + assert operands == [ 'BSD-3-Clause', 'GPL-3.0-only WITH GCC-exception-3.1', 'ISC', 'MIT', 'X11' ] + assert _is_expression(expression) + assert _operator(expression) == 'AND' + +def test_many_complex_expr(): + expression = parser.parse_license_expression('MIT AND X11 AND (BSD-3-Clause OR ( ISC OR GPL-3.0-only WITH GCC-exception-3.1) )') + assert _operator(expression) == 'AND' + operands = _operands(expression) + assert _license(operands[0]) == 'MIT' + assert _license(operands[1]) == 'X11' + assert _operator(operands[2]) == 'OR' + operands = _operands(operands[2]) + assert _license(operands[0]) == 'BSD-3-Clause' + assert _operator(operands[1]) == 'OR' + operands = _operands(operands[1]) + assert _license(operands[0]) == 'ISC' + assert _license(operands[1]) == 'GPL-3.0-only WITH GCC-exception-3.1' + + + From e8188f41057cf3b3e5e734009e144691990aaac3 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sun, 1 Jun 2025 23:34:34 +0200 Subject: [PATCH 15/31] check expressions instead of licenses --- licomp_toolkit/__main__.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/licomp_toolkit/__main__.py b/licomp_toolkit/__main__.py index da2d83f..bc40070 100755 --- a/licomp_toolkit/__main__.py +++ b/licomp_toolkit/__main__.py @@ -10,16 +10,17 @@ from licomp.interface import LicompException from licomp_toolkit.toolkit import LicompToolkit -from licomp_toolkit.toolkit import LicompToolkitFormatter +from licomp_toolkit.toolkit import ExpressionExpressionChecker +from licomp_toolkit.format import LicompToolkitFormatter from licomp_toolkit.config import cli_name from licomp_toolkit.config import description from licomp_toolkit.config import epilog -from licomp_toolkit.utils import licomp_results_to_return_code from licomp.main_base import LicompParser from licomp.interface import UseCase from licomp.interface import Provisioning from licomp.return_codes import ReturnCodes +from licomp.return_codes import compatibility_status_to_returncode from flame.license_db import FossLicenses from flame.exception import FlameException @@ -37,11 +38,14 @@ def __normalize_license(self, lic_name): def verify(self, args): formatter = LicompToolkitFormatter.formatter(self.args.output_format) try: - compatibilities = self.licomp_toolkit.outbound_inbound_compatibility(self.__normalize_license(args.out_license), - self.__normalize_license(args.in_license), - args.usecase, - args.provisioning) - ret_code = licomp_results_to_return_code(compatibilities['summary']['results']) + expr_checker = ExpressionExpressionChecker() + compatibilities = expr_checker.check_compatibility(self.__normalize_license(args.out_license), + self.__normalize_license(args.in_license), + args.usecase, + args.provisioning, + detailed_report=True) + + ret_code = compatibility_status_to_returncode(compatibilities['compatibility']) return formatter.format_compatibilities(compatibilities), ret_code, False except LicompException as e: return e, e.return_code.value, True From 8ae6956e9c5dbbf03ef4c5e1ef98a1f1cb617218 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sun, 1 Jun 2025 23:34:55 +0200 Subject: [PATCH 16/31] update with descriptions and controls --- licomp_toolkit/data/reply_schema.json | 112 +++++++++++++++++++++----- 1 file changed, 92 insertions(+), 20 deletions(-) diff --git a/licomp_toolkit/data/reply_schema.json b/licomp_toolkit/data/reply_schema.json index bec08c2..974c257 100644 --- a/licomp_toolkit/data/reply_schema.json +++ b/licomp_toolkit/data/reply_schema.json @@ -1,19 +1,40 @@ { "$schema" : "http://json-schema.org/draft-07/schema#", - "$id" : "http://spdx.org/rdf/terms", - "title" : "SPDX 2.2", + "$id" : "", + "title" : "Licomp Toolkit Reply", "type" : "object", "properties" : { "compatibility_report": { "type" : "object", "$ref": "#/$defs/compatibility_object" }, - "outbound" : {"type" : "string", "description" : "" }, - "inbound" : { "type" : "string", "description" : "" }, - "usecase" : { "type" : "string", "description" : "" }, - "provisioning" : { "type" : "string", "description" : "" }, - "modification" : { "type" : "string", "description" : "" }, - "compatibility" : { "type" : "string", "description" : "" } + "outbound" : { + "$ref": "#/$defs/license", + "description" : "The outbound license expression." + }, + "inbound" : { + "$ref": "#/$defs/license", + "description" : "The inbound license expression." + }, + "usecase" : { + "type" : "string", + "enum": ["library", "compiler", "snippet", "tool", "test"], + "description" : "Usecase for the compatibility check, e.g. library (as in using the inbound licensed component as a library)" + }, + "provisioning" : { + "type" : "string", + "enum": ["source-code-distribution", "binary-distribution", "local-use", "provide-service", "provide-webui"], + "description" : "The way the component is provided to the user for the compatibility check, e.g. binary-distribution" + }, + "modification" : { + "type" : "string", + "description" : "Has the component been modified. Currently not used/implemented.", + "enum": [ "unmodified", "modified"] + }, + "compatibility" : { + "$ref": "#/$defs/compatibility", + "description" : "The inbound license expression." + } }, "required" : [ "compatibility_report", "compatibility", "outbound", "inbound" , "usecase" ], "additionalProperties" : false, @@ -23,11 +44,25 @@ "oneOf": [ { "properties": { - "compatibility": { "type": "string" }, - "compatibility_type": { "enum": [ "license" ] }, - "compatibility_check": { "type": "string" }, - "inbound_license": { "type": "string" }, - "outbound_license": { "type": "string" }, + "compatibility": { + "$ref": "#/$defs/compatibility", + "description" : "The inbound license expression." + }, + "compatibility_type": { + "enum": [ "license" ], + "description": "Describing what is being checked. Can be either expression or license. In this case the value is \"license\"." + }, + "compatibility_check": { + "$ref": "#/$defs/compatibility_check" + }, + "inbound_license": { + "$ref": "#/$defs/license", + "description" : "The inbound license expression." + }, + "outbound_license": { + "$ref": "#/$defs/license", + "description" : "The outbound license expression." + }, "compatibility_object": { "type" : "object", "anyOf": [ @@ -45,12 +80,29 @@ }, { "properties": { - "compatibility": { "type": "string" }, - "compatibility_type": { "enum": [ "expression" ] }, - "compatibility_check": { "type": "string" }, - "inbound_license": { "type": "string" }, - "outbound_license": { "type": "string" }, - "operator": { "enum": [ "AND", "OR" ] }, + "compatibility" : { + "$ref": "#/$defs/compatibility", + "description" : "The inbound license expression." + }, + "compatibility_type": { + "enum": [ "expression" ], + "description": "Describing what is being checked. Can be either expression or license. In this case the value is \"expression\"." + }, + "compatibility_check": { + "$ref": "#/$defs/compatibility_check" + }, + "inbound_license": { + "$ref": "#/$defs/license", + "description" : "The inbound license expression." + }, + "outbound_license": { + "$ref": "#/$defs/license", + "description" : "The inbound license expression." + }, + "operator": { + "enum": [ "AND", "OR" ], + "description" : "The logical operator between two license expressions." + }, "operands": { "type": "array", "items": { @@ -59,13 +111,33 @@ "$ref": "#/$defs/compatibility_object" }, "compatibility": { "type": "string" } - } + }, + "description": "The operands for the operator. The operands can be either a license or an operator." } }, "required": [ "compatibility", "compatibility_type", "compatibility_check", "inbound_license" , "outbound_license" , "operator", "operands" ], "additionalProperties" : false } ] + }, + "compatibility" : { + "type" : "string", + "enum": [ "yes", "no", "depends", "unknown", "unsupported", "mixed", null], + "description" : "The compatbility between the Outbound and Inbound license expressions" + }, + "license" : { + "type" : "string", + "minLength": 1 + }, + "compatibility_check": { + "type": "string", + "enum": [ + "outbound-expression -> inbound-expression", + "outbound-expression -> inbound-license", + "outbound-license -> inbound-expression", + "outbound-license -> inbound-license" + ], + "description" : "A text describing if outbound license or expression is checked for compatiblility against inbound license or expression." } } } From f1d6eed0267857a546cd4fe198a40c48c274ff09 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sun, 1 Jun 2025 23:35:41 +0200 Subject: [PATCH 17/31] clean up --- licomp_toolkit/lic_expr.py | 86 +++++++++----------------------------- 1 file changed, 19 insertions(+), 67 deletions(-) diff --git a/licomp_toolkit/lic_expr.py b/licomp_toolkit/lic_expr.py index 9bb21df..f396a2c 100644 --- a/licomp_toolkit/lic_expr.py +++ b/licomp_toolkit/lic_expr.py @@ -2,15 +2,11 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -import json - import logging from license_expression import get_spdx_licensing from licomp_toolkit.toolkit import LicompToolkit -from licomp.interface import LicompException - -# from licomp.interface import UseCase -# from licomp.interface import Provisioning +from licomp.interface import UseCase +from licomp.interface import Provisioning AND = "AND" OR = "OR" @@ -29,9 +25,7 @@ def __init__(self): self.LICENSE_WITH_SYMBOL = "LicenseWithExceptionSymbol" def parse_license_expression(self, expression): - logging.debug(" ---------------------- " + expression + "-------------------------") p = self.__parse_expression(self.licensing.parse(expression).pretty().replace('\n', ' ')) - logging.debug(" ---------------------- " + expression + "------------------------->> \n" + json.dumps(p, indent=4)) return p def __is_license_with_exception(self, expression): @@ -92,7 +86,7 @@ def __cleanup_license(self, operand): return op, remains.strip() def __parse_expression(self, expression): - logging.debug("__parse_expression:" + expression) + logging.debug(f'__parse_expression: {expression}') if self.__is_operator(expression): operator = self.__get_operator(expression) @@ -124,13 +118,11 @@ def __parse_expression(self, expression): } elif self.__is_license(expression, with_exception=True): - # TODO: what if exception??? cleaned_up, rem = self.__cleanup_license(expression.strip()) return { COMPATIBILITY_TYPE: "license", 'license': cleaned_up, - } elif self.__is_close(expression): @@ -160,7 +152,6 @@ def outbound_inbound_compatibility(self, outbound, lic): def __compatibility_status(self, compatibility): status = compatibility['summary']['results'] - # nr_valid = status['nr_valid'] rets = [] for ret in status: @@ -175,48 +166,43 @@ def __compatibility_status(self, compatibility): def check_compatibility(self, outbound, parsed_expression, detailed_report=False): compat_object = { - #COMPATIBILITY_OUTBOUND_LICENSE: outbound, - #COMPATIBILITY_INBOUND_LICENSE: parsed_expression, COMPATIBILITY_TYPE: parsed_expression[COMPATIBILITY_TYPE], - 'compatiblity_check': 'outbound-operator -> inbound-license' + 'compatiblity_check': 'outbound-operator -> inbound-license', } - + if parsed_expression[COMPATIBILITY_TYPE] == 'license': - compat_object['compatiblity_check'] = 'outbound-license -> inbound-license' + compat_object['compatiblity_check'] = 'outbound-license -> inbound-license' lic = parsed_expression['license'] compat = self.outbound_inbound_compatibility(outbound, lic) compat_object['compatibility'] = self.__compatibility_status(compat) - #parsed_expression['compatibility'] = self.__compatibility_status(compat) if detailed_report: compat_object['compatibility_details'] = compat compat_object['inbound_license'] = lic compat_object['outbound_license'] = outbound - + else: operator = parsed_expression['operator'] operands = parsed_expression['operands'] compat_object['compatibility_object'] = { 'operator': operator, - 'operands': [] + 'operands': [], } operands_object = [] for operand in operands: operand_compat = self.check_compatibility(outbound, operand, detailed_report=detailed_report) operand_object = { - #'operand': operand, 'compatibility_object': operand_compat, - 'compatibility': operand_compat['compatibility'] + 'compatibility': operand_compat['compatibility'], } operands_object.append(operand_object) compat_object['compatibility'] = self.summarise_compatibilities(operator, operands_object) compat_object['compatibility_object']['operands'] = operands_object -# compat_object['HESA---------------'] = self.summarise_compatibilities(operator, operands_object) return compat_object - + def __init_summary(self, operands): summary = { "yes": 0, @@ -252,10 +238,9 @@ def __summarise_compatibilities_or(self, operands): def summarise_compatibilities(self, operator, operands): return { AND: self.__summarise_compatibilities_and, - OR: self.__summarise_compatibilities_or + OR: self.__summarise_compatibilities_or, }[operator](operands) - class ExpressionExpressionChecker(): def __init__(self): @@ -273,7 +258,8 @@ def check_compatibility(self, outbound, inbound, usecase, provisioning, detailed compatibility_report = self.__check_compatibility(outbound_parsed, inbound_parsed, - # TODO: add context + usecase, + provisioning, detailed_report) return { @@ -287,19 +273,17 @@ def check_compatibility(self, outbound, inbound, usecase, provisioning, detailed 'compatibility_report': compatibility_report, } - def __check_compatibility(self, outbound_parsed, inbound_parsed, detailed_report=False): + def __check_compatibility(self, outbound_parsed, inbound_parsed, usecase, provisioning, detailed_report=False): outbound_type = outbound_parsed[COMPATIBILITY_TYPE] compat_object = { - #COMPATIBILITY_OUTBOUND_LICENSE: outbound_parsed, - #COMPATIBILITY_INBOUND_LICENSE: inbound_parsed, COMPATIBILITY_TYPE: outbound_type, 'inbound_license': self.le_parser.to_string(inbound_parsed), - 'outbound_license': self.le_parser.to_string(outbound_parsed) + 'outbound_license': self.le_parser.to_string(outbound_parsed), } if outbound_type == 'license': - compat_object['compatiblity_check']: f'outbound-license -> inbound->{inbound_parsed["compatibility_type"]}' + compat_object['compatiblity_check'] = f'outbound-license -> inbound-{inbound_parsed["compatibility_type"]}' outbound_parsed_license = outbound_parsed['license'] # Check if: # outbound license @@ -311,18 +295,14 @@ def __check_compatibility(self, outbound_parsed, inbound_parsed, detailed_report compat_object['compatibility'] = compat['compatibility'] compat_object['compatibility_details'] = compat - # TODO: bring back details - # compat_object['compatibility_details'] = compat - elif outbound_type == 'operator': - compat_object['compatiblity_check']: f'outbound-operator -> inbound->{inbound_parsed["compatibility_type"]}' - outbound_parsed_operator = outbound_parsed['operator'] + compat_object['compatiblity_check'] = f'outbound-operator -> inbound-{inbound_parsed["compatibility_type"]}' operator = outbound_parsed['operator'] operands = outbound_parsed['operands'] compat_object['compatibility_object'] = { 'operator': operator, - 'operands': [] + 'operands': [], } operands_object = [] @@ -335,40 +315,12 @@ def __check_compatibility(self, outbound_parsed, inbound_parsed, detailed_report inbound_parsed, detailed_report) operand_object = { - #'operand': operand, 'compatibility_object': operand_compat, - 'compatibility': operand_compat['compatibility'] + 'compatibility': operand_compat['compatibility'], } - # operand['compatibility_details'] = inbound_compat - - #operand['compatibility_object'] = inbound_parsed - #operand['outbound_license_SANDKLEF'] = "whattt" - #operand['compatibility'] = inbound_compat['compatibility'] operands_object.append(operand_object) compat_object['compatibility'] = self.le_checker.summarise_compatibilities(operator, operands_object) compat_object['compatibility_object']['operands'] = operands_object - # TODO: bring back details - return compat_object - -# parser = LicenseExpressionParser() - - -expr_checker = ExpressionExpressionChecker() - -inbound = "Apache-2.0" -outbound = "GPL-2.0-only" -inbound = "MIT OR Apache-2.0 AND X11" -outbound = "GPL-2.0-only AND BSD-2-Clause" -from licomp.interface import UseCase -from licomp.interface import Provisioning -checked = expr_checker.check_compatibility(outbound, - inbound, - usecase=UseCase.LIBRARY, - provisioning=Provisioning.BIN_DIST, - detailed_report=False) - -#print(str(checked)) -print(json.dumps(checked, indent=4)) From ed66517383ce3e8989986886047879bee2e3c665 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sun, 1 Jun 2025 23:36:29 +0200 Subject: [PATCH 18/31] add expression parser and checker, remove format --- licomp_toolkit/toolkit.py | 326 ++++++++++++++++++++++++++------------ 1 file changed, 229 insertions(+), 97 deletions(-) diff --git a/licomp_toolkit/toolkit.py b/licomp_toolkit/toolkit.py index cbcd842..89ed65a 100644 --- a/licomp_toolkit/toolkit.py +++ b/licomp_toolkit/toolkit.py @@ -2,8 +2,6 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -import importlib -import json import logging from licomp.interface import Licomp @@ -11,6 +9,7 @@ from licomp.interface import Provisioning from licomp.interface import LicompException from licomp.return_codes import ReturnCodes +from licomp.interface import CompatibilityStatus from licomp_osadl.osadl import LicompOsadl from licomp_reclicense.reclicense import LicompReclicense @@ -23,71 +22,17 @@ from licomp_toolkit.config import licomp_toolkit_version from licomp_toolkit.config import cli_name -class LicompToolkitFormatter(): +from licomp_toolkit.expr_parser import LicenseExpressionParser +from licomp_toolkit.expr_parser import COMPATIBILITY_TYPE +from licomp_toolkit.expr_parser import AND +from licomp_toolkit.expr_parser import OR - @staticmethod - def formatter(fmt): - if fmt.lower() == 'json': - return JsonLicompToolkitFormatter() - if fmt.lower() == 'text': - return TextLicompToolkitFormatter() - - def format_compatibilities(self, compat): - return None - - def format_licomp_resources(self, licomp_resources): - return None - - def format_licomp_licenses(self, licomp_licenses): - return None - - def format_licomp_versions(self, licomp_versions): - return None - -class JsonLicompToolkitFormatter(): - - def format_compatibilities(self, compat): - return json.dumps(compat, indent=4) - - def format_licomp_resources(self, licomp_resources): - return json.dumps(licomp_resources, indent=4) - - def format_licomp_licenses(self, licomp_resources): - return json.dumps(licomp_licenses, indent=4) - - def format_licomp_versions(self, liczomp_versions): - return json.dumps(licomp_versions, indent=4) - -class TextLicompToolkitFormatter(): - - def format_licomp_resources(self, licomp_resources): - return "\n".join(licomp_resources) - - def format_licomp_licenses(self, licomp_licenses): - return "\n".join(licomp_licenses) - - def format_compatibilities(self, compat): - summary = compat['summary'] - output = [] - nr_valid = summary['results']['nr_valid'] - output.append(f'{nr_valid} succesfull response(s)') - if int(nr_valid) > 0: - output.append('Results:') - statuses = summary['compatibility_statuses'] - for status in statuses.keys(): - output.append(f' {status}: {", ".join(statuses[status])}') - return "\n".join(output) - - def format_licomp_versions(self, licomp_versions): - lt = 'licomp-toolkit' - res = [f'{lt}: {licomp_versions[lt]}'] - for k, v in licomp_versions['licomp-resources'].items(): - res.append(f'{k}: {v}') - return '\n'.join(res) +from licomp_toolkit.config import my_supported_api_version class LicompToolkit(Licomp): def __init__(self): + Licomp.__init__(self) self.LICOMP_RESOURCES = {} self.LICOMP_RESOURCE_NAMES = { "osadl": { @@ -108,6 +53,9 @@ def __init__(self): }, } + def supported_api_version(self): + return my_supported_api_version + def __add_to_list(self, store, data, name): if not data: return @@ -168,24 +116,13 @@ def __summarize_compatibility(self, compatibilities, outbound, inbound, usecase, def outbound_inbound_compatibility(self, outbound, inbound, usecase, provisioning): logging.debug(f'{inbound} {outbound} ') - # Check usecase - try: - usecase = UseCase.string_to_usecase(usecase) - except KeyError: - raise LicompException(f'Usecase {usecase} not supported.', ReturnCodes.LICOMP_UNSUPPORTED_USECASE) - - # Check provisioning - try: - provisioning = Provisioning.string_to_provisioning(provisioning) - except KeyError: - raise LicompException(f'Provisioning {provisioning} not supported.', ReturnCodes.LICOMP_UNSUPPORTED_PROVISIONING) - compatibilities = {} compatibilities['compatibilities'] = {} for resource_name in self.licomp_resources(): resource = self.licomp_resources()[resource_name] logging.debug(f'-- resource: {resource.name()}') + compat = resource.outbound_inbound_compatibility(outbound, inbound, usecase, provisioning=provisioning) compatibilities['compatibilities'][compat['resource_name']] = compat @@ -232,26 +169,221 @@ def versions(self, verbose=False): def name(self): return cli_name -def __class_instance(package, class_name): - licomp_resource = importlib.import_resource(f'{package}') - licomp_class = getattr(licomp_resource, class_name) - return licomp_class() - -def __check_api_version(subclass): - licomp_api_version = Licomp.api_version() - subclass_api_version = subclass.supported_api_version() - logging.debug(f'{licomp_api_version} == {subclass_api_version} ???') - - licomp_api_version_major = licomp_api_version.split('.')[0] - licomp_api_version_minor = licomp_api_version.split('.')[1] - - subclass_api_version_major = subclass_api_version.split('.')[0] - subclass_api_version_minor = subclass_api_version.split('.')[1] - assert licomp_api_version_major == subclass_api_version_major # noqa: S101 - assert licomp_api_version_minor == subclass_api_version_minor # noqa: S101 - -def _inc_map(_map, _name): - curr = _map.get(_name, 0) - new = curr + 1 - _map[_name] = new - return _map +class LicenseExpressionChecker(): + + def __init__(self): + self.le_parser = LicenseExpressionParser() + self.licomp = LicompToolkit() + + def outbound_inbound_compatibility(self, outbound, lic, usecase, provisioning): + return self.licomp.outbound_inbound_compatibility(outbound, + lic, + usecase, + provisioning) + + def __compatibility_status(self, compatibility): + status = compatibility['summary']['results'] + rets = [] + for ret in status: + if ret == 'nr_valid': + continue + elif not ret: + pass + elif ret == CompatibilityStatus.compat_status_to_string(CompatibilityStatus.UNSUPPORTED): + pass + else: + rets.append(ret) + + if len(rets) == 0: + return CompatibilityStatus.compat_status_to_string(CompatibilityStatus.UNSUPPORTED) + + if len(rets) == 1: + return rets[0] + + return CompatibilityStatus.compat_status_to_string(CompatibilityStatus.MIXED) + + def check_compatibility(self, + outbound, + parsed_expression, + usecase, + provisioning, + detailed_report=True): + + compat_object = { + COMPATIBILITY_TYPE: parsed_expression[COMPATIBILITY_TYPE], + 'compatibility_check': 'outbound-expression -> inbound-license', + } + + if parsed_expression[COMPATIBILITY_TYPE] == 'license': + compat_object['compatibility_check'] = 'outbound-license -> inbound-license' + lic = parsed_expression['license'] + compat = self.outbound_inbound_compatibility(outbound, + lic, + usecase, + provisioning) + compat_object['compatibility'] = self.__compatibility_status(compat) + if detailed_report: + compat_object['compatibility_details'] = compat + + compat_object['inbound_license'] = lic + compat_object['outbound_license'] = outbound + compat_object['compatibility_object'] = {} + + else: + operator = parsed_expression['operator'] + operands = parsed_expression['operands'] + compat_object['operator'] = operator + + compat_object['inbound_license'] = self.le_parser.to_string(parsed_expression) + compat_object['outbound_license'] = outbound + operands_object = [] + for operand in operands: + operand_compat = self.check_compatibility(outbound, operand, usecase, provisioning, detailed_report=detailed_report) + operand_object = { + 'compatibility_object': operand_compat, + 'compatibility': operand_compat['compatibility'], + } + operands_object.append(operand_object) + + compat_object['compatibility'] = self.summarise_compatibilities(operator, operands_object) + compat_object['operands'] = operands_object + + return compat_object + + def __init_summary(self, operands): + summary = { + "yes": 0, + "no": 0, + "depends": 0, + "unknown": 0, + "unsupported": 0, + "mixed": 0, + } + for operand in operands: + compat = operand['compatibility'] + summary[compat] = summary[compat] + 1 + return summary + + def __summarise_compatibilities_and(self, operands): + nr_operands = len(operands) + summary = self.__init_summary(operands) + + if summary['no'] != 0: + return 'no' + + if summary['yes'] == nr_operands: + return "yes" + + return "no" + + def __summarise_compatibilities_or(self, operands): + summary = self.__init_summary(operands) + + if summary['yes'] != 0: + return 'yes' + + return "no" + + def summarise_compatibilities(self, operator, operands): + return { + AND: self.__summarise_compatibilities_and, + OR: self.__summarise_compatibilities_or, + }[operator](operands) + + +class ExpressionExpressionChecker(): + + def __init__(self): + self.le_checker = LicenseExpressionChecker() + self.le_parser = LicenseExpressionParser() + + def __parsed_expression_to_name(self, parsed_expression): + return parsed_expression[parsed_expression[COMPATIBILITY_TYPE]] + + def check_compatibility(self, outbound, inbound, usecase, provisioning, detailed_report=False): + # Check usecase + try: + usecase = UseCase.string_to_usecase(usecase) + except KeyError: + raise LicompException(f'Usecase {usecase} not supported.', ReturnCodes.LICOMP_UNSUPPORTED_USECASE) + + # Check provisioning + try: + provisioning = Provisioning.string_to_provisioning(provisioning) + except KeyError: + raise LicompException(f'Provisioning {provisioning} not supported.', ReturnCodes.LICOMP_UNSUPPORTED_PROVISIONING) + + inbound_parsed = self.le_parser.parse_license_expression(inbound) + + outbound_parsed = self.le_parser.parse_license_expression(outbound) + compatibility_object = self.__check_compatibility(outbound_parsed, + inbound_parsed, + usecase, + provisioning, + detailed_report) + return { + 'inbound': inbound, + 'outbound': outbound, + 'usecase': UseCase.usecase_to_string(usecase), + 'provisioning': Provisioning.provisioning_to_string(provisioning), + 'compatibility': compatibility_object['compatibility'], + 'compatibility_report': compatibility_object, + } + + def __check_compatibility(self, + outbound_parsed, + inbound_parsed, + usecase, + provisioning, + detailed_report=False): + + outbound_type = outbound_parsed[COMPATIBILITY_TYPE] + compat_object = { + COMPATIBILITY_TYPE: outbound_type, + 'inbound_license': self.le_parser.to_string(inbound_parsed), + 'outbound_license': self.le_parser.to_string(outbound_parsed), + } + + if outbound_type == 'license': + compat_object['compatibility_check'] = f'outbound-license -> inbound-{inbound_parsed["compatibility_type"]}' + outbound_parsed_license = outbound_parsed['license'] + # Check if: + # outbound license + # is compatible with + # inbound license + compat = self.le_checker.check_compatibility(outbound_parsed_license, + inbound_parsed, + usecase, + provisioning, + detailed_report) + compat_object['compatibility'] = compat['compatibility'] + compat_object['compatibility_object'] = compat + + elif outbound_type == 'expression': + compat_object['compatibility_check'] = f'outbound-expression -> inbound-{inbound_parsed["compatibility_type"]}' + operator = outbound_parsed['operator'] + operands = outbound_parsed['operands'] + + compat_object['operator'] = operator + + operands_object = [] + for operand in operands: + # Check if: + # operand from outbound license + # is compatible with + # inbound license + operand_compat = self.__check_compatibility(operand, + inbound_parsed, + usecase, + provisioning, + detailed_report) + operand_object = { + 'compatibility_object': operand_compat, + 'compatibility': operand_compat['compatibility'], + } + operands_object.append(operand_object) + + compat_object['compatibility'] = self.le_checker.summarise_compatibilities(operator, operands_object) + compat_object['operands'] = operands_object + + return compat_object From c20f2cd6a3e836cbb017f5cf7c247001836a5470 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sun, 1 Jun 2025 23:36:55 +0200 Subject: [PATCH 19/31] move functions to utils --- licomp_toolkit/utils.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/licomp_toolkit/utils.py b/licomp_toolkit/utils.py index 9dea3aa..1597feb 100644 --- a/licomp_toolkit/utils.py +++ b/licomp_toolkit/utils.py @@ -2,6 +2,10 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +import importlib +import logging + +from licomp.interface import Licomp from licomp.return_codes import compatibility_status_to_returncode from licomp.return_codes import ReturnCodes @@ -21,3 +25,27 @@ def licomp_results_to_return_code(licomp_results): return compatibility_status_to_returncode(result) return ReturnCodes.LICOMP_INTERNAL_ERROR.value + +def __class_instance(package, class_name): + licomp_resource = importlib.import_resource(f'{package}') + licomp_class = getattr(licomp_resource, class_name) + return licomp_class() + +def __check_api_version(subclass): + licomp_api_version = Licomp.api_version() + subclass_api_version = subclass.supported_api_version() + logging.debug(f'{licomp_api_version} == {subclass_api_version} ???') + + licomp_api_version_major = licomp_api_version.split('.')[0] + licomp_api_version_minor = licomp_api_version.split('.')[1] + + subclass_api_version_major = subclass_api_version.split('.')[0] + subclass_api_version_minor = subclass_api_version.split('.')[1] + assert licomp_api_version_major == subclass_api_version_major # noqa: S101 + assert licomp_api_version_minor == subclass_api_version_minor # noqa: S101 + +def _inc_map(_map, _name): + curr = _map.get(_name, 0) + new = curr + 1 + _map[_name] = new + return _map From 36d01e8bd023b0ac02a2a4c9adfcba136acd39f0 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sun, 1 Jun 2025 23:37:36 +0200 Subject: [PATCH 20/31] adapt to new reply format --- tests/shell/test-cli.sh | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/tests/shell/test-cli.sh b/tests/shell/test-cli.sh index dd8d39e..cc5f975 100755 --- a/tests/shell/test-cli.sh +++ b/tests/shell/test-cli.sh @@ -6,6 +6,8 @@ LT_VERSION=$(grep licomp_toolkit_version licomp_toolkit/config.py | cut -d = -f 2 | sed "s,[' ]*,,g") +EXTRACT_COMPAT=".compatibility_report.compatibility_object.compatibility_details" + if [ "$1" == "--local" ] then IMPLEMENTATIONS=../licomp:../licomp-dwheeler:../licomp-hermione:../licomp-osadl:../licomp-reclicense:../licomp-proprietary::../licomp-gnuguide:. @@ -70,30 +72,30 @@ test_version() test_supp_unsupp() { echo "# test supported/unsupported licenses" - test_licomp_tk "verify -il MIT -ol MIT" ".summary.results.yes.count" 5 - test_licomp_tk "verify -il MIT -ol MIT2" ".summary.results.nr_valid" 0 - test_licomp_tk "verify -il MIT2 -ol MIT" ".summary.results.nr_valid" 0 - test_licomp_tk "verify -il MIT2 -ol MIT2" ".summary.results.nr_valid" 0 + test_licomp_tk "verify -il MIT -ol MIT" "${EXTRACT_COMPAT}.summary.results.yes.count" 5 + test_licomp_tk "verify -il MIT -ol MIT2" "${EXTRACT_COMPAT}.summary.results.nr_valid" 0 + test_licomp_tk "verify -il MIT2 -ol MIT" "${EXTRACT_COMPAT}.summary.results.nr_valid" 0 + test_licomp_tk "verify -il MIT2 -ol MIT2" "${EXTRACT_COMPAT}.summary.results.nr_valid" 0 } test_snippets() { echo "# test snippets only" - test_licomp_tk "-u snippet verify -il MIT -ol MIT" ".summary.results.nr_valid" 1 - test_licomp_tk "-u snippet verify -il MIT -ol MIT2" ".summary.results.nr_valid" 0 - test_licomp_tk "-u snippet verify -il BSD-3-Clause -ol LGPL-2.1-or-later" ".summary.results.nr_valid" 1 - test_licomp_tk "-u snippet verify -il LGPL-2.1-or-later -ol BSD-3-Clause" ".summary.results.nr_valid" 1 - test_licomp_tk "-u snippet verify -il BSD-3-Clause -ol LGPL-2.1-or-later" ".summary.results.yes.count" 1 - test_licomp_tk "-u snippet verify -il LGPL-2.1-or-later -ol BSD-3-Clause" ".summary.results.yes.count" null + test_licomp_tk "-u snippet verify -il MIT -ol MIT" "${EXTRACT_COMPAT}.summary.results.nr_valid" 1 + test_licomp_tk "-u snippet verify -il MIT -ol MIT2" "${EXTRACT_COMPAT}.summary.results.nr_valid" 0 + test_licomp_tk "-u snippet verify -il BSD-3-Clause -ol LGPL-2.1-or-later" "${EXTRACT_COMPAT}.summary.results.nr_valid" 1 + test_licomp_tk "-u snippet verify -il LGPL-2.1-or-later -ol BSD-3-Clause" "${EXTRACT_COMPAT}.summary.results.nr_valid" 1 + test_licomp_tk "-u snippet verify -il BSD-3-Clause -ol LGPL-2.1-or-later" "${EXTRACT_COMPAT}.summary.results.yes.count" 1 + test_licomp_tk "-u snippet verify -il LGPL-2.1-or-later -ol BSD-3-Clause" "${EXTRACT_COMPAT}.summary.results.yes.count" null } test_snippet_bindist() { echo "# snippet vs bin dist" - test_licomp_tk "-u snippet verify -il BSD-3-Clause -ol LGPL-2.1-or-later" ".summary.results.nr_valid" 1 - test_licomp_tk "-u snippet verify -il BSD-3-Clause -ol LGPL-2.1-or-later" ".summary.results.yes.count" 1 - test_licomp_tk "verify -il BSD-3-Clause -ol LGPL-2.1-or-later" ".summary.results.nr_valid" 4 - test_licomp_tk "verify -il BSD-3-Clause -ol LGPL-2.1-or-later" ".summary.results.yes.count" 4 + test_licomp_tk "-u snippet verify -il BSD-3-Clause -ol LGPL-2.1-or-later" "${EXTRACT_COMPAT}.summary.results.nr_valid" 1 + test_licomp_tk "-u snippet verify -il BSD-3-Clause -ol LGPL-2.1-or-later" "${EXTRACT_COMPAT}.summary.results.yes.count" 1 + test_licomp_tk "verify -il BSD-3-Clause -ol LGPL-2.1-or-later" "${EXTRACT_COMPAT}.summary.results.nr_valid" 4 + test_licomp_tk "verify -il BSD-3-Clause -ol LGPL-2.1-or-later" "${EXTRACT_COMPAT}.summary.results.yes.count" 4 } test_supports_license() From d60e1bc863b2852a1910fa73189847d3d8140d30 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sun, 1 Jun 2025 23:37:47 +0200 Subject: [PATCH 21/31] adapt to new reply format --- tests/shell/test_returns.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/shell/test_returns.sh b/tests/shell/test_returns.sh index bc9f1fc..460ea2f 100755 --- a/tests/shell/test_returns.sh +++ b/tests/shell/test_returns.sh @@ -47,7 +47,7 @@ test_verify() run_comp_test 0 "verify -il BSD-3-Clause -ol GPL-2.0-only" # Success and mixed compatibility - run_comp_test 1 "verify -ol 0BSD -il MS-PL" + run_comp_test 9 "verify -ol 0BSD -il MS-PL" # Success and incompatible run_comp_test 2 "verify -il GPL-2.0-only -ol BSD-3-Clause" From dbba3609964d39c75e6b924a87b1654c76ebaad8 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sun, 1 Jun 2025 23:38:07 +0200 Subject: [PATCH 22/31] adapt to new reply format --- tests/python/test_expr_expr.py | 121 ++++++++++++++++++++++-------- tests/python/test_same_compats.py | 4 +- tests/python/test_toolkit.py | 6 +- 3 files changed, 95 insertions(+), 36 deletions(-) diff --git a/tests/python/test_expr_expr.py b/tests/python/test_expr_expr.py index 1b74239..d52c0a8 100644 --- a/tests/python/test_expr_expr.py +++ b/tests/python/test_expr_expr.py @@ -12,11 +12,20 @@ from licomp.interface import UseCase from licomp.interface import CompatibilityStatus -from licomp_toolkit.lic_expr import ExpressionExpressionChecker - +from licomp_toolkit.toolkit import ExpressionExpressionChecker eec = ExpressionExpressionChecker() +GPLv2 = 'GPL-2.0-only' +MIT = 'MIT' +MIT_A_0BSD = 'MIT AND 0BSD' +APACHE2 = 'Apache-2.0' +APACHE2_A_ISC = 'Apache-2.0 AND ISC' +MIT_O_APACHE2 = 'MIT OR Apache-2.0' +MIT_A_APACHE2 = 'MIT AND Apache-2.0' +GPLv2_A_BSD3 = 'GPL-2.0-only AND BSD-3-Clause' + + def _compat_status(report): return report['compatibility_report']['compatibility'] @@ -29,17 +38,17 @@ def _compat_type(report): # GPL-2.0-only -> MIT are compatible def test_lic_lic_compat(): - compat_report = eec.check_compatibility('GPL-2.0-only', 'MIT', - usecase=UseCase.LIBRARY, - provisioning=Provisioning.BIN_DIST) + compat_report = eec.check_compatibility(GPLv2, MIT, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'yes' assert _compat_type(compat_report) == 'license' # MIT -> GPL-2.0-only -> are NOT compatible def test_lic_lic_incompat(): - compat_report = eec.check_compatibility('MIT', 'GPL-2.0-only', - usecase=UseCase.LIBRARY, - provisioning=Provisioning.BIN_DIST) + compat_report = eec.check_compatibility(MIT, GPLv2, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'no' assert _compat_type(compat_report) == 'license' @@ -49,17 +58,17 @@ def test_lic_lic_incompat(): # GPL-2.0-only -> MIT OR Apache-2.0 are compatible def test_lic_expr_compat(): - compat_report = eec.check_compatibility('GPL-2.0-only', 'MIT OR Apache-2.0', - usecase=UseCase.LIBRARY, - provisioning=Provisioning.BIN_DIST) + compat_report = eec.check_compatibility(GPLv2, MIT_O_APACHE2, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'yes' assert _compat_type(compat_report) == 'license' # GPL-2.0-only -> MIT AND Apache-2.0 are NOT compatible def test_lic_expr_incompat(): - compat_report = eec.check_compatibility('GPL-2.0-only', 'MIT AND Apache-2.0', - usecase=UseCase.LIBRARY, - provisioning=Provisioning.BIN_DIST) + compat_report = eec.check_compatibility(GPLv2, MIT_A_APACHE2, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'no' assert _compat_type(compat_report) == 'license' @@ -68,39 +77,89 @@ def test_lic_expr_incompat(): # # GPL-2.0-only AND BSD-3-Clause -> MIT are compatible def test_expr_lic_compat(): - compat_report = eec.check_compatibility('GPL-2.0-only AND BSD-3-Clause', 'MIT', - usecase=UseCase.LIBRARY, - provisioning=Provisioning.BIN_DIST) + compat_report = eec.check_compatibility(GPLv2_A_BSD3, MIT, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'yes' - assert _compat_type(compat_report) == 'operator' + assert _compat_type(compat_report) == 'expression' # GPL-2.0-only AND BSD-3-Clause -> Apache-2.0 are NOT compatible def test_expr_lic_incompat(): - compat_report = eec.check_compatibility('GPL-2.0-only AND BSD-3-Clause', 'Apache-2.0', - usecase=UseCase.LIBRARY, - provisioning=Provisioning.BIN_DIST) + compat_report = eec.check_compatibility(GPLv2_A_BSD3, APACHE2, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'no' - assert _compat_type(compat_report) == 'operator' + assert _compat_type(compat_report) == 'expression' # # expression compat with expression # -# GPL-2.0-only AND BSD-3-Clause -> MIT AND X11 are compatible +# GPL-2.0-only AND BSD-3-Clause -> MIT AND 0BSD are compatible def test_expr_expr_compat(): - compat_report = eec.check_compatibility('GPL-2.0-only AND BSD-3-Clause', 'MIT AND X11', - usecase=UseCase.LIBRARY, - provisioning=Provisioning.BIN_DIST) + compat_report = eec.check_compatibility(GPLv2_A_BSD3, MIT_A_0BSD, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'yes' - assert _compat_type(compat_report) == 'operator' + assert _compat_type(compat_report) == 'expression' # GPL-2.0-only AND BSD-3-Clause -> Apache-2.0 AND ISC are NOT compatible def test_expr_expr_incompat(): - compat_report = eec.check_compatibility('GPL-2.0-only AND BSD-3-Clause', 'Apache-2.0 AND ISC', - usecase=UseCase.LIBRARY, - provisioning=Provisioning.BIN_DIST) + compat_report = eec.check_compatibility(GPLv2_A_BSD3, APACHE2_A_ISC, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + assert _compat_status(compat_report) == 'no' + assert _compat_type(compat_report) == 'expression' + + + +OUTBOUND = f' ({GPLv2_A_BSD3} AND {GPLv2_A_BSD3} AND ({GPLv2_A_BSD3} AND ({GPLv2_A_BSD3} AND ({GPLv2_A_BSD3} AND {GPLv2_A_BSD3} ))) )' + +INBOUND = f' ( {MIT_A_0BSD} AND {MIT_A_0BSD} AND {MIT_A_0BSD} AND {MIT_A_0BSD} ) ' +INBOUND += f' AND ( {MIT_O_APACHE2} OR {MIT_O_APACHE2} OR {MIT_O_APACHE2} OR ( {MIT_O_APACHE2} OR {MIT_O_APACHE2} OR ( {MIT_O_APACHE2} AND {MIT_O_APACHE2} )) )' + +def test_expr_expr_large_compat(): + + compat_report = eec.check_compatibility(OUTBOUND, + INBOUND, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + assert _compat_status(compat_report) == 'yes' + assert _compat_type(compat_report) == 'expression' + + +def test_expr_expr_large_incompat(): + + compat_report = eec.check_compatibility(OUTBOUND, + f'{INBOUND} AND {APACHE2}', + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + assert _compat_status(compat_report) == 'no' + assert _compat_type(compat_report) == 'expression' + + + +def test_expr_expr_with_1(): + + # OSADL supports GPL-2.0-only WITH Classpath-exception-2.0 + # - usecase is SNIPPET + compat_report = eec.check_compatibility(OUTBOUND, + f'{INBOUND} AND GPL-2.0-only WITH Classpath-exception-2.0', + usecase=UseCase.usecase_to_string(UseCase.SNIPPET), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + + assert _compat_status(compat_report) == 'no' + assert _compat_type(compat_report) == 'expression' + + +def test_expr_expr_with_2(): + + compat_report = eec.check_compatibility(OUTBOUND, + f'{INBOUND} AND GPL-2.0-only WITH Classpath-exception-2.0 AND {APACHE2}', + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'no' - assert _compat_type(compat_report) == 'operator' + assert _compat_type(compat_report) == 'expression' diff --git a/tests/python/test_same_compats.py b/tests/python/test_same_compats.py index ff013f3..22514d9 100644 --- a/tests/python/test_same_compats.py +++ b/tests/python/test_same_compats.py @@ -46,8 +46,8 @@ def test_supported(): for out_lic in licenses: ret = lt.outbound_inbound_compatibility(out_lic, in_lic, - UseCase.usecase_to_string(usecase), - Provisioning.provisioning_to_string(provisioning)) + usecase, + provisioning) results = ret['summary']['results'] if int(results['nr_valid']) == 0: unsupported += 1 diff --git a/tests/python/test_toolkit.py b/tests/python/test_toolkit.py index e61c5f4..03c4761 100644 --- a/tests/python/test_toolkit.py +++ b/tests/python/test_toolkit.py @@ -30,18 +30,18 @@ def test_provisioning_is_not_supported(): assert not lt.provisioning_supported(provisioning=Provisioning.WEBUI) def test_compat(): - ret = lt.outbound_inbound_compatibility("GPL-2.0-only", "BSD-3-Clause", UseCase.usecase_to_string(UseCase.LIBRARY), Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + ret = lt.outbound_inbound_compatibility("GPL-2.0-only", "BSD-3-Clause", UseCase.LIBRARY, Provisioning.BIN_DIST) logging.debug("ret: " + str(ret['summary']['results'])) print("... hesa: " + str(ret['summary']['results']['yes'])) assert ret['summary']['results']['yes']['count'] == 4 def test_incompat(): - ret = lt.outbound_inbound_compatibility("BSD-3-Clause", "GPL-2.0-only", UseCase.usecase_to_string(UseCase.LIBRARY), Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + ret = lt.outbound_inbound_compatibility("BSD-3-Clause", "GPL-2.0-only", UseCase.LIBRARY, Provisioning.BIN_DIST) logging.debug("ret: " + str(ret['summary']['results'])) assert ret['summary']['results']['no']['count'] == 2 def test_incompat(): - ret = lt.outbound_inbound_compatibility("BSD-3-Clause", "GPL-2.0-only", UseCase.usecase_to_string(UseCase.LIBRARY), Provisioning.provisioning_to_string(Provisioning.WEBUI)) + ret = lt.outbound_inbound_compatibility("BSD-3-Clause", "GPL-2.0-only", UseCase.LIBRARY, Provisioning.WEBUI) logging.debug("ret: " + str(ret['summary']['statuses'])) # all five resources fail on webui assert len(ret['summary']['statuses']['failure']) == 6 From c6b1f6d578c577498d357b2c06754038bc38a13a Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 2 Jun 2025 00:23:27 +0200 Subject: [PATCH 23/31] fix typo --- licomp_toolkit/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/licomp_toolkit/config.py b/licomp_toolkit/config.py index c605e45..69746df 100644 --- a/licomp_toolkit/config.py +++ b/licomp_toolkit/config.py @@ -8,7 +8,7 @@ cli_name = 'licomp-toolkit' module_name = 'licomp_toolkit' -disclaimer = 'This software and the data come with no gurantee. For more information read the disclaimers from the individual compatibility resources, and contact a lawyer to make sure your software is compliant.' +disclaimer = 'This software and the data come with no guarantee. For more information read the disclaimers from the individual compatibility resources, and contact a lawyer to make sure your software is compliant.' description = """ Simple command line tool to check compatibility between two licenses, given context From 8cad7a52391dd0131b3277c19f1d5d31d257b73f Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 2 Jun 2025 01:32:28 +0200 Subject: [PATCH 24/31] detailed report by default --- licomp_toolkit/toolkit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/licomp_toolkit/toolkit.py b/licomp_toolkit/toolkit.py index 89ed65a..a669e1b 100644 --- a/licomp_toolkit/toolkit.py +++ b/licomp_toolkit/toolkit.py @@ -300,7 +300,7 @@ def __init__(self): def __parsed_expression_to_name(self, parsed_expression): return parsed_expression[parsed_expression[COMPATIBILITY_TYPE]] - def check_compatibility(self, outbound, inbound, usecase, provisioning, detailed_report=False): + def check_compatibility(self, outbound, inbound, usecase, provisioning, detailed_report=True): # Check usecase try: usecase = UseCase.string_to_usecase(usecase) @@ -335,7 +335,7 @@ def __check_compatibility(self, inbound_parsed, usecase, provisioning, - detailed_report=False): + detailed_report=True): outbound_type = outbound_parsed[COMPATIBILITY_TYPE] compat_object = { From bca8c13034e8c6cc5b21eab1f8657184284da416 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 2 Jun 2025 01:34:05 +0200 Subject: [PATCH 25/31] add vaildate method overriding the one from Licomp --- licomp_toolkit/__main__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/licomp_toolkit/__main__.py b/licomp_toolkit/__main__.py index bc40070..46c3ea2 100755 --- a/licomp_toolkit/__main__.py +++ b/licomp_toolkit/__main__.py @@ -15,6 +15,7 @@ from licomp_toolkit.config import cli_name from licomp_toolkit.config import description from licomp_toolkit.config import epilog +from licomp_toolkit.schema_checker import LicompToolkitSchemaChecker from licomp.main_base import LicompParser from licomp.interface import UseCase @@ -35,6 +36,10 @@ def __init__(self, name, description, epilog, default_usecase, default_provision def __normalize_license(self, lic_name): return self.flame.expression_license(lic_name, update_dual=False)['identified_license'] + def validate(self, args): + LicompToolkitSchemaChecker().validate_file(args.file_name, deep=False) + return "OK", ReturnCodes.LICOMP_OK.value, None + def verify(self, args): formatter = LicompToolkitFormatter.formatter(self.args.output_format) try: From 2a7352db30e5324b8f6dcbe1bd846e3ddd94e539 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 2 Jun 2025 01:35:29 +0200 Subject: [PATCH 26/31] remove blank --- licomp_toolkit/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/licomp_toolkit/__main__.py b/licomp_toolkit/__main__.py index 46c3ea2..7cd0060 100755 --- a/licomp_toolkit/__main__.py +++ b/licomp_toolkit/__main__.py @@ -39,7 +39,7 @@ def __normalize_license(self, lic_name): def validate(self, args): LicompToolkitSchemaChecker().validate_file(args.file_name, deep=False) return "OK", ReturnCodes.LICOMP_OK.value, None - + def verify(self, args): formatter = LicompToolkitFormatter.formatter(self.args.output_format) try: From e47c72ab5137777c1a8665cb488550b8ba62ef5c Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Tue, 3 Jun 2025 18:08:30 +0200 Subject: [PATCH 27/31] prevent out ot validation --- licomp_toolkit/__main__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/licomp_toolkit/__main__.py b/licomp_toolkit/__main__.py index 7cd0060..be16595 100755 --- a/licomp_toolkit/__main__.py +++ b/licomp_toolkit/__main__.py @@ -37,8 +37,8 @@ def __normalize_license(self, lic_name): return self.flame.expression_license(lic_name, update_dual=False)['identified_license'] def validate(self, args): - LicompToolkitSchemaChecker().validate_file(args.file_name, deep=False) - return "OK", ReturnCodes.LICOMP_OK.value, None + LicompToolkitSchemaChecker().validate_file(args.file_name, deep=True) + return None, ReturnCodes.LICOMP_OK.value, None def verify(self, args): formatter = LicompToolkitFormatter.formatter(self.args.output_format) @@ -142,7 +142,8 @@ def main(): res, code, err, func = lct_parser.run_noexit() if _working_return_code(code): - print(res) + if res: + print(res) else: print(res, file=sys.stderr) From bcc9a43836b74af0402c31d7ac11993011feac20 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Wed, 4 Jun 2025 20:47:15 +0200 Subject: [PATCH 28/31] update schema with licomp (copy for now) --- licomp_toolkit/data/reply_schema.json | 114 +++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/licomp_toolkit/data/reply_schema.json b/licomp_toolkit/data/reply_schema.json index 974c257..c64bc04 100644 --- a/licomp_toolkit/data/reply_schema.json +++ b/licomp_toolkit/data/reply_schema.json @@ -63,6 +63,24 @@ "$ref": "#/$defs/license", "description" : "The outbound license expression." }, + "compatibility_details": { + "oneOf": [ + { + "type": "object", + "properties": { + "compatibilities": { + "type": "array", + "items": { + "$ref": "#/$defs/licomp_object" + } + } + } + }, + { + "type": "null" + } + ] + }, "compatibility_object": { "type" : "object", "anyOf": [ @@ -75,7 +93,7 @@ ] } }, - "required": [ "compatibility", "compatibility_type", "compatibility_check", "inbound_license" , "outbound_license", "compatibility_object"], + "required": [ "compatibility", "compatibility_type", "compatibility_check", "inbound_license" , "outbound_license", "compatibility_object", "compatibility_details"], "additionalProperties" : false }, { @@ -99,6 +117,9 @@ "$ref": "#/$defs/license", "description" : "The inbound license expression." }, + "compatibility_details": { + "type": "null" + }, "operator": { "enum": [ "AND", "OR" ], "description" : "The logical operator between two license expressions." @@ -138,6 +159,97 @@ "outbound-license -> inbound-license" ], "description" : "A text describing if outbound license or expression is checked for compatiblility against inbound license or expression." + }, + "licomp_object": { + "type" : "object", + "properties" : { + "status" : { + "enum": [ "failure", "success" ], + "description" : "Overall status indicating if the compatibility check succeeded." + }, + "status_details" : { + "type" : "object", + "properties" : { + "provisioning_status": { + "enum": [ "failure", "success" ], + "description" : "Status indicating if the provisioning is supported." + }, + "usecase_status": { + "enum": [ "failure", "success" ], + "description" : "Status indicating if the usecase is supported." + }, + "license_supported_status": { + "enum": [ "failure", "success" ], + "description" : "Status indicating if the licenses are supported." + } + } + }, + "outbound" : { + "type" : "string", + "minLength": 1, + "description" : "The outbound license" + }, + "inbound" : { + "type" : "string", + "minLength": 1, + "description" : "The inbound license" + }, + "usecase" : { + "type" : "string", + "enum": ["library", "compiler", "snippet", "tool", "test"], + "description" : "Usecase for the compatibility check, e.g. library (as in using the inbound licensed component as a library)" + }, + "provisioning" : { + "type" : "string", + "enum": ["source-code-distribution", "binary-distribution", "local-use", "provide-service", "provide-webui"], + "description" : "The way the component is provided to the user for the compatibility check, e.g. binary-distribution" + }, + "modification" : { + "type" : "string", + "description" : "Has the component been modified. Currently not used/implemented.", + "enum": [ "unmodified", "modified"] + }, + "compatibility_status" : { + "enum": [ "yes", "no", "depends", "unknown", "unsupported", null], + "description" : "The compatbility between the Outbound and Inbound license expressions" + }, + "explanation" : { + "type" : [ "string", "null" ], + "description" : "A text describing the compatiblity, e.g. how the compatibility was determined" + }, + "api_version" : { + "type" : "string", + "pattern": "^[0-9].[0-9](.[0-9]){0,1}$", + "description" : "The api version of the program providing the reply" + }, + "resource_name1" : { + "type" : "string", + "minLength": 1, + "description" : "The name of the program providing the reply, e.g. licomp-osadl" + }, + "resource_version" : { + "type" : "string", + "pattern": "^[0-9].[0-9](.[0-9]){0,1}$", + "description" : "The versions of the program providing the reply, e.g. licomp-osadl" + }, + "resource_disclaimer" : { + "type" : "string", + "minLength": 10, + "description" : "A disclaimer of the program providing the reply." + }, + "data_url" : { + "type" : "string", + "minLength": 10, + "description" : "A URL pointing to the data used to provide the resulting compatibility." + }, + "resource_url" : { + "type" : "string", + "minLength": 10, + "description" : "A URL pointing to the project page (or similar) for the program providing the resulting compatibility." + } + }, + "required" : [ "status", "status_details", "outbound", "inbound", "usecase", "provisioning", "modification", "compatibility_status", "explanation", "api_version", "resource_name1", "resource_version", "resource_disclaimer", "data_url", "resource_url"], + "additionalProperties" : false } } } From 0318f847f3dfc7d09e4e3f4e5492df639385e96b Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Fri, 13 Jun 2025 01:38:39 +0200 Subject: [PATCH 29/31] simplify code --- licomp_toolkit/toolkit.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/licomp_toolkit/toolkit.py b/licomp_toolkit/toolkit.py index a669e1b..4852182 100644 --- a/licomp_toolkit/toolkit.py +++ b/licomp_toolkit/toolkit.py @@ -79,11 +79,12 @@ def __summarize_compatibility(self, compatibilities, outbound, inbound, usecase, statuses = {} compats = {} compatibilities['nr_licomp'] = len(self.licomp_resources()) - for resource_name in self.licomp_resources(): - compat = compatibilities["compatibilities"][resource_name] + # for resource_name in self.licomp_resources(): + for compat in compatibilities["compatibilities"]: + logging.debug(f': {compat}') logging.debug(f': {compat["resource_name"]}') - self.__add_to_list(statuses, compat['status'], compat['resource_name']) - self.__add_to_list(compats, compat['compatibility_status'], compat['resource_name']) + self.__add_to_list(statuses, compat['status'], compat) + self.__add_to_list(compats, compat['compatibility_status'], compat) compatibilities["summary"]["resources"] = [f'{x.name()}:{x.version()}' for x in self.licomp_resources().values()] compatibilities["summary"]["outbound"] = outbound compatibilities["summary"]["inbound"] = inbound @@ -117,14 +118,14 @@ def outbound_inbound_compatibility(self, outbound, inbound, usecase, provisionin logging.debug(f'{inbound} {outbound} ') compatibilities = {} - compatibilities['compatibilities'] = {} + compatibilities['compatibilities'] = [] for resource_name in self.licomp_resources(): resource = self.licomp_resources()[resource_name] logging.debug(f'-- resource: {resource.name()}') compat = resource.outbound_inbound_compatibility(outbound, inbound, usecase, provisioning=provisioning) - compatibilities['compatibilities'][compat['resource_name']] = compat + compatibilities['compatibilities'].append(compat) self.__summarize_compatibility(compatibilities, outbound, inbound, usecase, provisioning) self.__add_meta(compatibilities) @@ -224,7 +225,8 @@ def check_compatibility(self, compat_object['compatibility'] = self.__compatibility_status(compat) if detailed_report: compat_object['compatibility_details'] = compat - + else: + compat_object['compatibility_details'] = None compat_object['inbound_license'] = lic compat_object['outbound_license'] = outbound compat_object['compatibility_object'] = {} @@ -236,6 +238,7 @@ def check_compatibility(self, compat_object['inbound_license'] = self.le_parser.to_string(parsed_expression) compat_object['outbound_license'] = outbound + compat_object['compatibility_details'] = None operands_object = [] for operand in operands: operand_compat = self.check_compatibility(outbound, operand, usecase, provisioning, detailed_report=detailed_report) @@ -358,8 +361,10 @@ def __check_compatibility(self, detailed_report) compat_object['compatibility'] = compat['compatibility'] compat_object['compatibility_object'] = compat + compat_object['compatibility_details'] = None elif outbound_type == 'expression': + compat_object['compatibility_details'] = None compat_object['compatibility_check'] = f'outbound-expression -> inbound-{inbound_parsed["compatibility_type"]}' operator = outbound_parsed['operator'] operands = outbound_parsed['operands'] From 7406d8283e9ceadc95e412df1cf5422bb0f30710 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Fri, 13 Jun 2025 01:39:15 +0200 Subject: [PATCH 30/31] add log --- licomp_toolkit/schema_checker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/licomp_toolkit/schema_checker.py b/licomp_toolkit/schema_checker.py index 8ef2abd..142c0b5 100644 --- a/licomp_toolkit/schema_checker.py +++ b/licomp_toolkit/schema_checker.py @@ -32,7 +32,8 @@ def __validate_deeply(self, compat): else: details = compat_object['compatibility_details'] compatibilities = details['compatibilities'] - for compatibility_object in compatibilities.values(): + for compatibility_object in compatibilities: + logging.debug(f' {compatibility_object["resource_name"]}') inner_validations = lt.validate(compatibility_object) validations += 1 logging.debug('Validation OK') From 1634172341c7b9a2a331c74005c275fb56f2340e Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Fri, 13 Jun 2025 01:39:58 +0200 Subject: [PATCH 31/31] script for basic user tests --- tests/shell/test_validate.sh | 55 ++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100755 tests/shell/test_validate.sh diff --git a/tests/shell/test_validate.sh b/tests/shell/test_validate.sh new file mode 100755 index 0000000..b0ba71f --- /dev/null +++ b/tests/shell/test_validate.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +if [ "$1" == "--local" ] +then + IMPLEMENTATIONS=../licomp:../licomp-dwheeler:../licomp-hermione:../licomp-osadl:../licomp-reclicense:../licomp-proprietary::../licomp-gnuguide:. + shift +fi + + +check_return_value() +{ + EXPEXcTED=$1 + ACTUAL=$2 + COMMAND="$3" + + if [ $EXPECTED -ne $ACTUAL ] + then + echo "ERROR" + echo "Return values differ" + echo " Expected: $EXPECTED" + echo " Actual: $ACTUAL" + echo " Command: $COMMAND" + exit 1 + fi +} + + +REPLY_FILE=licomp-toolkit-reply.json + + +validate_reply() +{ + INBOUND="$1" + OUTBOUND="$2" + EXPECTED=$3 + PYTHONPATH=$IMPLEMENTATIONS:${PYTHONPATH}:. python3 licomp_toolkit/__main__.py verify -il "$INBOUND" -ol "$OUTBOUND" > $REPLY_FILE + PYTHONPATH=$IMPLEMENTATIONS:${PYTHONPATH}:. python3 licomp_toolkit/__main__.py validate $REPLY_FILE + RET=$? + printf "%-75s" "reply from verify -il \"$INBOUND\" -ol \"$OUTBOUND\"" + check_return_value $RET $EXPECTED "validate $REPLY_FILE (verify -il \"$INBOUND\" -ol \"$OUTBOUND\")" + echo OK +} + +validate_reply MIT MIT 0 +validate_reply MIT BSD-3-Clause 0 +validate_reply MIT "BSD-3-Clause OR MIT" 0 +validate_reply "BSD-3-Clause OR MIT" MIT 0 +validate_reply "BSD-3-Clause OR MIT" "X11 AND ISC" 0 + +exit +rm $REPLY_FILE