【Python】为PyInstaller打包后的程序搞一个小小的启动器
打包发布在之前的文章中,我们发布了一个Farmers World【农民世界】的脚本:《链游Farmers World【农民世界】爆火,发布一个免费开源的辅助挂机脚本》该脚本使用python开发,最终需要打包成exe方便发布给玩家使用一般我们会使用【PyInstaller】来打包python程序当然,近些年流行的【Nuitka】也很厉害,【Nuitka】打包Python程序的时候,会用C编译器将Py
打包发布
在之前的文章中,我们发布了一个Farmers World【农民世界】的脚本:
《链游Farmers World【农民世界】爆火,发布一个免费开源的辅助挂机脚本》
该脚本使用python开发,最终需要打包成exe方便发布给玩家使用
一般我们会使用【PyInstaller】来打包python程序
当然,近些年流行的【Nuitka】也很厉害,【Nuitka】打包Python程序的时候,会用C编译器将Python代码编译成本机代码,即最后生成的可执行文件exe里,只包含机器码,而不像【PyInstaller】,【PyInstaller】只是把Python代码以资源的形式压缩放到exe中,使用【pyinstxtractor】这样的工具,很容易把python代码反编译提取出来。
所以【Nuitka】对于需要保护源码的python程序来说非常有优势,但我们的python代码本来就是开源的,就不关心这个问题了,使用【PyInstaller】来打包,打包速度更快,兼容性更好。
不管【Nuitka】还是【PyInstaller】,在打包python程序的时候,都可以选择打包成一个单一的可执行文件exe,或者打包成一个目录,目录里面除了有可执行文件exe,还有一大堆依赖项。
通常,我们更推荐打包成一个目录。
因为从原理上来说,打包成一个单一的可执行文件,其实他只是把原本目录里那堆依赖文件压缩放到了exe里面,当你双击运行exe的时候,它会先把那堆文件解压到C盘的临时目录,再从临时目录启动exe。这样会来带一些缺点:
- 启动慢,双击后要等一会儿才能看到窗口,并且鼠标沙漏一直在转圈。
- 如果程序意外终止,临时目录将不会被清理
- 如果启动多个实例,会解压多次,解压到多个临时目录,占用C盘空间
所以我们一般都是打包成一个目录,和大多数商业软件一样,比如【有道云笔记】,你可以看到它的安装目录,也是一大堆文件,包含主程序.exe
当然商业软件一般都会提供一个安装包,安装包除了会把整个目录解压到指定位置,还会创建桌面快捷方式和开始菜单,这样用户就不需要到安装目录里找到exe去双击运行。
我们的脚本程序当然也可以做一个安装包,生成快捷方式,但是那样显然太不“绿色”了,我们希望以绿色软件的方式提供,但又不想让用户去一大堆文件的目录里寻找exe,而且数据文件和配置文件也混杂在里面,会非常难看。
如上图所示,打包成一个目录后的openfarmer,用户解压后,一堆依赖文件,用户需要双击其中的可执行文件gui.exe,而配置文件user.yml和日志文件夹logs也混在这个目录里,显得杂乱不堪。
启动器
最终我们参考了unity游戏打包发布的思路,提供一个launcher【启动器】,这个launcher负责启动游戏和更新游戏,而游戏本地和依赖项则放到一个目录中。
这个launcher.exe我们使用C++开发,并且使用/MT编译,静态链接到CRT运行库,这样这个launcher.exe在windows上运行就不依赖任何dll(系统dll除外),这样使得这个launcher.exe在绝大多数windows系统上直接双击就能运行。
launcher.exe要做的事情也很简单,启动dist目录中的目标exe,并且原封不动的传入参数,并且程序的工作目录是在launcher.exe所在的目录,而不是在dist目录。
openfarmer打包成目录,并且加上launcher.exe后,是这样的:
干净清爽,所有的杂乱都在PyInstaller打包出的dist目录中,但你永远不需要打开它。
使用方法
该启动器已开源:
✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱
https://github.com/encoderlee/launcher
✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱
需要使用VS2022编译,需要更换图标只需覆盖favicon.ico并重新编译即可。
当然最简单省事的方法就是直接从github右侧的【Releases】里下载我们编译好的版本,然后把launcher.exe改名为和dist文件夹里的目标exe文件名一样即可,注意文件夹的名字一定要是"dist"。
关键代码
#include <windows.h>
#include <string>
#include <filesystem>
using namespace std;
wstring GetExecutablePath()
{
wchar_t buffer[MAX_PATH + 1] = { 0 };
GetModuleFileName(NULL, buffer, MAX_PATH);
wstring path = buffer;
size_t pos = path.rfind(L'\\');
path.erase(pos + 1);
return path;
}
wstring GetExecutableName()
{
wchar_t buffer[MAX_PATH + 1] = { 0 };
GetModuleFileName(NULL, buffer, MAX_PATH);
wstring path = buffer;
size_t pos = path.rfind(L'\\');
path.erase(0, pos + 1);
return path;
}
bool Exec(wstring path, wstring cmdline)
{
cmdline = L"\"" + path + L"\" " + cmdline;
STARTUPINFO start_info = { sizeof(start_info) };
start_info.dwFlags = STARTF_FORCEOFFFEEDBACK;
PROCESS_INFORMATION process_info = { 0 };
if (!CreateProcess(NULL, (LPWSTR)cmdline.c_str(), NULL, NULL, FALSE, NULL, NULL, NULL, &start_info, &process_info))
return false;
WaitForInputIdle(process_info.hProcess, INFINITE);
CloseHandle(process_info.hThread);
CloseHandle(process_info.hProcess);
return true;
}
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
wstring path = GetExecutablePath();
wstring exe_name = GetExecutableName();
wstring path_target = path + L"dist\\" + exe_name;
if (!std::filesystem::exists(path_target))
{
MessageBox(NULL, (L"not find file: " + path_target).c_str(), L"error", MB_OK);
return 2;
}
if (!Exec(path_target, lpCmdLine))
return 3;
return 0;
}
交流讨论
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)