Python uses PyInstaller to package exe and dependencies and implement version management

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

  1. Packaged into a single exe file using PyInstaller
  2. exe needs to support Windows properties to view version information
  3. Package exe file and dependent files and folders into a zip file
  4. 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.

version management

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:

image-20230708113615921

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.