最近公司的一个Python
写的GUI
工具需要分发给公司员工使用,目前使用单文件打包模式,生成一个exe
文件(仅Windows
下使用),打包之后还有部分依赖的文件和文件夹需要一起分发,目前分发以及后面的自动更新不是很方便,因此使用Python
再写了一个打包脚本简化打包操作。
主要功能
- 使用
PyInstaller
打包成单个exe
文件 exe
要支持Windows
的属性查看版本信息- 把
exe
文件和依赖文件和文件夹打包成一个zip
文件 - 发布后支持检查版本更新
PyInstaller打包
PyInstaller
文档:https://pyinstaller.org/en/stable/usage.html
目前使用命令行通过pyinstaller
命令打包,要把打包功能集成到build.py
脚本中,最好使用Python
代码实现打包
命令行打包
注意:如果安装后没有pyinstaller
命令,可能是非管理员用户,又没有【以管理员身份运行】pip
安装命令
命令行打包(-F
是打包成单文件,-w
是以窗口模式运行):
pip install pyinstaller
pyinstaller -F -w app.py
代码打包
使用Python
代码打包,需要导入PyInstaller
的__main__
模块,然后用__main__
的run
方法,参数传递是以数组的形式传递,比较方便
这里使用config.py
文件配置软件的图标以及Windows
的version
文件,后面会介绍如何获取到Windows
的version
文件
from PyInstaller import __main__
def pyinstaller_package():
# 使用pyinstaller打包
__main__.run(['-F', '-w', f'--icon={config.ICO_FILE}', f'--version-file={config.VERSION_FILE_TXT}', 'app.py'])
这样就实现了用build.py
文件打包成单个文件exe
版本管理
要实现版本管理以及版本检查更新,这里使用一个versions.py
文件记录需要发布的版本,然后生成一个versions.json
文件,和zip
文件一起发布到服务器上。

一般情况下版本管理可以放到数据库,但是因为工具比较小,没有必要搞那么复杂,因此用versions.py
文件管理版本列表,每次发布的时候新增一条发布版本记录即可。
版本列表
import config
release_list = [{
"version": "1.0.1",
"publishDate": "2023-07-07 10:00",
"forceUpdate": True,
"publishNotes": ["1. 版本更新", "2. 版本1.0.1", "3. 更新内容:xxxx"],
"updateUrl": f'{config.INTERNAL_DOWNLOAD_URL}/app.1.0.1.20230707.zip'
}, {
"version": "1.0.0",
"publishDate": "2023-07-05 17:00",
"forceUpdate": True,
"publishNotes": ["1. 初始版本", "2. 生成1.0.0", "3. 更新内容:xxxx"],
"updateUrl": f'{config.INTERNAL_DOWNLOAD_URL}/app.1.0.0.20230705.zip'
}]
current_version = release_list[0]['version']
生成versions.json
通过versions.py
里面的release_list
信息生成一个json
文件,这个文件最后发布到Nginx
服务,用于检查版本
import json
import os
from versions import release_list, current_version
def generate_version_json(dist_path):
versions_json = json.dumps(release_list, indent=4, ensure_ascii=False)
if not os.path.exists(dist_path):
os.mkdir(dist_path)
with open(os.path.join(dist_path, 'versions.json'), 'w', encoding="utf8") as json_file:
json_file.write(versions_json)
生成的versions.json
示例,放到Nginx
服务器上能够通过URL
访问到就可以
[
{
"version": "1.0.1",
"publishDate": "2023-07-07 10:00",
"forceUpdate": true,
"publishNotes": [
"1. 版本更新",
"2. 版本1.0.1",
"3. 更新内容:xxxx"
],
"updateUrl": "https://xxxxx/app.1.0.1.xxxx.zip"
},
{
"version": "1.0.0",
"publishDate": "2023-07-05 17:00",
"forceUpdate": true,
"publishNotes": [
"1. 初始版本",
"2. 生成1.0.0",
"3. 更新内容:xxxx"
],
"updateUrl": "https://xxxx/app.1.0.0.xxxx.zip"
}
]
Windows版本文件
为了使exe
文件看起来比较正式,可以考虑增加图标以及Windows
的版本文件,打包后就有版权等信息了。
什么是版本文件
通常在Windows系统下,一个exe
文件通过属性可以看到软件的作者、版本、版权等等信息,我们自己用PyInstaller
打包的exe
文件如果没有指定–version-file
的话,属性里面没有这样的信息,如图:

