Original link: https://fugary.com/?p=516
Recently, a GUI
tool written in Python
of the company needs to be distributed to the company’s employees. Currently, the single-file packaging mode is used to generate an exe
file (only for Windows
). After packaging, some dependent files and folders need to be distributed together. Currently Distribution and subsequent automatic updates are not very convenient, so a packaging script was written in Python
to simplify the packaging operation.
The main function
- Packaged into a single
exe
file usingPyInstaller
-
exe
needs to supportWindows
properties to view version information - Package
exe
file and dependent files and folders into azip
file - Support checking for version updates after release
PyInstaller packaging
PyInstaller
documentation: https://pyinstaller.org/en/stable/usage.html
At present, the command line is used to package through the pyinstaller
command. To integrate the packaging function into the build.py
script, it is best to use Python
code to achieve packaging.
command line packaging
Note: If there is no pyinstaller
command after installation, it may be a non-administrator user, and there is no [run as administrator] pip
installation command
Command line packaging ( -F
is packaged into a single file, -w
is run in window mode):
pip install pyinstaller pyinstaller -F -w app.py
code packaging
To use Python
code packaging, you need to import the __main__
module of PyInstaller
, and then use __main__
run
method. The parameter passing is passed in the form of an array, which is more convenient
Here, config.py
file is used to configure the icon of the software and version
file of Windows
, and how to obtain the version
file of Windows
will be introduced later
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'])
In this way, the build.py
file is packaged into a single file exe
version management
To implement version management and version checking and updating, a versions.py
file is used here to record the version to be released, and then a versions.json
file is generated and released to the server together with zip
file.
Under normal circumstances, version management can be placed in the database, but because the tool is relatively small, there is no need to make it so complicated, so use versions.py
file to manage the version list, and add a release version record every time you release.
version list
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']
Generate versions.json
A json
file is generated through release_list
information in versions.py
, and this file is finally published to the Nginx
service for checking the version
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)
The generated versions.json
example can be accessed through URL
on the Nginx
server
[ { "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 version file
In order to make the exe
file look more formal, you can consider adding icons and Windows
version files. After packaging, there will be information such as copyright.
What is a version file
Usually in Windows system, an exe
file can see the author, version, copyright and other information of the software through the properties. If the exe
file packaged by ourselves with PyInstaller
does not specify –version-file
, there is no such information in the properties, such as picture:
Grab version information
After installing PyInstaller
, there will be a pyi-grab_version.exe
file in Scripts
under Python
installation directory, which can grab the version information of other third-party exe
(the Thunder I use here), and generate a file_version_info.txt
file, Based on this version file, we can modify it to our own version file
pyi-grab_version.exe "C:\softs\Thunder Network\Thunder\Program\Thunder.exe"
The version file is as follows. If you fail to generate it yourself, you can directly use the file information posted here to modify it:
# 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])]) ] )
write automatically
After modifying the basic information, you can replace the version information with regular expressions when packaging, and automatically write the new version information into 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)
package zip
app.exe
generated by packaging with PyInstaller
is already in dist
directory. We need to package app.exe
and dependent files or folders into a zip
file, and put it in the dist
directory, and package the code:
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打包相关文件
Just write the files to be packaged into the ZIP_FILES
list.
The complete steps are as follows:
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
is mainly to call the method of each step in order to realize the whole packaging process
Check for updates
Check for version updates, here use semver
to determine whether there is a new version:
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('已经是最新版本')
This implements checking for version updates.
This article is transferred from: https://fugary.com/?p=516
This site is only for collection, and the copyright belongs to the original author.