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:
Dominik Demuth 2023-04-08 13:28:13 +00:00
parent 6b71de8265
commit 0ec0021727
7 changed files with 176 additions and 76 deletions

View File

@ -42,22 +42,30 @@ AppDir:
- python3.9-minimal - python3.9-minimal
- python3-numpy - python3-numpy
- python3-scipy - python3-scipy
# - python3-matplotlib
# - python-matplotlib-data
- python3-bsddb3 - python3-bsddb3
- python3-h5py - python3-h5py
- python3-pyqt5 - python3-pyqt5
- python3-pyqtgraph - python3-pyqtgraph
- python3-requests
- python3-urllib3
# - python3-tk
exclude: 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-client3
- libavahi-common-data - libavahi-common-data
- libavahi-common3 - libavahi-common3
- libwacom2
- libwacom-common
after_bundle: | after_bundle: |
echo "MONSTER SED FOLLOWING...(uncomment if needed for mpl-data)" 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 # 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
runtime: runtime:
version: "continuous" version: "continuous"
env: env:

View File

@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'src/resources/_ui/basewindow.ui' # 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 # 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. # 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.actionBugs.setObjectName("actionBugs")
self.actionShow_error_log = QtWidgets.QAction(BaseWindow) self.actionShow_error_log = QtWidgets.QAction(BaseWindow)
self.actionShow_error_log.setObjectName("actionShow_error_log") 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.actionSave)
self.menuSave.addAction(self.actionExportGraphic) self.menuSave.addAction(self.actionExportGraphic)
self.menuSave.addAction(self.action_save_fit_parameter) self.menuSave.addAction(self.action_save_fit_parameter)
@ -422,6 +424,7 @@ class Ui_BaseWindow(object):
self.menuOptions.addSeparator() self.menuOptions.addSeparator()
self.menuOptions.addAction(self.action_colorcycle) self.menuOptions.addAction(self.action_colorcycle)
self.menuOptions.addAction(self.actionConfiguration) self.menuOptions.addAction(self.actionConfiguration)
self.menuOptions.addAction(self.actionCreate_starter)
self.menuView.addAction(self.actionTile) self.menuView.addAction(self.actionTile)
self.menuView.addAction(self.actionCascade_windows) self.menuView.addAction(self.actionCascade_windows)
self.menuWindow.addAction(self.actionNew_window) self.menuWindow.addAction(self.actionNew_window)
@ -612,6 +615,7 @@ class Ui_BaseWindow(object):
self.action_draw_object.setText(_translate("BaseWindow", "Draw objects...")) self.action_draw_object.setText(_translate("BaseWindow", "Draw objects..."))
self.actionBugs.setText(_translate("BaseWindow", "Bugs! Problems! Wishes!")) self.actionBugs.setText(_translate("BaseWindow", "Bugs! Problems! Wishes!"))
self.actionShow_error_log.setText(_translate("BaseWindow", "Show error log")) 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.datawidget.datawidget import DataWidget
from ..data.integral_widget import IntegralWidget from ..data.integral_widget import IntegralWidget
from ..data.point_select import PointSelectWidget from ..data.point_select import PointSelectWidget

