appimage-starter (#42)
create program launcher; reduced size of appimage; download of appimages working(?) Co-authored-by: Dominik Demuth <dominik.demuth@physik.tu-darmstadt.de> Reviewed-on: #42
This commit is contained in:
parent
6b71de8265
commit
0ec0021727
@ -42,19 +42,27 @@ AppDir:
|
||||
- python3.9-minimal
|
||||
- python3-numpy
|
||||
- python3-scipy
|
||||
# - python3-matplotlib
|
||||
# - python-matplotlib-data
|
||||
- python3-bsddb3
|
||||
- python3-h5py
|
||||
- python3-pyqt5
|
||||
- python3-pyqtgraph
|
||||
- python3-requests
|
||||
- python3-urllib3
|
||||
# - python3-tk
|
||||
exclude:
|
||||
# lots of qt stuff we do not use
|
||||
- libqt5designer5
|
||||
- libqt5help5
|
||||
- libqt5network5
|
||||
- libqt5sql5
|
||||
- libqt5test5
|
||||
- libqt5xml5
|
||||
- qtbase5-dev-tools
|
||||
- qtchooser
|
||||
- pyqt5-dev-tools
|
||||
- qtchooser
|
||||
- libavahi-client3
|
||||
- libavahi-common-data
|
||||
- libavahi-common3
|
||||
- libwacom2
|
||||
- libwacom-common
|
||||
after_bundle: |
|
||||
echo "MONSTER SED FOLLOWING...(uncomment if needed for mpl-data)"
|
||||
# sed -i s,\'/usr/share/matplotlib/mpl-data\',"f\"\{os.environ.get\('APPDIR'\,'/'\)\}/usr/share/matplotlib/mpl-data\"", ${TARGET_APPDIR}/usr/lib/python3/dist-packages/matplotlib/__init__.py
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file 'src/resources/_ui/basewindow.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.15.7
|
||||
# Created by: PyQt5 UI code generator 5.15.9
|
||||
#
|
||||
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
||||
# run again. Do not edit this file unless you know what you are doing.
|
||||
@ -358,6 +358,8 @@ class Ui_BaseWindow(object):
|
||||
self.actionBugs.setObjectName("actionBugs")
|
||||
self.actionShow_error_log = QtWidgets.QAction(BaseWindow)
|
||||
self.actionShow_error_log.setObjectName("actionShow_error_log")
|
||||
self.actionCreate_starter = QtWidgets.QAction(BaseWindow)
|
||||
self.actionCreate_starter.setObjectName("actionCreate_starter")
|
||||
self.menuSave.addAction(self.actionSave)
|
||||
self.menuSave.addAction(self.actionExportGraphic)
|
||||
self.menuSave.addAction(self.action_save_fit_parameter)
|
||||
@ -422,6 +424,7 @@ class Ui_BaseWindow(object):
|
||||
self.menuOptions.addSeparator()
|
||||
self.menuOptions.addAction(self.action_colorcycle)
|
||||
self.menuOptions.addAction(self.actionConfiguration)
|
||||
self.menuOptions.addAction(self.actionCreate_starter)
|
||||
self.menuView.addAction(self.actionTile)
|
||||
self.menuView.addAction(self.actionCascade_windows)
|
||||
self.menuWindow.addAction(self.actionNew_window)
|
||||
@ -612,6 +615,7 @@ class Ui_BaseWindow(object):
|
||||
self.action_draw_object.setText(_translate("BaseWindow", "Draw objects..."))
|
||||
self.actionBugs.setText(_translate("BaseWindow", "Bugs! Problems! Wishes!"))
|
||||
self.actionShow_error_log.setText(_translate("BaseWindow", "Show error log"))
|
||||
self.actionCreate_starter.setText(_translate("BaseWindow", "Create starter.."))
|
||||
from ..data.datawidget.datawidget import DataWidget
|
||||
from ..data.integral_widget import IntegralWidget
|
||||
from ..data.point_select import PointSelectWidget
|
||||
|
95
src/gui_qt/lib/starter.py
Normal file
95
src/gui_qt/lib/starter.py
Normal file
@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from shutil import copyfile
|
||||
from pathlib import Path
|
||||
import os
|
||||
from configparser import ConfigParser
|
||||
from importlib.resources import path as resource_path
|
||||
|
||||
from nmreval.configs import config_paths
|
||||
from nmreval.lib.logger import logger
|
||||
|
||||
|
||||
def make_starter(app_file: str | None):
|
||||
if app_file is not None:
|
||||
make_starter_appimage(Path(app_file))
|
||||
else:
|
||||
make_starter_src()
|
||||
|
||||
|
||||
def make_starter_appimage(app_file: Path):
|
||||
new_path = Path.home() / '.local' / 'bin' / app_file.name
|
||||
|
||||
if app_file != new_path:
|
||||
app_file.rename(new_path)
|
||||
|
||||
create_desktop_file(new_path)
|
||||
|
||||
|
||||
def make_starter_src():
|
||||
home = Path.home()
|
||||
p = Path.home()
|
||||
for p in Path(__file__).parents:
|
||||
if p.stem == 'src':
|
||||
break
|
||||
elif p == home:
|
||||
break
|
||||
|
||||
success = p != Path.home()
|
||||
if success:
|
||||
bin_path = p.with_name('bin') / 'evaluate.py'
|
||||
success = bin_path.exists()
|
||||
|
||||
if not success:
|
||||
logger.warning('Location of evaluate.py could not be determined')
|
||||
return False
|
||||
|
||||
create_desktop_file(bin_path)
|
||||
|
||||
|
||||
def create_desktop_file(new_path: Path):
|
||||
logo_path = config_paths() / 'logo.png'
|
||||
if not logo_path.exists():
|
||||
with resource_path('resources', 'logo.png') as fp:
|
||||
copyfile(fp, logo_path)
|
||||
desktop_entry = f"""\
|
||||
[Desktop Entry]
|
||||
Name=NMReval
|
||||
Comment=Best program ever (maybe)
|
||||
Exec={new_path}
|
||||
Icon={logo_path}
|
||||
Type=Application
|
||||
Terminal=false
|
||||
Categories=Science;NumericalAnalysis;Physics;DataVisualization;Other;
|
||||
NoDisplay=false
|
||||
"""
|
||||
file_name = 'pkm.vogel.nmreval.desktop'
|
||||
with Path('~/.local/share/applications/', file_name).expanduser().open('w') as f:
|
||||
f.write(desktop_entry)
|
||||
|
||||
desktop_dir = get_xkg_user_dirs('desktop')
|
||||
if desktop_dir is not None:
|
||||
desk_file = Path(desktop_dir, file_name)
|
||||
with desk_file.open('w') as f:
|
||||
f.write(desktop_entry)
|
||||
|
||||
desk_file.chmod(0o755)
|
||||
|
||||
|
||||
def get_xkg_user_dirs(dir_type: str) -> str | None:
|
||||
xdg_conf_home = os.getenv('XDG_CONFIG_HOME') or str(Path.home() / '.config')
|
||||
|
||||
with Path(xdg_conf_home, 'user-dirs.dirs').open('r') as f:
|
||||
conf_string = '[XDG_USER_DIRS]\n' + f.read()
|
||||
conf_string = re.sub(r'\$HOME', str(Path.home()), conf_string)
|
||||
conf_string = re.sub('"', '', conf_string)
|
||||
|
||||
config = ConfigParser()
|
||||
config.read_string(conf_string)
|
||||
|
||||
return config['XDG_USER_DIRS'].get(f'xdg_{dir_type}_dir')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
make_starter()
|
@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
import os
|
||||
import urllib.request
|
||||
from os import getenv, stat
|
||||
from os.path import exists
|
||||
import hashlib
|
||||
@ -8,8 +9,7 @@ import subprocess
|
||||
from datetime import datetime
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from urllib.error import HTTPError
|
||||
from numpy import linspace
|
||||
from scipy.interpolate import interp1d
|
||||
|
||||
@ -64,7 +64,7 @@ class RdBuCMap:
|
||||
|
||||
|
||||
class UpdateDialog(QtWidgets.QDialog):
|
||||
startDownload = QtCore.pyqtSignal(list)
|
||||
startDownload = QtCore.pyqtSignal(tuple)
|
||||
|
||||
def __init__(self, filename: str = None, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
@ -74,7 +74,6 @@ class UpdateDialog(QtWidgets.QDialog):
|
||||
filename = getenv('APPIMAGE')
|
||||
self._appfile = filename
|
||||
|
||||
self.success = False
|
||||
self.updater = Updater()
|
||||
|
||||
self.thread = QtCore.QThread(self)
|
||||
@ -144,24 +143,20 @@ class UpdateDialog(QtWidgets.QDialog):
|
||||
@QtCore.pyqtSlot()
|
||||
def update_appimage(self):
|
||||
if self._appfile is None:
|
||||
args = [self.updater.zsync_url]
|
||||
args = (self.updater.zsync_url,)
|
||||
else:
|
||||
# this breaks the download for some reason
|
||||
args = ['-i', self._appfile, self.updater.zsync_url]
|
||||
|
||||
args = [self.updater.zsync_url]
|
||||
args = (self.updater.zsync_url, self._appfile)
|
||||
|
||||
self.dialog_button.setEnabled(False)
|
||||
|
||||
self.startDownload.emit(args)
|
||||
self.status.show()
|
||||
|
||||
@QtCore.pyqtSlot(int)
|
||||
def finish_update(self, retcode: int):
|
||||
# print('finished with', retcode)
|
||||
self.success = retcode == 0
|
||||
@QtCore.pyqtSlot(int, str)
|
||||
def finish_update(self, retcode: int, file_loc: str):
|
||||
if retcode == 0:
|
||||
self.status.setText('Download complete.')
|
||||
self.status.setText(f'Download complete.New AppImage lies in <p><em>{file_loc}</em>.</p>')
|
||||
else:
|
||||
self.status.setText(f'Download failed :( with return code {retcode}.')
|
||||
self.dialog_button.setStandardButtons(QtWidgets.QDialogButtonBox.Close)
|
||||
@ -171,44 +166,49 @@ class UpdateDialog(QtWidgets.QDialog):
|
||||
self.thread.quit()
|
||||
self.thread.wait()
|
||||
|
||||
if self.success:
|
||||
appname = self.updater.get_zsync()[2]
|
||||
if self._appfile is not None:
|
||||
appimage_path = appname
|
||||
old_version = Path(self._appfile).rename(self._appfile+'.old')
|
||||
appimage_path = Path(appimage_path).replace(self._appfile)
|
||||
else:
|
||||
appimage_path = Path().cwd() / appname
|
||||
# rename to version-agnostic name
|
||||
appimage_path = appimage_path.rename('NMReval.AppImage')
|
||||
appimage_path.chmod(appimage_path.stat().st_mode | 73) # 73 = 0o111 = a+x
|
||||
|
||||
_ = QtWidgets.QMessageBox.information(self, 'Complete',
|
||||
f'New AppImage available at<br>{appimage_path}')
|
||||
|
||||
super().closeEvent(evt)
|
||||
|
||||
|
||||
class Downloader(QtCore.QObject):
|
||||
started = QtCore.pyqtSignal()
|
||||
finished = QtCore.pyqtSignal(int)
|
||||
finished = QtCore.pyqtSignal(int, str)
|
||||
progressChanged = QtCore.pyqtSignal(str)
|
||||
|
||||
@QtCore.pyqtSlot(list)
|
||||
def run_download(self, args: list[str]):
|
||||
logger.info(f'Download with args {args}')
|
||||
process = subprocess.Popen(['zsync'] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1, universal_newlines=True)
|
||||
while True:
|
||||
nextline = process.stdout.readline().strip()
|
||||
if nextline:
|
||||
self.progressChanged.emit(nextline)
|
||||
@QtCore.pyqtSlot(tuple)
|
||||
def run_download(self, args: tuple[str]):
|
||||
status = 0
|
||||
appimage_location = args[0][:-6]
|
||||
logger.info(f'Download {appimage_location}')
|
||||
if len(args) == 2:
|
||||
new_file = Path(args[1])
|
||||
else:
|
||||
new_file = Path.home() / 'Downloads' / 'NMReval-latest-x86_64.AppImage'
|
||||
|
||||
# line = process.stderr.readline().strip()
|
||||
if new_file.exists():
|
||||
os.rename(new_file, new_file.with_suffix('.AppImage.old'))
|
||||
|
||||
if process.poll() is not None:
|
||||
break
|
||||
try:
|
||||
with urllib.request.urlopen(appimage_location) as response:
|
||||
with new_file.open('wb') as f:
|
||||
f.write(response.read())
|
||||
|
||||
self.finished.emit(process.returncode)
|
||||
new_file.chmod(0o755)
|
||||
except HTTPError as e:
|
||||
logger.exception(f'Download failed with {e}')
|
||||
status = 3
|
||||
except Exception as e:
|
||||
logger.exception(f'Download failed with {e.args}')
|
||||
status = 1
|
||||
|
||||
if status != 0:
|
||||
logger.warning('Download failed, restore previous AppImage')
|
||||
try:
|
||||
os.rename(new_file.with_suffix('.AppImage.old'), new_file)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
# zsync does not support https
|
||||
|
||||
self.finished.emit(status, str(new_file))
|
||||
|
||||
|
||||
class Updater:
|
||||
@ -220,7 +220,6 @@ class Updater:
|
||||
return f'https://{Updater.host}/{Updater.version}.AppImage.zsync'
|
||||
|
||||
@staticmethod
|
||||
@lru_cache(3)
|
||||
def get_zsync():
|
||||
url_zsync = f'https://{Updater.host}/{Updater.version}.AppImage.zsync'
|
||||
m_time_zsync = None
|
||||
@ -228,11 +227,10 @@ class Updater:
|
||||
zsync_file = None
|
||||
filename = None
|
||||
try:
|
||||
response = requests.get(url_zsync)
|
||||
if response.status_code == 200:
|
||||
zsync_file = response.content
|
||||
else:
|
||||
logger.error(f'Request for zsync returned code {response.status_code}')
|
||||
with urllib.request.urlopen(url_zsync) as response:
|
||||
zsync_file = response.read()
|
||||
except HTTPError as e:
|
||||
logger.error(f'Request for zsync returned code {e}')
|
||||
except Exception as e:
|
||||
logger.exception(f'Download of zsync failed with exception {e.args}')
|
||||
|
||||
@ -287,23 +285,3 @@ class Updater:
|
||||
return None, m_time_file, m_time_zsync
|
||||
else:
|
||||
return checksum_file != checksum_zsync, m_time_file, m_time_zsync
|
||||
|
||||
|
||||
def open_bug_report():
|
||||
form_entries = {
|
||||
'description': 'Please state the nature of the medical emergency.',
|
||||
'title': 'Everything is awesome?',
|
||||
'assign[0]': 'dominik',
|
||||
'subscribers[0]': 'dominik',
|
||||
'tag': 'nmreval',
|
||||
'priority': 'normal',
|
||||
'status': 'open',
|
||||
}
|
||||
full_url = 'https://chaos3.fkp.physik.tu-darmstadt.de/maniphest/task/edit/?'
|
||||
|
||||
for k, v in form_entries.items():
|
||||
full_url += f'{k}={v}&'
|
||||
full_url.replace(' ', '+')
|
||||
|
||||
import webbrowser
|
||||
webbrowser.open(full_url)
|
||||
|
@ -23,6 +23,7 @@ from ..io.fcbatchreader import QFCReader
|
||||
from ..io.filedialog import *
|
||||
from ..lib import get_icon, make_action_icons
|
||||
from ..lib.pg_objects import RegionItem
|
||||
from ..lib.starter import make_starter
|
||||
from ..math.evaluation import QEvalDialog
|
||||
from ..math.interpol import InterpolDialog
|
||||
from ..math.mean_dialog import QMeanTimes
|
||||
@ -30,7 +31,7 @@ from ..math.smooth import QSmooth
|
||||
from ..nmr.coupling_calc import QCoupCalcDialog
|
||||
from ..nmr.t1_from_tau import QRelaxCalc
|
||||
from .._py.basewindow import Ui_BaseWindow
|
||||
from ..lib.utils import UpdateDialog, open_bug_report, Updater
|
||||
from ..lib.utils import UpdateDialog, Updater
|
||||
|
||||
|
||||
class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
||||
@ -1059,3 +1060,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
||||
self.status.setText('Autosave...')
|
||||
NMRWriter(self.management.graphs, self.management.data).export(self.__backup_path)
|
||||
self.status.setText('')
|
||||
|
||||
@QtCore.pyqtSlot(name='on_actionCreate_starter_triggered')
|
||||
def create_starter(self):
|
||||
make_starter(os.getenv('APPIMAGE'))
|
||||
|
@ -17,9 +17,13 @@ def check_for_config(make=True):
|
||||
conf_path.mkdir(parents=True)
|
||||
cwd = pathlib.Path(__file__).parent
|
||||
copyfile(cwd / 'models' / 'usermodels.py', conf_path / 'usermodels.py')
|
||||
|
||||
with resource_path('resources', 'Default.agr') as fp:
|
||||
copyfile(fp, conf_path / 'Default.agr')
|
||||
|
||||
with resource_path('resources', 'logo.png') as fp:
|
||||
copyfile(fp, conf_path / 'logo.png')
|
||||
|
||||
else:
|
||||
raise e
|
||||
|
||||
|
@ -263,6 +263,7 @@
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_colorcycle"/>
|
||||
<addaction name="actionConfiguration"/>
|
||||
<addaction name="actionCreate_starter"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuWindow">
|
||||
<property name="title">
|
||||
@ -999,6 +1000,11 @@
|
||||
<string>Show error log</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionCreate_starter">
|
||||
<property name="text">
|
||||
<string>Create starter..</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
|
Loading…
Reference in New Issue
Block a user