Addon Manager: Add support for license exclusion

This commit is contained in:
Chris Hennes
2024-02-04 12:58:32 +01:00
parent a79abfb576
commit fa4bea510d
10 changed files with 8108 additions and 11 deletions

View File

@@ -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"""

View File

@@ -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">

View 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

View File

@@ -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

View File

@@ -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>

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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)
)

View 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

View File

@@ -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 (