skywind3000/PyStand 是一个非常好用的Python独立部署环境,使用一个小巧的exe作为启动器拉起Python运行。可让Python代码在一台没有安装任何环境的机器上跑起来,极大方便了Python程序的打包和发布。
本项目基于原项目做了一些改动,在README中记录了一些我的经验和遇到的坑,并附带了一套使用 VS Code 开发内嵌式Python的解决方法(拥有完整的语法提示和调试功能)。
使用案例: Umi-OCR
本文可以指导小白通过PyStand部署项目,自定义项目名称和路径,享受愉快的Python开发体验。完事了可以轻松打包带走。不会C++或者Cmake也木有关系,这里提供了一键编译脚本。
不妨先看看 原项目 的Readme。
下面将详细介绍本项目的特点和使用方法。
在原版PyStand中,运行环境初始化是由内嵌在cpp中的一段Python代码字符串负责,这个字符串会作为-c
启动参数传给Python解释器。所以如果想自己设置运行环境会比较麻烦,要重新编译exe。
为了便于自定义和修改环境配置(比如指定包的路径),我将环境初始化这部分工作转移到外部的启动脚本.py中进行。同时,这样可以兼顾使用 VS Code 在完全相同的环境中进行开发和调试。
在原版PyStand中,启动脚本是与exe同级的 PyStand.int
或同名int文件。Python解释器路径则默认是runtime
目录。
而在我的版本中,需要在 PyStand.cpp
开头指定启动脚本和Python解释器路径:
PYSTAND_STATIC_PATH
:启动脚本路径PYSTAND_RUNTIME_PATH
:运行环境目录(即Python解释器的目录)
例:
#define PYSTAND_STATIC_PATH L"\\AppData\\main.py"
#define PYSTAND_RUNTIME_PATH "\\AppData\\.runtime"
上面这个例子中,我把解释器和脚本统统都放在 AppData
这个内容目录中,这样可以做到程序根目录下只有两个文件(夹),更加简洁优雅。
建议将内容目录改成你项目的名字便于用户区分。比如项目叫 MyApp ,那么文件夹可以叫 MyApp-data
。
示例,可以是这样的结构(假设不封包):
MyApp
├─ MyApp-data
│ ├─ .runtime
│ │ └─ Python解释器扔在这~
│ ├─ .site-packages
│ │ ├─ PyQt放在这~
│ │ └─ 其他Python模块也丢在这~
│ ├─ app
│ │ └─ 程序逻辑
│ ├─ assets
│ │ └─ 资源文件
│ ├─ ui
│ │ └─ 界面文件
│ ├─ utils
│ │ └─ 通用工具
│ └─ main.py
└─ MyApp.exe
当然,如果考虑封包和加密的话则可能不适用这种目录结构,可以参考原作者的 文章 。
配置好自己的启动脚本及解释器路径后就可以编译生成exe了,编译时可以指定生成32位还是64位程序。32位程序只能使用32位的Python解释器及模块(PyQt),64位同理。
提示:QT6(PySide6)只支持64位Win10以上系统,不支持32位系统或者64位Win7。
- 如果要用QT6,那么也必须编译64位PyStand。
- 如果要兼容Win7,那么必须使用QT5。可配合32位或64位PyStand。
- 安装Cmake、VS 2019。
- 替换
appicon.ico
为你的程序图标。
运行 build.bat
即可,会自动编译为32位程序。
这个脚本将在build/Release
目录下生成 PyStand.exe
,然后将exe拷贝到本目录下,重命名为与bat同名(build.exe
)。
如果你想修改生成exe的名称,则改动bat的文件名即可,如改为 我的程序.bat
。
在项目目录中打开cmd
构建工程,可自己调整参数或指定编译器
# 创建 build 子目录,用于存储构建过程中生成的临时文件
mkdir build
# 指定构建目录为build,生成器为VS2019,生成32位exe,当前目录为根目录
cmake -B build -G "Visual Studio 16 2019" -A Win32 .
编译
# 生成到 build/Release 目录下
cmake --build build --config Release
完成以上步骤后,就可以抛开跟c++有关的东西,专注于我们的Python了。
后续的步骤也可以转移到一台空白的机器上进行,不需要安装VS或Python。
比如可以在模拟生产环境/用户日常使用的环境中进行,以便及时发现兼容性问题,补充必要的运行库,及手动裁切Python模块。
以32位为例:
- 打开官网 https://www.python.org/downloads/windows/
- Ctrl+F,搜索
Python 3.8.10
Download Windows embeddable package (32-bit)
下载32位嵌入式包- 最终得到一个
python-3.8.10-embed-win32.zip
。
- 在合适的位置创建你Python项目的文件夹。(小技巧:为了保证日后的兼容性,可以特意在中文及含空格目录中开发,以便及时发现和处理路径兼容性问题。如
E:/工程/My App/
。) - 将之前编译得到的
PyStand.exe
复制过去。直接运行,应该会弹窗报错无法找到Python解释器……
,就正常。 - 创建目录:
AppData/.runtime
,将刚下载好的Python嵌入式包解压进入。再创建一个.site-packages
预留给未来放置第三方库。形成这样的结构:再运行PyStand,会弹窗报错My Project ├─ AppData │ ├─ .site-packages │ └─ .runtime │ ├─ python38.dll │ ├─ python38.zip │ ├─ pythonw.exe │ ├─ ……一堆东西 └─ PyStand.exe
无法找到启动脚本……
,就正常。 - 在
AppData
目录下写一个简单的main.py
:直接运行,会展示弹窗import os import sys # 重定向输出流到控制台窗口 try: fd = os.open('CONOUT$', os.O_RDWR | os.O_BINARY) fp = os.fdopen(fd, 'w') sys.stdout = fp sys.stderr = fp except Exception as e: fp = open(os.devnull, 'w') sys.stdout = fp sys.stderr = fp # 向控制台输出 print("Cmd: Hello PyStand!") # 定义一个消息弹窗函数 def MessageBox(msg, info='Message'): import ctypes ctypes.windll.user32.MessageBoxW(None, str(msg), str(info), 0) return 0 # 展示弹窗 MessageBox("Msg: Hello PyStand!")
Msg: Hello PyStand!
。如果在控制台中运行,还会在控制台打印Cmd: Hello PyStand!
。
由于我个人喜欢 VS Code ,就以它为例。如果你习惯用PyCharm或别的编辑器,应该也是可以的。
- 打开官网: https://code.visualstudio.com/Download
- 下载 System Installer x64
- 安装过程中注意:建议勾选
将“通过Code打开”操作添加到Windows资源管理器文件(目录)上下文菜单
1.70.3
版是最后一个支持win7的版本,请在下述地址下载。(选Windows System)- https://code.visualstudio.com/updates/v1_70
- 打开 VS Code ,按
Ctrl+Shift+X
打开插件商店,搜索并安装Python
。这是辅助语法高亮的插件。 - 如果有需要,搜索并安装
Chinese (Simplified)
语言插件。 - 关闭 VS Code 。
- 在上文创建好的项目目录(假设是
My Project
)中,将本项目的VSCode Environment
中的.vscode
目录复制过去。形成这样的结构:My Project ├─ .vscode │ ├─ launch.json │ └─ settings.json ├─ AppData │ └─ ...... └─ PyStand.exe
- 文件管理器在
My Project
中,右键 → 通过Code打开。 - VS Code中,点左上角
文件
→将工作区另存为
,保存到.vscode
目录中或者任意你喜欢的目录。以后你再次打开VS Code时,就可以直接在欢迎页面打开这个工作区了。 - 修改一下
.vscode
目录中两个json配置文件,将启动脚本、解释器和第三方库的路径改为你自己的。需要修改的地方,已经用//【改】……
的注释标出来了。其他条目就不要乱动。 - 点F5调试程序。如果有弹窗
Msg: Hello PyStand!
,且VS Code内部的控制台打印了Cmd: Hello PyStand!
,就说明VS Code开发环境已经配置好了。
若你的机器上已经安装过常规版本Python可以跳过这一部分,用常规Python即可。
嵌入式Python默认是不带pip的。PyStand也不鼓励通过pip来安装模块,我们推荐手动配置和维护第三方模块。
不过,我们仍需要通过pip来方便下载第三方模块,为此,需要在嵌入式Python环境中安装pip。请参考以下步骤。
(记得挂好科学上网。)
- 为了不影响原始环境,我们可以复制一份环境(
.runtime
文件夹)放在另外的目录下,命名为runpip
。这份环境专门用来下载模块,与你的项目本体并不关联。 - 安装pip,在控制台中执行下列操作:(建议在VS Code内置控制台进行)
如果最后输出类似
# 下载pip安装脚本到当前目录下 curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py # 用runpip环境中的Python解释器运行该脚本 runpip/python.exe get-pip.py
Successfully installed pip-xxx
说明安装成功了。 - 添加路径:打开
runpip/python38._pth
,把#import site
前面的#删掉,变成import site
。保存。
以 PySide2 为例,讲解如何下载、安装和使用模块。
- 通过pip下载PySide2包(而不安装)。注意平台是win32。需要下载别的模块的话,将指令最后的PySide2改为你需要的包名。
runpip/python.exe -m pip download --only-binary=:all: --platform win32 PySide2
- 在runpip的同级目录就可以看到下载好了两个包。用压缩软件(如7z,winrar)可以直接打开它们,或者后缀改为.zip也能打开。
├─ PySide2-5.15.2.1-5.15.2-cp35.cp36.cp37.cp38.cp39.cp310-none-win32.whl └─ shiboken2-5.15.2.1-5.15.2-cp35.cp36.cp37.cp38.cp39.cp310-none-win32.whl
- 在你的项目的AppData下(即
.runtime
同级目录)新建一个文件夹.site-packages
,将以上两个包的本体解压进去。 (比如PySide2
是本体,必须导入;PySide2-5.15.2.1.dist-info
则不需要。shiboken2
也是同理。)
将 main.py 的内容全删了,粘贴以下内容:
点击展开
def initRuntimeEnvironment(startup_script):
"""初始化运行环境。startup_script: 启动脚本路径"""
import os
import sys
import site
# 重定向输出流到控制台窗口
try:
fd = os.open('CONOUT$', os.O_RDWR | os.O_BINARY)
fp = os.fdopen(fd, 'w')
sys.stdout = fp
sys.stderr = fp
except Exception as e:
fp = open(os.devnull, 'w')
sys.stdout = fp
sys.stderr = fp
# 定义一个最简单的消息弹窗
def MessageBox(msg, info='Message'):
import ctypes
ctypes.windll.user32.MessageBoxW(None, str(msg), str(info), 0)
return 0
os.MessageBox = MessageBox
# 初始化工作目录和Python搜索路径
script = os.path.abspath(startup_script) # 启动脚本.py的路径
home = os.path.dirname(script) # 工作目录
os.chdir(home) # 重新设定工作目录(不在最顶层,而在UmiOCR-data文件夹下)
for n in ['.', '.site-packages']: # 将模块目录添加到 Python 搜索路径中
path = os.path.abspath(os.path.join(home, n))
if os.path.exists(path):
site.addsitedir(path)
# 初始化Qt搜索路径,采用相对路径,避免中文路径编码问题
try:
from PySide2.QtCore import QCoreApplication
QCoreApplication.addLibraryPath('./.site-packages/PySide2/plugins')
except Exception as e:
print(e)
os.MessageBox(f'addLibraryPath fail!\n\n{e}')
os._exit(0)
print('Init runtime environment complete!')
if __name__ == '__main__':
initRuntimeEnvironment(__file__) # 初始化运行环境
在VS Code编辑器中,如果 from PySide2.QtCore import QCoreApplication
这一句有语法高亮,没有波浪线,说明VS Code已经识别了第三方库。
按F5运行一下,如果输出Init runtime environment complete!
,说明第三方库PySide2已经成功导入。
上述代码连带解决了PySide(PyQt同理)在中文路径下无法使用的问题。如果你不需要导入Qt,则将代码中“初始化Qt搜索路径
”那段 try……except…… 删掉即可。
一个非常重要的事项:上述代码将程序的工作目录修改为了当前所在的路径(即main.py
同级目录),而不是PyStand.exe所在的目录。如果你需要在代码中使用相对路径,要特别注意这一点。
在 AppData
下编写你项目的正式代码。举个例子,假设直接创建一个 app.py
:
from PySide2.QtWidgets import QApplication, QLabel
app = QApplication([])
label = QLabel('Hello World!')
label.setFixedSize(800, 500)
label.show()
app.exec_()
然后,在初始化脚本 main.py
的最后来引用它:
if __name__ == '__main__':
initRuntimeEnvironment(__file__) # 初始化运行环境
# 进入程序正式入口
import app
按F5运行一下试试,窗口显示出来了。
如果在中文路径运行qml,除了需要在main.py中初始化Qt搜索路径,还需要在创建qml引擎时初始化qml库路径。如下:
app.py
import os
import sys
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.addImportPath("./.site-packages/PySide2/qml") # 相对路径重新导入包
current_dir = os.path.dirname(os.path.abspath(__file__)) # 同级目录
engine.load(f"{current_dir}/app.qml") # 启动同级目录下的app.qml
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())
app.qml
import QtQuick 2.15
import QtQuick.Window 2.15
Window {
visible: true
width: 800
height: 500
title: "Hello World!"
Text {
text: "Hello World!"
anchors.centerIn: parent
}
}
为了给qml提供语法补全,可以在VS Code插件商店中搜索并安装 QML
、QML Snippets
等插件。
调试直接在VS Code中F5运行,支持断点等功能。发布则直接将代码连同exe压缩成一个文件即可,当然你也可以进一步封包和加密。
使用本方案的好处是调试与发布是完全相同的环境,VS Code调用了同一个Python解释器和第三方库文件,可以在开发阶段就检查出兼容性问题。
待填坑…………