188 lines
8.0 KiB
Python
188 lines
8.0 KiB
Python
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
# ***************************************************************************
|
|
# * *
|
|
# * Copyright (c) 2024 FreeCAD Project Association *
|
|
# * *
|
|
# * This file is part of FreeCAD. *
|
|
# * *
|
|
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
|
# * under the terms of the GNU Lesser General Public License as *
|
|
# * published by the Free Software Foundation, either version 2.1 of the *
|
|
# * License, or (at your option) any later version. *
|
|
# * *
|
|
# * FreeCAD is distributed in the hope that it will be useful, but *
|
|
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
|
# * Lesser General Public License for more details. *
|
|
# * *
|
|
# * You should have received a copy of the GNU Lesser General Public *
|
|
# * License along with FreeCAD. If not, see *
|
|
# * <https://www.gnu.org/licenses/>. *
|
|
# * *
|
|
# ***************************************************************************
|
|
|
|
""" Utilities for working with licenses. Based on SPDX info downloaded from
|
|
https://github.com/spdx/license-list-data and stored as part of the FreeCAD repo, loaded into a Qt
|
|
resource. """
|
|
|
|
import json
|
|
|
|
# Get whatever version of PySide we can
|
|
try:
|
|
import PySide # Use the FreeCAD wrapper
|
|
except ImportError:
|
|
try:
|
|
import PySide6 # Outside FreeCAD, try Qt6 first
|
|
|
|
PySide = PySide6
|
|
except ImportError:
|
|
import PySide2 # Fall back to Qt5 (if this fails, Python will kill this module's import)
|
|
|
|
PySide = PySide2
|
|
|
|
from PySide import QtCore
|
|
|
|
import addonmanager_freecad_interface as fci
|
|
|
|
|
|
class SPDXLicenseManager:
|
|
"""A class that loads a list of licenses from an internal Qt resource and provides access to
|
|
some information about those licenses."""
|
|
|
|
def __init__(self):
|
|
self.license_data = {}
|
|
self._load_license_data()
|
|
|
|
def _load_license_data(self):
|
|
qf = QtCore.QFile(f":/licenses/spdx.json")
|
|
if qf.exists():
|
|
qf.open(QtCore.QIODevice.ReadOnly)
|
|
byte_data = qf.readAll()
|
|
qf.close()
|
|
|
|
string_data = str(byte_data, encoding="utf-8")
|
|
raw_license_data = json.loads(string_data)
|
|
|
|
self._process_raw_spdx_json(raw_license_data)
|
|
|
|
def _process_raw_spdx_json(self, raw_license_data: dict):
|
|
"""The raw JSON data is a list of licenses, with the ID as an element of the contained
|
|
data members. More useful for our purposes is a dictionary with the SPDX IDs as the keys
|
|
and the remaining data as the values."""
|
|
for entry in raw_license_data["licenses"]:
|
|
self.license_data[entry["licenseId"]] = entry
|
|
|
|
def is_osi_approved(self, spdx_id: str) -> bool:
|
|
"""Check to see if the license is OSI-approved, according to the SPDX database. Returns
|
|
False if the license is not in the database, or is not marked as "isOsiApproved"."""
|
|
if spdx_id == "UNLICENSED" or spdx_id == "UNLICENCED" or spdx_id.startswith("SEE LIC"):
|
|
return False
|
|
if spdx_id not in self.license_data:
|
|
fci.Console.PrintWarning(
|
|
f"WARNING: License ID {spdx_id} is not in the SPDX license "
|
|
f"list. The Addon author must correct their metadata.\n"
|
|
)
|
|
return False
|
|
return (
|
|
"isOsiApproved" in self.license_data[spdx_id]
|
|
and self.license_data[spdx_id]["isOsiApproved"]
|
|
)
|
|
|
|
def is_fsf_libre(self, spdx_id: str) -> bool:
|
|
"""Check to see if the license is FSF Free/Libre, according to the SPDX database. Returns
|
|
False if the license is not in the database, or is not marked as "isFsfLibre"."""
|
|
if spdx_id == "UNLICENSED" or spdx_id == "UNLICENCED" or spdx_id.startswith("SEE LIC"):
|
|
return False
|
|
if spdx_id not in self.license_data:
|
|
fci.Console.PrintWarning(
|
|
f"WARNING: License ID {spdx_id} is not in the SPDX license "
|
|
f"list. The Addon author must correct their metadata.\n"
|
|
)
|
|
return False
|
|
return (
|
|
"isFsfLibre" in self.license_data[spdx_id] and self.license_data[spdx_id]["isFsfLibre"]
|
|
)
|
|
|
|
def name(self, spdx_id: str) -> str:
|
|
if spdx_id == "UNLICENSED":
|
|
return "All rights reserved"
|
|
if spdx_id.startswith("SEE LIC"): # "SEE LICENSE IN" or "SEE LICENCE IN"
|
|
return f"Custom license: {spdx_id}"
|
|
if spdx_id not in self.license_data:
|
|
return ""
|
|
return self.license_data[spdx_id]["name"]
|
|
|
|
def url(self, spdx_id: str) -> str:
|
|
if spdx_id not in self.license_data:
|
|
return ""
|
|
return self.license_data[spdx_id]["reference"]
|
|
|
|
def details_json_url(self, spdx_id: str):
|
|
"""The "detailsUrl" entry in the SPDX database, which is a link to a JSON file containing
|
|
the details of the license. As of SPDX v3 the fields are:
|
|
* isDeprecatedLicenseId
|
|
* isFsfLibre
|
|
* licenseText
|
|
* standardLicenseHeaderTemplate
|
|
* standardLicenseTemplate
|
|
* name
|
|
* licenseId
|
|
* standardLicenseHeader
|
|
* crossRef
|
|
* seeAlso
|
|
* isOsiApproved
|
|
* licenseTextHtml
|
|
* standardLicenseHeaderHtml"""
|
|
if spdx_id not in self.license_data:
|
|
return ""
|
|
return self.license_data[spdx_id]["detailsUrl"]
|
|
|
|
def normalize(self, license_string: str) -> str:
|
|
"""Given a potentially non-compliant license string, attempt to normalize it to match an
|
|
SPDX record. Takes a conservative view and tries not to over-expand stated rights (e.g.
|
|
it will select 'GPL-3.0-only' rather than 'GPL-3.0-or-later' when given just GPL3)."""
|
|
if self.name(license_string):
|
|
return license_string
|
|
fci.Console.PrintLog(
|
|
f"Attempting to normalize non-compliant license '" f"{license_string}'... "
|
|
)
|
|
normed = license_string.replace("lgpl", "LGPL").replace("gpl", "GPL")
|
|
normed = (
|
|
normed.replace(" ", "-")
|
|
.replace("v", "-")
|
|
.replace("GPL2", "GPL-2")
|
|
.replace("GPL3", "GPL-3")
|
|
)
|
|
or_later = ""
|
|
if normed.endswith("+"):
|
|
normed = normed[:-1]
|
|
or_later = "-or-later"
|
|
if self.name(normed + or_later):
|
|
fci.Console.PrintLog(f"found valid SPDX license ID {normed}\n")
|
|
return normed + or_later
|
|
# If it still doesn't match, try some other things
|
|
while "--" in normed:
|
|
normed = normed.replace("--", "-")
|
|
|
|
if self.name(normed + or_later):
|
|
fci.Console.PrintLog(f"found valid SPDX license ID {normed}\n")
|
|
return normed + or_later
|
|
normed += ".0"
|
|
if self.name(normed + or_later):
|
|
fci.Console.PrintLog(f"found valid SPDX license ID {normed}\n")
|
|
return normed + or_later
|
|
fci.Console.PrintLog(f"failed to normalize (typo in ID or invalid version number??)\n")
|
|
return license_string # We failed to normalize this one
|
|
|
|
|
|
_LICENSE_MANAGER = None # Internal use only, see get_license_manager()
|
|
|
|
|
|
def get_license_manager() -> SPDXLicenseManager:
|
|
"""Get the license manager. Prevents multiple re-loads of the license list by keeping a
|
|
single copy of the manager."""
|
|
global _LICENSE_MANAGER
|
|
if _LICENSE_MANAGER is None:
|
|
_LICENSE_MANAGER = SPDXLicenseManager()
|
|
return _LICENSE_MANAGER
|