HaujetZhao / PyInstaller-Perfect-Build-Method

如果我要写一个 Python 项目,打包成 exe 运行(方便在没有 Python 的电脑上使用),我需要打包出的根目录结构美观,没有多余的、杂乱的依赖文件在那里碍眼,而且需要在发现 bug 时,我还需要能够修改里面的代码后,无需再次打包,就能正常运行,该怎么做呢? 就以一个 Hello 项目为例,记一下我找到的完美方法。
97 stars 24 forks source link

分享 pyInstaller 6.3.0 打包成功经验 #6

Open Suvan-L opened 10 months ago

Suvan-L commented 10 months ago

image

1. 整体思路(start_tcp_server_19099.py 是主启动文件)

cd E:\gitee-procjet\iot-automatic-pet-feeding\python\wxhelper\py\release

1. 进入项目目录,执行 pyi-makespec .\start_tcp_server_19099.py 
   - 此处会生成 spec 文件,我们后续将进行修改
   - 【注意】不要加 -F,表示所有依赖都将拷贝到 dist 目录下

2. 参考我的 start_tcp_server_19099.spec 文件
    1. 设置 exe 程序名称
    2. 主动脚本拷贝配置文件目录
    3. 指定需要手动添加依赖库的文件
      - 解决打包成功,但执行 exe 失败问题,例如:FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\liush\\AppData\\Local\\Temp\\_MEI214962\\borax\\calendars\\FestivalData.csv'
    4. 声明项目内所有 python 脚本

3. 执行打包脚本 pyinstaller .\start_tcp_server_19099.spec 
  - 中途可能要输入 y

4. 启动程序  .\dist\start_tcp_server_19099\start_tcp_server_19099.exe

2. 分享 spec 文件

# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.building.api import PYZ, EXE, COLLECT
from PyInstaller.building.build_main import Analysis

import shutil
import os

# 1. 设置 exe 程序名称
exeName = "start_tcp_server_19099"

# 2. 主动脚本拷贝配置文件目录
# (因为 datas 参数会将资源文件都拷贝到 dist/_internal/ 路径下,有些程序的搜索路径会不行,所以此处进行脚本拷贝)
copyDirectory_list = [
    ("config_file", 'dist\\start_tcp_server_19099\\config_file')
]

# 3. 指定需要手动添加依赖库的文件
# (第三方库含有静态文件,需要在下面目录,编写 hook-xxxx.py 主动添加依赖)
hookspath = "PyInstaller-hookspath\\"
if not os.path.exists(hookspath):
    # 判断是否存在文件夹如果不存在则创建为文件夹
    os.makedirs(hookspath)

# 导入模块名称
importModuleName_list = [
    "borax"
]

# 生成文件,这里是固定模板的,所以直接代码生成
for moduleName in importModuleName_list:
    fileName = hookspath + "hook-" + moduleName + ".py"
    if os.path.exists(fileName):
        print(fileName, " 文件已存在,不重新创建")
        continue

    # 追加写入内容到文件
    with open(fileName, "a") as file:
        file.write("from PyInstaller.utils.hooks import collect_data_files\n")
        file.write("datas = collect_data_files('" + moduleName + "')\n")

    print("已生成 ", fileName, " 文件!")

# ==================新建 a 变量,分析脚本============================
# 4. 声明项目内所有 python 脚本
a = Analysis(
    [
        'config.py',
        'daily_schedule_task.py',
        'file_db.py',
        'start_tcp_server_19099.py',
        'type_1_handler.py',
        'type_3_handler.py',
        'type_43_handler.py',
        'type_49_handler.py',
        'custom_http/__init__.py',
        'custom_http/http_ali_ai.py',
        'custom_http/http_aliCloudDiskSearch_api.py',
        'custom_http/http_chatgpt_ai.py',
        'custom_http/http_eastmoney_api.py',
        'custom_http/http_gaode_weather_api.py',
        'custom_http/http_iflytek_ai.py',
        'custom_http/http_iflytek_translation.py',
        'custom_http/http_iot_api.py',
        'custom_http/http_qqMusicDownload_api.py',
        'custom_http/http_wxHelper_client.py',
        'custom_http/util_borax.py',
        'custom_http/util_cnlunar.py',
        'custom_http/util_dailyMusicXmlParse.py',
        'custom_http/util_efinance.py'
    ],
    pathex=[],
    binaries=[],
    datas=[
        # 写明所有需要拷贝的静态资源文件(来源路径,目标路的地址)
        # 注意会全部拷贝到 dist/_internal 路径下所以项目中相对路径可能会有问题
        # 所以建议使用上面的  shutil.copytree 进行脚本拷贝
        # ('config_file\\*', 'config_file')
    ],
    hiddenimports=[],
    # 针对第三方库,内部含有文件的需要手动添加
    hookspath=[hookspath],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    noarchive=False,
)

# ========================为 a 生成 exe =========================
pyz = PYZ(a.pure)

exe = EXE(
    pyz,
    a.scripts,
    [],
    exclude_binaries=True,
    name=exeName,
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    console=True,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)
coll = COLLECT(
    exe,
    a.binaries,
    a.datas,
    strip=False,
    upx=True,
    upx_exclude=[],
    name=exeName,
)

# 放到最后,在生成玩 exe 后再执行
for directory in copyDirectory_list:
    src = directory[0]
    target = directory[1]

    if os.path.exists(target):
        print("目标路径已存在,先执行删除 ", target)
        shutil.rmtree(target)

    # 拷贝目录
    shutil.copytree(src, target)
    print("成功拷贝路径 ", src, " 到 ", target)

3. 最终效果

image

4. 可以写个 bat 脚本,用于自动编译

rem 当遇到确认的时候,自动输入 y
echo y |pyinstaller .\start_tcp_server_19099.spec
HaujetZhao commented 10 months ago

6.0 进化了很多,主要是所有的打包文件最后都会进入 _internal 文件,有些路径也会改变到 _internal 里。只是我懒得更新文章了。你可以参考下这个我打包脚本:https://github.com/HaujetZhao/CapsWriter-Offline/blob/master/build.spec