打包发布

在之前的文章中,我们发布了一个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;
}

交流讨论

在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