配色: 字号:
基于esky实现python应用的自动升级
2016-11-04 | 阅:  转:  |  分享 
  
基于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),

)

复制代码

献花(0)
+1
(本文系thedust79首藏)