Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
7085bd6
first attempt to apply policy in report
hesa Jan 12, 2026
c8ec28f
unit tests for policy
hesa Jan 12, 2026
a46fc9b
add more policy tests
hesa Jan 12, 2026
e90786a
new strategy to add policy calcuations to report
hesa Jan 13, 2026
0182f89
add command: apply-license-policy
hesa Jan 17, 2026
5d9e798
remove compatibility_type, add check_class, fix typo
hesa Jan 18, 2026
673c75f
add policy format
hesa Jan 18, 2026
2b949e5
only check license if needed, remove printoutd
hesa Jan 18, 2026
e0538ec
remove unused defs
hesa Jan 19, 2026
d53ec34
add (c) and license for test policy
hesa Jan 20, 2026
7cfb4ac
format text for outbound-license works
hesa Jan 20, 2026
bbca608
add command and option for applying license policy
hesa Jan 20, 2026
76605d9
format text for outbound-license works
hesa Jan 20, 2026
9901e7f
add support for applying license policy on outbound
hesa Jan 20, 2026
2ba5c89
resources, usecase, provisioning handled in verify
hesa Jan 22, 2026
7709198
add simple exception class
hesa Jan 22, 2026
1976c13
expr-expr seems to work
hesa Feb 5, 2026
ffd1dfc
using the precalculated values
hesa Feb 6, 2026
b60940c
remove test variables
hesa Feb 6, 2026
d8276c7
add " around arguments
hesa Feb 6, 2026
9a0e418
update to new data struct
hesa Feb 6, 2026
d42c24d
add verbose to formatting
hesa Feb 7, 2026
48aaf7c
simplify formatting
hesa Feb 10, 2026
1d6f179
test license policy
hesa Feb 12, 2026
aa7b741
remove debug printouts, fix typos
hesa Feb 16, 2026
756a105
add --debug option, use verbose for verbose oputput
hesa Feb 16, 2026
5c32a54
clean up
hesa Feb 16, 2026
30a2ce3
remove unused code
hesa Feb 16, 2026
defe5e0
fix supported resources failure
hesa Feb 16, 2026
a39288c
fix flake issues
hesa Feb 16, 2026
b134ccd
adjust to new format
hesa Feb 16, 2026
a3f6c61
extended license expression tests
hesa Feb 16, 2026
bf80c19
adjust to new format
hesa Feb 16, 2026
83b3eeb
adjust to new format
hesa Feb 16, 2026
df48fc7
add test file
hesa Feb 16, 2026
1ccc064
remove tmp file
hesa Feb 16, 2026
89999cb
Merge branch 'main' into hesa-add-policy
hesa Feb 16, 2026
e9a663b
remove file
hesa Feb 16, 2026
a790bb9
Merge remote-tracking branch 'refs/remotes/origin/hesa-add-policy' in…
hesa Feb 16, 2026
c21aa31
add licomp_toolkit_file_version
hesa Feb 16, 2026
b61694a
add license policy
hesa Feb 16, 2026
b9a2911
add return codes
hesa Feb 16, 2026
d6b64a4
Autofix (Push test pipeline / generate)
github-actions[bot] Feb 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .reuse/dep5
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ Files: README.md docs/*.md
Copyright: Henrik Sandklef
License: GPL-3.0-or-later

Files: tests/policy/license-policy.json tests/policy/bad-license-policy.json
Copyright: Henrik Sandklef
License: GPL-3.0-or-later
9 changes: 8 additions & 1 deletion devel/licomp-toolkit
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ fi
if [ "$1" = "" ]
then
ARGS="verify -il MIT -ol \"MIT OR X11\""
else
ARGS=""
for arg in "$@"
do
ARGS="$ARGS \"$arg\""
done
fi

echo PYTHONPATH=${EXTRA_PYTHONPATH}:${PYTHONPATH} ${SCRIPT_DIR}/licomp_toolkit/__main__.py $* $ARGS | bash

echo PYTHONPATH=${EXTRA_PYTHONPATH}:${PYTHONPATH} ${SCRIPT_DIR}/licomp_toolkit/__main__.py $ARGS | bash
87 changes: 82 additions & 5 deletions licomp_toolkit/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,28 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later

import json
from json.decoder import JSONDecodeError
import logging
import sys

from licomp.interface import LicompException

from licomp_toolkit.return_codes import LicompToolkitReturnCodes
from licomp_toolkit.toolkit import LicompToolkit
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 module_name
from licomp_toolkit.config import licomp_toolkit_file_version
from licomp_toolkit.config import epilog
from licomp_toolkit.schema_checker import LicompToolkitSchemaChecker
from licomp_toolkit.suggester import OutboundSuggester
from licomp_toolkit.display_compatibility import DisplayCompatibility
from licomp_toolkit.utils import resources_to_use
from licomp_toolkit.utils import default_resources
from licomp_toolkit.license_policy import LicensePolicyHandler

from licomp.main_base import LicompParser
from licomp.interface import UseCase
Expand All @@ -43,6 +50,47 @@ def validate(self, args):
LicompToolkitSchemaChecker().validate_file(args.file_name, deep=True)
return None, ReturnCodes.LICOMP_OK.value, None

def _read_report_file(self, report_file):
try:
with open(report_file) as fp:
report = json.load(fp)
meta = report['meta']
meta_OK = meta['tool'] == module_name
meta_OK = meta['file_version'] == licomp_toolkit_file_version
file_OK = meta['file'] == 'verification'
if not (meta_OK and file_OK):
err_msg = f'File "{report_file}" not in Licomp Toolkit\'s license policy format.'
err_code = LicompToolkitReturnCodes.LICOMP_TOOLKIT_INVALID_FILE.value
return None, err_code, err_msg
return report, ReturnCodes.LICOMP_OK.value, None
except (FileNotFoundError, JSONDecodeError):
err_msg = f'File "{report_file}" not found or not in JSON format'
err_code = LicompToolkitReturnCodes.LICOMP_TOOLKIT_INVALID_FILE.value
return None, err_code, err_msg
except (KeyError):
err_msg = f'File "{report_file}" not in Licomp Toolkit\'s license policy format.'
err_code = LicompToolkitReturnCodes.LICOMP_TOOLKIT_INVALID_FILE.value
return None, err_code, err_msg

def apply_license_policy(self, args):
report, err_code, err_msg = self._read_report_file(args.report_file)
if err_code != ReturnCodes.LICOMP_OK.value:
return None, err_code, err_msg
if args.resources:
logging.warning(f'User specified resources are ignored. Using the resources as specified in the report file ("{args.report_file}").')
resources = report['resources']
usecase = report['usecase']
provisioning = report['provisioning']
lph = LicensePolicyHandler(policy_file=args.license_policy_file,
resources=resources,
usecase=usecase,
provisioning=provisioning)
policy_report = lph.apply_policy(report, ignore_missing=True)
ret_code = compatibility_status_to_returncode(policy_report['compatibility'])
formatter = LicompToolkitFormatter.formatter(self.args.output_format)
formatted_report = formatter.format_policy_report(report, verbose=args.verbose)
return formatted_report, ret_code, False

def verify(self, args):
formatter = LicompToolkitFormatter.formatter(self.args.output_format)
try:
Expand All @@ -63,7 +111,20 @@ def verify(self, args):
detailed_report=detailed_report)

ret_code = compatibility_status_to_returncode(compatibilities['compatibility'])
return formatter.format_compatibilities(compatibilities), ret_code, False
if args.apply_license_policy:
if args.license_policy_file:
lph = LicensePolicyHandler(policy_file=args.license_policy_file)
else:
resources, unsupported = resources_to_use(args)
if unsupported:
return f'Resource(s) {", ".join(unsupported)} is/are not supported', ReturnCodes.LICOMP_UNSUPPORTED_RESOURCE.value, True
lph = LicensePolicyHandler(resources=resources,
usecase=args.usecase,
provisioning=args.provisioning)
policy_report = lph.apply_policy(compatibilities)
return formatter.format_policy_report(policy_report, verbose=args.verbose), ret_code, False
else:
return formatter.format_compatibilities(compatibilities, verbose=args.verbose), ret_code, False
except LicompException as e:
return e, e.return_code.value, True
except FlameException as e:
Expand Down Expand Up @@ -125,11 +186,14 @@ def outbound_candidate(self, args):
if args.all_licenses:
licenses_to_check = self.licomp_toolkit.supported_licenses()

resources, unsupported = resources_to_use(args)
if unsupported:
return f'Resource(s) {", ".join(unsupported)} is/are not supported', ReturnCodes.LICOMP_UNSUPPORTED_RESOURCE.value, True
candidates = suggester.compat_licenses(args.license_expression,
args.usecase,
args.provisioning,
licenses_to_check,
args.resources)
resources)
if args.least_compatible:
candidates.reverse()

Expand All @@ -139,10 +203,13 @@ def outbound_candidate(self, args):

def display_compatibility(self, args):
display_compat = DisplayCompatibility(self.licomp_toolkit)
resources, unsupported = resources_to_use(args)
if unsupported:
return f'Resource(s) {", ".join(unsupported)} is/are not supported', ReturnCodes.LICOMP_UNSUPPORTED_RESOURCE.value, True
compats = display_compat.display_compatibility(args.licenses,
UseCase.string_to_usecase(args.usecase),
Provisioning.string_to_provisioning(args.provisioning),
args.resources)
resources)
formatter = LicompToolkitFormatter.formatter(args.output_format)
formatted = formatter.format_display_compatibilities(compats,
{'discard_unsupported': args.discard_unsupported_licenses})
Expand All @@ -154,7 +221,7 @@ def versions(self, args):


def _working_return_code(return_code):
return return_code >= 0 and return_code < ReturnCodes.LICOMP_LAST_SUCCESSFUL_CODE.value
return return_code >= 0 and return_code < LicompToolkitReturnCodes.LICOMP_TOOLKIT_LAST_ERROR_CODE.value

def main():
logging.debug("Licomp Toolkit")
Expand All @@ -171,14 +238,18 @@ def main():
parser.add_argument('-r', '--resources',
type=str,
action='append',
help='use specified licomp resource. For a list use the commands \'supported-resources\'. Use \'all\' to use all',
help=f'use specified licomp resource. For a list use the commands \'supported-resources\'. Use \'all\' to use all. Default: {", ".join(default_resources())}',
default=[])

parser.add_argument('-nv', '--no-verbose',
action='store_true',
help='keep compatibility report as short as possible',
default=[])

parser_v = subparsers.choices['verify']
parser_v.add_argument("--apply-license-policy", action='store_true', help='Apply license policy', default=False)
parser_v.add_argument("--license-policy-file", type=str, help='License policy file. Defaults to use default license policy.', default=None)

# Commands
parser_si = subparsers.add_parser('simplify', help='Normalize and simplify a license expression')
parser_si.set_defaults(which="simplify", func=lct_parser.simplify)
Expand Down Expand Up @@ -226,6 +297,12 @@ def main():
parser_sr = subparsers.add_parser('versions', help='Output version of licomp-toolkit and all the licomp resources')
parser_sr.set_defaults(which="versions", func=lct_parser.versions)

# Command: apply policy
parser_sr = subparsers.add_parser('apply-license-policy', help='')
parser_sr.set_defaults(which="apply_license_policy", func=lct_parser.apply_license_policy)
parser_sr.add_argument('--license-policy-file', '-lpf', type=str, help='License policy file', default=None)
parser_sr.add_argument("report_file", type=str)

res, code, err, func = lct_parser.run_noexit()
if _working_return_code(code):
if res:
Expand Down
1 change: 1 addition & 0 deletions licomp_toolkit/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

licomp_toolkit_version = '0.5.18'
my_supported_api_version = '0.5'
licomp_toolkit_file_version = '0.5'

cli_name = 'licomp-toolkit'
module_name = 'licomp_toolkit'
Expand Down
41 changes: 29 additions & 12 deletions licomp_toolkit/data/reply_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@
"title" : "Licomp Toolkit Reply",
"type" : "object",
"properties" : {
"meta": {
"type" : "object",
"properties" : {
"status" : {
"type": "string",
"description" : ""
},
"tool" : {
"type": "string",
"description" : ""
},
"file_version" : {
"type": "string",
"description" : ""
}
}
},
"compatibility_report": {
"type" : "object",
"$ref": "#/$defs/compatibility_object"
Expand Down Expand Up @@ -78,13 +95,13 @@
"$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"
},
"check_class": {
"type": "string",
"description": "Names the class/function used to determine. For development purposes"
},
"inbound_license": {
"$ref": "#/$defs/license",
"description" : "The inbound license expression."
Expand Down Expand Up @@ -123,7 +140,7 @@
]
}
},
"required": [ "compatibility", "compatibility_type", "compatibility_check", "inbound_license" , "outbound_license", "compatibility_object", "compatibility_details"],
"required": [ "compatibility", "compatibility_check", "inbound_license" , "outbound_license", "compatibility_object", "compatibility_details"],
"additionalProperties" : false
},
{
Expand All @@ -132,13 +149,13 @@
"$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"
},
"check_class": {
"type": "string",
"description": "Names the class/function used to determine. For development purposes"
},
"inbound_license": {
"$ref": "#/$defs/license",
"description" : "The inbound license expression."
Expand Down Expand Up @@ -166,7 +183,7 @@
"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" ],
"required": [ "compatibility", "compatibility_check", "inbound_license" , "outbound_license" , "operator", "operands" ],
"additionalProperties" : false
}
]
Expand Down Expand Up @@ -255,7 +272,7 @@
"pattern": "^[0-9].[0-9](.[0-9]){0,1}$",
"description" : "The api version of the program providing the reply"
},
"resource_name1" : {
"resource_name" : {
"type" : "string",
"minLength": 1,
"description" : "The name of the program providing the reply, e.g. licomp-osadl"
Expand All @@ -281,7 +298,7 @@
"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"],
"required" : [ "status", "status_details", "outbound", "inbound", "usecase", "provisioning", "modification", "compatibility_status", "explanation", "api_version", "resource_name", "resource_version", "resource_disclaimer", "data_url", "resource_url"],
"additionalProperties" : false
}
}
Expand Down
11 changes: 11 additions & 0 deletions licomp_toolkit/exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2026 Henrik Sandklef
#
# SPDX-License-Identifier: GPL-3.0-or-later

class LicompToolkitException(Exception):

def __init__(self, message, error_code, orig_exception=None):
self.message = message
super().__init__(self.message)
self.error_code = error_code
self.original_exception = orig_exception
4 changes: 1 addition & 3 deletions licomp_toolkit/expr_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
OR = "OR"

COMPATIBILITY_TYPE = 'compatibility_type'
COMPATIBILITY_OUTBOUND_LICENSE = 'outbound_license'
COMPATIBILITY_INBOUND_LICENSE = 'inbound_license'

class LicenseExpressionParser():

Expand Down Expand Up @@ -127,7 +125,7 @@ def __parse_expression(self, expression):
elif self.__is_close(expression):
return ""

raise Exception("Bottom reached")
raise Exception("Bottom reached in __parse_expression (expr_parser.py)")

def to_string(self, parsed_license):
license_type = parsed_license['compatibility_type']
Expand Down
Loading