抓取版本信息
安装好PyInstaller
之后在Python
的安装目录下面的Scripts
中会有一个pyi-grab_version.exe
文件,可以抓取其他第三方的exe
(我这里使用的迅雷)的版本信息,并生成一个file_version_info.txt
文件,我们可以这个版本文件为基础,修改为自己的版本文件
pyi-grab_version.exe "C:\softs\Thunder Network\Thunder\Program\Thunder.exe"
版本文件如下,如果自己生成失败,可以直接用这里贴出来的文件信息做修改:
# UTF-8
#
# For more details about fixed file info 'ffi' see:
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
VSVersionInfo(
ffi=FixedFileInfo(
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
# Set not needed items to zero 0.
filevers=(11, 4, 7, 2104),
prodvers=(11, 4, 7, 2104),
# Contains a bitmask that specifies the valid bits 'flags'r
mask=0x3f,
# Contains a bitmask that specifies the Boolean attributes of the file.
flags=0x0,
# The operating system for which this file was designed.
# 0x4 - NT and there is no need to change it.
OS=0x40004,
# The general type of file.
# 0x1 - the file is an application.
fileType=0x1,
# The function of the file.
# 0x0 - the function is not defined for this fileType
subtype=0x0,
# Creation date and time stamp.
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
'080404b0',
[StringStruct('CompanyName', '深圳市迅雷网络技术有限公司'),
StringStruct('FileDescription', '迅雷11'),
StringStruct('FileVersion', '11,4,7,2104'),
StringStruct('InternalName', 'Thunder 2'),
StringStruct('LegalCopyright', '版权所有 (C) 2023 深圳市迅雷网络技术有限公司'),
StringStruct('OriginalFilename', 'Thunder'),
StringStruct('ProductName', '迅雷11'),
StringStruct('ProductVersion', '11.4.7.2104'),
StringStruct('LegalTrademarks', '迅雷11'),
StringStruct('SpecialBuild', '100017')])
]),
VarFileInfo([VarStruct('Translation', [2052, 1200])])
]
)
自动写入
修改好基本信息之后,可以在打包的时候用正则表达式替换版本信息,自动实现新版本信息写入file_version_info.txt
def process_version_info():
ver = current_version.split('.')
with open('file_version_info.txt', 'r+', encoding='utf8') as ver_file:
txt = ver_file.read()
txt = re.sub('\\(\\d+, \\d+, \\d+, 0\\),', f'({ver[0]}, {ver[1]}, {ver[2]}, 0),', txt)
txt = re.sub("u'\\d+\\.\\d+\\.\\d+\\.0'", f"u'{current_version}.0'", txt)
txt = re.sub("\\(u'FileDescription', u'.+'\\)", f"(u'FileDescription', u'{config.PRODUCT_NAME}')", txt)
txt = re.sub("\\(u'ProductName', u'.+'\\)", f"(u'ProductName', u'{config.PRODUCT_NAME}')", txt)
ver_file.seek(0)
ver_file.truncate()
ver_file.write(txt)
打包zip
通过PyInstaller
打包生成的app.exe
已经在dist
目录,我们需要把app.exe
和依赖的文件或文件夹打包成zip
文件,并放到dist
目录,打包代码:
import os
import shutil
import time
import zipfile
OUT_PATH = 'dist' # 输出路径
ZIP_FILES = ['files', 'resources', 'dist\\app.exe', 'data.xlsx'] # 压缩包需要文件
CLEAN_PATH = ['dist', 'build'] # 清理路径
def zip_dir_list(input_path_list: list, output_file):
with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as output_zip:
for input_path in input_path_list:
if os.path.isdir(input_path):
zip_dir(input_path, output_zip)
elif os.path.isfile(input_path):
filename = input_path.split(os.sep)[-1]
print('zip adding file %s' % input_path)
output_zip.write(input_path, filename)
def zip_dir(input_path, output_zip):
for path, dir_names, file_names in os.walk(input_path):
for filename in file_names:
full_path = os.path.join(path, filename)
print('zip adding file %s' % full_path)
# 文件路径,压缩路径
output_zip.write(full_path)
if __name__ == '__main__':
date_str = time.strftime('%Y%m%d')
zip_dir_list(ZIP_FILES, f'{OUT_PATH}/app.{current_version}.{date_str}.zip') # zip打包相关文件
把需要打包的文件写到ZIP_FILES
列表中即可。
完整步骤如下:
def clean_last_build():
# 清理上次文件
for c_path in CLEAN_PATH:
if os.path.exists(c_path):
print('clean path %s' % c_path)
shutil.rmtree(c_path)
if __name__ == '__main__':
clean_last_build() # 清理上次构建目录
generate_version_json(OUT_PATH) # 生成版本文件
process_version_info() # 处理windows版本生成文件
pyinstaller_package() # 调用pyinstaller打包
date_str = time.strftime('%Y%m%d')
zip_dir_list(ZIP_FILES, f'{OUT_PATH}/app.{current_version}.{date_str}.zip') # zip打包相关文件
print('打包完成!')
build.py
主要就是按照顺序调用各个步骤的方法实现整个打包过程
检查更新
检查版本更新,这里使用semver
判断是否有新版本:
import requests
import semver
class Updater:
def __init__(self, base_url: str):
self.base_url = base_url
def check_for_update(self, version):
check_url = self.base_url + '/versions.json'
res = requests.get(check_url).json()
if len(res) and semver.compare(res[0]['version'], version) > 0:
return res[0]
if __name__ == '__main__':
updater = Updater('https://xxxx/app')
result = updater.check_for_update('1.0.0')
if result is not None:
print('有新版本:', result)
else:
print('已经是最新版本')
这样就实现了检查版本更新。