基于esky实现python应用的自动升级
一、esky介绍
Eskyisanauto-updateframeworkforfrozenPythonapplications.ItprovidesasimpleAPIthroughwhichappscanfind,fetchandinstallupdates,andabootstrappingmechanismthatkeepstheappsafeinthefaceoffailedorpartialupdates.Updatescanalsobesentasdifferentialpatches.
Eskyiscurrentlycapableoffreezingappswithpy2exe,py2app,cxfreezeandbbfreeze.Addingsupportforotherfreezerprogramsshouldbeeasy;patcheswillbegratefullyaccepted.
WearetestedandrunningonPython2.7Py2appwillworkonpython3fine,theotherfreezersnotsomuch.
Esky是一个python编译程序的自动升级框架,提供简单的api实现应用的自动更新(包括比较版本、更新版本),esky支持py2exe,py2app,cxfreeze以及bbfreeze等多种python打包框架。
二、esky安装及说明
1、pip安装
pipinstallesky
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
#
#Created03-20-2010
#
#Author:MikeDriscoll
#----------------------------------------
importglob
importos
importwx
fromwx.lib.pubsubimportsetuparg1
fromwx.lib.pubsubimportpubasPublisher
#申明语句
fromwx.lib.softwareupdateimportSoftwareUpdate
importversion
########################################################################
classViewerPanel(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,("updateimages"))
self.slideTimer=wx.Timer(None)
self.slideTimer.Bind(wx.EVT_TIMER,self.update)
self.layout()
#----------------------------------------------------------------------
deflayout(self):
"""
Layoutthewidgetsonthepanel
"""
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),
("SlideShow",btnSizer,self.onSlideShow),
("Next",btnSizer,self.onNext)]
fordatainbtnData:
label,sizer,handler=data
self.btnBuilder(label,sizer,handler)
self.mainSizer.Add(btnSizer,0,wx.CENTER)
self.SetSizer(self.mainSizer)
#----------------------------------------------------------------------
defbtnBuilder(self,label,sizer,handler):
"""
Buildsabutton,bindsittoaneventhandlerandaddsittoasizer
"""
btn=wx.Button(self,label=label)
btn.Bind(wx.EVT_BUTTON,handler)
sizer.Add(btn,0,wx.ALL|wx.CENTER,5)
#----------------------------------------------------------------------
defloadImage(self,image):
""""""
image_name=os.path.basename(image)
img=wx.Image(image,wx.BITMAP_TYPE_ANY)
#scaletheimage,preservingtheaspectratio
W=img.GetWidth()
H=img.GetHeight()
ifW>H:
NewW=self.photoMaxSize
NewH=self.photoMaxSizeH/W
else:
NewH=self.photoMaxSize
NewW=self.photoMaxSizeW/H
img=img.Scale(NewW,NewH)
self.imageCtrl.SetBitmap(wx.BitmapFromImage(img))
self.imageLabel.SetLabel(image_name)
self.Refresh()
Publisher.sendMessage("resize","")
#----------------------------------------------------------------------
defnextPicture(self):
"""
Loadsthenextpictureinthedirectory
"""
ifself.currentPicture==self.totalPictures-1:
self.currentPicture=0
else:
self.currentPicture+=1
self.loadImage(self.picPaths[self.currentPicture])
#----------------------------------------------------------------------
defpreviousPicture(self):
"""
Displaysthepreviouspictureinthedirectory
"""
ifself.currentPicture==0:
self.currentPicture=self.totalPictures-1
else:
self.currentPicture-=1
self.loadImage(self.picPaths[self.currentPicture])
#----------------------------------------------------------------------
defupdate(self,event):
"""
CalledwhentheslideTimer''stimereventfires.Loadsthenext
picturefromthefolderbycallingthnextPicturemethod
"""
self.nextPicture()
#----------------------------------------------------------------------
defupdateImages(self,msg):
"""
UpdatesthepicPathslisttocontainthecurrentfolder''simages
"""
self.picPaths=msg.data
self.totalPictures=len(self.picPaths)
self.loadImage(self.picPaths[0])
#----------------------------------------------------------------------
defonNext(self,event):
"""
CallsthenextPicturemethod
"""
self.nextPicture()
#----------------------------------------------------------------------
defonPrevious(self,event):
"""
CallsthepreviousPicturemethod
"""
self.previousPicture()
#----------------------------------------------------------------------
defonSlideShow(self,event):
"""
Startsandstopstheslideshow
"""
btn=event.GetEventObject()
label=btn.GetLabel()
iflabel=="SlideShow":
self.slideTimer.Start(3000)
btn.SetLabel("Stop")
else:
self.slideTimer.Stop()
btn.SetLabel("SlideShow")
########################################################################
classViewerFrame(wx.Frame):
""""""
#----------------------------------------------------------------------
def__init__(self):
"""Constructor"""
title=''ImageViewer%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()
#----------------------------------------------------------------------
definitToolbar(self):
"""
Initializethetoolbar
"""
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","OpenanImageDirectory")
self.Bind(wx.EVT_MENU,self.onOpenwww.baiyuewang.netDirectory,openTool)
self.toolbar.Realize()
#----------------------------------------------------------------------
defonOpenDirectory(self,event):
"""
OpensaDirDialogtoallowtheusertoopenafolderwithpictures
"""
dlg=wx.DirDialog(self,"Chooseadirectory",
style=wx.DD_DEFAULT_STYLE)
ifdlg.ShowModal()==wx.ID_OK:
self.folderPath=dlg.GetPath()
printself.folderPath
picPaths=glob.glob(self.folderPath+"\\.jpg")
printpicPaths
Publisher.sendMessage("updateimages",picPaths)
#----------------------------------------------------------------------
defresizeFrame(self,msg):
""""""
self.sizer.Fit(self)
########################################################################
#注意基类是两个
classImageApp(wx.App,SoftwareUpdate):
""""""
#----------------------------------------------------------------------
defOnInit(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(''ImageViewer'')
#ViewerPanel..SetValue(''ImageViewer%s''%(version.VERSION))
returnTrue
#----------------------------------------------------------------------
if__name__=="__main__":
app=wx.PySimpleApp()
frame=ViewerFrame()
app.MainLoop()
复制代码
总结:
1、先声明类
fromwx.lib.softwareupdateimportSoftwareUpdate
2、在app中调用声明的类,做为基类之一
classUpApp(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
#---------------------------------------------------------------------------
#Thissetupfileservesasamodelforhowtostructureyour
#distutilssetupfilesformakingself-updatingapplicationsusing
#Esky.Whenyourunthisscriptuse
#
#pythonsetup.pybdist_esky
#
#Eskywillthenusepy2apporpy2exeasappropriatetocreatethe
#bundledapplicationandalsoitsownshellthatwillhelpmanage
#doingtheupdates.Seewx.lib.softwareupdatefortheclassyoucan
#usetoaddself-updatestoyourapplications,andyoucanseehow
#thatcodeisusedhereinthesuperdoodle.pymodule.
#---------------------------------------------------------------------------
fromeskyimportbdist_esky
fromsetuptoolsimportsetup
#Commonsettings
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,
)
#Buildtheappandtheeskybundle
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、编译完整包
pythonsetup.pybdist_esky
编译之后会在dist目录生成名为wxImageViewer-1.0.win-amd64.zip的打包文件,注意这个文件名本身就包含了版本信息:
1)、wxImageViewer是应用名,对应setup.py中的name定义
2)、1.0是版本号,对应setup.py中version定义
3)、amd64代表64位编译版本,跟python的版本一致。
2、编译增量补丁包
pythonsetup.pybdist_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
#---------------------------------------------------------------------------
''''''
Createby:joshuazou2016.10.08
Purpose:调用esky打包成执行文件,支持自动升级。
Example:pythonsetup.pybdist_esky/pythonsetup.pybdist_esky_patch
''''''
#---------------------------------------------------------------------------
fromeskyimportbdist_esky
fromsetuptoolsimportsetupimportdistutils
#Dependenciesareautomaticallydetected,butitmightneedfinetuning.
VER=''1.16.1102.1''#Commonsettings
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,
)
#Buildtheappandtheeskybundle
setup(name=NAME,
version=VER,
scripts=APP,
data_files=DATA_FILES,
options=dict(bdist_esky=ESKY_OPTIONS),
)
复制代码
|
|