Addon manager: install dependencies (#5376)
Addon Manager: Dependency Installation The Addon Manager can now attempt to use pip directly to install required packages as specified in either metadata.txt or requirements.txt files provided by AddOns. The packages are installed into FreeCAD.getUserAppDataDir()/AdditionalPythonPackages directory. Only simplified requirements.txt data is supported: any version information is stripped out, and only packages are supported (that is, no pip arguments, included files, etc.). Further, packages are checked against a list of allowed packages prior to being suggested for installation. Addon authors should submit a PR to the FreeCAD repo adding their requirements to the allowed list, for packages that are not already on the list (this is a malware-prevention mechanism).
This commit is contained in:
@@ -162,9 +162,15 @@ def InitApplications():
|
|||||||
|
|
||||||
# also add these directories to the sys.path to
|
# also add these directories to the sys.path to
|
||||||
# not change the old behaviour. once we have moved to
|
# not change the old behaviour. once we have moved to
|
||||||
# proper python modules this can eventuelly be removed.
|
# proper python modules this can eventually be removed.
|
||||||
sys.path = [ModDir] + libpaths + [ExtDir] + sys.path
|
sys.path = [ModDir] + libpaths + [ExtDir] + sys.path
|
||||||
|
|
||||||
|
# The AddonManager may install additional Python packages in
|
||||||
|
# this path:
|
||||||
|
additional_packages_path = os.path.join(FreeCAD.getUserAppDataDir(),"AdditionalPythonPackages")
|
||||||
|
if os.path.isdir(additional_packages_path):
|
||||||
|
sys.path.append(additional_packages_path)
|
||||||
|
|
||||||
def RunInitPy(Dir):
|
def RunInitPy(Dir):
|
||||||
InstallFile = os.path.join(Dir,"Init.py")
|
InstallFile = os.path.join(Dir,"Init.py")
|
||||||
if (os.path.exists(InstallFile)):
|
if (os.path.exists(InstallFile)):
|
||||||
|
|||||||
29
src/Mod/AddonManager/ALLOWED_PYTHON_PACKAGES.txt
Normal file
29
src/Mod/AddonManager/ALLOWED_PYTHON_PACKAGES.txt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# This file lists the Python packages that the Addon Manager allows to be installed
|
||||||
|
# automatically via pip. To request that a package be added to this list, submit a
|
||||||
|
# pull request to the FreeCAD git repository with the requested package added. Only
|
||||||
|
# packages in this list will be processed from the metadata.txt and requirements.txt
|
||||||
|
# files specified by an Addon. Note that this is NOT a requirements.txt-format file,
|
||||||
|
# no version information may be specified, and no wildcards are supported.
|
||||||
|
|
||||||
|
# Allow these packages to be installed:
|
||||||
|
ezdxf
|
||||||
|
gmsh
|
||||||
|
lxml
|
||||||
|
markdown
|
||||||
|
matplotlib
|
||||||
|
numpy
|
||||||
|
olefile
|
||||||
|
openpyxl
|
||||||
|
pandas
|
||||||
|
pillow
|
||||||
|
ply
|
||||||
|
pycollada
|
||||||
|
pygit2
|
||||||
|
pynastran
|
||||||
|
requests
|
||||||
|
rhino3dm
|
||||||
|
scipy
|
||||||
|
xlrd
|
||||||
|
xlutils
|
||||||
|
xlwt
|
||||||
|
PyYAML
|
||||||
@@ -97,6 +97,25 @@ class CommandAddonManager:
|
|||||||
"Addon Manager",
|
"Addon Manager",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.allowed_packages = set()
|
||||||
|
allow_file = os.path.join(
|
||||||
|
os.path.dirname(__file__), "ALLOWED_PYTHON_PACKAGES.txt"
|
||||||
|
)
|
||||||
|
if os.path.exists(allow_file):
|
||||||
|
with open(allow_file, "r", encoding="utf8") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
for line in lines:
|
||||||
|
if line and len(line) > 0 and line[0] != "#":
|
||||||
|
self.allowed_packages.add(line.strip())
|
||||||
|
else:
|
||||||
|
FreeCAD.PrintWarning(
|
||||||
|
translate(
|
||||||
|
"AddonsInstaller",
|
||||||
|
"Addon Manager installation problem: could not locate ALLOWED_PYTHON_PACKAGES.txt",
|
||||||
|
)
|
||||||
|
+ "\n"
|
||||||
|
)
|
||||||
|
|
||||||
def GetResources(self) -> Dict[str, str]:
|
def GetResources(self) -> Dict[str, str]:
|
||||||
return {
|
return {
|
||||||
"Pixmap": "AddonManager",
|
"Pixmap": "AddonManager",
|
||||||
@@ -262,9 +281,13 @@ class CommandAddonManager:
|
|||||||
last_cache_update = date.fromisoformat(last_cache_update_string)
|
last_cache_update = date.fromisoformat(last_cache_update_string)
|
||||||
else:
|
else:
|
||||||
# Python 3.6 and earlier don't have date.fromisoformat
|
# Python 3.6 and earlier don't have date.fromisoformat
|
||||||
date_re = re.compile("([0-9]{4})-?(1[0-2]|0[1-9])-?(3[01]|0[1-9]|[12][0-9])")
|
date_re = re.compile(
|
||||||
matches = date_re.match (last_cache_update_string)
|
"([0-9]{4})-?(1[0-2]|0[1-9])-?(3[01]|0[1-9]|[12][0-9])"
|
||||||
last_cache_update = date(int(matches.group(1)),int(matches.group(2)),int(matches.group(3)))
|
)
|
||||||
|
matches = date_re.match(last_cache_update_string)
|
||||||
|
last_cache_update = date(
|
||||||
|
int(matches.group(1)), int(matches.group(2)), int(matches.group(3))
|
||||||
|
)
|
||||||
delta_update = timedelta(days=days_between_updates)
|
delta_update = timedelta(days=days_between_updates)
|
||||||
if date.today() >= last_cache_update + delta_update:
|
if date.today() >= last_cache_update + delta_update:
|
||||||
self.update_cache = True
|
self.update_cache = True
|
||||||
@@ -859,10 +882,42 @@ class CommandAddonManager:
|
|||||||
# Check the Python dependencies:
|
# Check the Python dependencies:
|
||||||
missing_python_requirements = []
|
missing_python_requirements = []
|
||||||
for py_dep in deps.python_required:
|
for py_dep in deps.python_required:
|
||||||
try:
|
if py_dep not in missing_python_requirements:
|
||||||
__import__(py_dep)
|
try:
|
||||||
except ImportError:
|
__import__(py_dep)
|
||||||
missing_python_requirements.append(py_dep)
|
except ImportError:
|
||||||
|
missing_python_requirements.append(py_dep)
|
||||||
|
|
||||||
|
bad_packages = []
|
||||||
|
for dep in missing_python_requirements:
|
||||||
|
if dep not in self.allowed_packages:
|
||||||
|
bad_packages.append(dep)
|
||||||
|
|
||||||
|
if bad_packages:
|
||||||
|
message = translate(
|
||||||
|
"AddonsInstaller",
|
||||||
|
"The Addon {repo.name} requires Python packages that are not installed, and cannot be installed automatically. To use this workbench you must install the following Python packages manually:",
|
||||||
|
)
|
||||||
|
if len(bad_packages) < 15:
|
||||||
|
for dep in bad_packages:
|
||||||
|
message += f"\n * {dep}"
|
||||||
|
else:
|
||||||
|
message += (
|
||||||
|
"\n * (" + translate("AddonsInstaller", "Too many to list") + ")"
|
||||||
|
)
|
||||||
|
QtWidgets.QMessageBox.critical(
|
||||||
|
None, translate("AddonsInstaller", "Connection failed"), message
|
||||||
|
)
|
||||||
|
FreeCAD.Console.PrintMessage(
|
||||||
|
translate(
|
||||||
|
"AddonsInstaller",
|
||||||
|
"The following Python packages are allowed to be automatically installed",
|
||||||
|
)
|
||||||
|
+ ":\n"
|
||||||
|
)
|
||||||
|
for package in self.allowed_packages:
|
||||||
|
FreeCAD.Console.PrintMessage(f" * {package}\n")
|
||||||
|
return
|
||||||
|
|
||||||
missing_python_optionals = []
|
missing_python_optionals = []
|
||||||
for py_dep in deps.python_optional:
|
for py_dep in deps.python_optional:
|
||||||
@@ -925,16 +980,19 @@ class CommandAddonManager:
|
|||||||
self.dependency_dialog.listWidgetPythonRequired.addItem(mod)
|
self.dependency_dialog.listWidgetPythonRequired.addItem(mod)
|
||||||
for mod in missing_python_optionals:
|
for mod in missing_python_optionals:
|
||||||
item = QtWidgets.QListWidgetItem(mod)
|
item = QtWidgets.QListWidgetItem(mod)
|
||||||
item.setFlags(Qt.ItemIsUserCheckable)
|
item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
|
||||||
|
item.setCheckState(QtCore.Qt.Unchecked)
|
||||||
self.dependency_dialog.listWidgetPythonOptional.addItem(item)
|
self.dependency_dialog.listWidgetPythonOptional.addItem(item)
|
||||||
|
|
||||||
# For now, we don't offer to automatically install the dependencies
|
self.dependency_dialog.buttonBox.button(
|
||||||
# self.dependency_dialog.buttonBox.button(
|
QtWidgets.QDialogButtonBox.Yes
|
||||||
# QtWidgets.QDialogButtonBox.Yes
|
).clicked.connect(lambda: self.dependency_dialog_yes_clicked(repo))
|
||||||
# ).clicked.connect(lambda: self.dependency_dialog_yes_clicked(repo))
|
|
||||||
self.dependency_dialog.buttonBox.button(
|
self.dependency_dialog.buttonBox.button(
|
||||||
QtWidgets.QDialogButtonBox.Ignore
|
QtWidgets.QDialogButtonBox.Ignore
|
||||||
).clicked.connect(lambda: self.dependency_dialog_ignore_clicked(repo))
|
).clicked.connect(lambda: self.dependency_dialog_ignore_clicked(repo))
|
||||||
|
self.dependency_dialog.buttonBox.button(
|
||||||
|
QtWidgets.QDialogButtonBox.Cancel
|
||||||
|
).setDefault(True)
|
||||||
self.dependency_dialog.exec()
|
self.dependency_dialog.exec()
|
||||||
else:
|
else:
|
||||||
self.install(repo)
|
self.install(repo)
|
||||||
@@ -957,13 +1015,22 @@ class CommandAddonManager:
|
|||||||
python_optional = []
|
python_optional = []
|
||||||
for row in range(self.dependency_dialog.listWidgetPythonOptional.count()):
|
for row in range(self.dependency_dialog.listWidgetPythonOptional.count()):
|
||||||
item = self.dependency_dialog.listWidgetPythonOptional.item(row)
|
item = self.dependency_dialog.listWidgetPythonOptional.item(row)
|
||||||
if item.checked():
|
if item.checkState() == QtCore.Qt.Checked:
|
||||||
python_optional.append(item.text())
|
python_optional.append(item.text())
|
||||||
|
|
||||||
self.dependency_installation_worker = DependencyInstallationWorker(
|
self.dependency_installation_worker = DependencyInstallationWorker(
|
||||||
addons, python_required, python_optional
|
addons, python_required, python_optional
|
||||||
)
|
)
|
||||||
self.dependency_installation_worker.finished.connect(lambda: self.install(repo))
|
self.dependency_installation_worker.no_python_exe.connect(
|
||||||
|
lambda: self.no_python_exe(repo)
|
||||||
|
)
|
||||||
|
self.dependency_installation_worker.no_pip.connect(
|
||||||
|
lambda command: self.no_pip(command, repo)
|
||||||
|
)
|
||||||
|
self.dependency_installation_worker.failure.connect(
|
||||||
|
self.dependency_installation_failure
|
||||||
|
)
|
||||||
|
self.dependency_installation_worker.success.connect(lambda: self.install(repo))
|
||||||
self.dependency_installation_dialog = QtWidgets.QMessageBox(
|
self.dependency_installation_dialog = QtWidgets.QMessageBox(
|
||||||
QtWidgets.QMessageBox.Information,
|
QtWidgets.QMessageBox.Information,
|
||||||
translate("AddonsInstaller", "Installing dependencies"),
|
translate("AddonsInstaller", "Installing dependencies"),
|
||||||
@@ -977,6 +1044,59 @@ class CommandAddonManager:
|
|||||||
self.dependency_installation_dialog.show()
|
self.dependency_installation_dialog.show()
|
||||||
self.dependency_installation_worker.start()
|
self.dependency_installation_worker.start()
|
||||||
|
|
||||||
|
def no_python_exe(self, repo: AddonManagerRepo) -> None:
|
||||||
|
if hasattr(self, "dependency_installation_dialog"):
|
||||||
|
self.dependency_installation_dialog.hide()
|
||||||
|
result = QtWidgets.QMessageBox.critical(
|
||||||
|
self.dialog,
|
||||||
|
translate("AddonsInstaller", "Cannot execute Python"),
|
||||||
|
translate(
|
||||||
|
"AddonsInstaller",
|
||||||
|
"Failed to automatically locate your Python executable, or the path is set incorrectly. Please check the Addon Manager preferences setting for the path to Python.",
|
||||||
|
)
|
||||||
|
+ "\n\n"
|
||||||
|
+ translate(
|
||||||
|
"AddonsInstaller",
|
||||||
|
f"Dependencies could not be installed. Continue with installation of {repo.name} anyway?",
|
||||||
|
),
|
||||||
|
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
|
||||||
|
)
|
||||||
|
if result == QtWidgets.QMessageBox.Yes:
|
||||||
|
self.install(repo)
|
||||||
|
|
||||||
|
def no_pip(self, command: str, repo: AddonManagerRepo) -> None:
|
||||||
|
if hasattr(self, "dependency_installation_dialog"):
|
||||||
|
self.dependency_installation_dialog.hide()
|
||||||
|
result = QtWidgets.QMessageBox.critical(
|
||||||
|
self.dialog,
|
||||||
|
translate("AddonsInstaller", "Cannot execute pip"),
|
||||||
|
translate(
|
||||||
|
"AddonsInstaller",
|
||||||
|
"Failed to execute pip, which may be missing from your Python installation. Please ensure your system has pip installed and try again. The failed command was: ",
|
||||||
|
)
|
||||||
|
+ f"\n\n{command}\n\n"
|
||||||
|
+ translate(
|
||||||
|
"AddonsInstaller",
|
||||||
|
f"Continue with installation of {repo.name} anyway?",
|
||||||
|
),
|
||||||
|
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
|
||||||
|
)
|
||||||
|
if result == QtWidgets.QMessageBox.Yes:
|
||||||
|
self.install(repo)
|
||||||
|
|
||||||
|
def dependency_installation_failure(self, short_message: str, details: str) -> None:
|
||||||
|
if hasattr(self, "dependency_installation_dialog"):
|
||||||
|
self.dependency_installation_dialog.hide()
|
||||||
|
FreeCAD.Console.PrintError(details)
|
||||||
|
QtWidgets.QMessageBox.critical(
|
||||||
|
self.dialog,
|
||||||
|
translate("AddonsInstaller", "Package installation failed"),
|
||||||
|
short_message
|
||||||
|
+ "\n\n"
|
||||||
|
+ translate("AddonsInstaller", "See Report View for detailed failure log."),
|
||||||
|
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
|
||||||
|
)
|
||||||
|
|
||||||
def dependency_dialog_ignore_clicked(self, repo: AddonManagerRepo) -> None:
|
def dependency_dialog_ignore_clicked(self, repo: AddonManagerRepo) -> None:
|
||||||
self.install(repo)
|
self.install(repo)
|
||||||
|
|
||||||
@@ -1034,8 +1154,7 @@ class CommandAddonManager:
|
|||||||
if not failed:
|
if not failed:
|
||||||
message = translate(
|
message = translate(
|
||||||
"AddonsInstaller",
|
"AddonsInstaller",
|
||||||
"Macro successfully installed. The macro is "
|
"Macro successfully installed. The macro is now available from the Macros dialog.",
|
||||||
"now available from the Macros dialog.",
|
|
||||||
)
|
)
|
||||||
self.on_package_installed(repo, message)
|
self.on_package_installed(repo, message)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>388</width>
|
<width>757</width>
|
||||||
<height>621</height>
|
<height>621</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
@@ -232,6 +232,42 @@ of the line after a space (e.g. https://github.com/FreeCAD/FreeCAD master).</str
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="fclabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Python executable (optional):</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="Gui::PrefFileChooser" name="gui::preffilechooser" native="true">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>300</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>The path to the Python executable for package installation with pip. Autodetected if needed and not specified.</string>
|
||||||
|
</property>
|
||||||
|
<property name="prefEntry" stdset="0">
|
||||||
|
<cstring>PythonExecutableForPip</cstring>
|
||||||
|
</property>
|
||||||
|
<property name="prefPath" stdset="0">
|
||||||
|
<cstring>Addons</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="verticalSpacer">
|
<spacer name="verticalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
@@ -273,6 +309,11 @@ of the line after a space (e.g. https://github.com/FreeCAD/FreeCAD master).</str
|
|||||||
<extends>QLineEdit</extends>
|
<extends>QLineEdit</extends>
|
||||||
<header>Gui/PrefWidgets.h</header>
|
<header>Gui/PrefWidgets.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>Gui::PrefFileChooser</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>Gui/PrefWidgets.h</header>
|
||||||
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections/>
|
<connections/>
|
||||||
|
|||||||
@@ -98,9 +98,10 @@ class AddonManagerRepo:
|
|||||||
self.description = None
|
self.description = None
|
||||||
from addonmanager_utilities import construct_git_url
|
from addonmanager_utilities import construct_git_url
|
||||||
|
|
||||||
self.metadata_url = (
|
if "github" in self.url or "gitlab" in self.url or "salsa" in self.url:
|
||||||
"" if not self.url else construct_git_url(self, "package.xml")
|
self.metadata_url = construct_git_url(self, "package.xml")
|
||||||
)
|
else:
|
||||||
|
self.metadata_url = None
|
||||||
self.metadata = None
|
self.metadata = None
|
||||||
self.icon = None
|
self.icon = None
|
||||||
self.cached_icon_filename = ""
|
self.cached_icon_filename = ""
|
||||||
@@ -113,7 +114,7 @@ class AddonManagerRepo:
|
|||||||
self.requires: Set[str] = set()
|
self.requires: Set[str] = set()
|
||||||
self.blocks: Set[str] = set()
|
self.blocks: Set[str] = set()
|
||||||
|
|
||||||
# And maintains a list of required and optional Python dependencies
|
# And maintains a list of required and optional Python dependencies from metadata.txt
|
||||||
self.python_requires: Set[str] = set()
|
self.python_requires: Set[str] = set()
|
||||||
self.python_optional: Set[str] = set()
|
self.python_optional: Set[str] = set()
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ SET(AddonManager_SRCS
|
|||||||
addonmanager_workers.py
|
addonmanager_workers.py
|
||||||
AddonManager.ui
|
AddonManager.ui
|
||||||
AddonManagerOptions.ui
|
AddonManagerOptions.ui
|
||||||
|
ALLOWED_PYTHON_PACKAGES.txt
|
||||||
first_run.ui
|
first_run.ui
|
||||||
compact_view.py
|
compact_view.py
|
||||||
dependency_resolution_dialog.ui
|
dependency_resolution_dialog.ui
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import FreeCAD
|
|||||||
import os
|
import os
|
||||||
import io
|
import io
|
||||||
import hashlib
|
import hashlib
|
||||||
from typing import Dict, List
|
from typing import Dict, List, Set
|
||||||
|
|
||||||
from PySide2 import QtCore, QtNetwork
|
from PySide2 import QtCore, QtNetwork
|
||||||
from PySide2.QtCore import QObject
|
from PySide2.QtCore import QObject
|
||||||
@@ -53,6 +53,16 @@ class DownloadWorker(QObject):
|
|||||||
QtNetwork.QNetworkRequest.RedirectPolicyAttribute,
|
QtNetwork.QNetworkRequest.RedirectPolicyAttribute,
|
||||||
QtNetwork.QNetworkRequest.UserVerifiedRedirectPolicy,
|
QtNetwork.QNetworkRequest.UserVerifiedRedirectPolicy,
|
||||||
)
|
)
|
||||||
|
self.request.setAttribute(
|
||||||
|
QtNetwork.QNetworkRequest.CacheSaveControlAttribute, True
|
||||||
|
)
|
||||||
|
self.request.setAttribute(
|
||||||
|
QtNetwork.QNetworkRequest.CacheLoadControlAttribute,
|
||||||
|
QtNetwork.QNetworkRequest.PreferCache,
|
||||||
|
)
|
||||||
|
self.request.setAttribute(
|
||||||
|
QtNetwork.QNetworkRequest.BackgroundRequestAttribute, True
|
||||||
|
)
|
||||||
|
|
||||||
self.fetch_task = network_manager.get(self.request)
|
self.fetch_task = network_manager.get(self.request)
|
||||||
self.fetch_task.finished.connect(self.resolve_fetch)
|
self.fetch_task.finished.connect(self.resolve_fetch)
|
||||||
@@ -188,7 +198,7 @@ class MetadataDownloadWorker(DownloadWorker):
|
|||||||
self.updated.emit(self.repo)
|
self.updated.emit(self.repo)
|
||||||
|
|
||||||
|
|
||||||
class DependencyDownloadWorker(DownloadWorker):
|
class MetadataTxtDownloadWorker(DownloadWorker):
|
||||||
"""A worker for downloading metadata.txt"""
|
"""A worker for downloading metadata.txt"""
|
||||||
|
|
||||||
def __init__(self, parent, repo: AddonManagerRepo):
|
def __init__(self, parent, repo: AddonManagerRepo):
|
||||||
@@ -239,17 +249,67 @@ class DependencyDownloadWorker(DownloadWorker):
|
|||||||
elif line.startswith("pylibs="):
|
elif line.startswith("pylibs="):
|
||||||
depspy = line.split("=")[1].split(",")
|
depspy = line.split("=")[1].split(",")
|
||||||
for pl in depspy:
|
for pl in depspy:
|
||||||
if pl.strip():
|
dep = pl.strip()
|
||||||
self.repo.python_requires.add(pl.strip())
|
if dep:
|
||||||
|
self.repo.python_requires.add(dep)
|
||||||
FreeCAD.Console.PrintLog(
|
FreeCAD.Console.PrintLog(
|
||||||
f"{self.repo.display_name} requires python package '{pl.strip()}'\n"
|
f"{self.repo.display_name} requires python package '{dep}'\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
elif line.startswith("optionalpylibs="):
|
elif line.startswith("optionalpylibs="):
|
||||||
opspy = line.split("=")[1].split(",")
|
opspy = line.split("=")[1].split(",")
|
||||||
for pl in opspy:
|
for pl in opspy:
|
||||||
if pl.strip():
|
dep = pl.strip()
|
||||||
self.repo.python_optional.add(pl.strip())
|
if dep:
|
||||||
|
self.repo.python_optional.add(dep)
|
||||||
FreeCAD.Console.PrintLog(
|
FreeCAD.Console.PrintLog(
|
||||||
f"{self.repo.display_name} optionally imports python package '{pl.strip()}'\n"
|
f"{self.repo.display_name} optionally imports python package '{pl.strip()}'\n"
|
||||||
)
|
)
|
||||||
self.updated.emit(self.repo)
|
self.updated.emit(self.repo)
|
||||||
|
|
||||||
|
|
||||||
|
class RequirementsTxtDownloadWorker(DownloadWorker):
|
||||||
|
"""A worker for downloading requirements.txt"""
|
||||||
|
|
||||||
|
def __init__(self, parent, repo: AddonManagerRepo):
|
||||||
|
super().__init__(parent, utils.construct_git_url(repo, "requirements.txt"))
|
||||||
|
self.repo = repo
|
||||||
|
|
||||||
|
def resolve_fetch(self):
|
||||||
|
"""Called when the data fetch completed, either with an error, or if it found the metadata file"""
|
||||||
|
|
||||||
|
if self.fetch_task.error() == QtNetwork.QNetworkReply.NetworkError.NoError:
|
||||||
|
FreeCAD.Console.PrintLog(
|
||||||
|
f"Found a requirements.txt file for {self.repo.name}\n"
|
||||||
|
)
|
||||||
|
new_deps = self.fetch_task.readAll()
|
||||||
|
self.parse_file(new_deps.data().decode("utf8"))
|
||||||
|
elif (
|
||||||
|
self.fetch_task.error()
|
||||||
|
== QtNetwork.QNetworkReply.NetworkError.ContentNotFoundError
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
elif (
|
||||||
|
self.fetch_task.error()
|
||||||
|
== QtNetwork.QNetworkReply.NetworkError.OperationCanceledError
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
FreeCAD.Console.PrintWarning(
|
||||||
|
translate("AddonsInstaller", "Failed to connect to URL")
|
||||||
|
+ f":\n{self.url}\n {self.fetch_task.error()}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_file(self, data: str) -> None:
|
||||||
|
f = io.StringIO(data)
|
||||||
|
lines = f.readlines()
|
||||||
|
for line in lines:
|
||||||
|
break_chars = " <>=~!+#"
|
||||||
|
package = line
|
||||||
|
for n, c in enumerate(line):
|
||||||
|
if c in break_chars:
|
||||||
|
package = line[:n].strip()
|
||||||
|
break
|
||||||
|
if package:
|
||||||
|
self.repo.python_requires.add(package)
|
||||||
|
self.updated.emit(self.repo)
|
||||||
|
|||||||
@@ -197,7 +197,6 @@ def get_zip_url(repo):
|
|||||||
):
|
):
|
||||||
# https://framagit.org/freecad-france/mooc-workbench/-/archive/master/mooc-workbench-master.zip
|
# https://framagit.org/freecad-france/mooc-workbench/-/archive/master/mooc-workbench-master.zip
|
||||||
# https://salsa.debian.org/mess42/pyrate/-/archive/master/pyrate-master.zip
|
# https://salsa.debian.org/mess42/pyrate/-/archive/master/pyrate-master.zip
|
||||||
reponame = baseurl.strip("/").split("/")[-1]
|
|
||||||
return f"{repo.url}/-/archive/{repo.branch}/{repo.name}-{repo.branch}.zip"
|
return f"{repo.url}/-/archive/{repo.branch}/{repo.name}-{repo.branch}.zip"
|
||||||
else:
|
else:
|
||||||
FreeCAD.Console.PrintLog(
|
FreeCAD.Console.PrintLog(
|
||||||
@@ -237,7 +236,7 @@ def get_readme_url(repo):
|
|||||||
def get_metadata_url(url):
|
def get_metadata_url(url):
|
||||||
"Returns the location of a package.xml metadata file"
|
"Returns the location of a package.xml metadata file"
|
||||||
|
|
||||||
return construct_git_url(repo, "package.xml")
|
return construct_git_url(url, "package.xml")
|
||||||
|
|
||||||
|
|
||||||
def get_desc_regex(repo):
|
def get_desc_regex(repo):
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import io
|
|||||||
import time
|
import time
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import platform
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Union, List
|
from typing import Union, List
|
||||||
|
|
||||||
@@ -46,7 +47,11 @@ if FreeCAD.GuiUp:
|
|||||||
|
|
||||||
import addonmanager_utilities as utils
|
import addonmanager_utilities as utils
|
||||||
from addonmanager_macro import Macro
|
from addonmanager_macro import Macro
|
||||||
from addonmanager_metadata import MetadataDownloadWorker, DependencyDownloadWorker
|
from addonmanager_metadata import (
|
||||||
|
MetadataDownloadWorker,
|
||||||
|
MetadataTxtDownloadWorker,
|
||||||
|
RequirementsTxtDownloadWorker,
|
||||||
|
)
|
||||||
from AddonManagerRepo import AddonManagerRepo
|
from AddonManagerRepo import AddonManagerRepo
|
||||||
|
|
||||||
translate = FreeCAD.Qt.translate
|
translate = FreeCAD.Qt.translate
|
||||||
@@ -642,8 +647,7 @@ class FillMacroListWorker(QtCore.QThread):
|
|||||||
FreeCAD.Console.PrintWarning(
|
FreeCAD.Console.PrintWarning(
|
||||||
translate(
|
translate(
|
||||||
"AddonsInstaller",
|
"AddonsInstaller",
|
||||||
"There appears to be an issue connecting to the Wiki, "
|
"Error connecting to the Wiki, FreeCAD cannot retrieve the Wiki macro list at this time",
|
||||||
"therefore FreeCAD cannot retrieve the Wiki macro list at this time",
|
|
||||||
)
|
)
|
||||||
+ "\n"
|
+ "\n"
|
||||||
)
|
)
|
||||||
@@ -894,7 +898,7 @@ class ShowWorker(QtCore.QThread):
|
|||||||
return
|
return
|
||||||
message = desc
|
message = desc
|
||||||
if self.repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
|
if self.repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
|
||||||
# Addon is installed but we haven't checked it yet, so lets check if it has an update
|
# Addon is installed but we haven't checked it yet, so let's check if it has an update
|
||||||
upd = False
|
upd = False
|
||||||
# checking for updates
|
# checking for updates
|
||||||
if not NOGIT and have_git:
|
if not NOGIT and have_git:
|
||||||
@@ -916,49 +920,7 @@ class ShowWorker(QtCore.QThread):
|
|||||||
AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE
|
AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE
|
||||||
)
|
)
|
||||||
self.update_status.emit(self.repo)
|
self.update_status.emit(self.repo)
|
||||||
|
|
||||||
if QtCore.QThread.currentThread().isInterruptionRequested():
|
|
||||||
return
|
|
||||||
|
|
||||||
# If the Addon is obsolete, let the user know through the Addon UI
|
|
||||||
if self.repo.name in obsolete:
|
|
||||||
message = """
|
|
||||||
<div style="width: 100%; text-align:center; background: #FFB3B3;">
|
|
||||||
<strong style="color: #FFFFFF; background: #FF0000;">
|
|
||||||
"""
|
|
||||||
message += (
|
|
||||||
translate("AddonsInstaller", "This addon is marked as obsolete")
|
|
||||||
+ "</strong><br/><br/>"
|
|
||||||
)
|
|
||||||
message += (
|
|
||||||
translate(
|
|
||||||
"AddonsInstaller",
|
|
||||||
"This usually means it is no longer maintained, "
|
|
||||||
"and some more advanced addon in this list "
|
|
||||||
"provides the same functionality.",
|
|
||||||
)
|
|
||||||
+ "<br/></div><hr/>"
|
|
||||||
+ desc
|
|
||||||
)
|
|
||||||
|
|
||||||
# If the Addon is Python 2 only, let the user know through the Addon UI
|
|
||||||
if self.repo.name in py2only:
|
|
||||||
message = """
|
|
||||||
<div style="width: 100%; text-align:center; background: #ffe9b3;">
|
|
||||||
<strong style="color: #FFFFFF; background: #ff8000;">
|
|
||||||
"""
|
|
||||||
message += (
|
|
||||||
translate("AddonsInstaller", "This addon is marked as Python 2 Only")
|
|
||||||
+ "</strong><br/><br/>"
|
|
||||||
)
|
|
||||||
message += translate(
|
|
||||||
"AddonsInstaller",
|
|
||||||
"This workbench may no longer be maintained and "
|
|
||||||
"installing it on a Python 3 system will more than "
|
|
||||||
"likely result in errors at startup or while in use.",
|
|
||||||
)
|
|
||||||
message += "<br/></div><hr/>" + desc
|
|
||||||
|
|
||||||
if QtCore.QThread.currentThread().isInterruptionRequested():
|
if QtCore.QThread.currentThread().isInterruptionRequested():
|
||||||
return
|
return
|
||||||
self.readme_updated.emit(message)
|
self.readme_updated.emit(message)
|
||||||
@@ -1117,8 +1079,7 @@ class InstallWorkbenchWorker(QtCore.QThread):
|
|||||||
FreeCAD.Console.PrintError(
|
FreeCAD.Console.PrintError(
|
||||||
translate(
|
translate(
|
||||||
"AddonsInstaller",
|
"AddonsInstaller",
|
||||||
"Your version of Python doesn't appear to support ZIP "
|
"Your version of Python doesn't appear to support ZIP files. Unable to proceed.",
|
||||||
"files. Unable to proceed.",
|
|
||||||
)
|
)
|
||||||
+ "\n"
|
+ "\n"
|
||||||
)
|
)
|
||||||
@@ -1166,8 +1127,7 @@ class InstallWorkbenchWorker(QtCore.QThread):
|
|||||||
FreeCAD.Console.PrintWarning(
|
FreeCAD.Console.PrintWarning(
|
||||||
translate(
|
translate(
|
||||||
"AddonsInstaller",
|
"AddonsInstaller",
|
||||||
"You are installing a Python 2 workbench on "
|
"You are installing a Python 2 workbench on a system running Python 3 - ",
|
||||||
"a system running Python 3 - ",
|
|
||||||
)
|
)
|
||||||
+ str(self.repo.name)
|
+ str(self.repo.name)
|
||||||
+ "\n"
|
+ "\n"
|
||||||
@@ -1176,11 +1136,10 @@ class InstallWorkbenchWorker(QtCore.QThread):
|
|||||||
utils.repair_git_repo(self.repo.url, clonedir)
|
utils.repair_git_repo(self.repo.url, clonedir)
|
||||||
repo = git.Git(clonedir)
|
repo = git.Git(clonedir)
|
||||||
try:
|
try:
|
||||||
repo.pull() # Refuses to take a progress object?
|
repo.pull() # Refuses to take a progress object?
|
||||||
answer = translate(
|
answer = translate(
|
||||||
"AddonsInstaller",
|
"AddonsInstaller",
|
||||||
"Workbench successfully updated. "
|
"Workbench successfully updated. Please restart FreeCAD to apply the changes.",
|
||||||
"Please restart FreeCAD to apply the changes.",
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
answer = (
|
answer = (
|
||||||
@@ -1207,8 +1166,7 @@ class InstallWorkbenchWorker(QtCore.QThread):
|
|||||||
FreeCAD.Console.PrintWarning(
|
FreeCAD.Console.PrintWarning(
|
||||||
translate(
|
translate(
|
||||||
"AddonsInstaller",
|
"AddonsInstaller",
|
||||||
"You are installing a Python 2 workbench on "
|
"You are installing a Python 2 workbench on a system running Python 3 - ",
|
||||||
"a system running Python 3 - ",
|
|
||||||
)
|
)
|
||||||
+ str(self.repo.name)
|
+ str(self.repo.name)
|
||||||
+ "\n"
|
+ "\n"
|
||||||
@@ -1234,8 +1192,7 @@ class InstallWorkbenchWorker(QtCore.QThread):
|
|||||||
|
|
||||||
answer = translate(
|
answer = translate(
|
||||||
"AddonsInstaller",
|
"AddonsInstaller",
|
||||||
"Workbench successfully installed. Please restart "
|
"Workbench successfully installed. Please restart FreeCAD to apply the changes.",
|
||||||
"FreeCAD to apply the changes.",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH:
|
if self.repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH:
|
||||||
@@ -1260,8 +1217,7 @@ class InstallWorkbenchWorker(QtCore.QThread):
|
|||||||
).SetString("destination", clonedir)
|
).SetString("destination", clonedir)
|
||||||
answer += "\n\n" + translate(
|
answer += "\n\n" + translate(
|
||||||
"AddonsInstaller",
|
"AddonsInstaller",
|
||||||
"A macro has been installed and is available "
|
"A macro has been installed and is available under Macro -> Macros menu",
|
||||||
"under Macro -> Macros menu",
|
|
||||||
)
|
)
|
||||||
answer += ":\n<b>" + f + "</b>"
|
answer += ":\n<b>" + f + "</b>"
|
||||||
self.update_metadata()
|
self.update_metadata()
|
||||||
@@ -1383,7 +1339,12 @@ class InstallWorkbenchWorker(QtCore.QThread):
|
|||||||
|
|
||||||
|
|
||||||
class DependencyInstallationWorker(QtCore.QThread):
|
class DependencyInstallationWorker(QtCore.QThread):
|
||||||
"""Install dependencies: not yet implemented, DO NOT CALL"""
|
"""Install dependencies using Addonmanager for FreeCAD, and pip for python"""
|
||||||
|
|
||||||
|
no_python_exe = QtCore.Signal()
|
||||||
|
no_pip = QtCore.Signal(str) # Attempted command
|
||||||
|
failure = QtCore.Signal(str, str) # Short message, detailed message
|
||||||
|
success = QtCore.Signal()
|
||||||
|
|
||||||
def __init__(self, addons, python_required, python_optional):
|
def __init__(self, addons, python_required, python_optional):
|
||||||
QtCore.QThread.__init__(self)
|
QtCore.QThread.__init__(self)
|
||||||
@@ -1397,50 +1358,113 @@ class DependencyInstallationWorker(QtCore.QThread):
|
|||||||
if QtCore.QThread.currentThread().isInterruptionRequested():
|
if QtCore.QThread.currentThread().isInterruptionRequested():
|
||||||
return
|
return
|
||||||
worker = InstallWorkbenchWorker(repo)
|
worker = InstallWorkbenchWorker(repo)
|
||||||
# Don't bother with a separate thread for this right now, just run it here:
|
worker.start()
|
||||||
FreeCAD.Console.PrintMessage(f"Pretending to install {repo.name}")
|
while worker.isRunning():
|
||||||
time.sleep(3)
|
if QtCore.QThread.currentThread().isInterruptionRequested():
|
||||||
continue
|
worker.requestInterruption()
|
||||||
# worker.run()
|
worker.wait()
|
||||||
|
return
|
||||||
|
time.sleep(0.1)
|
||||||
|
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50)
|
||||||
|
|
||||||
if self.python_required or self.python_optional:
|
if self.python_required or self.python_optional:
|
||||||
# See if we have pip available:
|
|
||||||
try:
|
# Find Python. In preference order
|
||||||
subprocess.check_call(["pip", "--version"])
|
# A) The value of the PythonExecutableForPip user preference
|
||||||
except subprocess.CalledProcessError as e:
|
# B) The executable located in the same bin directory as FreeCAD and called "python3"
|
||||||
FreeCAD.Console.PrintError(
|
# C) The executable located in the same bin directory as FreeCAD and called "python"
|
||||||
translate(
|
# D) The result of an shutil search for your system's "python3" executable
|
||||||
"AddonsInstaller", "Failed to execute pip. Returned error was:"
|
# E) The result of an shutil search for your system's "python" executable
|
||||||
)
|
prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||||
+ f"\n{e.output}"
|
python_exe = prefs.GetString("PythonExecutableForPip", "Not set")
|
||||||
)
|
if (
|
||||||
|
not python_exe
|
||||||
|
or python_exe == "Not set"
|
||||||
|
or not os.path.exists(python_exe)
|
||||||
|
):
|
||||||
|
fc_dir = FreeCAD.getHomePath()
|
||||||
|
python_exe = os.path.join(fc_dir, "bin", "python3")
|
||||||
|
if "Windows" in platform.system():
|
||||||
|
python_exe += ".exe"
|
||||||
|
|
||||||
|
if not python_exe or not os.path.exists(python_exe):
|
||||||
|
python_exe = os.path.join(fc_dir, "bin", "python")
|
||||||
|
if "Windows" in platform.system():
|
||||||
|
python_exe += ".exe"
|
||||||
|
|
||||||
|
if not python_exe or not os.path.exists(python_exe):
|
||||||
|
python_exe = shutil.which("python3")
|
||||||
|
|
||||||
|
if not python_exe or not os.path.exists(python_exe):
|
||||||
|
python_exe = shutil.which("python")
|
||||||
|
|
||||||
|
if not python_exe or not os.path.exists(python_exe):
|
||||||
|
self.no_python_exe.emit()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
prefs.SetString("PythonExecutableForPip", python_exe)
|
||||||
|
|
||||||
|
pip_failed = False
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
[python_exe, "-m", "pip", "--version"], stdout=subprocess.PIPE
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
pip_failed = True
|
||||||
|
if proc.returncode != 0:
|
||||||
|
pip_failed = True
|
||||||
|
if pip_failed:
|
||||||
|
self.no_pip.emit(f"{python_exe} -m pip --version")
|
||||||
|
return
|
||||||
|
FreeCAD.Console.PrintMessage(proc.stdout)
|
||||||
|
FreeCAD.Console.PrintWarning(proc.stderr)
|
||||||
|
result = proc.stdout
|
||||||
|
FreeCAD.Console.PrintMessage(result.decode())
|
||||||
|
vendor_path = os.path.join(
|
||||||
|
FreeCAD.getUserAppDataDir(), "AdditionalPythonPackages"
|
||||||
|
)
|
||||||
|
if not os.path.exists(vendor_path):
|
||||||
|
os.makedirs(vendor_path)
|
||||||
|
|
||||||
for pymod in self.python_required:
|
for pymod in self.python_required:
|
||||||
if QtCore.QThread.currentThread().isInterruptionRequested():
|
if QtCore.QThread.currentThread().isInterruptionRequested():
|
||||||
return
|
return
|
||||||
FreeCAD.Console.PrintMessage(f"Pretending to install {pymod}")
|
proc = subprocess.run(
|
||||||
time.sleep(3)
|
[python_exe, "-m", "pip", "install", "--target", vendor_path, pymod],
|
||||||
continue
|
stdout=subprocess.PIPE,
|
||||||
# subprocess.check_call(["pip", "install", pymod])
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
FreeCAD.Console.PrintMessage(proc.stdout.decode())
|
||||||
|
if proc.returncode != 0:
|
||||||
|
self.emit.failure(
|
||||||
|
translate(
|
||||||
|
"AddonsInstaller",
|
||||||
|
f"Installation of Python package {pymod} failed",
|
||||||
|
),
|
||||||
|
proc.stderr,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
for pymod in self.python_optional:
|
for pymod in self.python_optional:
|
||||||
if QtCore.QThread.currentThread().isInterruptionRequested():
|
if QtCore.QThread.currentThread().isInterruptionRequested():
|
||||||
return
|
return
|
||||||
try:
|
proc = subprocess.run(
|
||||||
FreeCAD.Console.PrintMessage(f"Pretending to install {pymod}")
|
[python_exe, "-m", "pip", "install", "--target", vendor_path, pymod],
|
||||||
time.sleep(3)
|
stdout=subprocess.PIPE,
|
||||||
continue
|
stderr=subprocess.PIPE,
|
||||||
# subprocess.check_call([sys.executable, "-m", "pip", "install", pymod])
|
)
|
||||||
except subprocess.CalledProcessError as e:
|
FreeCAD.Console.PrintMessage(proc.stdout.decode())
|
||||||
FreeCAD.Console.PrintError(
|
if proc.returncode != 0:
|
||||||
|
self.emit.failure(
|
||||||
translate(
|
translate(
|
||||||
"AddonsInstaller",
|
"AddonsInstaller",
|
||||||
"Failed to install option dependency {pymod}. Returned error was:",
|
f"Installation of Python package {pymod} failed",
|
||||||
)
|
),
|
||||||
+ f"\n{e.output}"
|
proc.stderr,
|
||||||
)
|
)
|
||||||
# This is not fatal, we can just continue without it
|
return
|
||||||
|
|
||||||
|
self.success.emit()
|
||||||
|
|
||||||
|
|
||||||
class CheckSingleWorker(QtCore.QThread):
|
class CheckSingleWorker(QtCore.QThread):
|
||||||
@@ -1531,6 +1555,16 @@ class UpdateMetadataCacheWorker(QtCore.QThread):
|
|||||||
) # Must be created on this thread
|
) # Must be created on this thread
|
||||||
download_queue.finished.connect(self.on_finished)
|
download_queue.finished.connect(self.on_finished)
|
||||||
|
|
||||||
|
# Prevent strange internal Qt errors about cache setup by pre-emptively setting
|
||||||
|
# up a cache. The error this fixes is:
|
||||||
|
# "caching was enabled after some bytes had been written"
|
||||||
|
qnam_cache = os.path.join(
|
||||||
|
FreeCAD.getUserCachePath(), "AddonManager", "QNAM_CACHE"
|
||||||
|
)
|
||||||
|
diskCache = QtNetwork.QNetworkDiskCache()
|
||||||
|
diskCache.setCacheDirectory(qnam_cache)
|
||||||
|
download_queue.setCache(diskCache)
|
||||||
|
|
||||||
self.downloaders = []
|
self.downloaders = []
|
||||||
for repo in self.repos:
|
for repo in self.repos:
|
||||||
if repo.metadata_url:
|
if repo.metadata_url:
|
||||||
@@ -1541,7 +1575,13 @@ class UpdateMetadataCacheWorker(QtCore.QThread):
|
|||||||
self.downloaders.append(downloader)
|
self.downloaders.append(downloader)
|
||||||
|
|
||||||
# metadata.txt
|
# metadata.txt
|
||||||
downloader = DependencyDownloadWorker(None, repo)
|
downloader = MetadataTxtDownloadWorker(None, repo)
|
||||||
|
downloader.start_fetch(download_queue)
|
||||||
|
downloader.updated.connect(self.on_updated)
|
||||||
|
self.downloaders.append(downloader)
|
||||||
|
|
||||||
|
# requirements.txt
|
||||||
|
downloader = RequirementsTxtDownloadWorker(None, repo)
|
||||||
downloader.start_fetch(download_queue)
|
downloader.start_fetch(download_queue)
|
||||||
downloader.updated.connect(self.on_updated)
|
downloader.updated.connect(self.on_updated)
|
||||||
self.downloaders.append(downloader)
|
self.downloaders.append(downloader)
|
||||||
@@ -1593,8 +1633,13 @@ class UpdateMetadataCacheWorker(QtCore.QThread):
|
|||||||
self.package_updated.emit(repo)
|
self.package_updated.emit(repo)
|
||||||
|
|
||||||
def send_ui_update(self):
|
def send_ui_update(self):
|
||||||
self.progress_made.emit(
|
completed = self.num_downloads_completed.get()
|
||||||
self.num_downloads_completed.get(), self.num_downloads_required
|
required = self.num_downloads_required
|
||||||
|
percentage = int(100 * completed / required)
|
||||||
|
self.progress_made.emit(completed, required)
|
||||||
|
self.status_message.emit(
|
||||||
|
translate("AddonsInstaller", "Retrieving package metadata...")
|
||||||
|
+ f" {completed} / {required} ({percentage}%)"
|
||||||
)
|
)
|
||||||
|
|
||||||
def terminate_all(self):
|
def terminate_all(self):
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>455</width>
|
<width>455</width>
|
||||||
<height>200</height>
|
<height>260</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@@ -20,7 +20,9 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label">
|
<widget class="QLabel" name="label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>This Addon has the following required and optional dependencies. You must install them before this Addon can be used.</string>
|
<string>This Addon has the following required and optional dependencies. You must install them before this Addon can be used.
|
||||||
|
|
||||||
|
Do you want the Addon Manager to install them automatically? Choose "Ignore" to install the Addon without installing the dependencies.</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="wordWrap">
|
<property name="wordWrap">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
@@ -73,7 +75,7 @@
|
|||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="standardButtons">
|
<property name="standardButtons">
|
||||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ignore</set>
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ignore|QDialogButtonBox::Yes</set>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
Reference in New Issue
Block a user