FreeCAD
187 строк · 8.0 Кб
1# SPDX-License-Identifier: LGPL-2.1-or-later
2# ***************************************************************************
3# * *
4# * Copyright (c) 2024 FreeCAD Project Association *
5# * *
6# * This file is part of FreeCAD. *
7# * *
8# * FreeCAD is free software: you can redistribute it and/or modify it *
9# * under the terms of the GNU Lesser General Public License as *
10# * published by the Free Software Foundation, either version 2.1 of the *
11# * License, or (at your option) any later version. *
12# * *
13# * FreeCAD is distributed in the hope that it will be useful, but *
14# * WITHOUT ANY WARRANTY; without even the implied warranty of *
15# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
16# * Lesser General Public License for more details. *
17# * *
18# * You should have received a copy of the GNU Lesser General Public *
19# * License along with FreeCAD. If not, see *
20# * <https://www.gnu.org/licenses/>. *
21# * *
22# ***************************************************************************
23
24""" Utilities for working with licenses. Based on SPDX info downloaded from
25https://github.com/spdx/license-list-data and stored as part of the FreeCAD repo, loaded into a Qt
26resource. """
27
28import json
29
30# Get whatever version of PySide we can
31try:
32import PySide # Use the FreeCAD wrapper
33except ImportError:
34try:
35import PySide6 # Outside FreeCAD, try Qt6 first
36
37PySide = PySide6
38except ImportError:
39import PySide2 # Fall back to Qt5 (if this fails, Python will kill this module's import)
40
41PySide = PySide2
42
43from PySide import QtCore
44
45import addonmanager_freecad_interface as fci
46
47
48class SPDXLicenseManager:
49"""A class that loads a list of licenses from an internal Qt resource and provides access to
50some information about those licenses."""
51
52def __init__(self):
53self.license_data = {}
54self._load_license_data()
55
56def _load_license_data(self):
57qf = QtCore.QFile(f":/licenses/spdx.json")
58if qf.exists():
59qf.open(QtCore.QIODevice.ReadOnly)
60byte_data = qf.readAll()
61qf.close()
62
63string_data = str(byte_data, encoding="utf-8")
64raw_license_data = json.loads(string_data)
65
66self._process_raw_spdx_json(raw_license_data)
67
68def _process_raw_spdx_json(self, raw_license_data: dict):
69"""The raw JSON data is a list of licenses, with the ID as an element of the contained
70data members. More useful for our purposes is a dictionary with the SPDX IDs as the keys
71and the remaining data as the values."""
72for entry in raw_license_data["licenses"]:
73self.license_data[entry["licenseId"]] = entry
74
75def is_osi_approved(self, spdx_id: str) -> bool:
76"""Check to see if the license is OSI-approved, according to the SPDX database. Returns
77False if the license is not in the database, or is not marked as "isOsiApproved"."""
78if spdx_id == "UNLICENSED" or spdx_id == "UNLICENCED" or spdx_id.startswith("SEE LIC"):
79return False
80if spdx_id not in self.license_data:
81fci.Console.PrintWarning(
82f"WARNING: License ID {spdx_id} is not in the SPDX license "
83f"list. The Addon author must correct their metadata.\n"
84)
85return False
86return (
87"isOsiApproved" in self.license_data[spdx_id]
88and self.license_data[spdx_id]["isOsiApproved"]
89)
90
91def is_fsf_libre(self, spdx_id: str) -> bool:
92"""Check to see if the license is FSF Free/Libre, according to the SPDX database. Returns
93False if the license is not in the database, or is not marked as "isFsfLibre"."""
94if spdx_id == "UNLICENSED" or spdx_id == "UNLICENCED" or spdx_id.startswith("SEE LIC"):
95return False
96if spdx_id not in self.license_data:
97fci.Console.PrintWarning(
98f"WARNING: License ID {spdx_id} is not in the SPDX license "
99f"list. The Addon author must correct their metadata.\n"
100)
101return False
102return (
103"isFsfLibre" in self.license_data[spdx_id] and self.license_data[spdx_id]["isFsfLibre"]
104)
105
106def name(self, spdx_id: str) -> str:
107if spdx_id == "UNLICENSED":
108return "All rights reserved"
109if spdx_id.startswith("SEE LIC"): # "SEE LICENSE IN" or "SEE LICENCE IN"
110return f"Custom license: {spdx_id}"
111if spdx_id not in self.license_data:
112return ""
113return self.license_data[spdx_id]["name"]
114
115def url(self, spdx_id: str) -> str:
116if spdx_id not in self.license_data:
117return ""
118return self.license_data[spdx_id]["reference"]
119
120def details_json_url(self, spdx_id: str):
121"""The "detailsUrl" entry in the SPDX database, which is a link to a JSON file containing
122the details of the license. As of SPDX v3 the fields are:
123* isDeprecatedLicenseId
124* isFsfLibre
125* licenseText
126* standardLicenseHeaderTemplate
127* standardLicenseTemplate
128* name
129* licenseId
130* standardLicenseHeader
131* crossRef
132* seeAlso
133* isOsiApproved
134* licenseTextHtml
135* standardLicenseHeaderHtml"""
136if spdx_id not in self.license_data:
137return ""
138return self.license_data[spdx_id]["detailsUrl"]
139
140def normalize(self, license_string: str) -> str:
141"""Given a potentially non-compliant license string, attempt to normalize it to match an
142SPDX record. Takes a conservative view and tries not to over-expand stated rights (e.g.
143it will select 'GPL-3.0-only' rather than 'GPL-3.0-or-later' when given just GPL3)."""
144if self.name(license_string):
145return license_string
146fci.Console.PrintLog(
147f"Attempting to normalize non-compliant license '" f"{license_string}'... "
148)
149normed = license_string.replace("lgpl", "LGPL").replace("gpl", "GPL")
150normed = (
151normed.replace(" ", "-")
152.replace("v", "-")
153.replace("GPL2", "GPL-2")
154.replace("GPL3", "GPL-3")
155)
156or_later = ""
157if normed.endswith("+"):
158normed = normed[:-1]
159or_later = "-or-later"
160if self.name(normed + or_later):
161fci.Console.PrintLog(f"found valid SPDX license ID {normed}\n")
162return normed + or_later
163# If it still doesn't match, try some other things
164while "--" in normed:
165normed = normed.replace("--", "-")
166
167if self.name(normed + or_later):
168fci.Console.PrintLog(f"found valid SPDX license ID {normed}\n")
169return normed + or_later
170normed += ".0"
171if self.name(normed + or_later):
172fci.Console.PrintLog(f"found valid SPDX license ID {normed}\n")
173return normed + or_later
174fci.Console.PrintLog(f"failed to normalize (typo in ID or invalid version number??)\n")
175return license_string # We failed to normalize this one
176
177
178_LICENSE_MANAGER = None # Internal use only, see get_license_manager()
179
180
181def get_license_manager() -> SPDXLicenseManager:
182"""Get the license manager. Prevents multiple re-loads of the license list by keeping a
183single copy of the manager."""
184global _LICENSE_MANAGER
185if _LICENSE_MANAGER is None:
186_LICENSE_MANAGER = SPDXLicenseManager()
187return _LICENSE_MANAGER
188