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:
parent
0ad39ac41f
commit
b635fbb048
4
.gitignore
vendored
4
.gitignore
vendored
@ -2,8 +2,12 @@
|
||||
build
|
||||
dist.*
|
||||
*.bak
|
||||
*.json
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.dll
|
||||
*.pdb
|
||||
*.msi
|
||||
*.wixobj
|
||||
*.xml
|
||||
*.zip
|
||||
|
31
EDMarketConnector.manifest
Normal file
31
EDMarketConnector.manifest
Normal 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>
|
@ -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()
|
||||
|
@ -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" />
|
||||
|
15
README.md
15
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
|
||||
|
10
config.py
10
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)
|
||||
|
55
setup.py
55
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<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
97
update.py
Normal 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user