95
src/gui_qt/lib/starter.py Normal file
View 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()

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from functools import lru_cache import os
import urllib.request
from os import getenv, stat from os import getenv, stat
from os.path import exists from os.path import exists
import hashlib import hashlib
@ -8,8 +9,7 @@ import subprocess
from datetime import datetime from datetime import datetime
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from urllib.error import HTTPError
import requests
from numpy import linspace from numpy import linspace
from scipy.interpolate import interp1d from scipy.interpolate import interp1d
@ -64,7 +64,7 @@ class RdBuCMap:
class UpdateDialog(QtWidgets.QDialog): class UpdateDialog(QtWidgets.QDialog):
startDownload = QtCore.pyqtSignal(list) startDownload = QtCore.pyqtSignal(tuple)
def __init__(self, filename: str = None, parent=None): def __init__(self, filename: str = None, parent=None):
super().__init__(parent=parent) super().__init__(parent=parent)
@ -74,7 +74,6 @@ class UpdateDialog(QtWidgets.QDialog):
filename = getenv('APPIMAGE') filename = getenv('APPIMAGE')
self._appfile = filename self._appfile = filename
self.success = False
self.updater = Updater() self.updater = Updater()
self.thread = QtCore.QThread(self) self.thread = QtCore.QThread(self)
@ -144,24 +143,20 @@ class UpdateDialog(QtWidgets.QDialog):
@QtCore.pyqtSlot() @QtCore.pyqtSlot()
def update_appimage(self): def update_appimage(self):
if self._appfile is None: if self._appfile is None:
args = [self.updater.zsync_url] args = (self.updater.zsync_url,)
else: else:
# this breaks the download for some reason # this breaks the download for some reason
args = ['-i', self._appfile, self.updater.zsync_url] args = (self.updater.zsync_url, self._appfile)
args = [self.updater.zsync_url]
self.dialog_button.setEnabled(False) self.dialog_button.setEnabled(False)
self.startDownload.emit(args) self.startDownload.emit(args)
self.status.show() self.status.show()
@QtCore.pyqtSlot(int) @QtCore.pyqtSlot(int, str)
def finish_update(self, retcode: int): def finish_update(self, retcode: int, file_loc: str):
# print('finished with', retcode)
self.success = retcode == 0
if retcode == 0: 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: else:
self.status.setText(f'Download failed :( with return code {retcode}.') self.status.setText(f'Download failed :( with return code {retcode}.')
self.dialog_button.setStandardButtons(QtWidgets.QDialogButtonBox.Close) self.dialog_button.setStandardButtons(QtWidgets.QDialogButtonBox.Close)
@ -171,44 +166,49 @@ class UpdateDialog(QtWidgets.QDialog):
self.thread.quit() self.thread.quit()
self.thread.wait() 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) super().closeEvent(evt)
class Downloader(QtCore.QObject): class Downloader(QtCore.QObject):
started = QtCore.pyqtSignal() started = QtCore.pyqtSignal()
finished = QtCore.pyqtSignal(int) finished = QtCore.pyqtSignal(int, str)
progressChanged = QtCore.pyqtSignal(str) progressChanged = QtCore.pyqtSignal(str)
@QtCore.pyqtSlot(list) @QtCore.pyqtSlot(tuple)
def run_download(self, args: list[str]): def run_download(self, args: tuple[str]):
logger.info(f'Download with args {args}') status = 0
process = subprocess.Popen(['zsync'] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1, universal_newlines=True) appimage_location = args[0][:-6]
while True: logger.info(f'Download {appimage_location}')
nextline = process.stdout.readline().strip() if len(args) == 2:
if nextline: new_file = Path(args[1])
self.progressChanged.emit(nextline) 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: try:
break 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: class Updater:
@ -220,7 +220,6 @@ class Updater:
return f'https://{Updater.host}/{Updater.version}.AppImage.zsync' return f'https://{Updater.host}/{Updater.version}.AppImage.zsync'
@staticmethod @staticmethod
@lru_cache(3)
def get_zsync(): def get_zsync():
url_zsync = f'https://{Updater.host}/{Updater.version}.AppImage.zsync' url_zsync = f'https://{Updater.host}/{Updater.version}.AppImage.zsync'
m_time_zsync = None m_time_zsync = None
@ -228,11 +227,10 @@ class Updater:
zsync_file = None zsync_file = None
filename = None filename = None
try: try:
response = requests.get(url_zsync) with urllib.request.urlopen(url_zsync) as response:
if response.status_code == 200: zsync_file = response.read()
zsync_file = response.content except HTTPError as e:
else: logger.error(f'Request for zsync returned code {e}')
logger.error(f'Request for zsync returned code {response.status_code}')
except Exception as e: except Exception as e:
logger.exception(f'Download of zsync failed with exception {e.args}') 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 return None, m_time_file, m_time_zsync
else: else:
return checksum_file != checksum_zsync, m_time_file, m_time_zsync 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)

View File

@ -23,6 +23,7 @@ from ..io.fcbatchreader import QFCReader
from ..io.filedialog import * from ..io.filedialog import *
from ..lib import get_icon, make_action_icons from ..lib import get_icon, make_action_icons
from ..lib.pg_objects import RegionItem from ..lib.pg_objects import RegionItem
from ..lib.starter import make_starter
from ..math.evaluation import QEvalDialog from ..math.evaluation import QEvalDialog
from ..math.interpol import InterpolDialog from ..math.interpol import InterpolDialog
from ..math.mean_dialog import QMeanTimes from ..math.mean_dialog import QMeanTimes
@ -30,7 +31,7 @@ from ..math.smooth import QSmooth
from ..nmr.coupling_calc import QCoupCalcDialog from ..nmr.coupling_calc import QCoupCalcDialog
from ..nmr.t1_from_tau import QRelaxCalc from ..nmr.t1_from_tau import QRelaxCalc
from .._py.basewindow import Ui_BaseWindow 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): class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
@ -1059,3 +1060,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
self.status.setText('Autosave...') self.status.setText('Autosave...')
NMRWriter(self.management.graphs, self.management.data).export(self.__backup_path) NMRWriter(self.management.graphs, self.management.data).export(self.__backup_path)
self.status.setText('') self.status.setText('')
@QtCore.pyqtSlot(name='on_actionCreate_starter_triggered')
def create_starter(self):
make_starter(os.getenv('APPIMAGE'))

View File

@ -17,9 +17,13 @@ def check_for_config(make=True):
conf_path.mkdir(parents=True) conf_path.mkdir(parents=True)
cwd = pathlib.Path(__file__).parent cwd = pathlib.Path(__file__).parent
copyfile(cwd / 'models' / 'usermodels.py', conf_path / 'usermodels.py') copyfile(cwd / 'models' / 'usermodels.py', conf_path / 'usermodels.py')
with resource_path('resources', 'Default.agr') as fp: with resource_path('resources', 'Default.agr') as fp:
copyfile(fp, conf_path / 'Default.agr') copyfile(fp, conf_path / 'Default.agr')
with resource_path('resources', 'logo.png') as fp:
copyfile(fp, conf_path / 'logo.png')
else: else:
raise e raise e

View File

@ -263,6 +263,7 @@
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="action_colorcycle"/> <addaction name="action_colorcycle"/>
<addaction name="actionConfiguration"/> <addaction name="actionConfiguration"/>
<addaction name="actionCreate_starter"/>
</widget> </widget>
<widget class="QMenu" name="menuWindow"> <widget class="QMenu" name="menuWindow">
<property name="title"> <property name="title">
@ -999,6 +1000,11 @@
<string>Show error log</string> <string>Show error log</string>
</property> </property>
</action> </action>
<action name="actionCreate_starter">
<property name="text">
<string>Create starter..</string>
</property>
</action>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>