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,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

View File

@ -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
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 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)

View File

@ -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'))

View File

@ -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

View File

@ -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>