一、esky介绍
Esky is an auto-update framework for frozen Python applications. It provides a simple API through which apps can find, fetch and install updates, and a bootstrapping mechanism that keeps the app safe in the face of failed or partial updates. Updates can also be sent as differential patches.
Esky is currently capable of freezing apps with py2exe, py2app, cxfreeze and bbfreeze. Adding support for other freezer programs should be easy; patches will be gratefully accepted.
We are tested and running on Python 2.7 Py2app will work on python3 fine, the other freezers not so much.
Esky是一个python编译程序的自动升级框架,提供简单的api实现应用的自动更新(包括比较版本、更新版本),esky支持py2exe,py2app,cxfreeze以及bbfreeze等多种python打包框架。
二、esky安装及说明
1、pip安装
pip install esky
2、esky说明
https://github.com/cloudmatrix/esky/
3、esky教学视频
http://pyvideo.org/pycon-au-2010/pyconau-2010--esky--keep-your-frozen-apps-fresh.html
三、esky用法示例
esky用起来比较简单,我们这里以常用的基于wx的windows应用举例。
wxpython下有个wx.lib.softwareupdate 类,对wxpython应用的esky升级进行了二次封装。
网上有个现成的示范例子,具体网址:http://www.blog.pythonlibrary.org/2013/07/12/wxpython-updating-your-application-with-esky/
代码很简单,对其中的关键部分进行注释说明(红色字体部分):
# ---------------------------------------- # image_viewer2.py # # Created 03-20-2010 # # Author: Mike Driscoll # ---------------------------------------- import glob import os import wx from wx.lib.pubsub import setuparg1 from wx.lib.pubsub import pub as Publisher
#申明语句
from wx.lib.softwareupdate import SoftwareUpdate import version ######################################################################## class ViewerPanel(wx.Panel): """""" #---------------------------------------------------------------------- def __init__(self, parent): """Constructor""" wx.Panel.__init__(self, parent) width, height = wx.DisplaySize() self.picPaths = [] self.currentPicture = 0 self.totalPictures = 0 self.photoMaxSize = height - 200 Publisher.subscribe(self.updateImages, ("update images")) self.slideTimer = wx.Timer(None) self.slideTimer.Bind(wx.EVT_TIMER, self.update) self.layout() #---------------------------------------------------------------------- def layout(self): """ Layout the widgets on the panel """ self.mainSizer = wx.BoxSizer(wx.VERTICAL) btnSizer = wx.BoxSizer(wx.HORIZONTAL) img = wx.EmptyImage(self.photoMaxSize,self.photoMaxSize) self.imageCtrl = wx.StaticBitmap(self, wx.ID_ANY, wx.BitmapFromImage(img)) self.mainSizer.Add(self.imageCtrl, 0, wx.ALL|wx.CENTER, 5) self.imageLabel = wx.StaticText(self, label="") self.mainSizer.Add(self.imageLabel, 0, wx.ALL|wx.CENTER, 5) btnData = [("Previous", btnSizer, self.onPrevious), ("Slide Show", btnSizer, self.onSlideShow), ("Next", btnSizer, self.onNext)] for data in btnData: label, sizer, handler = data self.btnBuilder(label, sizer, handler) self.mainSizer.Add(btnSizer, 0, wx.CENTER) self.SetSizer(self.mainSizer) #---------------------------------------------------------------------- def btnBuilder(self, label, sizer, handler): """ Builds a button, binds it to an event handler and adds it to a sizer """ btn = wx.Button(self, label=label) btn.Bind(wx.EVT_BUTTON, handler) sizer.Add(btn, 0, wx.ALL|wx.CENTER, 5) #---------------------------------------------------------------------- def loadImage(self, image): """""" image_name = os.path.basename(image) img = wx.Image(image, wx.BITMAP_TYPE_ANY) # scale the image, preserving the aspect ratio W = img.GetWidth() H = img.GetHeight() if W > H: NewW = self.photoMaxSize NewH = self.photoMaxSize * H / W else: NewH = self.photoMaxSize NewW = self.photoMaxSize * W / H img = img.Scale(NewW,NewH) self.imageCtrl.SetBitmap(wx.BitmapFromImage(img)) self.imageLabel.SetLabel(image_name) self.Refresh() Publisher.sendMessage("resize", "") #---------------------------------------------------------------------- def nextPicture(self): """ Loads the next picture in the directory """ if self.currentPicture == self.totalPictures-1: self.currentPicture = 0 else: self.currentPicture += 1 self.loadImage(self.picPaths[self.currentPicture]) #---------------------------------------------------------------------- def previousPicture(self): """ Displays the previous picture in the directory """ if self.currentPicture == 0: self.currentPicture = self.totalPictures - 1 else: self.currentPicture -= 1 self.loadImage(self.picPaths[self.currentPicture]) #---------------------------------------------------------------------- def update(self, event): """ Called when the slideTimer‘s timer event fires. Loads the next picture from the folder by calling th nextPicture method """ self.nextPicture() #---------------------------------------------------------------------- def updateImages(self, msg): """ Updates the picPaths list to contain the current folder‘s images """ self.picPaths = msg.data self.totalPictures = len(self.picPaths) self.loadImage(self.picPaths[0]) #---------------------------------------------------------------------- def onNext(self, event): """ Calls the nextPicture method """ self.nextPicture() #---------------------------------------------------------------------- def onPrevious(self, event): """ Calls the previousPicture method """ self.previousPicture() #---------------------------------------------------------------------- def onSlideShow(self, event): """ Starts and stops the slideshow """ btn = event.GetEventObject() label = btn.GetLabel() if label == "Slide Show": self.slideTimer.Start(3000) btn.SetLabel("Stop") else: self.slideTimer.Stop() btn.SetLabel("Slide Show") ######################################################################## class ViewerFrame(wx.Frame): """""" #---------------------------------------------------------------------- def __init__(self): """Constructor""" title = ‘Image Viewer %s‘ %(version.VERSION) wx.Frame.__init__(self, None, title=title) panel = ViewerPanel(self) self.folderPath = "" Publisher.subscribe(self.resizeFrame, ("resize")) self.initToolbar() self.sizer = wx.BoxSizer(wx.VERTICAL) self.sizer.Add(panel, 1, wx.EXPAND) self.SetSizer(self.sizer) self.Show() self.sizer.Fit(self) self.Center() #---------------------------------------------------------------------- def initToolbar(self): """ Initialize the toolbar """ self.toolbar = self.CreateToolBar() self.toolbar.SetToolBitmapSize((16,16)) open_ico = wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, wx.ART_TOOLBAR, (16,16)) openTool = self.toolbar.AddSimpleTool(wx.ID_ANY, open_ico, "Open", "Open an Image Directory") self.Bind(wx.EVT_MENU, self.onOpenDirectory, openTool) self.toolbar.Realize() #---------------------------------------------------------------------- def onOpenDirectory(self, event): """ Opens a DirDialog to allow the user to open a folder with pictures """ dlg = wx.DirDialog(self, "Choose a directory", style=wx.DD_DEFAULT_STYLE) if dlg.ShowModal() == wx.ID_OK: self.folderPath = dlg.GetPath() print self.folderPath picPaths = glob.glob(self.folderPath + "\\*.jpg") print picPaths Publisher.sendMessage("update images", picPaths) #---------------------------------------------------------------------- def resizeFrame(self, msg): """""" self.sizer.Fit(self) #########################################################################注意基类是两个 class ImageApp(wx.App, SoftwareUpdate): """""" #---------------------------------------------------------------------- def OnInit(self): """Constructor""" BASEURL = "http://127.0.0.1:8000" #升级初始化,参数1:检查升级包的网页地址,参数2:升级说明文件,升级网页地址与升级说明文件可以不在一个目录。 self.InitUpdates(BASEURL,BASEURL + ‘ChangeLog.txt‘) #启动升级检查,参数:是否显示升级提示,默认显示提示。显然该语句可以放到按钮或者菜单中触发。 self.CheckForUpdate(silentUnlessUpdate=False) frame = ViewerFrame() self.SetTopWindow(frame) self.SetAppDisplayName(‘Image Viewer‘) #ViewerPanel..SetValue(‘Image Viewer %s‘ %(version.VERSION)) return True #---------------------------------------------------------------------- if __name__ == "__main__": app = wx.PySimpleApp() frame = ViewerFrame() app.MainLoop()
总结:
1、先声明类
from wx.lib.softwareupdate import SoftwareUpdate
2、在app中调用声明的类,做为基类之一
class UpApp(wx.App, SoftwareUpdate):
3、在app的中初始化softwareupate,一般放在OnInit()中
wx.GetApp().InitUpdates(‘http://127.0.0.1/update.html‘, ‘http://127.0.0.1/ChangeLog.txt‘)
4、在窗口事件中调用升级检查,可以放到菜单或者按钮中
wx.GetApp().CheckForUpdate()
四、esky编译脚本编写
esky本身不支持编译,所以必须调用cx_freeze或者py2exe之类进行python编译,由于本人比较熟悉cx_freeze,所以……以下例子均是基于cx_freeze。
其编译脚本跟cx_freeze的setup.py有点类似,先来一个简单例子:
#coding=utf-8 #--------------------------------------------------------------------------- # This setup file serves as a model for how to structure your # distutils setup files for making self-updating applications using # Esky. When you run this script use # # python setup.py bdist_esky # # Esky will then use py2app or py2exe as appropriate to create the # bundled application and also its own shell that will help manage # doing the updates. See wx.lib.softwareupdate for the class you can # use to add self-updates to your applications, and you can see how # that code is used here in the superdoodle.py module. #--------------------------------------------------------------------------- from esky import bdist_esky from setuptools import setup # Common settings exeICON = ‘mondrian.ico‘ NAME = "wxImageViewer"#明确调用cx_freeze进行编译 FREEZER =‘cx_freeze‘ #cx_freeze的编译options FREEZER_OPTIONS = { "excludes": ["tkinter","collections.sys",‘collections._weakref‘]#, #剔除wx里tkinter包 }; APP = [bdist_esky.Executable("image_viewer.py", gui_only=True, icon=exeICON, )] DATA_FILES = [ ‘mondrian.ico‘ ] ESKY_OPTIONS = dict( freezer_module = FREEZER, freezer_options = FREEZER_OPTIONS, enable_appdata_dir = True, bundle_msvcrt = False, ) # Build the app and the esky bundle setup( name = NAME, version = ‘1.0‘, scripts = APP, data_files = DATA_FILES, options = dict(bdist_esky=ESKY_OPTIONS), )
这个是编译脚本,具体的编译命令,如下。
五、编译命令
注意setup.py中的version=1.0就是版本定义,若是要发布升级版,只要把version修改成1.1或者2.0,程序就会判断为升级包,进行更新。
编译分两种方式,一种是编译完整包,一种是编译增量补丁包。
特别说明一下补丁包的生成机制:先编译完整包,再比较老版本完整包、新版本完整包,生成差异补丁包。
1、编译完整包
python setup.py bdist_esky
编译之后会在dist目录生成名为wxImageViewer-1.0.win-amd64.zip的打包文件,注意这个文件名本身就包含了版本信息:
1)、wxImageViewer是应用名,对应setup.py中的name定义
2)、1.0是版本号,对应setup.py中version定义
3)、amd64代表64位编译版本,跟python的版本一致。
2、编译增量补丁包
python setup.py bdist_esky_path
注意每次重新编译,需要修改version,会自动生成会自动增量包。
譬如第二次编译,修改version=2.0,则增量包为:wxImageViewer-1.0.win-amd64.from-2.0.patch
1)增量包文件基本很小
2)升级时会自动判断是下载全新包,还是下载增量包。
譬如本地程序是1.0版本,服务器端发了2.0版本的升级文件:wxImageViewer-2.0.win-amd64.zip、wxImageViewer-1.0.win-amd64.from-2.0.patch,esky会自动只下载patch文件。
六、复杂的esky编译脚本
1、实现目录打包
2、实现应用程序版本信息设置
#coding=utf-8 #--------------------------------------------------------------------------- ‘‘‘ Create by: joshua zou 2016.10.08 Purpose: 调用esky打包成执行文件,支持自动升级。 Example: python setup.py bdist_esky / python setup.py bdist_esky_patch ‘‘‘ #--------------------------------------------------------------------------- from esky import bdist_esky from setuptools import setupimport distutils # Dependencies are automatically detected, but it might need fine tuning. VER = ‘1.16.1102.1‘# Common settings exeICON = ‘et.ico‘ NAME = "eTaxMain.exe" FREEZER =‘cx_freeze‘ ESKY_VERSION ={ "version": VER, "company": u"****公司", "description": u"****程序", "product": u"****系统", ‘copyright‘:u"***@版权所有 2016-2020" } #版本申明部分 metadata= distutils.dist.DistributionMetadata() metadata.version = ESKY_VERSION[‘version‘] metadata.description = ESKY_VERSION[‘description‘] metadata.copyright = ESKY_VERSION[‘copyright‘] metadata.name = ESKY_VERSION[‘product‘] #版本申明结束 FREEZER_OPTIONS = { "packages": ["os","wx","requests","lxml","lxml.etree"], #包含package "includes": ["PIL","traceback",‘HTMLParser‘,‘appdirs‘,‘pyDes‘], "excludes": [‘MSVCP90.dll‘, ‘mswsock.dll‘, ‘powrprof.dll‘, ‘USP10.dll‘, ‘_gtkagg‘, ‘_tkagg‘, ‘bsddb‘, ‘curses‘, ‘pywin.debugger‘,‘pywin.debugger.dbgcon‘, ‘pywin.dialogs‘,‘tcl‘, ‘Tkconstants‘, ‘Tkinter‘, ‘wx.tools.*‘,‘wx.py.*‘, "collections.sys",‘collections._weakref‘], #剔除wx里tkinter包 "metadata":metadata }; APP = [bdist_esky.Executable("eTaxMain.py", gui_only=True, icon=exeICON, )] #打包et.ico,helpinfo.txt放到应用目录下#打包.\lang\zh_CN\LC_MESSAGES\eTaxMain.mo到lang\zh_CN下。 DATA_FILES=[(‘‘, [‘et.ico‘,‘helpinfo.txt‘]), (‘lang\zh_CN‘, [‘.\lang\zh_CN\LC_MESSAGES\eTaxMain.mo‘,‘.\lang\zh_CN\LC_MESSAGES\eTaxMain.po‘]) ] ESKY_OPTIONS = dict( freezer_module = FREEZER, freezer_options = FREEZER_OPTIONS, enable_appdata_dir = True, bundle_msvcrt = False, ) # Build the app and the esky bundle setup( name = NAME, version = VER, scripts = APP, data_files = DATA_FILES, options = dict(bdist_esky=ESKY_OPTIONS), )
前前后后,为了这个esky,折腾了快2个礼拜,总算圆满成功,写的比较简单,感兴趣的留言交流。