背景
之前在打包自动化工具的时候, 我用过 pyinstaller 和 py2exe, 主要遇到了以下这些问题:
-
依赖导致的体积过大 (我用到了 numpy, opencv-python, pyside2 等第三方库)
-
依赖缺失 (可能跟我没有配置好 hidden-import 参数有关, 一些第三方包隐式调用了其他包, 修复这个问题仍然让人头疼)
-
路径错位, 导致我自己的模块找不到包的位置, 以及相对路径也容易出错
-
如何在打包中加入非代码类的资源文件
有一个比较典型的场景是, 我的程序会在 data 目录生成一些数据. 在打包前, data 目录有很多缓存数据 (这些数据我还用得到, 不能删), 但我又想把 data 目录和它的子目录仅作为 "空文件夹" 包含到打包工具内, 为此折腾了很久.
-
怎么去做增量更新. 如果用户之前已经安装过 1.0.0 版, 我新发布了 1.0.1 版, 第三方依赖都没有变, 如果还把依赖打包进去, 体积增加得有些多余
每个问题解决起来都很不容易, 另外 pyinstaller 的参数也值得人多多研究 (网上有很多深入讲解了)... 那么有没有 "懒人专用" 的一键式打包工具呢? 这就是本文要介绍的 python 库: pyportable-installer.
PyPortable Installer
pyportable-installer
是一个 Python 项目打包工具, 它受启发于 poetry, 并旨在作为 pyinstaller 的替代品出现.
pyportable-installer
通过一个 all-in-one 配置文件 来管理打包工作, 通过该文件可将您的 Python 项目打包为 "免安装版" 的软件, 用户无需安装 Python 程序或第三方依赖 (注: 该特性需要您在配置中启用虚拟环境选项), 真正做到 "开箱即用, 双击启动".
特性
pyportable-installer
具有以下特点:
-
打包后的体积很小. 在不附带虚拟环境的情况下, 与您的源代码同等量级 (这通常只有几百 KB)
-
易于使用. 您只需要维护一个 pyproject.json 配置文件即可. 在快速迭代的环境下, 您甚至只需要更改版本号就能立即生成新的打包结果
-
打包速度快. 一个中小型项目在数秒间即可生成打包结果
-
源代码加密. 使用 pyarmor 库 对源代码进行混淆, 保障代码安全
-
开箱即用.
pyportable-installer
打包后的目录结构非常清晰, 如下示例:my_project |= dist |= hello_world_0.1.0 |= checkup: 一些随附的检查工具 (可选) |- doctor.pyc |- update.pyc |- mainifest.json |= src: 您的源代码将被编译并放置在此目录下 |- ... |= lib: 一些自定义的第三方库会放在此目录下 |= pytransform: 用于运行加密后的源代码, 保障代码安全 |- __init__.py |- _pytransform.dll |= venv: 自带的虚拟环境 (可选) |- ... |- README.html: 自述文档 |- Hello World.exe: 双击即可启动!
-
不破坏相对路径. 在打包后的
~/src
目录下, 所有文件夹仍然维持着原项目的目录结构. 程序在启动时会将工作目录切换到启动脚本所在的目录, 这意味着您在原项目中启动所使用的相对路径, 在打包后仍然保持一致 -
无痛更新 (该特性将在后续版本提供). 双击软件目录下的
checkup/update.pyc
即可获取软件的最新版本 -
激活和授权 (该特性将在后续版本提供). 该特性由 pyarmor 提供,
pyportable-installer
将其同样整合在 all-in-one 配置文件中
工作流程
它的流程可以概括如下:
- 准备您要打包的项目
- 在项目的根目录下新建一个 all-in-one 配置文件: 'pyproject.json'
- 通过
pyportable-installer
处理此配置文件, 完成打包:
from pyportable_installer import full_build
full_build('pyproject.json')
pyportable-installer
会为您的项目生成:
-
加密后的源代码文件
- 加密后的文件后缀仍然是 '.py'
- 加密后的文件由
~/lib/pytransform
包在运行时解码 - 使用文本编辑器打开加密文件, 其密文如下所示:
-
一个 exe 格式的启动器
-
自定义的启动器图标 (注: 缺省图标为 python.ico)
-
一个干净的虚拟环境 (这是可选的)
-
整个打包后的结果会以文件夹的形式存在
之后, 您可以将该文件夹制作为压缩文件, 并作为 "免安装版" 的软件发布.
安装和使用
通过 pip 安装 pyportable-installer
:
pip install pyportable-installer
下面以一个 "Hello World" 项目为例, 介绍具体的打包工作:
假设 "Hello World" 的项目结构如下:
hello_world
|= data
|- names.txt ::
| Elena
| Lorez
| Mei
|= hello_world
|- main.py ::
| def say_hello(file):
| with open(file, 'r') as f:
| for name in f:
| print(f'Hello {name}!')
|
| if __name__ == '__main__':
| say_hello('../data/names.txt')
|- README.md
在项目根目录下新建 'pyproject.json' (这里 有一个模板文件可供使用), 填写以下内容:
{
"app_name": "Hello World",
"app_version": "0.1.0",
"description": "Say hello to everyone.",
"author": "Likianta <likianta@foxmail.com>",
"build": {
"proj_dir": "hello_world",
"dist_dir": "dist/{app_name_lower}_{app_version}",
"icon": "",
"target": {
"file": "hello_world/main.py",
"function": "say_hello",
"args": ["../data/names.txt"],
"kwargs": {}
},
"readme": "README.md",
"module_paths": [],
"attachments": {
"data": "assets"
},
"required": {
"python_version": "3.6",
"enable_venv": true,
"venv": ""
},
"enable_console": true
},
"note": ""
}
注: 更多用法请参考 Pyproject Template.
运行以下代码即可生成安装包:
from pyportable_installer import full_build
full_build('pyproject.json')
# 当增量更新时, 运行以下:
# from pyportable_installer import min_build
# min_build('pyproject.json')
# 如不需要加密源代码, 运行以下 (仅用于调试!):
# from pyportable_installer import debug_build
# debug_build('pyproject.json')
生成的安装包位于 hello_world/dist/hello_world_0.1.0
:
hello_world
|= dist
|= hello_world_0.1.0
|= checkup
|- doctor.pyc
|- update.pyc
|- manifest.json
|= src
|= data
|- names.txt
|= hello_world
|- main.py: 这是加密后的脚本, 与源文件同名
|- bootloader.py
|= lib
|= pytransform
|- __init__.py
|- _pytransform.dll
|= venv
|= site-packages
|- python.exe
|- ...
|- README.md
|- Hello World.exe: 双击启动
|- ...
注意事项
- 如果您启用了虚拟环境选项, 则安装路径不能包含中文, 否则会导致启动失败 (该问题可能与 Embed Python 解释器有关)
pyportable-installer
需要 Python 3.9 解释器
FAQ
运行报错: 没有找到 tkinter 库
这是因为您发布的项目中的虚拟环境使用的是嵌入式 Python, 而嵌入式 Python 并没有自带 tkinter 库.
解决方法: 请参考此文: 如何将 Tk 套件加入到嵌入式 Python 中.
pywin32, win32, win32clipboard 等相关问题
简答
将 venv/lib/site-packages/pywin32_system32
下的两个 dll 文件复制到 venv/lib/site-packages/win32/lib
即可.
详细操作
假设您的项目结构为:
my_project
|= assets
|= src
|- main.py
|= venv
|= lib
|= site-packages: 如果您的项目有用到 pywin32 库, 则会有以下目录
|= win32
|= win32com
|= win32comext
|= pywin32_system32
|= ...
|- pyproject.json
- 复制
venv/lib/site-packages/win32
到assets/win32
- 复制
venv/lib/site-packages/pywin32_system32
下的两个 dll 文件到assets/win32/lib
目录下 - 在
pyproject.json
添加以下内容 (重点见◆
符号处):
{
"app_name": "My Project",
"app_version": "...",
"description": "...",
"author": "...",
"build": {
"proj_dir": "src",
"dist_dir": "dist/{app_name_lower}_{app_version}",
"icon": "",
"target": {
"file": "src/main.py",
"function": "main",
"args": [],
"kwargs": {}
},
"readme": "",
"attachments": {
// ◆ 把 `assets/win32` 复制到打包目录的 lib 目录下
"assets/win32": "assets,dist_lib"
},
"module_paths": [
// ◆ 注意将 win32 以及 win32/lib 目录都加入到 python `sys.path` 中
"{dist_lib}/win32",
"{dist_lib}/win32/lib"
],
"required": {
"python_version": "3.9",
"enable_venv": true,
// ◆ 这里填您的项目自带的 venv 的目录路径
"venv": "./venv"
},
"enable_console": true
},
"note": ""
}