From 152d063a24918dc23c24adafd896072e467699d4 Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Wed, 20 Aug 2014 09:33:59 +0200 Subject: [PATCH] Fixes for windows packaging and running from frozen state --- libqnotero/preferences.py | 23 ++++---- libqnotero/qnotero.py | 29 +++++----- libqnotero/uiloader.py | 55 ++++++++++++++++++ qnotero | 3 + qnotero.nsi | 116 ++++++++++++++++++++++++++++++++++++++ setup-windows.py | 89 ++++++++++++++++++++++++----- 6 files changed, 274 insertions(+), 41 deletions(-) create mode 100644 libqnotero/uiloader.py create mode 100644 qnotero.nsi diff --git a/libqnotero/preferences.py b/libqnotero/preferences.py index 5ddbe25..576c3ec 100644 --- a/libqnotero/preferences.py +++ b/libqnotero/preferences.py @@ -24,9 +24,10 @@ from PyQt4.QtGui import QDialog, QFileDialog, QMessageBox, QApplication from PyQt4 import uic from libqnotero.config import getConfig, setConfig +from libqnotero.uiloader import UiLoader from libzotero.libzotero import valid_location -class Preferences(QDialog): +class Preferences(QDialog, UiLoader): """Qnotero preferences dialog""" @@ -45,17 +46,16 @@ def __init__(self, qnotero, firstRun=False): QDialog.__init__(self) self.qnotero = qnotero - uiPath = os.path.join(os.path.dirname(__file__), 'ui', 'preferences.ui') - self.ui = uic.loadUi(uiPath, self) + self.loadUi('preferences') self.ui.labelLocatePath.hide() if not firstRun: self.ui.labelFirstRun.hide() - self.ui.labelTitleMsg.setText( \ - self.ui.labelTitleMsg.text().replace(u"[version]", \ - self.qnotero.version)) - self.ui.pushButtonZoteroPathAutoDetect.clicked.connect( \ + self.ui.labelTitleMsg.setText( + self.ui.labelTitleMsg.text().replace(u"[version]", + self.qnotero.version)) + self.ui.pushButtonZoteroPathAutoDetect.clicked.connect( self.zoteroPathAutoDetect) - self.ui.pushButtonZoteroPathBrowse.clicked.connect( \ + self.ui.pushButtonZoteroPathBrowse.clicked.connect( self.zoteroPathBrowse) self.ui.checkBoxAutoUpdateCheck.setChecked(getConfig(u"autoUpdateCheck")) self.ui.lineEditZoteroPath.setText(getConfig(u"zoteroPath")) @@ -79,7 +79,7 @@ def accept(self): print('saving!') setConfig(u"firstRun", False) setConfig(u"pos", self.ui.comboBoxPos.currentText()) - setConfig(u"autoUpdateCheck", \ + setConfig(u"autoUpdateCheck", self.ui.checkBoxAutoUpdateCheck.isChecked()) setConfig(u"zoteroPath", self.ui.lineEditZoteroPath.text()) setConfig(u"theme", self.ui.comboBoxTheme.currentText().capitalize()) @@ -134,7 +134,7 @@ def setZoteroPath(self, path): if valid_location(path): self.ui.lineEditZoteroPath.setText(path) else: - QMessageBox.information(self, u"Invalid Zotero path", \ + QMessageBox.information(self, u"Invalid Zotero path", u"The folder you selected does not contain 'zotero.sqlite'") def zoteroPathAutoDetect(self): @@ -148,7 +148,7 @@ def zoteroPathAutoDetect(self): home = os.environ[u"HOME"] zoteroPath = self.locate(home, u"zotero.sqlite") if zoteroPath == None: - QMessageBox.information(self, u"Unable to find Zotero", \ + QMessageBox.information(self, u"Unable to find Zotero", u"Unable to find Zotero. Please specify the Zotero folder manually.") else: self.ui.lineEditZoteroPath.setText(zoteroPath) @@ -161,4 +161,3 @@ def zoteroPathBrowse(self): path = QFileDialog.getExistingDirectory(self, u"Locate Zotero folder") if path != u"": self.setZoteroPath(path) - diff --git a/libqnotero/qnotero.py b/libqnotero/qnotero.py index 880c67c..2d5ceeb 100644 --- a/libqnotero/qnotero.py +++ b/libqnotero/qnotero.py @@ -28,13 +28,14 @@ from libqnotero.config import saveConfig, restoreConfig, setConfig, getConfig from libqnotero.qnoteroItemDelegate import QnoteroItemDelegate from libqnotero.qnoteroItem import QnoteroItem +from libqnotero.uiloader import UiLoader from libzotero.libzotero import LibZotero -class Qnotero(QMainWindow): +class Qnotero(QMainWindow, UiLoader): """The main class of the Qnotero GUI""" - version = '1.0.0~pre1' + version = '1.0.0' def __init__(self, systray=True, debug=False, reset=False, parent=None): @@ -49,8 +50,7 @@ def __init__(self, systray=True, debug=False, reset=False, parent=None): """ QMainWindow.__init__(self, parent) - uiPath = os.path.join(os.path.dirname(__file__), 'ui', 'qnotero.ui') - self.ui = uic.loadUi(uiPath, self) + self.loadUi('qnotero') if not reset: self.restoreState() self.debug = debug @@ -336,17 +336,18 @@ def updateCheck(self): if not getConfig(u"autoUpdateCheck"): return True - - import urllib + import urllib.request + from distutils.version import LooseVersion print(u"qnotero.updateCheck(): opening %s" % getConfig(u"updateUrl")) try: - fd = urllib.urlopen(getConfig(u"updateUrl")) - mrv = float(fd.read().strip()) + fd = urllib.request.urlopen(getConfig(u"updateUrl")) + mrv = fd.read().decode('utf-8').strip() except: - print(u"qnotero.updateCheck(): failed to check for update") + print('qnotero.updateCheck(): failed to check for update') return - print(u"qnotero.updateCheck(): most recent version is %.2f" % mrv) - if mrv > self.version: - QMessageBox.information(self, u"Update found", \ - u"A new version of Qnotero %s is available! Please visit http://www.cogsci.nl/ for more information." \ - % mrv) + print("qnotero.updateCheck(): most recent = %s, current = %s" \ + % (mrv, self.version)) + if LooseVersion(mrv) > LooseVersion(self.version): + QMessageBox.information(self, 'Update found', + ('A new version of Qnotero is available! Please visit ' + 'http://www.cogsci.nl/qnotero for more information.')) diff --git a/libqnotero/uiloader.py b/libqnotero/uiloader.py new file mode 100644 index 0000000..c1eaa60 --- /dev/null +++ b/libqnotero/uiloader.py @@ -0,0 +1,55 @@ +#-*- coding:utf-8 -*- + +""" +This file is part of qnotero. + +qnotero is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +qnotero 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with qnotero. If not, see . +""" + +import os +from PyQt4 import QtCore, QtGui, uic + +class UiLoader(QtCore.QObject): + + """ + desc: + A base object from classes that dynamically load a UI file. + """ + + def loadUi(self, ui): + + """ + desc: + Dynamically loads a UI file. + + arguments: + ui: + desc: The name of a UI file, which should match. + libqnotero/ui/[name].ui + type: str + """ + + path = os.path.dirname(__file__) + # If we are running from a frozen state (i.e. packaged by py2exe), we + # need to find the UI files relative to the executable directory, + # because the modules are packaged into library.zip. + if os.name == 'nt': + import imp + import sys + if (hasattr(sys, 'frozen') or hasattr(sys, 'importers') or \ + imp.is_frozen('__main__')): + path = os.path.join(os.path.dirname(sys.executable), + 'libqnotero') + uiPath = os.path.join(path, 'ui', '%s.ui' % ui) + self.ui = uic.loadUi(uiPath, self) diff --git a/qnotero b/qnotero index a8290cb..9c369d4 100755 --- a/qnotero +++ b/qnotero @@ -19,6 +19,9 @@ along with qnotero. If not, see . if __name__ == "__main__": import sys + # Check if we're running a supported Python (>= 3.3) + if sys.version_info < (3,3,0): + raise Exception('Qnotero requires Python >= 3.3') if '--version' in sys.argv: from libqnotero.qnotero import Qnotero print(Qnotero.version) diff --git a/qnotero.nsi b/qnotero.nsi new file mode 100644 index 0000000..37c6ffa --- /dev/null +++ b/qnotero.nsi @@ -0,0 +1,116 @@ +; This file is part of Qnotero. + +; Qnotero is free software: you can redistribute it and/or modify +; it under the terms of the GNU General Public License as published by +; the Free Software Foundation, either version 3 of the License, or +; (at your option) any later version. + +; Qnotero 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 General Public License for more details. + +; You should have received a copy of the GNU General Public License +; along with Qnotero. If not, see . + +; USAGE +; ----- +; This script assumes that the binary is located in +; C:\Users\Dévélõpe®\Documents\gît\Qnotero\dist +; +; The extension FileAssociation.nsh must be installed. This can be +; done by downloading the script from the link below and copying it +; to a file named FileAssociation.nsh in the Include folder of NSIS. + +; For each new release, adjust the PRODUCT_VERSION as follows: +; version-win32-package# + +; After compilation, rename the .exe file to (e.g.) +; qnotero_{PRODUCT_VERSION}.exe + +; This script must be ANSI encoded. + +; HM NIS Edit Wizard helper defines +!define PRODUCT_NAME "Qnotero" +!define PRODUCT_VERSION "1.0.0-win32-1" +!define PRODUCT_PUBLISHER "Sebastiaan Mathot" +!define PRODUCT_WEB_SITE "http://www.cogsci.nl/qnotero" +!define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" +!define PRODUCT_UNINST_ROOT_KEY "HKLM" + +; MUI 1.67 compatible ------ +!include "MUI.nsh" +!include "FileAssociation.nsh" + +; MUI Settings +!define MUI_ABORTWARNING +!define MUI_ICON "C:\Users\Dévélõpe®\Documents\gît\Qnotero\data\qnotero.ico" +!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninstall.ico" + +; Welcome page +!insertmacro MUI_PAGE_WELCOME +; Directory page +!insertmacro MUI_PAGE_DIRECTORY +; Instfiles page +!insertmacro MUI_PAGE_INSTFILES +; Finish page +!define MUI_FINISHPAGE_RUN "qnotero.exe" +!insertmacro MUI_PAGE_FINISH + +; Uninstaller pages +!insertmacro MUI_UNPAGE_INSTFILES + +; Language files +!insertmacro MUI_LANGUAGE "English" + +; MUI end ------ + +Name "${PRODUCT_NAME} ${PRODUCT_VERSION}" +OutFile "qnotero_X-win32-X.exe" +InstallDir "$PROGRAMFILES\Qnotero" +ShowInstDetails hide +ShowUnInstDetails hide + +Section "Qnotero" SEC01 + SetOutPath "$INSTDIR" + SetOverwrite try + File /r "C:\Users\Dévélõpe®\Documents\gît\Qnotero\dist\*.*" +SectionEnd + +Section -AdditionalIcons + WriteIniStr "$INSTDIR\${PRODUCT_NAME}.url" "InternetShortcut" "URL" "${PRODUCT_WEB_SITE}" + CreateDirectory "$SMPROGRAMS\Qnotero" + CreateShortCut "$SMPROGRAMS\Qnotero\Qnotero.lnk" "$INSTDIR\qnotero.exe" "" "$INSTDIR\data\qnotero.ico" + CreateShortCut "$SMPROGRAMS\Qnotero\Website.lnk" "$INSTDIR\${PRODUCT_NAME}.url" + CreateShortCut "$SMPROGRAMS\Qnotero\Uninstall.lnk" "$INSTDIR\uninst.exe" +SectionEnd + +Section -Post + WriteUninstaller "$INSTDIR\uninst.exe" + WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)" + WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\uninst.exe" + WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}" + WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}" + WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}" +SectionEnd + +Function un.onUninstSuccess + HideWindow + MessageBox MB_ICONINFORMATION|MB_OK "$(^Name) was successfully removed from your computer." +FunctionEnd + +Function un.onInit + MessageBox MB_ICONQUESTION|MB_YESNO|MB_DEFBUTTON2 "Are you sure you want to completely remove $(^Name) and all of its components?" IDYES +2 + Abort +FunctionEnd + +Section Uninstall + Delete "$SMPROGRAMS\Qnotero\Qnotero.lnk" + Delete "$SMPROGRAMS\Qnotero\Qnotero (runtime).lnk" + Delete "$SMPROGRAMS\Qnotero\Website.lnk" + Delete "$SMPROGRAMS\Qnotero\Uninstall.lnk" + RMDir "$SMPROGRAMS\Qnotero" + RMDir /r "$INSTDIR" + DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" + SetAutoClose true +SectionEnd diff --git a/setup-windows.py b/setup-windows.py index 594e38f..b7358b5 100644 --- a/setup-windows.py +++ b/setup-windows.py @@ -1,11 +1,60 @@ #!/usr/bin/env python3 +""" +This file is part of qnotero. + +qnotero is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +qnotero 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with qnotero. If not, see . + +--- +desc: + Windows packaging procedure: + + 1. Build Qnotero into `dist` with `setup-win.py py2exe` + 2. Create `.exe` installer with `.nsi` script + 3. Rename `.exe` installer + 4. Rename `dist` and pack it into `.zip` for portable distribution +--- +""" + from distutils.core import setup import glob import py2exe import os -import os.path import shutil +import sys + +class safe_print(object): + + """ + desc: + Used to redirect standard output, so that Python doesn't crash when + printing special characters to a terminal. + """ + + errors = 'strict' + encoding = 'utf-8' + + def write(self, msg): + if isinstance(msg, str): + msg = msg.encode('ascii', 'ignore') + sys.__stdout__.write(msg.decode('ascii')) + + def flush(self): + pass + +# Redirect standard output to safe printer +sys.stdout = safe_print() # Create empty destination folders if os.path.exists("dist"): @@ -14,24 +63,34 @@ # Setup options setup( - - # Use 'console' to have the programs run in a terminal and - # 'windows' to run them normally. windows = [{ "script" : "qnotero", - 'icon_resources': [(0, os.path.join("data", "qnotero.ico"))], + 'icon_resources': [ + (0, os.path.join("data", "qnotero.ico")) + ], }], + data_files = [ + ('resources/default', glob.glob('resources/default/*')), + ('resources/elementary', glob.glob('resources/elementary/*')), + ('resources/tango', glob.glob('resources/tango/*')), + ('libqnotero/ui', glob.glob('libqnotero/ui/*')), + ('data', ['data/qnotero.ico']) + ], options = { 'py2exe' : { - 'compressed' : True, - 'optimize': 2, - 'bundle_files': 3, - 'includes': 'sip, libqnotero._themes.*', - "dll_excludes" : ["MSVCP90.DLL"] - }, + 'compressed' : True, + 'optimize': 2, + 'bundle_files': 3, + 'includes': [ + 'sip', + ], + 'packages' : [ + "libqnotero", + "libzotero", + "libqnotero._themes", + "libzotero._noteProvider", + ], + "dll_excludes" : ["MSVCP90.DLL"], + }, }, ) - -# Copy the resources -shutil.copytree("resources", os.path.join("dist", "resources")) -