######################################################################################################################
# Copyright (C) 2017-2022 Spine project consortium
# Copyright Spine Toolbox contributors
# This file is part of Spine Toolbox.
# Spine Toolbox 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 3 of the License, or (at your option)
# any later version. This program 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
# this program. If not, see <http://www.gnu.org/licenses/>.
######################################################################################################################
"""Classes for custom QDialogs for julia setup."""
from enum import IntEnum, auto
from PySide6.QtWidgets import (
QWidget,
QWizard,
QWizardPage,
QLabel,
QPushButton,
QVBoxLayout,
QHBoxLayout,
QLineEdit,
QFileDialog,
QCheckBox,
QRadioButton,
)
from PySide6.QtCore import Slot, Qt
from PySide6.QtGui import QCursor
from ..execution_managers import QProcessExecutionManager
from ..config import REQUIRED_SPINE_OPT_VERSION
from .custom_qtextbrowser import MonoSpaceFontTextBrowser
from .custom_qwidgets import WrapLabel, HyperTextLabel, QWizardProcessPage
[docs]class _PageId(IntEnum):
[docs] SELECT_JULIA = auto()
[docs] CHECK_PREVIOUS_INSTALL = auto()
[docs] ADD_UP_SPINE_OPT = auto()
[docs] TROUBLESHOOT_PROBLEMS = auto()
[docs] TROUBLESHOOT_SOLUTION = auto()
[docs] RESET_REGISTRY = auto()
[docs] ADD_UP_SPINE_OPT_AGAIN = auto()
[docs] TOTAL_FAILURE = auto()
[docs]class AddUpSpineOptWizard(QWizard):
"""A wizard to install & upgrade SpineOpt."""
def __init__(self, parent, julia_exe, julia_project):
"""
Args:
parent (QWidget): the parent widget (SettingsWidget)
julia_exe (str): path to Julia executable
julia_project (str): path to Julia project
"""
super().__init__(parent)
self.process_log = None
self.required_action = None
self.setWindowTitle("SpineOpt Installer")
self.setPage(_PageId.INTRO, IntroPage(self))
self.setPage(_PageId.SELECT_JULIA, SelectJuliaPage(self, julia_exe, julia_project))
self.setPage(_PageId.CHECK_PREVIOUS_INSTALL, CheckPreviousInstallPage(self))
self.setPage(_PageId.ADD_UP_SPINE_OPT, AddUpSpineOptPage(self))
self.setPage(_PageId.SUCCESS, SuccessPage(self))
self.setPage(_PageId.FAILURE, FailurePage(self))
self.setPage(_PageId.TROUBLESHOOT_PROBLEMS, TroubleshootProblemsPage(self))
self.setPage(_PageId.TROUBLESHOOT_SOLUTION, TroubleshootSolutionPage(self))
self.setPage(_PageId.RESET_REGISTRY, ResetRegistryPage(self))
self.setPage(_PageId.ADD_UP_SPINE_OPT_AGAIN, AddUpSpineOptAgainPage(self))
self.setPage(_PageId.TOTAL_FAILURE, TotalFailurePage(self))
self.setStartId(_PageId.INTRO)
[docs]class IntroPage(QWizardPage):
def __init__(self, parent):
super().__init__(parent)
self.setTitle("Welcome")
label = HyperTextLabel(
"This wizard will help you install or upgrade "
"<a title='spine opt' href='https://github.com/spine-tools/SpineOpt.jl#spineoptjl'>SpineOpt</a> "
"in a Julia project of your choice."
)
layout = QVBoxLayout(self)
layout.addWidget(label)
[docs] def nextId(self):
return _PageId.SELECT_JULIA
[docs]class SelectJuliaPage(QWizardPage):
def __init__(self, parent, julia_exe, julia_project):
super().__init__(parent)
self.setTitle("Select Julia project")
self._julia_exe = julia_exe
self._julia_project = julia_project
self._julia_exe_line_edit = QLineEdit()
self._julia_project_line_edit = QLineEdit()
self._julia_project_line_edit.setPlaceholderText("Use Julia's default project")
self.registerField("julia_exe*", self._julia_exe_line_edit)
self.registerField("julia_project", self._julia_project_line_edit)
layout = QVBoxLayout(self)
layout.addWidget(QLabel("Julia executable:"))
julia_exe_widget = QWidget()
julia_exe_layout = QHBoxLayout(julia_exe_widget)
julia_exe_layout.addWidget(self._julia_exe_line_edit)
julia_exe_button = QPushButton("Browse")
julia_exe_layout.addWidget(julia_exe_button)
layout.addWidget(julia_exe_widget)
layout.addWidget(QLabel("Julia project (directory):"))
julia_project_widget = QWidget()
julia_project_layout = QHBoxLayout(julia_project_widget)
julia_project_layout.addWidget(self._julia_project_line_edit)
julia_project_button = QPushButton("Browse")
julia_project_layout.addWidget(julia_project_button)
layout.addWidget(julia_project_widget)
julia_exe_button.clicked.connect(self._select_julia_exe)
julia_project_button.clicked.connect(self._select_julia_project)
[docs] def initializePage(self):
self._julia_exe_line_edit.setText(self._julia_exe)
self._julia_project_line_edit.setText(self._julia_project)
@Slot(bool)
[docs] def _select_julia_exe(self, _):
julia_exe, _ = QFileDialog.getOpenFileName(self, "Select Julia executable", self.field("julia_exe"))
if not julia_exe:
return
self.setField("julia_exe", julia_exe)
@Slot(bool)
[docs] def _select_julia_project(self, _):
julia_project = QFileDialog.getExistingDirectory(
self, "Select Julia project (directory)", self.field("julia_project")
)
if not julia_project:
return
self.setField("julia_project", julia_project)
[docs] def nextId(self):
return _PageId.CHECK_PREVIOUS_INSTALL
[docs]class CheckPreviousInstallPage(QWizardPage):
def __init__(self, parent):
super().__init__(parent)
self.setTitle("Checking previous installation")
self._exec_mngr = None
self._errored = False
QVBoxLayout(self)
[docs] def isComplete(self):
return self._exec_mngr is None and not self._errored
[docs] def cleanupPage(self):
super().cleanupPage()
if self._exec_mngr is not None:
self._exec_mngr.stop_execution()
[docs] def initializePage(self):
_clear_layout(self.layout())
julia_exe = self.field("julia_exe")
julia_project = self.field("julia_project")
args = [
f"--project={julia_project}",
"-e",
"import Pkg; "
'manifest = joinpath(dirname(Base.active_project()), "Manifest.toml"); '
"pkgs = isfile(manifest) ? Pkg.TOML.parsefile(manifest) : Dict(); "
'manifest_format = get(pkgs, "manifest_format", missing); '
"if manifest_format === missing "
'spine_opt = get(pkgs, "SpineOpt", nothing) '
'else spine_opt = get(pkgs["deps"], "SpineOpt", nothing) end; '
'if spine_opt != nothing println(spine_opt[1]["version"]) end; ',
]
self._exec_mngr = QProcessExecutionManager(self, julia_exe, args, silent=True)
self.completeChanged.emit()
self._exec_mngr.execution_finished.connect(self._handle_check_install_finished)
qApp.setOverrideCursor(QCursor(Qt.BusyCursor)) # pylint: disable=undefined-variable
self._exec_mngr.start_execution()
[docs] def _handle_check_install_finished(self, ret):
qApp.restoreOverrideCursor() # pylint: disable=undefined-variable
self._exec_mngr.execution_finished.disconnect(self._handle_check_install_finished)
if self.wizard().currentPage() is not self:
return
output_log = self._exec_mngr.process_output
error_log = self._exec_mngr.process_error
self._exec_mngr = None
if ret != 0:
msg = "<p>Julia failed to run.</p><p>Please go back and check your selections.</p>"
self.layout().addWidget(WrapLabel(msg))
if error_log:
self.layout().addWidget(WrapLabel("Below is the error log."))
log = MonoSpaceFontTextBrowser(self)
log.append(error_log)
self.layout().addWidget(log)
self._errored = True
self.completeChanged.emit()
return
spine_opt_version = output_log
if spine_opt_version:
if [int(x) for x in spine_opt_version.split(".")] >= [
int(x) for x in REQUIRED_SPINE_OPT_VERSION.split(".")
]:
msg = f"SpineOpt version {spine_opt_version} is installed and is already the required version."
self.layout().addWidget(WrapLabel(msg))
self.setFinalPage(True)
return
msg = (
f"SpineOpt version {spine_opt_version} is installed, "
f"but version {REQUIRED_SPINE_OPT_VERSION} is required."
)
self.layout().addWidget(WrapLabel(msg))
self.wizard().required_action = "update"
self.setFinalPage(False)
self.setCommitPage(True)
self.setButtonText(QWizard.CommitButton, "Update SpineOpt")
self.completeChanged.emit()
return
self.layout().addWidget(QLabel("SpineOpt is not installed."))
self.wizard().required_action = "add"
self.setFinalPage(False)
self.setCommitPage(True)
self.setButtonText(QWizard.CommitButton, "Install SpineOpt")
self.completeChanged.emit()
[docs] def nextId(self):
if self.wizard().required_action is None:
return -1
return _PageId.ADD_UP_SPINE_OPT
[docs]class AddUpSpineOptPage(QWizardProcessPage):
[docs] def initializePage(self):
processing, code, process = {
"add": (
"Installing",
'using Pkg; pkg"registry add General https://github.com/spine-tools/SpineJuliaRegistry.git"; '
'pkg"add SpineOpt"',
"installation",
),
"update": ("Updating", 'using Pkg; pkg"up SpineOpt"', "update"),
}[self.wizard().required_action]
self.setTitle(f"{processing} SpineOpt")
julia_exe = self.field("julia_exe")
julia_project = self.field("julia_project")
args = [f"--project={julia_project}", "-e", code]
self._exec_mngr = QProcessExecutionManager(self, julia_exe, args, semisilent=True)
self.completeChanged.emit()
self._exec_mngr.execution_finished.connect(self._handle_spine_opt_add_up_finished)
self.msg_success.emit(f"SpineOpt {process} started")
cmd = julia_exe + " " + " ".join(args)
self.msg.emit(f"$ <b>{cmd}<b/>")
qApp.setOverrideCursor(QCursor(Qt.BusyCursor)) # pylint: disable=undefined-variable
self._exec_mngr.start_execution()
[docs] def _handle_spine_opt_add_up_finished(self, ret):
qApp.restoreOverrideCursor() # pylint: disable=undefined-variable
self._exec_mngr.execution_finished.disconnect(self._handle_spine_opt_add_up_finished)
if self.wizard().currentPage() is not self:
return
self._exec_mngr = None
self._successful = ret == 0
self.completeChanged.emit()
if self._successful:
configured = {"add": "installed", "update": "updated"}[self.wizard().required_action]
self.msg_success.emit(f"SpineOpt successfully {configured}")
return
process = {"add": "installation", "update": "updatee"}[self.wizard().required_action]
self.msg_error.emit(f"SpineOpt {process} failed")
self.wizard().process_log = self._log.toHtml()
[docs] def nextId(self):
if self._successful:
return _PageId.SUCCESS
return _PageId.FAILURE
[docs]class SuccessPage(QWizardPage):
def __init__(self, parent):
super().__init__(parent)
self._label = WrapLabel()
layout = QVBoxLayout(self)
layout.addWidget(self._label)
[docs] def initializePage(self):
process = {"add": "Installation", "update": "Update"}[self.wizard().required_action]
self.setTitle(f"{process} successful")
[docs] def nextId(self):
return -1
[docs]class FailurePage(QWizardPage):
def __init__(self, parent):
super().__init__(parent)
check_box = QCheckBox("Troubleshoot problems")
check_box.setChecked(True)
self.registerField("troubleshoot", check_box)
layout = QVBoxLayout(self)
msg = "Apologies."
layout.addWidget(WrapLabel(msg))
layout.addStretch()
layout.addWidget(check_box)
layout.addStretch()
layout.addStretch()
check_box.clicked.connect(self._handle_check_box_clicked)
@Slot(bool)
[docs] def _handle_check_box_clicked(self, checked=False):
self.setFinalPage(not checked)
[docs] def initializePage(self):
process = {"add": "Installation", "update": "Update"}[self.wizard().required_action]
self.setTitle(f"{process} failed")
[docs] def nextId(self):
if self.field("troubleshoot"):
return _PageId.TROUBLESHOOT_PROBLEMS
return -1
[docs]class TroubleshootProblemsPage(QWizardPage):
def __init__(self, parent):
super().__init__(parent)
self.setTitle("Troubleshooting")
msg = "Select your problem from the list."
self._button1 = QRadioButton("Installing SpineOpt fails with one of the following messages (or similar):")
msg1a = MonoSpaceFontTextBrowser(self)
msg1b = MonoSpaceFontTextBrowser(self)
msg1a.append(
"""
\u22ee<br>
Updating git-repo `https://github.com/spine-tools/SpineJuliaRegistry`<br>
Resolving package versions...<br>
ERROR: expected package `UUIDs [cf7118a7]` to be registered<br>
\u22ee
"""
)
msg1b.append(
"""
\u22ee<br>
Updating git-repo `https://github.com/spine-tools/SpineJuliaRegistry`<br>
Resolving package versions...<br>
ERROR: cannot find name corresponding to UUID f269a46b-ccf7-5d73-abea-4c690281aa53 in a registry<br>
\u22ee
"""
)
self._button2 = QRadioButton("On Windows 7, installing SpineOpt fails with the following message (or similar):")
msg2 = MonoSpaceFontTextBrowser(self)
msg2.append(
"""
\u22ee<br>
Downloading artifact: OpenBLAS32<br>
Exception setting "SecurityProtocol": "Cannot convert null to type "System.Net.<br>
SecurityProtocolType" due to invalid enumeration values. Specify one of the fol<br>
lowing enumeration values and try again. The possible enumeration values are "S<br>
sl3, Tls"."<br>
At line:1 char:35<br>
+ [System.Net.ServicePointManager]:: <<<< SecurityProtocol =<br>
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException<br>
+ FullyQualifiedErrorId : PropertyAssignmentException<br>
\u22ee
"""
)
layout = QVBoxLayout(self)
layout.addWidget(WrapLabel(msg))
layout.addStretch()
layout.addWidget(self._button1)
layout.addWidget(msg1a)
layout.addWidget(msg1b)
layout.addStretch()
layout.addWidget(self._button2)
layout.addWidget(msg2)
layout.addStretch()
button_view_log = QPushButton("View process log")
widget_view_log = QWidget()
layout_view_log = QHBoxLayout(widget_view_log)
layout_view_log.addStretch()
layout_view_log.addWidget(button_view_log)
layout.addWidget(widget_view_log)
layout.addStretch()
self.registerField("problem1", self._button1)
self.registerField("problem2", self._button2)
self._button1.toggled.connect(lambda _: self.completeChanged.emit())
self._button2.toggled.connect(lambda _: self.completeChanged.emit())
button_view_log.clicked.connect(self._show_log)
[docs] def isComplete(self):
return self.field("problem1") or self.field("problem2")
@Slot(bool)
[docs] def _show_log(self, _=False):
log_widget = QWidget(self, f=Qt.Window)
layout = QVBoxLayout(log_widget)
log = MonoSpaceFontTextBrowser(log_widget)
log.append(self.wizard().process_log)
layout.addWidget(log)
log_widget.show()
[docs] def nextId(self):
return _PageId.TROUBLESHOOT_SOLUTION
[docs]class TroubleshootSolutionPage(QWizardPage):
def __init__(self, parent):
super().__init__(parent)
self.setCommitPage(True)
QVBoxLayout(self)
[docs] def cleanupPage(self):
super().cleanupPage()
self.wizard().reset_registry = False
[docs] def initializePage(self):
_clear_layout(self.layout())
if self.field("problem1"):
self._initialize_page_solution1()
elif self.field("problem2"):
self._initialize_page_solution2()
[docs] def _initialize_page_solution1(self):
self.wizard().reset_registry = True
self.setTitle("Reset Julia General Registry")
description = (
"<p>The issue you're facing can be due to an error in the installation of the Julia General registry "
"from the Julia Package Server.</p>"
"<p>The simplest solution is to delete any trace of the registry and install it again, from GitHub.</p>"
"<p>However, <b>this will also remove all your installed packages</b>.</p>"
)
self.layout().addWidget(HyperTextLabel(description))
self.setButtonText(QWizard.CommitButton, "Reset registry")
[docs] def _initialize_page_solution2(self):
action = {"add": "Install SpineOpt", "update": "Update SpineOpt"}[self.wizard().required_action]
self.setTitle("Update Windows Managemet Framework")
description = (
"<p>The issue you're facing can be solved by installing Windows Managemet Framework 3 or greater, "
"as follows:<ul>"
"<li>Install .NET 4.5 "
"from <a href=https://dotnet.microsoft.com/download/dotnet-framework/thank-you/"
"net45-web-installer>here</a>.</li>"
"<li>Install Windows management framework 3 or later "
"from <a href=https://docs.microsoft.com/en-us/powershell/scripting/windows-powershell/wmf/"
"overview?view=powershell-7.1>here</a>.</li>"
f"<li>{action} again.</li>"
"</ul></p>"
)
self.layout().addWidget(HyperTextLabel(description))
self.setButtonText(QWizard.CommitButton, action)
[docs] def nextId(self):
if self.field("problem1"):
return _PageId.RESET_REGISTRY
return _PageId.ADD_UP_SPINE_OPT_AGAIN
[docs]class ResetRegistryPage(QWizardProcessPage):
[docs] def initializePage(self):
code = (
"using Pkg; "
'rm(joinp ath(DEPOT_PATH[1], "registries", "General"); force=true, recursive=true); '
'withenv("JULIA_PKG_SERVER"=>"") do pkg"registry add" end'
)
self.setTitle("Resetting Julia General Registry")
julia_exe = self.field("julia_exe")
julia_project = self.field("julia_project")
args = [f"--project={julia_project}", "-e", code]
self._exec_mngr = QProcessExecutionManager(self, julia_exe, args, semisilent=True)
self.completeChanged.emit()
self._exec_mngr.execution_finished.connect(self._handle_registry_reset_finished)
self.msg_success.emit("Registry reset started")
cmd = julia_exe + " " + " ".join(args)
self.msg.emit(f"$ <b>{cmd}<b/>")
qApp.setOverrideCursor(QCursor(Qt.BusyCursor)) # pylint: disable=undefined-variable
self._exec_mngr.start_execution()
[docs] def _handle_registry_reset_finished(self, ret):
qApp.restoreOverrideCursor() # pylint: disable=undefined-variable
self._exec_mngr.execution_finished.disconnect(self._handle_registry_reset_finished)
if self.wizard().currentPage() is not self:
return
self._exec_mngr = None
self._successful = ret == 0
if self._successful:
self.msg_success.emit("Registry successfully reset")
self.setCommitPage(True)
action = {"add": "Install SpineOpt", "update": "Update SpineOpt"}[self.wizard().required_action]
self.setButtonText(QWizard.CommitButton, action)
else:
# FIXME: Rather, add a button to copy log to clipboard?
# self.wizard().process_log = self._log.toHtml()
self.msg_error.emit("Registry reset failed")
self.completeChanged.emit()
[docs] def nextId(self):
if self._successful:
return _PageId.ADD_UP_SPINE_OPT_AGAIN
return _PageId.TOTAL_FAILURE
[docs]class AddUpSpineOptAgainPage(AddUpSpineOptPage):
[docs] def nextId(self):
if self._successful:
return _PageId.SUCCESS
return _PageId.TOTAL_FAILURE
[docs]class TotalFailurePage(QWizardPage):
def __init__(self, parent):
super().__init__(parent)
self.setTitle("Troubleshooting failed")
msg = "<p>Please <a href=https://github.com/spine-tools/SpineOpt.jl/issues>open an issue with SpineOpt</a>."
layout = QVBoxLayout(self)
layout.addWidget(HyperTextLabel(msg))
[docs] def nextId(self):
return -1
[docs]def _clear_layout(layout):
while True:
child = layout.takeAt(0)
if child is None:
break
child.widget().deleteLater()