Python为PyInstaller打包后的程序搞一个小小的启动器

Posted encoderlee

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python为PyInstaller打包后的程序搞一个小小的启动器相关的知识,希望对你有一定的参考价值。

打包发布

在之前的文章中,我们发布了一个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。这样会来带一些缺点:

  1. 启动慢,双击后要等一会儿才能看到窗口,并且鼠标沙漏一直在转圈。
  2. 如果程序意外终止,临时目录将不会被清理
  3. 如果启动多个实例,会解压多次,解压到多个临时目录,占用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;


交流讨论

以上是关于Python为PyInstaller打包后的程序搞一个小小的启动器的主要内容,如果未能解决你的问题,请参考以下文章

Python为PyInstaller打包后的程序搞一个小小的启动器

python使用pyinstaller将程序打包为exe文件

python桌面应用(pyinstaller打包多个py文件)

python打包后的程序在window下运行360报毒

pyinstaller打包Windows的exe文件后,多进程导致程序反复重启,python

用pyinstaller打包出现找不到指定的模块?