diff --git a/devel/licomp-toolkit b/devel/licomp-toolkit index c9ad5d1..f061b27 100755 --- a/devel/licomp-toolkit +++ b/devel/licomp-toolkit @@ -16,4 +16,4 @@ then ARGS="verify -il MIT -ol \"MIT OR X11\"" fi -PYTHONPATH=${EXTRA_PYTHONPATH}:${PYTHONPATH} ${SCRIPT_DIR}/licomp_toolkit/__main__.py $* $ARGS +echo PYTHONPATH=${EXTRA_PYTHONPATH}:${PYTHONPATH} ${SCRIPT_DIR}/licomp_toolkit/__main__.py $* $ARGS | bash diff --git a/licomp_toolkit/__main__.py b/licomp_toolkit/__main__.py index be16595..9c128d6 100755 --- a/licomp_toolkit/__main__.py +++ b/licomp_toolkit/__main__.py @@ -43,12 +43,35 @@ def validate(self, args): def verify(self, args): formatter = LicompToolkitFormatter.formatter(self.args.output_format) try: + if args.no_verbose: + detailed_report = False + else: + detailed_report = True + + resources = args.resources + new_resources = [] + unsupported = [] + for resource in resources: + if 'licomp' not in resource: + resource = f'licomp_{resource}' + else: + resource = resource.replace('-', '_') + + if not self.resource_avilable(resource): + unsupported.append(resource) + else: + new_resources.append(resource) + if unsupported: + return f'Resource(s) {", ".join(unsupported)} is/are not supported', ReturnCodes.LICOMP_UNSUPPORTED_RESOURCE.value, True + + resources = new_resources 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) + resources=resources, + detailed_report=detailed_report) ret_code = compatibility_status_to_returncode(compatibilities['compatibility']) return formatter.format_compatibilities(compatibilities), ret_code, False @@ -106,6 +129,9 @@ def versions(self, args): formatter = LicompToolkitFormatter.formatter(args.output_format) return formatter.format_licomp_versions(self.licomp_toolkit.versions()), ReturnCodes.LICOMP_OK.value, False + def resource_avilable(self, resource): + return resource in self.licomp_toolkit.licomp_resources().keys() + def _working_return_code(return_code): return return_code < ReturnCodes.LICOMP_LAST_SUCCESSFUL_CODE.value @@ -118,8 +144,20 @@ def main(): UseCase.LIBRARY, Provisioning.BIN_DIST) + parser = lct_parser.parser subparsers = lct_parser.sub_parsers() + parser.add_argument('-r', '--resources', + type=str, + action='append', + help='use only specified licomp resource', + default=[]) + + parser.add_argument('-nv', '--no-verbose', + action='store_true', + help='keep compatibility report as short as possible', + default=[]) + # Command: list supported parser_sr = subparsers.add_parser('supported-resources', help='List all supported Licomp resources') parser_sr.set_defaults(which="supported_resources", func=lct_parser.supported_resources) diff --git a/licomp_toolkit/data/reply_schema.json b/licomp_toolkit/data/reply_schema.json index c64bc04..d02713a 100644 --- a/licomp_toolkit/data/reply_schema.json +++ b/licomp_toolkit/data/reply_schema.json @@ -31,12 +31,42 @@ "description" : "Has the component been modified. Currently not used/implemented.", "enum": [ "unmodified", "modified"] }, + "resources": { + "description": "Which resources are available, regardless of context", + "type": "array", + "items": { + "type" : "string" + } + }, + "available_resources": { + "description": "Which resources are available given the context", + "type": "array", + "items": { + "type" : "string" + } + }, + "unavailable_resources": { + "description": "Which resources are not available given the context", + "type": "array", + "items": { + "type": "object", + "properties": { + "resource": { + "type": "string", + "description": "The resource name" +}, + "reasons": { + "type": "string", + "description": "The reason the resource is not available" } + } + } + }, "compatibility" : { "$ref": "#/$defs/compatibility", "description" : "The inbound license expression." } }, - "required" : [ "compatibility_report", "compatibility", "outbound", "inbound" , "usecase" ], + "required" : [ "compatibility_report", "compatibility", "outbound", "inbound" , "usecase", "resources", "unavailable_resources", "available_resources" ], "additionalProperties" : false, "$defs": { "compatibility_object": { diff --git a/licomp_toolkit/format.py b/licomp_toolkit/format.py index 7b7b0f6..263ef75 100644 --- a/licomp_toolkit/format.py +++ b/licomp_toolkit/format.py @@ -47,16 +47,93 @@ def format_licomp_resources(self, licomp_resources): def format_licomp_licenses(self, licomp_licenses): return "\n".join(licomp_licenses) + def __get_responses(self, results, indent=""): + output = [] + for res in ['yes', 'no', 'schneben']: + result = results.get(res) + if not result: + count = 0 + else: + count = result["count"] + output.append(f'{indent}{res}: {count}') + + return output + + def __compatibility_statuses(self, statuses, indent=""): + output = [] + for status, values in statuses.items(): + resources = [] + for value_object in values: + resources.append(value_object['resource_name']) + output.append(f'{indent}{status}: {", ".join(resources)}') + + return output + + def __statuses(self, statuses, indent=""): + output = [] + for status, values in statuses.items(): + resources = [] + for value_object in values: + resources.append(value_object['resource_name']) + output.append(f'{indent}{status}: {", ".join(resources)}') + + return output + + def _format_compat(self, compat): + PAREN_OPEN = '(' + PAREN_START = ')' + return f'{PAREN_OPEN}{compat}{PAREN_START}' + + def format_compatibilities_object(self, compat_object, indent=''): + compatibility_check = compat_object["compatibility_check"] + output = [] + + if compatibility_check == "outbound-license -> inbound-license": + if not compat_object["compatibility_object"]: + pass + else: + compat_object = compat_object["compatibility_object"] + details = compat_object["compatibility_details"] + summary = details["summary"] + + output.append(f'{indent}{compat_object["outbound_license"]} -> {compat_object["inbound_license"]} {self._format_compat(compat_object["compatibility"])}') + output.append(f'{indent} compatibility: {compat_object["compatibility"]}') + output.append(f'{indent} compatibility details:') + output += self.__compatibility_statuses(summary['compatibility_statuses'], f'{indent} ') + if compatibility_check == "outbound-license -> inbound-expression": + operator = compat_object["compatibility_object"]["operator"] + output.append(f'{indent}{operator} {self._format_compat(compat_object["compatibility"])}') + for operand in compat_object["compatibility_object"]["operands"]: + res = self.format_compatibilities_object(operand['compatibility_object'], indent=f'{indent} ') + output.append(res) + + if compatibility_check == "outbound-expression -> inbound-license": + operator = compat_object["operator"] + output.append(f'{indent}{operator} {self._format_compat(compat_object["compatibility"])}') + for operand in compat_object["operands"]: + res = self.format_compatibilities_object(operand['compatibility_object'], indent=f'{indent} ') + output.append(res) + if compatibility_check == "outbound-expression -> inbound-expression": + operator = compat_object["operator"] + compat = compat_object["compatibility"] + output.append(f'{indent}{operator} {self._format_compat(compat)}') + for operand in compat_object['operands']: + res = self.format_compatibilities_object(operand['compatibility_object'], indent=f'{indent} ') + output.append(f'{res}') + + return "\n".join(output) + 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])}') + output.append(f'outbound: {compat["outbound"]}') + output.append(f'inbound: {compat["inbound"]}') + output.append(f'resources: {", ".join(compat["resources"])}') + output.append(f'provisioning: {compat["provisioning"]}') + output.append(f'usecase: {compat["usecase"]}') + output.append(f'compatibility: {compat["compatibility"]}') + output.append('report:') + output.append(self.format_compatibilities_object(compat["compatibility_report"], ' ')) + return "\n".join(output) def format_licomp_versions(self, licomp_versions): diff --git a/licomp_toolkit/toolkit.py b/licomp_toolkit/toolkit.py index 4852182..b3466ee 100644 --- a/licomp_toolkit/toolkit.py +++ b/licomp_toolkit/toolkit.py @@ -30,6 +30,13 @@ from licomp_toolkit.config import my_supported_api_version class LicompToolkit(Licomp): + """A class implementing Licomp, but for a misc Licomp resources + and packaging the responses into a new reply + (licomp_toolkit/reply_schema.json). + + LicompToolkit can check a single license agaisnt another for + compatibility, but not license expressions. + """ def __init__(self): Licomp.__init__(self) @@ -74,11 +81,11 @@ def licomp_resources(self): self.LICOMP_RESOURCES[licomp_instance.name()] = licomp_instance return self.LICOMP_RESOURCES - def __summarize_compatibility(self, compatibilities, outbound, inbound, usecase, provisioning): + def __summarize_compatibility(self, compatibilities, outbound, inbound, usecase, provisioning, resources): compatibilities["summary"] = {} statuses = {} compats = {} - compatibilities['nr_licomp'] = len(self.licomp_resources()) + compatibilities['nr_licomp'] = len(resources) # for resource_name in self.licomp_resources(): for compat in compatibilities["compatibilities"]: logging.debug(f': {compat}') @@ -114,20 +121,23 @@ def __summarize_compatibility(self, compatibilities, outbound, inbound, usecase, compatibilities['summary']['results'] = results # override top class - def outbound_inbound_compatibility(self, outbound, inbound, usecase, provisioning): + def outbound_inbound_compatibility(self, outbound, inbound, usecase, provisioning, resources=None): logging.debug(f'{inbound} {outbound} ') compatibilities = {} compatibilities['compatibilities'] = [] - for resource_name in self.licomp_resources(): + if not resources: + resources = self.licomp_resources().keys() + + for resource_name in 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'].append(compat) - self.__summarize_compatibility(compatibilities, outbound, inbound, usecase, provisioning) + self.__summarize_compatibility(compatibilities, outbound, inbound, usecase, provisioning, resources) self.__add_meta(compatibilities) return compatibilities @@ -171,16 +181,14 @@ def name(self): return cli_name class LicenseExpressionChecker(): + """This class can check compatibility between a single outbound + license (e.g GPL-2.0-only) against an inbound license expression + (e.g. MIT OR X11) + """ 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) + self.licomp_toolkit = LicompToolkit() def __compatibility_status(self, compatibility): status = compatibility['summary']['results'] @@ -208,6 +216,7 @@ def check_compatibility(self, parsed_expression, usecase, provisioning, + resources, detailed_report=True): compat_object = { @@ -218,10 +227,11 @@ def check_compatibility(self, 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 = self.licomp_toolkit.outbound_inbound_compatibility(outbound, + lic, + usecase, + provisioning, + resources) compat_object['compatibility'] = self.__compatibility_status(compat) if detailed_report: compat_object['compatibility_details'] = compat @@ -241,7 +251,7 @@ def check_compatibility(self, compat_object['compatibility_details'] = None operands_object = [] for operand in operands: - operand_compat = self.check_compatibility(outbound, operand, usecase, provisioning, detailed_report=detailed_report) + operand_compat = self.check_compatibility(outbound, operand, usecase, provisioning, resources, detailed_report=detailed_report) operand_object = { 'compatibility_object': operand_compat, 'compatibility': operand_compat['compatibility'], @@ -295,42 +305,82 @@ def summarise_compatibilities(self, operator, operands): class ExpressionExpressionChecker(): + """ + This class can check, for compatibility; + * inbound license expression (e.g. MIT OR Apache-2.0) + * against outbound license expression (e.g. GPL-2.0-only OR BSD-2-Clause) + """ def __init__(self): self.le_checker = LicenseExpressionChecker() self.le_parser = LicenseExpressionParser() + self.licomp_toolkit = LicompToolkit() 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=True): + def check_compatibility(self, outbound, inbound, usecase, provisioning, resources=None, detailed_report=True): + # Check usecase try: - usecase = UseCase.string_to_usecase(usecase) + usecase_obj = 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) + provisioning_obj = 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) + licomp_resources = list(self.licomp_toolkit.licomp_resources().keys()) + if not resources: + resources = licomp_resources + else: + resources = resources + + unavailable_resources = [] + for resource in resources: + resource_object = self.licomp_toolkit.licomp_resources()[resource] + unavailable_reasons = [] + + # is usecase supported by resource + if not resource_object.usecase_supported(UseCase.string_to_usecase(usecase)): + unavailable_reasons.append(f'Usecase "{usecase}" not supported') + + # is prov case supported by resource + if not resource_object.provisioning_supported(Provisioning.string_to_provisioning(provisioning)): + unavailable_reasons.append(f'Provisioning case "{provisioning}" not supported') + + if unavailable_reasons: + unavailable_resources.append({ + "resource": resource, + 'reasons': ", ".join(unavailable_reasons), + }) + + unavailable_resource_keys = [resource['resource'] for resource in unavailable_resources] + available_resources = [resource for resource in resources if resource not in unavailable_resource_keys] + + 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, + usecase_obj, + provisioning_obj, + resources, detailed_report) return { 'inbound': inbound, 'outbound': outbound, - 'usecase': UseCase.usecase_to_string(usecase), - 'provisioning': Provisioning.provisioning_to_string(provisioning), + 'usecase': usecase, + 'resources': resources, + 'provisioning': provisioning, 'compatibility': compatibility_object['compatibility'], 'compatibility_report': compatibility_object, + 'unavailable_resources': unavailable_resources, + 'available_resources': available_resources, } def __check_compatibility(self, @@ -338,6 +388,7 @@ def __check_compatibility(self, inbound_parsed, usecase, provisioning, + resources, detailed_report=True): outbound_type = outbound_parsed[COMPATIBILITY_TYPE] @@ -358,6 +409,7 @@ def __check_compatibility(self, inbound_parsed, usecase, provisioning, + resources, detailed_report) compat_object['compatibility'] = compat['compatibility'] compat_object['compatibility_object'] = compat @@ -381,6 +433,7 @@ def __check_compatibility(self, inbound_parsed, usecase, provisioning, + resources, detailed_report) operand_object = { 'compatibility_object': operand_compat, diff --git a/tests/shell/test_validate.sh b/tests/shell/test_validate.sh index b0ba71f..12fbc11 100755 --- a/tests/shell/test_validate.sh +++ b/tests/shell/test_validate.sh @@ -13,8 +13,8 @@ fi check_return_value() { - EXPEXcTED=$1 - ACTUAL=$2 + ACTUAL=$1 + EXPECTED=$2 COMMAND="$3" if [ $EXPECTED -ne $ACTUAL ] @@ -36,20 +36,50 @@ validate_reply() { INBOUND="$1" OUTBOUND="$2" - EXPECTED=$3 - PYTHONPATH=$IMPLEMENTATIONS:${PYTHONPATH}:. python3 licomp_toolkit/__main__.py verify -il "$INBOUND" -ol "$OUTBOUND" > $REPLY_FILE + VERIFY_EXPECTED=$3 + VALIDATE_EXPECTED=$4 + PYTHONPATH=$IMPLEMENTAIONS:${PYTHONPATH}:. python3 licomp_toolkit/__main__.py verify -il "$INBOUND" -ol "$OUTBOUND" > $REPLY_FILE + RET=$? +# echo " ---------------------------||||| RET: $RET == $VERIFY_EXPECTED" + printf "%-80s" "verify -il \"$INBOUND\" -ol \"$OUTBOUND\"" + check_return_value $RET $VERIFY_EXPECTED "verify -il \"$INBOUND\" -ol \"$OUTBOUND\"" + echo " OK" + 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 + printf "\\ %-78s" "validate -il \"$INBOUND\" -ol \"$OUTBOUND\"" + check_return_value $RET $VALIDATE_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 +compatibles() +{ + validate_reply MIT MIT 0 0 + validate_reply MIT BSD-3-Clause 0 0 + validate_reply MIT "BSD-3-Clause OR MIT" 0 0 + validate_reply "BSD-3-Clause OR MIT" MIT 0 0 + validate_reply "BSD-3-Clause OR MIT" "BSD-2-Clause AND ISC" 0 0 + validate_reply "BSD-3-Clause OR GPL-2.0-only" "BSD-2-Clause AND ISC" 0 0 + validate_reply "BSD-3-Clause OR GPL-2.0-only" "BSD-2-Clause AND Apache-2.0" 0 0 + validate_reply "BSD-2-Clause OR Apache-2.0" "GPL-2.0-only" 0 0 + validate_reply "Apache-2.0" "GPL-3.0-only" 0 0 +# validate_reply "GPL-3.0-only" "Apache-2.0" 0 0 +} + +incompatibles() +{ + validate_reply GPL-2.0-only MIT 2 0 + validate_reply "BSD-3-Clause AND GPL-2.0-only" "BSD-2-Clause AND ISC" 2 0 + validate_reply "GPL-2.0-only" "BSD-2-Clause AND Apache-2.0" 2 0 + validate_reply "BSD-3-Clause AND GPL-2.0-only" "BSD-2-Clause AND Apache-2.0" 2 0 + validate_reply "GPL-2.0-only" "BSD-2-Clause AND Apache-2.0" 2 0 + validate_reply "BSD-2-Clause AND Apache-2.0" "GPL-2.0-only" 2 0 + validate_reply "Apache-2.0" "GPL-2.0-only" 2 0 +} + +echo "Compatibles" +compatibles +echo "Incompatibles" +incompatibles rm $REPLY_FILE