1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-13 07:47:14 +03:00

Sparkle & WinSparkle integration for automatic updates.

This commit is contained in:
Jonathan Harris 2015-06-08 13:37:51 +01:00
parent 0ad39ac41f
commit b635fbb048
8 changed files with 221 additions and 17 deletions

4
.gitignore vendored
View File

@ -2,8 +2,12 @@
build
dist.*
*.bak
*.json
*.pyc
*.pyo
*.dll
*.pdb
*.msi
*.wixobj
*.xml
*.zip

View File

@ -0,0 +1,31 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level='asInvoker' uiAccess='false' />
</requestedPrivileges>
</security>
</trustInfo>
<dependency>
<dependentAssembly>
<assemblyIdentity
type='win32'
name='Microsoft.VC90.CRT'
version='9.0.21022.8'
processorArchitecture='*'
publicKeyToken='1fc8b3b9a1e18e3b' />
</dependentAssembly>
</dependency>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*" />
</dependentAssembly>
</dependency>
</assembly>

View File

@ -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('<<Quit>>', 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()

View File

@ -17,6 +17,9 @@
Description="$(var.PRODUCTLONGNAME) installer"
InstallerVersion="300" Compressed="yes" />
<!-- Always reinstall since patching is problematic -->
<MajorUpgrade AllowSameVersionUpgrades="yes" DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
<Media Id="1" Cabinet="product.cab" EmbedCab="yes" />
<Icon Id="EDMarketConnector.exe" SourceFile="EDMarketConnector.ico"/>
@ -87,6 +90,9 @@
<Component Id="cmpF53EFD7F16A85DF32B289806E475CEC1" Guid="{E8E3701A-8AA1-4D46-A56D-7AF08D6AFCD4}">
<File Id="filEB07038D0C43A2635802F5F0874B19F4" KeyPath="yes" Source="SourceDir\unicodedata.pyd" />
</Component>
<Component Id="cmp775D0CC23474B987B407951838E0DCC3" Guid="{3117D2CF-1D87-4B99-BE44-7BDDFE8C8E60}">
<File Id="fil293C8E7AF8C6F689C7E50983C257DD68" KeyPath="yes" Source="SourceDir\WinSparkle.dll" />
</Component>
<Directory Id="dirBEE0D5863824B80A537E14FE56402A0C" Name="tcl">
<Directory Id="dir9FADF4954D330D96E48B9D59CBE9A964" Name="tcl8.5">
<Component Id="cmpE60B2042B4B6A6FC78D8B97E5C31EFE2" Guid="{C085794D-6644-4915-B1C3-3060BE9E3F3B}">
@ -314,6 +320,7 @@
<ComponentRef Id="cmp47BC2E37E918091103765A3E7BD81D06" />
<ComponentRef Id="cmp4854ADD13616F64F327353F410CEDC44" />
<ComponentRef Id="cmpF53EFD7F16A85DF32B289806E475CEC1" />
<ComponentRef Id="cmp775D0CC23474B987B407951838E0DCC3" />
<ComponentRef Id="cmpE60B2042B4B6A6FC78D8B97E5C31EFE2" />
<ComponentRef Id="cmp89797FC50D96847B20F83F5F004B305D" />
<ComponentRef Id="cmp46F96A08F279F9A56AA11B7A48864E3B" />

View File

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

View File

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

View File

@ -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<item>
\t\t\t<title>Release {0}</title>
\t\t\t<description>
\t\t\t\t<![CDATA[
<h2>Release {0}</h2>
<ul>
</ul>
\t\t\t\t]]>
\t\t\t</description>
\t\t\t<enclosure
\t\t\t\turl="https://github.com/Marginal/EDMarketConnector/releases/download/rel-{1}/{2}"
\t\t\t\tsparkle:os="{3}"
\t\t\t\tsparkle:version="{4}"
\t\t\t\tlength="{5}"
\t\t\t\ttype="application/octet-stream"
\t\t\t/>
\t\t</item>
'''.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))

97
update.py Normal file
View File

@ -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('<<Quit>>', 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