From 0a112c6c53fee4aaf5510a91d46cc015d995c54a Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 17 Feb 2025 08:51:36 +0100 Subject: [PATCH] Gather and display details of licences for pip packages CURA-12400 --- CuraVersion.py.jinja | 2 + conanfile.py | 67 +++++++++++++++++ cura/CuraApplication.py | 2 + cura/UI/OpenSourceDependenciesModel.py | 23 ++++++ cura/UI/OpenSourceDependency.py | 45 ++++++++++++ resources/qml/Dialogs/AboutDialog.qml | 98 +++++++++---------------- resources/qml/Dialogs/LicenseDialog.qml | 43 +++++++++++ 7 files changed, 218 insertions(+), 62 deletions(-) create mode 100644 cura/UI/OpenSourceDependenciesModel.py create mode 100644 cura/UI/OpenSourceDependency.py create mode 100644 resources/qml/Dialogs/LicenseDialog.qml diff --git a/CuraVersion.py.jinja b/CuraVersion.py.jinja index 76a75b4e3a..77d48775b9 100644 --- a/CuraVersion.py.jinja +++ b/CuraVersion.py.jinja @@ -17,3 +17,5 @@ CuraLatestURL = "{{ cura_latest_url }}" ConanInstalls = {{ conan_installs }} PythonInstalls = {{ python_installs }} + +DependenciesDescriptions = {{ dependencies_description }} diff --git a/conanfile.py b/conanfile.py index 3a71d8bca2..6c4db48754 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,4 +1,8 @@ import os +import requests +import yaml +import tempfile +import tarfile from io import StringIO from pathlib import Path @@ -135,6 +139,67 @@ class CuraConan(ConanFile): return python_installs + def _make_pip_dependency_description(self, package, version, dependencies): + url = ["https://pypi.org/pypi", package] + if version is not None: + url.append(version) + url.append("json") + + data = requests.get("/".join(url)).json() + + # print('++++++++++++++++++++++++++++++++++++++++') + # print(data) + + dependency_description = { + "author": data["info"]["author"], + "summary": data["info"]["summary"], + "version": data["info"]["version"], + "license": data["info"]["license"] + } + + for url_data in data["urls"]: + if url_data["packagetype"] == "sdist": + sources_url = url_data["url"] + dependency_description["sources_url"] = sources_url + + # Download the sources to get the license file inside + self.output.info(f"Retrieving license for {package}") + response = requests.get(sources_url) + response.raise_for_status() + + with tempfile.TemporaryDirectory() as temp_dir: + sources_path = os.path.join(temp_dir, "sources.tar.gz") + with open(sources_path, 'wb') as sources_file: + sources_file.write(response.content) + + with tarfile.open(sources_path, 'r:gz') as sources_archive: + license_file = "LICENSE" + + for source_file in sources_archive.getnames(): + if Path(source_file).name == license_file: + sources_archive.extract(source_file, temp_dir) + + license_file_path = os.path.join(temp_dir, source_file) + with open(license_file_path, 'r') as file: + dependency_description["license_full"] = file.read() + + if dependency_description["license"] is not None and len(dependency_description["license"]) > 32: + # Some packages have their full license in this field + dependency_description["license_full"] = dependency_description["license"] + dependency_description["license"] = data["info"]["name"] + + dependencies[data["info"]["name"]] = dependency_description + + def _dependencies_description(self): + dependencies = {} + + pip_requirements_summary = os.path.abspath(Path(self.generators_folder, "pip_requirements_summary.yml") ) + with open(pip_requirements_summary, 'r') as file: + for package_name, package_version in yaml.safe_load(file).items(): + self._make_pip_dependency_description(package_name, package_version, dependencies) + + return dependencies + def _generate_cura_version(self, location): with open(os.path.join(self.recipe_folder, "CuraVersion.py.jinja"), "r") as f: cura_version_py = Template(f.read()) @@ -165,6 +230,7 @@ class CuraConan(ConanFile): cura_latest_url=self.conan_data["urls"][self._urls]["cura_latest_url"], conan_installs=self._conan_installs(), python_installs=self._python_installs(), + dependencies_description=self._dependencies_description(), )) def _delete_unwanted_binaries(self, root): @@ -483,6 +549,7 @@ class CuraConan(ConanFile): copy(self, "*", src = os.path.join(self.source_folder, "plugins"), dst = os.path.join(self.package_folder, self.cpp.package.resdirs[1])) copy(self, "*", src = os.path.join(self.source_folder, "packaging"), dst = os.path.join(self.package_folder, self.cpp.package.resdirs[2])) copy(self, "pip_requirements_*.txt", src = self.generators_folder, dst = os.path.join(self.package_folder, self.cpp.package.resdirs[-1])) + copy(self, "pip_requirements_summary.yml", src = self.generators_folder, dst = os.path.join(self.package_folder, self.cpp.package.resdirs[-1])) # Remove the fdm_materials from the package rmdir(self, os.path.join(self.package_folder, self.cpp.package.resdirs[0], "materials")) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 97c4c7e2fc..e7f32ed8b3 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -110,6 +110,7 @@ from cura.UI.MachineActionManager import MachineActionManager from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel from cura.UI.MachineSettingsManager import MachineSettingsManager from cura.UI.ObjectsModel import ObjectsModel +from cura.UI.OpenSourceDependenciesModel import OpenSourceDependenciesModel from cura.UI.RecommendedMode import RecommendedMode from cura.UI.TextManager import TextManager from cura.UI.WelcomePagesModel import WelcomePagesModel @@ -1308,6 +1309,7 @@ class CuraApplication(QtApplication): qmlRegisterType(AddPrinterPagesModel, "Cura", 1, 0, "AddPrinterPagesModel") qmlRegisterType(TextManager, "Cura", 1, 0, "TextManager") qmlRegisterType(RecommendedMode, "Cura", 1, 0, "RecommendedMode") + qmlRegisterType(OpenSourceDependenciesModel, "Cura", 1, 0, "OpenSourceDependenciesModel") self.processEvents() qmlRegisterType(NetworkMJPGImage, "Cura", 1, 0, "NetworkMJPGImage") diff --git a/cura/UI/OpenSourceDependenciesModel.py b/cura/UI/OpenSourceDependenciesModel.py new file mode 100644 index 0000000000..5c48234804 --- /dev/null +++ b/cura/UI/OpenSourceDependenciesModel.py @@ -0,0 +1,23 @@ +# Copyright (c) 2025 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import List + +from PyQt6.QtCore import QObject, pyqtProperty + +from cura import CuraVersion +from .OpenSourceDependency import OpenSourceDependency + + +class OpenSourceDependenciesModel(QObject): + + def __init__(self, parent=None): + super().__init__(parent) + self._dependencies = [] + + for name, data in CuraVersion.DependenciesDescriptions.items(): + self._dependencies.append(OpenSourceDependency(name, data)) + + @pyqtProperty(list, constant=True) + def dependencies(self) -> List[OpenSourceDependency]: + return self._dependencies \ No newline at end of file diff --git a/cura/UI/OpenSourceDependency.py b/cura/UI/OpenSourceDependency.py new file mode 100644 index 0000000000..a9423e9b6d --- /dev/null +++ b/cura/UI/OpenSourceDependency.py @@ -0,0 +1,45 @@ +# Copyright (c) 2025 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from PyQt6.QtCore import QObject, pyqtProperty, pyqtEnum + + +class OpenSourceDependency(QObject): + + def __init__(self, name, data): + super().__init__() + self._name = name + self._author = data['author'] if data['author'] is not None else '' + self._version = data['version'] if data['version'] is not None else '' + self._summary = data['summary'] if data['summary'] is not None else '' + self._license = data['license'] if data['license'] is not None and len(data['license']) > 0 else name + self._license_full = data['license_full'] if 'license_full' in data else '' + self._sources_url = data['sources_url'] if 'sources_url' in data else '' + + @pyqtProperty(str, constant=True) + def name(self): + return self._name + + @pyqtProperty(str, constant=True) + def author(self): + return self._author + + @pyqtProperty(str, constant=True) + def version(self): + return self._version + + @pyqtProperty(str, constant=True) + def summary(self): + return self._summary + + @pyqtProperty(str, constant=True) + def license(self): + return self._license + + @pyqtProperty(str, constant=True) + def license_full(self): + return self._license_full + + @pyqtProperty(str, constant=True) + def sources_url(self): + return self._sources_url \ No newline at end of file diff --git a/resources/qml/Dialogs/AboutDialog.qml b/resources/qml/Dialogs/AboutDialog.qml index 5c80964e38..07f084e5fe 100644 --- a/resources/qml/Dialogs/AboutDialog.qml +++ b/resources/qml/Dialogs/AboutDialog.qml @@ -86,7 +86,6 @@ UM.Dialog return name; } } - visible: text !== "" Layout.fillWidth: true Layout.preferredWidth: 1 onLinkActivated: Qt.openUrlExternally(url) @@ -95,23 +94,45 @@ UM.Dialog UM.Label { text: description - visible: text !== "" Layout.fillWidth: true Layout.preferredWidth: 2 } UM.Label { - text: license - visible: text !== "" + text: + { + if (license_full !== "") + { + return `${license}`; + } + else + { + return license; + } + } Layout.fillWidth: true Layout.preferredWidth: 1 + + Component + { + id: componentLicenseDialog + + LicenseDialog + { + } + } + + onLinkActivated: + { + var license_dialog = componentLicenseDialog.createObject(base, {name: name, version: version, license: license_full}); + license_dialog.open(); + } } UM.Label { text: version - visible: text !== "" Layout.fillWidth: true Layout.preferredWidth: 1 } @@ -160,65 +181,16 @@ UM.Dialog { sourceComponent: dependency_row width: parent.width - property string name: model.name - property string description: model.description - property string license: model.license - property string url: model.url - property string version: "" + property string name: modelData.name + property string description: modelData.summary + property string license: modelData.license + property string license_full: modelData.license_full + property string url: modelData.sources_url + property string version: modelData.version } - model: ListModel - { - id: projectsModel - } - Component.onCompleted: - { - //Do NOT add dependencies of our dependencies here, nor CI-dependencies! - //UltiMaker's own projects and forks. - projectsModel.append({ name: "Cura", description: catalog.i18nc("@label Description for application component", "Graphical user interface"), license: "LGPLv3", url: "https://github.com/Ultimaker/Cura" }); - projectsModel.append({ name: "Uranium", description: catalog.i18nc("@label Description for application component", "Application framework"), license: "LGPLv3", url: "https://github.com/Ultimaker/Uranium" }); - projectsModel.append({ name: "CuraEngine", description: catalog.i18nc("@label Description for application component", "G-code generator"), license: "AGPLv3", url: "https://github.com/Ultimaker/CuraEngine" }); - projectsModel.append({ name: "libArcus", description: catalog.i18nc("@label Description for application component", "Interprocess communication library"), license: "LGPLv3", url: "https://github.com/Ultimaker/libArcus" }); - projectsModel.append({ name: "pynest2d", description: catalog.i18nc("@label Description for application component", "Python bindings for libnest2d"), license: "LGPL", url: "https://github.com/Ultimaker/pynest2d" }); - projectsModel.append({ name: "libnest2d", description: catalog.i18nc("@label Description for application component", "Polygon packing library, developed by Prusa Research"), license: "LGPL", url: "https://github.com/tamasmeszaros/libnest2d" }); - projectsModel.append({ name: "libSavitar", description: catalog.i18nc("@label Description for application component", "Support library for handling 3MF files"), license: "LGPLv3", url: "https://github.com/ultimaker/libsavitar" }); - projectsModel.append({ name: "libCharon", description: catalog.i18nc("@label Description for application component", "Support library for file metadata and streaming"), license: "LGPLv3", url: "https://github.com/ultimaker/libcharon" }); - - //Direct dependencies of the front-end. - projectsModel.append({ name: "Python", description: catalog.i18nc("@label Description for application dependency", "Programming language"), license: "Python", url: "http://python.org/" }); - projectsModel.append({ name: "Qt6", description: catalog.i18nc("@label Description for application dependency", "GUI framework"), license: "LGPLv3", url: "https://www.qt.io/" }); - projectsModel.append({ name: "PyQt", description: catalog.i18nc("@label Description for application dependency", "GUI framework bindings"), license: "GPL", url: "https://riverbankcomputing.com/software/pyqt" }); - projectsModel.append({ name: "SIP", description: catalog.i18nc("@label Description for application dependency", "C/C++ Binding library"), license: "GPL", url: "https://riverbankcomputing.com/software/sip" }); - projectsModel.append({ name: "Protobuf", description: catalog.i18nc("@label Description for application dependency", "Data interchange format"), license: "BSD", url: "https://developers.google.com/protocol-buffers" }); - projectsModel.append({ name: "Noto Sans", description: catalog.i18nc("@label", "Font"), license: "Apache 2.0", url: "https://www.google.com/get/noto/" }); - - //CuraEngine's dependencies. - projectsModel.append({ name: "Clipper", description: catalog.i18nc("@label Description for application dependency", "Polygon clipping library"), license: "Boost", url: "http://www.angusj.com/delphi/clipper.php" }); - projectsModel.append({ name: "RapidJSON", description: catalog.i18nc("@label Description for application dependency", "JSON parser"), license: "MIT", url: "https://rapidjson.org/" }); - projectsModel.append({ name: "STB", description: catalog.i18nc("@label Description for application dependency", "Utility functions, including an image loader"), license: "Public Domain", url: "https://github.com/nothings/stb" }); - projectsModel.append({ name: "Boost", description: catalog.i18nc("@label Description for application dependency", "Utility library, including Voronoi generation"), license: "Boost", url: "https://www.boost.org/" }); - - //Python modules. - projectsModel.append({ name: "Certifi", description: catalog.i18nc("@label Description for application dependency", "Root Certificates for validating SSL trustworthiness"), license: "MPL", url: "https://github.com/certifi/python-certifi" }); - projectsModel.append({ name: "Cryptography", description: catalog.i18nc("@label Description for application dependency", "Root Certificates for validating SSL trustworthiness"), license: "APACHE and BSD", url: "https://cryptography.io/" }); - projectsModel.append({ name: "Future", description: catalog.i18nc("@label Description for application dependency", "Compatibility between Python 2 and 3"), license: "MIT", url: "https://python-future.org/" }); - projectsModel.append({ name: "keyring", description: catalog.i18nc("@label Description for application dependency", "Support library for system keyring access"), license: "MIT", url: "https://github.com/jaraco/keyring" }); - projectsModel.append({ name: "NumPy", description: catalog.i18nc("@label Description for application dependency", "Support library for faster math"), license: "BSD", url: "http://www.numpy.org/" }); - projectsModel.append({ name: "NumPy-STL", description: catalog.i18nc("@label Description for application dependency", "Support library for handling STL files"), license: "BSD", url: "https://github.com/WoLpH/numpy-stl" }); - projectsModel.append({ name: "PyClipper", description: catalog.i18nc("@label Description for application dependency", "Python bindings for Clipper"), license: "MIT", url: "https://github.com/fonttools/pyclipper" }); - projectsModel.append({ name: "PySerial", description: catalog.i18nc("@label Description for application dependency", "Serial communication library"), license: "Python", url: "http://pyserial.sourceforge.net/" }); - projectsModel.append({ name: "SciPy", description: catalog.i18nc("@label Description for application dependency", "Support library for scientific computing"), license: "BSD-new", url: "https://www.scipy.org/" }); - projectsModel.append({ name: "Sentry", description: catalog.i18nc("@Label Description for application dependency", "Python Error tracking library"), license: "BSD 2-Clause 'Simplified'", url: "https://sentry.io/for/python/" }); - projectsModel.append({ name: "Trimesh", description: catalog.i18nc("@label Description for application dependency", "Support library for handling triangular meshes"), license: "MIT", url: "https://trimsh.org" }); - projectsModel.append({ name: "python-zeroconf", description: catalog.i18nc("@label Description for application dependency", "ZeroConf discovery library"), license: "LGPL", url: "https://github.com/jstasiak/python-zeroconf" }); - - //Building/packaging. - projectsModel.append({ name: "CMake", description: catalog.i18nc("@label Description for development tool", "Universal build system configuration"), license: "BSD 3-Clause", url: "https://cmake.org/" }); - projectsModel.append({ name: "Conan", description: catalog.i18nc("@label Description for development tool", "Dependency and package manager"), license: "MIT", url: "https://conan.io/" }); - projectsModel.append({ name: "Pyinstaller", description: catalog.i18nc("@label Description for development tool", "Packaging Python-applications"), license: "GPLv2", url: "https://pyinstaller.org/" }); - projectsModel.append({ name: "AppImageKit", description: catalog.i18nc("@label Description for development tool", "Linux cross-distribution application deployment"), license: "MIT", url: "https://github.com/AppImage/AppImageKit" }); - projectsModel.append({ name: "NSIS", description: catalog.i18nc("@label Description for development tool", "Generating Windows installers"), license: "Zlib", url: "https://nsis.sourceforge.io/" }); - } + property var dependencies_model: Cura.OpenSourceDependenciesModel {} + model: dependencies_model.dependencies } } @@ -247,6 +219,7 @@ UM.Dialog property string name: modelData.name property string version: modelData.version property string license: "" + property string license_full: "" property string url: "" property string description: "" } @@ -274,6 +247,7 @@ UM.Dialog property string name: modelData.name property string version: modelData.version property string license: "" + property string license_full: "" property string url: "" property string description: "" } diff --git a/resources/qml/Dialogs/LicenseDialog.qml b/resources/qml/Dialogs/LicenseDialog.qml new file mode 100644 index 0000000000..ea8b83958e --- /dev/null +++ b/resources/qml/Dialogs/LicenseDialog.qml @@ -0,0 +1,43 @@ +// Copyright (c) 2023 UltiMaker +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.4 +import QtQuick.Controls 2.9 +import QtQuick.Layouts 1.3 + +import UM 1.6 as UM +import Cura 1.6 as Cura + +UM.Dialog +{ + readonly property UM.I18nCatalog catalog: UM.I18nCatalog { name: "cura" } + + property var name + property var version + property var license + + id: base + title: catalog.i18nc("@title:window The argument is a package name, and the second is the version.", "License for %1 %2").arg(name).arg(version) + minimumWidth: 500 * screenScaleFactor + + Flickable + { + anchors.fill: parent + contentHeight: labelLicense.height + ScrollBar.vertical: UM.ScrollBar { } + + UM.Label + { + id: labelLicense + width: parent.width + text: license + } + } + + rightButtons: Cura.TertiaryButton + { + id: closeButton + text: catalog.i18nc("@action:button", "Close") + onClicked: reject() + } +}