######################################################################################################################
# Copyright (C) 2017-2021 Spine project consortium
# 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.
:author: M. Marin (KTH)
:date: 13.5.2018
"""
import sys
import os
from enum import IntEnum, auto
try:
import jill.install as jill_install
except ModuleNotFoundError:
from PySide2.QtWidgets import (
QWidget,
QWizard,
QWizardPage,
QLabel,
QPushButton,
QVBoxLayout,
QHBoxLayout,
QLineEdit,
QFileDialog,
QCheckBox,
)
from PySide2.QtCore import Signal, Slot, Qt
from PySide2.QtGui import QCursor
from ..execution_managers import QProcessExecutionManager
from ..config import APPLICATION_PATH
from .custom_qwidgets import HyperTextLabel, QWizardProcessPage, LabelWithCopyButton
from spine_engine.utils.helpers import resolve_python_interpreter
[docs]class _PageId(IntEnum):
[docs] SELECT_DIRS = auto()
[docs]class InstallJuliaWizard(QWizard):
"""A wizard to install julia
"""
[docs] julia_exe_selected = Signal(str)
def __init__(self, parent):
"""Initialize class.
Args:
parent (QWidget): the parent widget (SettingsWidget)
"""
super().__init__(parent)
if jill_install is None:
self.addPage(JillNotFoundPage(self))
return
self.julia_exe = None
self.setWindowTitle("Julia Installer")
self.setPage(_PageId.INTRO, IntroPage(self))
self.setPage(_PageId.SELECT_DIRS, SelectDirsPage(self))
self.setPage(_PageId.INSTALL, InstallJuliaPage(self))
self.setPage(_PageId.SUCCESS, SuccessPage(self))
self.setPage(_PageId.FAILURE, FailurePage(self))
self.setStartId(_PageId.INTRO)
[docs] def set_julia_exe(self):
basename = next(
(file for file in os.listdir(self.field('symlink_dir')) if file.lower().startswith("julia")), None
)
if basename is None:
self.julia_exe = None
return
self.julia_exe = os.path.join(self.field('symlink_dir'), basename)
[docs] def accept(self):
super().accept()
if jill_install is not None and self.field("use_julia"):
self.julia_exe_selected.emit(self.julia_exe)
[docs]class JillNotFoundPage(QWizardPage):
def __init__(self, parent):
super().__init__(parent)
self.setTitle("Unable to find jill")
conda_env = os.environ.get('CONDA_DEFAULT_ENV', 'base')
toolbox_dir = os.path.dirname(APPLICATION_PATH)
header = (
"<p>Spine Toolbox needs the <a href='https://pypi.org/project/jill/'>jill</a> package "
"to install Julia. "
"To get the right version of jill for Spine Toolbox, "
"please upgrade requirements as follows:</p>"
)
indent = 4 * " "
point1 = f"{indent}1. Open Anaconda prompt."
point2 = f"{indent}2. Activate the {conda_env} environment:"
point3 = f"{indent}3. Navigate to your Spine Toolbox directory:"
point4 = f"{indent}4. Upgrade requirements using <b>pip</b>:"
point5 = f"{indent}5. Restart Spine Toolbox."
layout = QVBoxLayout(self)
layout.addWidget(HyperTextLabel(header))
layout.addWidget(HyperTextLabel(point1))
layout.addWidget(HyperTextLabel(point2))
layout.addWidget(LabelWithCopyButton(f"conda activate {conda_env}"))
layout.addWidget(HyperTextLabel(point3))
layout.addWidget(LabelWithCopyButton(f"cd {toolbox_dir}"))
layout.addWidget(HyperTextLabel(point4))
layout.addWidget(LabelWithCopyButton("python -m pip install --upgrade -r requirements.txt"))
layout.addWidget(HyperTextLabel(point5))
[docs]class IntroPage(QWizardPage):
def __init__(self, parent):
super().__init__(parent)
self.setTitle("Welcome")
label = HyperTextLabel(
"This wizard will help you install "
"<a title='julia language' href='https://julialang.org/'>Julia</a> in your computer."
)
layout = QVBoxLayout(self)
layout.addWidget(label)
[docs] def nextId(self):
return _PageId.SELECT_DIRS
[docs]class SelectDirsPage(QWizardPage):
def __init__(self, parent):
super().__init__(parent)
self.setTitle("Select directories")
self._install_dir_line_edit = QLineEdit()
self._symlink_dir_line_edit = QLineEdit()
self.registerField("install_dir*", self._install_dir_line_edit)
self.registerField("symlink_dir*", self._symlink_dir_line_edit)
layout = QVBoxLayout(self)
layout.addWidget(QLabel("Directory for Julia packages:"))
install_dir_widget = QWidget()
install_dir_layout = QHBoxLayout(install_dir_widget)
install_dir_layout.addWidget(self._install_dir_line_edit)
install_dir_button = QPushButton("Browse")
install_dir_layout.addWidget(install_dir_button)
layout.addWidget(install_dir_widget)
layout.addWidget(QLabel("Directory for Julia executable:"))
symlink_dir_widget = QWidget()
symlink_dir_layout = QHBoxLayout(symlink_dir_widget)
symlink_dir_layout.addWidget(self._symlink_dir_line_edit)
symlink_dir_button = QPushButton("Browse")
symlink_dir_layout.addWidget(symlink_dir_button)
layout.addWidget(symlink_dir_widget)
install_dir_button.clicked.connect(self._select_install_dir)
symlink_dir_button.clicked.connect(self._select_symlink_dir)
self.setCommitPage(True)
self.setButtonText(QWizard.CommitButton, "Install Julia")
[docs] def initializePage(self):
self._install_dir_line_edit.setText(jill_install.default_install_dir())
self._symlink_dir_line_edit.setText(jill_install.default_symlink_dir())
[docs] def _select_install_dir(self):
install_dir = QFileDialog.getExistingDirectory(
self, "Select directory for Julia packages", self.field("install_dir")
)
if not install_dir:
return
self.setField("install_dir", install_dir)
[docs] def _select_symlink_dir(self):
symlink_dir = QFileDialog.getExistingDirectory(
self, "Select directory for Julia executable", self.field("symlink_dir")
)
if not symlink_dir:
return
self.setField("symlink_dir", symlink_dir)
[docs] def nextId(self):
return _PageId.INSTALL
[docs]class InstallJuliaPage(QWizardProcessPage):
def __init__(self, parent):
super().__init__(parent)
self.setTitle("Installing Julia")
[docs] def cleanupPage(self):
super().cleanupPage()
if self._exec_mngr is not None:
self._exec_mngr.stop_execution()
self.msg_error.emit("Aborted by the user")
[docs] def initializePage(self):
args = [
"-m",
"jill",
"install",
"--confirm",
"--install_dir",
self.field("install_dir"),
"--symlink_dir",
self.field("symlink_dir"),
]
# Resolve Python in this order
# 1. sys.executable when not frozen
# 2. PATH python if frozen (This fails if no jill installed)
# 3. If no PATH python, uses embedded python <install_dir>/tools/python.exe
python = resolve_python_interpreter("")
self._exec_mngr = QProcessExecutionManager(self, python, args, semisilent=True)
self.completeChanged.emit()
self._exec_mngr.execution_finished.connect(self._handle_julia_install_finished)
self.msg_success.emit("Julia installation started")
cmd = python + " " + " ".join(args)
self.msg.emit(f"$ <b>{cmd}<b/>")
qApp.setOverrideCursor(QCursor(Qt.BusyCursor)) # pylint: disable=undefined-variable
self._exec_mngr.start_execution()
@Slot(int)
[docs] def _handle_julia_install_finished(self, ret):
qApp.restoreOverrideCursor() # pylint: disable=undefined-variable
self._exec_mngr.execution_finished.disconnect(self._handle_julia_install_finished)
if self.wizard().currentPage() != self:
return
self._exec_mngr = None
self.completeChanged.emit()
if ret == 0:
self.wizard().set_julia_exe()
if self.wizard().julia_exe is not None:
self.msg_success.emit("Julia successfully installed")
return
self.msg_error.emit("Julia installation failed")
[docs] def nextId(self):
if self.wizard().julia_exe is not None:
return _PageId.SUCCESS
return _PageId.FAILURE
[docs]class SuccessPage(QWizardPage):
def __init__(self, parent):
super().__init__(parent)
self.setTitle("Installation successful")
self._label = HyperTextLabel()
layout = QVBoxLayout(self)
use_julia_check_box = QCheckBox("Use this Julia with Spine Toolbox")
self.registerField("use_julia", use_julia_check_box)
layout.addWidget(self._label)
layout.addStretch()
layout.addWidget(use_julia_check_box)
layout.addStretch()
layout.addStretch()
[docs] def initializePage(self):
self._label.setText(f"Julia executable created at <b>{self.wizard().julia_exe}</b>")
self.setField("use_julia", True)
[docs] def nextId(self):
return -1
[docs]class FailurePage(QWizardPage):
def __init__(self, parent):
super().__init__(parent)
self.setTitle("Installation failed")
self._label = HyperTextLabel()
layout = QVBoxLayout(self)
layout.addWidget(self._label)
[docs] def initializePage(self):
self._label.setText(
"Apologies. Please install Julia manually "
"from <a title='julia downloads' href='https://julialang.org/downloads/'>here</a>."
)
[docs] def nextId(self):
return -1