diff --git a/.gitignore b/.gitignore
index e2505dea..187b1a1a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,8 +2,12 @@
build
dist.*
*.bak
+*.json
*.pyc
*.pyo
+*.dll
+*.pdb
*.msi
*.wixobj
+*.xml
*.zip
diff --git a/EDMarketConnector.manifest b/EDMarketConnector.manifest
new file mode 100644
index 00000000..b8bb6efe
--- /dev/null
+++ b/EDMarketConnector.manifest
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/EDMarketConnector.py b/EDMarketConnector.py
index f094e6c9..1a555f5e 100755
--- a/EDMarketConnector.py
+++ b/EDMarketConnector.py
@@ -66,8 +66,15 @@ class AppWindow:
child.grid_configure(padx=5, pady=(platform=='darwin' and 3 or 2))
menubar = tk.Menu()
- self.w['menu'] = menubar
if platform=='darwin':
+ from Foundation import NSBundle
+ # https://www.tcl.tk/man/tcl/TkCmd/menu.htm
+ apple_menu = tk.Menu(menubar, name='apple')
+ apple_menu.add_command(label="About %s" % applongname, command=lambda:root.call('tk::mac::standardAboutPanel'))
+ apple_menu.add_command(label="Check for Update", command=lambda:self.updater.checkForUpdates())
+ menubar.add_cascade(menu=apple_menu)
+ window_menu = tk.Menu(menubar, name='window')
+ menubar.add_cascade(menu=window_menu)
# https://www.tcl.tk/man/tcl/TkCmd/tk_mac.htm
root.createcommand('tkAboutDialog', lambda:root.call('tk::mac::standardAboutPanel'))
root.createcommand("::tk::mac::Quit", self.onexit)
@@ -76,10 +83,12 @@ class AppWindow:
root.protocol("WM_DELETE_WINDOW", self.w.withdraw) # close button shouldn't quit app
else:
file_menu = tk.Menu(menubar, tearoff=tk.FALSE)
+ file_menu.add_command(label="Check for Update", command=lambda:self.updater.checkForUpdates())
file_menu.add_command(label="Settings", command=lambda:prefs.PreferencesDialog(self.w, self.login))
file_menu.add_command(label="Exit", command=self.onexit)
menubar.add_cascade(label="File", menu=file_menu)
root.protocol("WM_DELETE_WINDOW", self.onexit)
+ self.w['menu'] = menubar
# update geometry
if config.get('geometry'):
@@ -97,6 +106,12 @@ class AppWindow:
else:
self.login()
+ # Load updater after UI creation (for WinSparkle)
+ import update
+ self.updater = update.Updater(master)
+ master.bind_all('<>', self.onexit) # user-generated
+
+
# call after credentials have changed
def login(self):
self.status['text'] = 'Logging in...'
@@ -190,7 +205,7 @@ class AppWindow:
self.status['text'] = status
self.w.update_idletasks()
- def onexit(self):
+ def onexit(self, event=None):
config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+')))
config.close()
self.session.close()
diff --git a/EDMarketConnector.wxs b/EDMarketConnector.wxs
index ca19c7f1..398e8a5f 100644
--- a/EDMarketConnector.wxs
+++ b/EDMarketConnector.wxs
@@ -17,6 +17,9 @@
Description="$(var.PRODUCTLONGNAME) installer"
InstallerVersion="300" Compressed="yes" />
+
+
+
@@ -87,6 +90,9 @@
+
+
+
@@ -314,6 +320,7 @@
+
diff --git a/README.md b/README.md
index a77671c1..50baeb18 100644
--- a/README.md
+++ b/README.md
@@ -59,7 +59,20 @@ Running from source & packaging
--------
The application requires Python 2.7, plus the "requests" module. Run with `python EDMarketConnector.py`.
-To package up for distribution requires py2app 0.9.x on Mac, and py2exe 0.6.x plus the [WiX Toolset](http://wixtoolset.org/) on Windows. Run `setup.py py2app` or `setup.py py2exe`.
+To package up for distribution:
+
+Mac:
+
+* requires py2app 0.9.x
+* [Sparkle.framework](https://github.com/sparkle-project/Sparkle) installed in /Library/Frameworks
+* Run `setup.py py2app`
+
+Windows:
+
+* requires py2exe 0.6.x
+* winsparkle.dll & .pdb from [WinSparkle](https://github.com/vslavik/winsparkle) copied to the current directory
+* [WiX Toolset](http://wixtoolset.org/)
+* Run `setup.py py2exe`
Acknowledgements
diff --git a/config.py b/config.py
index 1aaf20d7..c14cda9c 100644
--- a/config.py
+++ b/config.py
@@ -131,6 +131,16 @@ class Config:
SHDeleteKey(oldkey, '')
RegCloseKey(oldkey)
+ # set WinSparkle defaults - https://github.com/vslavik/winsparkle/wiki/Registry-Settings
+ sparklekey = HKEY()
+ if not RegCreateKeyEx(self.hkey, 'WinSparkle', 0, None, 0, KEY_ALL_ACCESS, None, ctypes.byref(sparklekey), ctypes.byref(disposition)):
+ if disposition.value == REG_CREATED_NEW_KEY:
+ buf = ctypes.create_unicode_buffer('1')
+ RegSetValueEx(sparklekey, 'CheckForUpdates', 0, 1, buf, len(buf)*2)
+ buf = ctypes.create_unicode_buffer(unicode(47*60*60))
+ RegSetValueEx(sparklekey, 'UpdateInterval', 0, 1, buf, len(buf)*2)
+ RegCloseKey(sparklekey)
+
if not self.get('outdir') or not isdir(self.get('outdir')):
ctypes.windll.shell32.SHGetSpecialFolderPathW(0, buf, CSIDL_PERSONAL, 0)
self.set('outdir', buf.value)
diff --git a/setup.py b/setup.py
index ed4bdda3..1eab8726 100755
--- a/setup.py
+++ b/setup.py
@@ -54,17 +54,23 @@ SHORTVERSION = ''.join(VERSION.split('.')[:3])
PY2APP_OPTIONS = {'dist_dir': dist_dir,
'optimize': 2,
'packages': [ 'requests' ],
+ 'frameworks': [ 'Sparkle.framework' ],
'excludes': [ 'PIL' ],
'iconfile': '%s.icns' % APPNAME,
'semi_standalone': True,
'site_packages': False,
'plist': {
- 'CFBundleName': APPNAME,
+ 'CFBundleName': APPLONGNAME,
'CFBundleIdentifier': 'uk.org.marginal.%s' % APPNAME.lower(),
'CFBundleShortVersionString': VERSION,
'CFBundleVersion': VERSION,
'LSMinimumSystemVersion': '.'.join(platform.mac_ver()[0].split('.')[:2]), # minimum version = build version
'NSHumanReadableCopyright': u'© 2015 Jonathan Harris',
+ 'SUEnableAutomaticChecks': True,
+ 'SUShowReleaseNotes': True,
+ 'SUAllowsAutomaticUpdates': False,
+ 'SUFeedURL': 'http://marginal.org.uk/edmarketconnector.xml',
+ 'SUScheduledCheckInterval': 47*60*60,
},
'graph': True, # output dependency graph in dist
}
@@ -78,6 +84,7 @@ PY2EXE_OPTIONS = {'dist_dir': dist_dir,
if sys.platform=='win32':
import requests
DATA_FILES = [ ('', [requests.certs.where(),
+ 'WinSparkle.dll',
'%s.ico' % APPNAME ] ) ]
else:
DATA_FILES = [ ]
@@ -89,6 +96,9 @@ setup(
windows = [ {'script': APP,
'icon_resources': [(0, '%s.ico' % APPNAME)],
'copyright': u'© 2015 Jonathan Harris',
+ 'name': APPNAME, # WinSparkle
+ 'company_name': 'Marginal', # WinSparkle
+ 'other_resources': [(24, 1, open(APPNAME+'.manifest').read())],
} ],
data_files = DATA_FILES,
options = { 'py2app': PY2APP_OPTIONS,
@@ -98,22 +108,39 @@ setup(
if sys.platform == 'darwin':
- if isdir('%s/%s.app' % (dist_dir, APPNAME)):
+ if isdir('%s/%s.app' % (dist_dir, APPLONGNAME)): # from CFBundleName
+ os.rename('%s/%s.app' % (dist_dir, APPLONGNAME), '%s/%s.app' % (dist_dir, APPNAME))
if macdeveloperid:
os.system('codesign --deep -v -s "Developer ID Application: %s" %s/%s.app' % (macdeveloperid, dist_dir, APPNAME))
# Make zip for distribution, preserving signature
- os.system('cd %s; ditto -ck --keepParent --sequesterRsrc %s.app ../%s_mac_%s.zip; cd ..' % (dist_dir, APPNAME, APPNAME, SHORTVERSION))
+ PKG = '%s_mac_%s.zip' % (APPNAME, SHORTVERSION)
+ os.system('cd %s; ditto -ck --keepParent --sequesterRsrc %s.app ../%s; cd ..' % (dist_dir, APPNAME, PKG))
else:
- # Manually trim the tcl/tk folders
- os.unlink(join(dist_dir, 'w9xpopen.exe'))
- for d in [ r'tcl\tcl8.5\encoding',
- r'tcl\tcl8.5\http1.0',
- r'tcl\tcl8.5\msgs',
- r'tcl\tcl8.5\tzdata',
- r'tcl\tk8.5\demos',
- r'tcl\tk8.5\images',
- r'tcl\tk8.5\msgs', ]:
- shutil.rmtree(join(dist_dir, d))
+ shutil.copy('WinSparkle.pdb', dist_dir) # For debugging - not included in package
os.system(r'"C:\Program Files (x86)\WiX Toolset v3.9\bin\candle.exe" -out %s\ %s.wxs' % (dist_dir, APPNAME))
if exists('%s/%s.wixobj' % (dist_dir, APPNAME)):
- os.system(r'"C:\Program Files (x86)\WiX Toolset v3.9\bin\light.exe" -sacl -spdb -sw1076 %s\%s.wixobj -out %s_win_%s.msi' % (dist_dir, APPNAME, APPNAME, SHORTVERSION))
+ PKG = '%s_win_%s.msi' % (APPNAME, SHORTVERSION)
+ os.system(r'"C:\Program Files (x86)\WiX Toolset v3.9\bin\light.exe" -sacl -spdb -sw1076 %s\%s.wixobj -out %s' % (dist_dir, APPNAME, PKG))
+
+# Make appcast entry
+appcast = open('appcast_%s_%s.xml' % (sys.platform=='darwin' and 'mac' or 'win', SHORTVERSION), 'w')
+appcast.write('''
+\t\t-
+\t\t\tRelease {0}
+\t\t\t
+\t\t\t\tRelease {0}
+
+\t\t\t\t]]>
+\t\t\t
+\t\t\t
+\t\t
+'''.format(float(SHORTVERSION)/100, SHORTVERSION, PKG, sys.platform=='darwin' and 'osx' or 'windows"\n\t\t\t\tsparkle:installerArguments="/passive', VERSION, os.stat(PKG).st_size))
diff --git a/update.py b/update.py
new file mode 100644
index 00000000..d1d7b2f0
--- /dev/null
+++ b/update.py
@@ -0,0 +1,97 @@
+import os
+from os.path import dirname, join
+import sys
+
+
+# ensure registry is set up on Windows before we start
+import config
+
+class NullUpdater:
+
+ def __init__(self, master):
+ pass
+
+ def checkForUpdates(self):
+ pass
+
+ def close(self):
+ pass
+
+
+if not getattr(sys, 'frozen', False):
+
+ class Updater(NullUpdater):
+ pass
+
+elif sys.platform=='darwin':
+
+ import objc
+
+ class Updater(NullUpdater):
+
+ # https://github.com/sparkle-project/Sparkle/wiki/Customization
+
+ def __init__(self, master):
+ try:
+ objc.loadBundle('Sparkle', globals(), join(dirname(sys.executable), os.pardir, 'Frameworks', 'Sparkle.framework'))
+ self.updater = SUUpdater.sharedUpdater()
+ except:
+ # can't load framework - not frozen or not included in app bundle?
+ self.updater = None
+
+ def checkForUpdates(self):
+ if self.updater:
+ self.updater.checkForUpdates_(None)
+
+ def close():
+ self.updater = None
+
+
+elif sys.platform=='win32':
+
+ import ctypes
+
+ # https://github.com/vslavik/winsparkle/blob/master/include/winsparkle.h#L272
+ root = None
+
+ def shutdown_request():
+ root.event_generate('<>', when="tail")
+
+ class Updater(NullUpdater):
+
+ # https://github.com/vslavik/winsparkle/wiki/Basic-Setup
+
+ def __init__(self, master):
+ try:
+ sys.frozen # don't want to try updating python.exe
+ self.updater = ctypes.cdll.WinSparkle
+ self.updater.win_sparkle_set_appcast_url('http://marginal.org.uk/edmarketconnector.xml') # py2exe won't let us embed this in resources
+
+ # set up shutdown callback
+ global root
+ root = master
+ self.callback_t = ctypes.CFUNCTYPE(None) # keep reference
+ self.callback_fn = self.callback_t(shutdown_request)
+ self.updater.win_sparkle_set_shutdown_request_callback(self.callback_fn)
+
+ self.updater.win_sparkle_init()
+
+ except:
+ from traceback import print_exc
+ print_exc()
+ self.updater = None
+
+ def checkForUpdates(self):
+ if self.updater:
+ self.updater.win_sparkle_check_update_with_ui()
+
+ def close(self):
+ if self.updater:
+ updater.win_sparkle_cleanup()
+ self.updater = None
+
+else:
+
+ class Updater(NullUpdater):
+ pass
+