【Python】 基于嵌入式 Python 的项目启动器

pylaunch 是一个基于嵌入式 Python(Embedded Python)的 Windows 启动器,支持将 Python 项目与运行时一起分发,无需在目标机器上全局安装 Python 即可独立运行。本教程面向希望了解其原理、使用方法和打包方式的开发者。

内附源码,参考自Pystand

项目特点

  • 独立启动器:使用 C++ 编写的启动器程序,默认不显示命令行窗口(可通过参数配置无窗口模式)。
  • 嵌入式 Python 支持:自动加载本地目录中的 python3.dll(来自官方嵌入式 Python 发行版)。
  • 环境隔离:通过环境变量和动态路径设置,构建项目的私有运行环境。
  • 主脚本识别:默认查找并运行 app/main.py,与 Python 脚本逻辑解耦。
  • 自动路径配置:支持将 packagesapp 目录动态添加到 sys.path

分发结构

以下是典型的打包分发的项目目录结构:

your_project/
├── app/
│   └── main.py              # 启动入口脚本
├── packages/                # 可选:项目依赖(纯 Python 库)
├── runtime/                 # 嵌入式 Python 运行时(解压官方 zip 包)
│   ├── python3.dll
│   └── ...
└── pylaunch.exe             # 编译好的 C++ 启动器

启动器运行流程

  1. 参数解析:解析命令行参数,并支持将参数转发给 Python 脚本。
  2. 环境管理
    • 检测当前工作路径。
    • 设置运行时路径(runtime 目录)。
    • 验证 python3.dll 是否存在。
    • 设置环境变量(如 PYSTAND_HOMEPYSTAND_RUNTIME 等)。
  3. 动态加载 Python DLL
    • 使用 LoadLibraryW 加载 python3.dll
    • 获取 Py_Main 函数指针。
  4. 脚本检测:检查 app/main.py 是否存在,作为入口脚本。
  5. 执行 Python 逻辑
    • 通过 -c 参数构造启动脚本,调用 Python 内嵌代码。
    • 修改 sys.pathsys.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.pyruntime/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

下载后步骤

  1. 解压到项目目录,例如:

    your_project/
    └── runtime/
        ├── python3.dll
        ├── python312.zip
        ├── _asyncio.pyd
        └── ...
    
  2. 可选优化:

    • 删除不需要的 .pyd.exe 文件。
    • 保留最小运行子集(如 .dll.zip 和部分 .pyd)。

packages 目录:项目依赖库

packages/ 目录用于存放项目的本地依赖包,相当于简化的 site-packages,支持在无需安装依赖的情况下运行 Python 脚本。启动器会自动将 packages/ 添加到 sys.path,无需额外配置。

使用 pip 安装到目录

在虚拟环境或本地运行:

pip install -t packages requests numpy

这会将 requestsnumpy 等库安装到 packages/ 目录。

从现有虚拟环境拷贝

从现有虚拟环境的 Lib/site-packages/ 拷贝相关模块或包到 packages/

cp -r venv/Lib/site-packages/some_package packages/

注意:C 扩展模块(如 numpypandas)需保留 .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 文件,默认设置已适用于大多数场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值