pylaunch
是一个基于嵌入式 Python(Embedded Python)的 Windows 启动器,支持将 Python 项目与运行时一起分发,无需在目标机器上全局安装 Python 即可独立运行。本教程面向希望了解其原理、使用方法和打包方式的开发者。
内附源码,参考自Pystand
项目特点
- 独立启动器:使用 C++ 编写的启动器程序,默认不显示命令行窗口(可通过参数配置无窗口模式)。
- 嵌入式 Python 支持:自动加载本地目录中的
python3.dll
(来自官方嵌入式 Python 发行版)。 - 环境隔离:通过环境变量和动态路径设置,构建项目的私有运行环境。
- 主脚本识别:默认查找并运行
app/main.py
,与 Python 脚本逻辑解耦。 - 自动路径配置:支持将
packages
和app
目录动态添加到sys.path
。
分发结构
以下是典型的打包分发的项目目录结构:
your_project/
├── app/
│ └── main.py # 启动入口脚本
├── packages/ # 可选:项目依赖(纯 Python 库)
├── runtime/ # 嵌入式 Python 运行时(解压官方 zip 包)
│ ├── python3.dll
│ └── ...
└── pylaunch.exe # 编译好的 C++ 启动器
启动器运行流程
- 参数解析:解析命令行参数,并支持将参数转发给 Python 脚本。
- 环境管理:
- 检测当前工作路径。
- 设置运行时路径(
runtime
目录)。 - 验证
python3.dll
是否存在。 - 设置环境变量(如
PYSTAND_HOME
、PYSTAND_RUNTIME
等)。
- 动态加载 Python DLL:
- 使用
LoadLibraryW
加载python3.dll
。 - 获取
Py_Main
函数指针。
- 使用
- 脚本检测:检查
app/main.py
是否存在,作为入口脚本。 - 执行 Python 逻辑:
- 通过
-c
参数构造启动脚本,调用 Python 内嵌代码。 - 修改
sys.path
和sys.argv
,执行目标脚本。
- 通过
程序源码
#include <filesystem>
#include <iostream>
#include <string>
#include <vector>
#include <windows.h>
namespace fs = std::filesystem;
// 解析命令行参数为宽字符串向量
class ArgParser {
public:
explicit ArgParser(std::vector<std::wstring> &outArgs) {
int argc;
LPWSTR *argvw = CommandLineToArgvW(GetCommandLineW(), &argc);
outArgs.assign(argvw, argvw + argc);
LocalFree(argvw);
}
};
// 管理环境设置:路径和环境变量
class EnvironmentManager {
public:
EnvironmentManager(const std::vector<std::wstring> &argv,
const std::wstring &runtimeSubdir)
: _argv(argv), _cwd(fs::current_path()), _pystand(GetExePath()),
_home(_pystand.parent_path()), _runtime(ResolveRuntime(runtimeSubdir)) {
ValidateRuntime();
SetEnvVars();
}
const fs::path &home() const { return _home; }
const std::vector<std::wstring> &argv() const { return _argv; }
private:
std::vector<std::wstring> _argv;
fs::path _cwd, _pystand, _home, _runtime;
// 获取当前可执行文件路径
fs::path GetExePath() {
wchar_t buf[MAX_PATH];
GetModuleFileNameW(NULL, buf, MAX_PATH);
return fs::path(buf);
}
// 解析运行时路径,支持绝对或相对路径
fs::path ResolveRuntime(const std::wstring &rtp) {
return fs::absolute((rtp.size() >= 3 && rtp[1] == L':') ? fs::path(rtp)
: _home / rtp);
}
// 验证Python运行时目录和python3.dll是否存在
void ValidateRuntime() {
if (!fs::exists(_runtime) || !fs::exists(_runtime / L"python3.dll")) {
std::wcerr << L"Missing embedded Python3 in: " << _runtime << L"\n";
std::exit(1);
}
}
// 设置Python运行时的环境变量
void SetEnvVars() {
SetEnvironmentVariableW(L"PYSTAND", _pystand.wstring().c_str());
SetEnvironmentVariableW(L"PYSTAND_HOME", _home.wstring().c_str());
SetEnvironmentVariableW(L"PYSTAND_RUNTIME", _runtime.wstring().c_str());
}
};
// 动态加载Python DLL并获取Py_Main函数
class DynamicLoader {
public:
using t_Py_Main = int (*)(int, wchar_t **);
explicit DynamicLoader(const fs::path &runtime) {
auto previous = fs::current_path();
fs::current_path(runtime);
SetDllDirectoryW(runtime.wstring().c_str());
auto dllPath = runtime / L"python3.dll";
_hDLL = LoadLibraryW(dllPath.wstring().c_str());
if (!_hDLL) {
std::wcerr << L"Cannot load python3.dll from: " << dllPath << L"\n";
std::exit(2);
}
_Py_Main = reinterpret_cast<t_Py_Main>(GetProcAddress(_hDLL, "Py_Main"));
if (!_Py_Main) {
std::wcerr << L"Cannot find Py_Main() in: " << dllPath << L"\n";
FreeLibrary(_hDLL);
std::exit(3);
}
fs::current_path(previous);
}
~DynamicLoader() {
if (_hDLL)
FreeLibrary(_hDLL);
}
t_Py_Main entry() const { return _Py_Main; }
private:
HMODULE _hDLL = nullptr;
t_Py_Main _Py_Main = nullptr;
};
// 在app目录下检测main.py脚本
class ScriptDetector {
public:
explicit ScriptDetector(const fs::path &home) : _home(home) {
auto scriptDir = _home / L"app";
auto candidate = scriptDir / L"main.py";
if (fs::exists(candidate)) {
_script = candidate;
SetEnvironmentVariableW(L"PYSTAND_SCRIPT", _script.wstring().c_str());
} else {
std::wcerr << L"Can't find script file: app/main.py\n";
std::exit(4);
}
}
const fs::path &path() const { return _script; }
private:
fs::path _home, _script;
};
// 使用加载的Py_Main函数执行Python脚本
class PyExecutor {
public:
PyExecutor(DynamicLoader::t_Py_Main entry,
const std::vector<std::wstring> &argv, const fs::path &script)
: _entry(entry), _origArgs(argv) {
BuildPyArgs(script);
}
int run() { return _entry(static_cast<int>(_pyArgs.size()), _pyArgs.data()); }
private:
DynamicLoader::t_Py_Main _entry;
std::vector<std::wstring> _origArgs;
std::vector<std::wstring> _pyArgStrs;
std::vector<wchar_t *> _pyArgs;
// 构建Py_Main的参数列表
void BuildPyArgs(const fs::path &script) {
_pyArgStrs.push_back(_origArgs[0]);
_pyArgStrs.insert(_pyArgStrs.end(),
{L"-I", L"-s", L"-S", L"-c", BuildInitScript(script)});
for (size_t i = 1; i < _origArgs.size(); ++i) {
_pyArgStrs.push_back(_origArgs[i]);
}
for (auto &s : _pyArgStrs) {
_pyArgs.push_back(const_cast<wchar_t *>(s.c_str()));
}
}
// 构造Python初始化脚本
std::wstring BuildInitScript(const fs::path &script) {
return LR"(
import sys, os, site
PYSTAND_HOME=os.environ['PYSTAND_HOME']
PYSTAND_SCRIPT=os.environ['PYSTAND_SCRIPT']
sys.path.insert(0, os.path.join(PYSTAND_HOME,'app'))
site.addsitedir(os.path.join(PYSTAND_HOME,'packages'))
sys.argv=[PYSTAND_SCRIPT]+sys.argv[1:]
code=compile(open(PYSTAND_SCRIPT,'rb').read(),PYSTAND_SCRIPT,'exec')
exec(code,{'__file__':PYSTAND_SCRIPT,'__name__':'__main__'})
)";
}
};
// 主程序入口
int wmain(int, wchar_t **) {
std::vector<std::wstring> argv;
ArgParser parser(argv);
EnvironmentManager env(argv, L"runtime");
DynamicLoader loader(env.home() / L"runtime");
ScriptDetector detector(env.home());
PyExecutor exec(loader.entry(), env.argv(), detector.path());
return exec.run();
}
源码编译
编译(使用 MinGW)
使用 MinGW 编译器生成轻量级的 Windows 原生可执行文件:
g++ pylaunch.cpp -o pylaunch.exe -municode -static-libstdc++ -static-libgcc -Os -s -flto -fno-exceptions -fno-rtti
若需 无窗口版本,可添加
-mwindows
选项:
g++ pylaunch.cpp -o pylaunch.exe -mwindows ...
添加应用图标(*.ico)
如果你希望为 pylaunch.exe
添加一个自定义图标(icon.ico
),请执行以下步骤:
创建资源文件 pylaunch.rc
内容如下(假设你的图标文件为 icon.ico
):
IDI_ICON1 ICON "icon.ico"
编译资源文件为 .o
windres pylaunch.rc -o pylaunch_res.o
windres
是 MinGW 自带的资源编译器。
编译可执行文件并链接资源
g++ pylaunch.cpp pylaunch_res.o -o pylaunch.exe -municode -static-libstdc++ -static-libgcc -Os -s -flto -fno-exceptions -fno-rtti
若使用无窗口模式:
g++ pylaunch.cpp pylaunch_res.o -o pylaunch.exe -mwindows ...
工作路径说明
pylaunch.exe
会将当前工作目录设置为其所在目录,所有相对路径(如 app/main.py
、runtime/
、packages/
)均以 pylaunch.exe
为基准解析。
cd your_project/
pylaunch.exe arg1 arg2
启动器将:
- 设置当前目录为
pylaunch.exe
所在路径。 - 查找并执行
app/main.py
。 - 加载
runtime/
下的python3.dll
。 - 将
packages/
添加到sys.path
。 - 将命令行参数转发给 Python 脚本。
runtime 目录:嵌入式 Python 运行时
runtime/
目录存放嵌入式 Python 运行时环境,必须包含 python3.dll
,供 pylaunch.exe
加载 Python 解释器。
获取方式
从 Python 官网 下载 Windows 嵌入式包(embeddable package),例如:
示例路径:
https://www.python.org/ftp/python/3.12.2/python-3.12.2-embed-amd64.zip
下载后步骤
-
解压到项目目录,例如:
your_project/ └── runtime/ ├── python3.dll ├── python312.zip ├── _asyncio.pyd └── ...
-
可选优化:
- 删除不需要的
.pyd
和.exe
文件。 - 保留最小运行子集(如
.dll
、.zip
和部分.pyd
)。
- 删除不需要的
packages 目录:项目依赖库
packages/
目录用于存放项目的本地依赖包,相当于简化的 site-packages
,支持在无需安装依赖的情况下运行 Python 脚本。启动器会自动将 packages/
添加到 sys.path
,无需额外配置。
使用 pip
安装到目录
在虚拟环境或本地运行:
pip install -t packages requests numpy
这会将 requests
、numpy
等库安装到 packages/
目录。
从现有虚拟环境拷贝
从现有虚拟环境的 Lib/site-packages/
拷贝相关模块或包到 packages/
:
cp -r venv/Lib/site-packages/some_package packages/
注意:C 扩展模块(如
numpy
、pandas
)需保留.pyd
文件,并确保与python3.dll
的版本和位数匹配。
app 目录:主程序入口
app/
目录存放主 Python 脚本,相当于应用的启动入口。pylaunch.exe
会自动查找并执行其中的 main.py
。
your_project/
└── app/
└── main.py # 项目启动主脚本
- 必须包含
main.py
文件(启动器会检查其是否存在)。 - 可包含其他模块或资源文件(只要脚本能正确
import
)。 - 所有脚本自动加入
sys.path
,无需手动配置路径。
总结
通过 pylaunch
,开发者可以轻松将 Python 项目打包为独立的 Windows 应用程序,无需用户安装 Python 环境。项目结构简单,适合快速封装脚本工具或便携式应用。如需支持自定义脚本路径或运行时配置,可扩展添加 config.json
或 INI 文件,默认设置已适用于大多数场景。