Addon Manager: Add support for license exclusion
This commit is contained in:
@@ -41,11 +41,14 @@ from addonmanager_metadata import (
|
||||
Version,
|
||||
DependencyType,
|
||||
)
|
||||
from AddonStats import AddonStats
|
||||
|
||||
translate = fci.translate
|
||||
|
||||
# A list of internal workbenches that can be used as a dependency of an Addon
|
||||
INTERNAL_WORKBENCHES = {
|
||||
"arch": "Arch",
|
||||
"assembly": "Assembly",
|
||||
"draft": "Draft",
|
||||
"fem": "FEM",
|
||||
"mesh": "Mesh",
|
||||
@@ -163,6 +166,7 @@ class Addon:
|
||||
self.description = None
|
||||
self.tags = set() # Just a cache, loaded from Metadata
|
||||
self.last_updated = None
|
||||
self.stats = AddonStats()
|
||||
|
||||
# To prevent multiple threads from running git actions on this repo at the
|
||||
# same time
|
||||
@@ -205,6 +209,7 @@ class Addon:
|
||||
self.python_min_version = {"major": 3, "minor": 0}
|
||||
|
||||
self._icon_file = None
|
||||
self._cached_license: str = ""
|
||||
|
||||
def __str__(self) -> str:
|
||||
result = f"FreeCAD {self.repo_type}\n"
|
||||
@@ -215,6 +220,15 @@ class Addon:
|
||||
result += "Has linked Macro object\n"
|
||||
return result
|
||||
|
||||
@property
|
||||
def license(self):
|
||||
if not self._cached_license:
|
||||
if self.metadata and self.metadata.license:
|
||||
self._cached_license = self.metadata.license
|
||||
elif self.stats and self.stats.license:
|
||||
self._cached_license = self.stats.license
|
||||
return self._cached_license
|
||||
|
||||
@classmethod
|
||||
def from_macro(cls, macro: Macro):
|
||||
"""Create an Addon object from a Macro wrapper object"""
|
||||
|
||||
@@ -90,6 +90,38 @@ installed addons will be checked for available updates
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxhidenonfsf">
|
||||
<property name="text">
|
||||
<string>Hide Addons with non-FSF Free/Libre license</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>HideNonFSFFreeLibre</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxnonosi">
|
||||
<property name="text">
|
||||
<string>Hide Addons with non-OSI-approved license</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>HideNonOSIApproved</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Addons</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxhidepy2">
|
||||
<property name="text">
|
||||
|
||||
58
src/Mod/AddonManager/AddonStats.py
Normal file
58
src/Mod/AddonManager/AddonStats.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# 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/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
""" Classes and structures related to Addon sidecar information """
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def to_int_or_zero(inp: [str | int | None]):
|
||||
try:
|
||||
return int(inp)
|
||||
except TypeError:
|
||||
return 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class AddonStats:
|
||||
"""Statistics about an addon: not all stats apply to all addon types"""
|
||||
|
||||
last_update_time: datetime | None = None
|
||||
stars: int = 0
|
||||
open_issues: int = 0
|
||||
forks: int = 0
|
||||
license: str = ""
|
||||
page_views_last_month: int = 0
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_dict: dict):
|
||||
new_stats = AddonStats()
|
||||
if "pushed_at" in json_dict:
|
||||
new_stats.last_update_time = datetime.fromisoformat(json_dict["pushed_at"])
|
||||
new_stats.stars = to_int_or_zero(json_dict["stargazers_count"])
|
||||
new_stats.forks = to_int_or_zero(json_dict["forks_count"])
|
||||
new_stats.open_issues = to_int_or_zero(json_dict["open_issues_count"])
|
||||
new_stats.license = json_dict["license"] # Might be None or "NOASSERTION"
|
||||
return new_stats
|
||||
@@ -6,6 +6,7 @@ ENDIF (BUILD_GUI)
|
||||
SET(AddonManager_SRCS
|
||||
add_toolbar_button_dialog.ui
|
||||
Addon.py
|
||||
AddonStats.py
|
||||
AddonManager.py
|
||||
AddonManager.ui
|
||||
addonmanager_cache.py
|
||||
@@ -25,6 +26,7 @@ SET(AddonManager_SRCS
|
||||
addonmanager_git.py
|
||||
addonmanager_installer.py
|
||||
addonmanager_installer_gui.py
|
||||
addonmanager_licenses.py
|
||||
addonmanager_macro.py
|
||||
addonmanager_macro_parser.py
|
||||
addonmanager_metadata.py
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
<file>licenses/LGPLv3.txt</file>
|
||||
<file>licenses/MIT.txt</file>
|
||||
<file>licenses/MPL-2.0.txt</file>
|
||||
<file>licenses/spdx.json</file>
|
||||
<file>translations/AddonManager_af.qm</file>
|
||||
<file>translations/AddonManager_ar.qm</file>
|
||||
<file>translations/AddonManager_ca.qm</file>
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
.st0{fill:none;stroke:#000000;stroke-miterlimit:10;}
|
||||
.st1{fill:none;stroke:#000000;stroke-width:0.9259;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<line class="st0" x1="0.3" y1="3.5" x2="6.7" y2="3.5"/>
|
||||
<line class="st0" x1="0.3" y1="8.5" x2="6.7" y2="8.5"/>
|
||||
<line class="st0" x1="0.3" y1="1.5" x2="6.7" y2="1.5"/>
|
||||
<line class="st0" x1="0.3" y1="4.5" x2="6.7" y2="4.5"/>
|
||||
<line class="st0" x1="0.3" y1="7.5" x2="6.7" y2="7.5"/>
|
||||
<line class="st0" x1="0.3" y1="10.5" x2="6.7" y2="10.5"/>
|
||||
<line class="st0" x1="0.3" y1="13.5" x2="6.7" y2="13.5"/>
|
||||
<rect x="8.9" y="2" class="st1" width="5.2" height="12.1"/>
|
||||
<rect x="9.5" y="1.5" class="st1" width="5" height="13"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 743 B After Width: | Height: | Size: 855 B |
7835
src/Mod/AddonManager/Resources/licenses/spdx.json
Normal file
7835
src/Mod/AddonManager/Resources/licenses/spdx.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022-2024 FreeCAD Project Association *
|
||||
# * Copyright (c) 2024 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
@@ -99,5 +99,5 @@ class WidgetSearch(QtWidgets.QWidget):
|
||||
|
||||
def retranslateUi(self, _):
|
||||
self.filter_line_edit.setPlaceholderText(
|
||||
QtCore.QCoreApplication.translate("AddonsInstaller", "Filter", None)
|
||||
QtCore.QCoreApplication.translate("AddonsInstaller", "Search...", None)
|
||||
)
|
||||
|
||||
127
src/Mod/AddonManager/addonmanager_licenses.py
Normal file
127
src/Mod/AddonManager/addonmanager_licenses.py
Normal file
@@ -0,0 +1,127 @@
|
||||
# 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
|
||||
|
||||
|
||||
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 not in self.license_data:
|
||||
return False
|
||||
return 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 not in self.license_data:
|
||||
return False
|
||||
return self.license_data[spdx_id]["isFsfLibre"]
|
||||
|
||||
def name(self, spdx_id: str) -> str:
|
||||
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"]
|
||||
|
||||
|
||||
_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
|
||||
@@ -40,6 +40,7 @@ from addonmanager_metadata import get_first_supported_freecad_version, Version
|
||||
from Widgets.addonmanager_widget_view_control_bar import WidgetViewControlBar
|
||||
from Widgets.addonmanager_widget_view_selector import AddonManagerDisplayStyle
|
||||
from Widgets.addonmanager_widget_filter_selector import StatusFilter, Filter, ContentFilter
|
||||
from addonmanager_licenses import get_license_manager, SPDXLicenseManager
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
@@ -91,6 +92,8 @@ class PackageList(QtWidgets.QWidget):
|
||||
|
||||
self.item_filter.setHidePy2(pref.GetBool("HidePy2", True))
|
||||
self.item_filter.setHideObsolete(pref.GetBool("HideObsolete", True))
|
||||
self.item_filter.setHideNonOSIApproved(pref.GetBool("HideNonOSIApproved", True))
|
||||
self.item_filter.setHideNonFSFLibre(pref.GetBool("HideNonFSFFreeLibre", True))
|
||||
self.item_filter.setHideNewerFreeCADRequired(pref.GetBool("HideNewerFreeCADRequired", True))
|
||||
|
||||
def on_listPackages_clicked(self, index: QtCore.QModelIndex):
|
||||
@@ -464,6 +467,8 @@ class PackageListFilter(QtCore.QSortFilterProxyModel):
|
||||
self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
self.hide_obsolete = False
|
||||
self.hide_py2 = False
|
||||
self.hide_non_OSI_approved = False
|
||||
self.hide_non_FSF_libre = False
|
||||
self.hide_newer_freecad_required = False
|
||||
|
||||
def setPackageFilter(
|
||||
@@ -490,6 +495,16 @@ class PackageListFilter(QtCore.QSortFilterProxyModel):
|
||||
self.hide_obsolete = hide_obsolete
|
||||
self.invalidateFilter()
|
||||
|
||||
def setHideNonOSIApproved(self, hide: bool) -> None:
|
||||
"""Sets whether to hide Addons with non-OSI-approved licenses"""
|
||||
self.hide_non_OSI_approved = hide
|
||||
self.invalidateFilter()
|
||||
|
||||
def setHideNonFSFLibre(self, hide: bool) -> None:
|
||||
"""Sets whether to hide Addons with non-FSF-Libre licenses"""
|
||||
self.hide_non_FSF_libre = hide
|
||||
self.invalidateFilter()
|
||||
|
||||
def setHideNewerFreeCADRequired(self, hide_nfr: bool) -> None:
|
||||
"""Sets whether to hide packages that have indicated they need a newer version
|
||||
of FreeCAD than the one currently running."""
|
||||
@@ -529,13 +544,24 @@ class PackageListFilter(QtCore.QSortFilterProxyModel):
|
||||
if data.status() != Addon.Status.UPDATE_AVAILABLE:
|
||||
return False
|
||||
|
||||
# If it's not installed, check to see if it's Py2 only
|
||||
if data.status() == Addon.Status.NOT_INSTALLED and self.hide_py2 and data.python2:
|
||||
return False
|
||||
license_manager = get_license_manager()
|
||||
if data.status() == Addon.Status.NOT_INSTALLED:
|
||||
|
||||
# If it's not installed, check to see if it's marked obsolete
|
||||
if data.status() == Addon.Status.NOT_INSTALLED and self.hide_obsolete and data.obsolete:
|
||||
return False
|
||||
# If it's not installed, check to see if it's Py2 only
|
||||
if self.hide_py2 and data.python2:
|
||||
return False
|
||||
|
||||
# If it's not installed, check to see if it's marked obsolete
|
||||
if self.hide_obsolete and data.obsolete:
|
||||
return False
|
||||
|
||||
# If it is not an OSI-approved license, check to see if we are hiding those
|
||||
if self.hide_non_OSI_approved and not license_manager.is_osi_approved(data.license):
|
||||
return False
|
||||
|
||||
# If it is not an FSF Free/Libre license, check to see if we are hiding those
|
||||
if self.hide_non_FSF_libre and not license_manager.is_fsf_libre(data.license):
|
||||
return False
|
||||
|
||||
# If it's not installed, check to see if it's for a newer version of FreeCAD
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user