Files
GitVer/GitVer/GitVer_pyinstaller.cpp

562 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "pch.h"
#include "gitver.h"
#include "gitver_cli.h"
#include "gitver_common.h"
#include "gitver_process.h"
#include "gitver_version.h"
#include "gitver_pyinstaller.h"
extern TCHAR g_szCurModuleDir[MAX_PATH];
const UINT DEFAULT_MAJOR_WHEN_NO_TAG_FOR_PYINSTALLER = 1;
const UINT DEFAULT_MINOR_WHEN_NO_TAG_FOR_PYINSTALLER = 0;
static int ParsePyinstallerOptions(int argc, TCHAR* argv[], LPCTSTR& lpRepoPath, CString& strExtraArgs, BOOL& bTestMode)
{
lpRepoPath = NULL;
strExtraArgs.Empty();
bTestMode = FALSE;
for (int i = 2; i < argc; ++i)
{
CString strArg = argv[i];
if (strArg.CompareNoCase(_T("-test")) == 0)
{
bTestMode = TRUE;
continue;
}
if (strArg.GetLength() > 8 && strArg.Left(8).CompareNoCase(_T("repodir=")) == 0)
{
if (lpRepoPath != NULL)
{
_tprintf(_T("错误: repodir 参数重复: %s\n"), argv[i]);
return 23;
}
LPCTSTR lpPath = argv[i] + 8;
if (!PathIsDirectory(lpPath))
{
PrintInvalidRepoPathError(lpPath);
return 23;
}
lpRepoPath = lpPath;
continue;
}
if (strArg.GetLength() > 7 && strArg.Left(7).CompareNoCase(_T("params=")) == 0)
{
if (!strExtraArgs.IsEmpty())
{
_tprintf(_T("错误: params 参数重复。\n"));
return 3;
}
CString strVal = strArg.Mid(7);
// 去掉外层双引号(如 params="--onefile"
if (strVal.GetLength() >= 2 && strVal[0] == _T('"') && strVal[strVal.GetLength() - 1] == _T('"'))
strVal = strVal.Mid(1, strVal.GetLength() - 2);
strExtraArgs = strVal;
continue;
}
_tprintf(_T("错误: 未知的参数 %s\n"), argv[i]);
return 3;
}
if (lpRepoPath == NULL)
{
lpRepoPath = g_szCurModuleDir;
}
return 0;
}
// 解析版本字符串 "a.b.c.d" → 四个分量
static BOOL ParseVersionTuple(LPCTSTR lpVer, UINT& v1, UINT& v2, UINT& v3, UINT& v4)
{
CString str(lpVer);
int p1 = str.Find(_T('.'));
if (p1 < 0) return FALSE;
int p2 = str.Find(_T('.'), p1 + 1);
if (p2 < 0) return FALSE;
int p3 = str.Find(_T('.'), p2 + 1);
if (p3 < 0) return FALSE;
v1 = (UINT)_ttoi(str.Left(p1));
v2 = (UINT)_ttoi(str.Mid(p1 + 1, p2 - p1 - 1));
v3 = (UINT)_ttoi(str.Mid(p2 + 1, p3 - p2 - 1));
v4 = (UINT)_ttoi(str.Mid(p3 + 1));
return TRUE;
}
// 提取 params 中第一个空格分隔的 token支持双引号括起的路径
static CString ExtractFirstToken(const CString& strParams)
{
CString str = strParams;
str.TrimLeft();
if (str.IsEmpty()) return _T("");
if (str[0] == _T('"'))
{
int nEnd = str.Find(_T('"'), 1);
if (nEnd < 0) return str.Mid(1);
return str.Mid(1, nEnd - 1);
}
int nSpace = str.Find(_T(' '));
if (nSpace < 0) return str;
return str.Left(nSpace);
}
// 生成 version_info.txt 到 lpDir 目录PyInstaller VSVersionInfo 格式)
static BOOL WriteVersionInfoFile(LPCTSTR lpDir, LPCTSTR lpProductVersion, LPCTSTR lpFileVersion, CString& strOutPath)
{
UINT pv1 = 0, pv2 = 0, pv3 = 0, pv4 = 0;
UINT fv1 = 0, fv2 = 0, fv3 = 0, fv4 = 0;
if (!ParseVersionTuple(lpProductVersion, pv1, pv2, pv3, pv4)) return FALSE;
if (!ParseVersionTuple(lpFileVersion, fv1, fv2, fv3, fv4)) return FALSE;
CStringA strProdVerA(lpProductVersion);
CStringA strFileVerA(lpFileVersion);
const char* szProdVer = strProdVerA;
const char* szFileVer = strFileVerA;
strOutPath.Format(_T("%s\\version_info.txt"), lpDir);
FILE* fp = NULL;
_tfopen_s(&fp, strOutPath.GetString(), _T("wb"));
if (!fp) return FALSE;
fprintf(fp,
"# UTF-8\n"
"VSVersionInfo(\n"
" ffi=FixedFileInfo(\n"
" filevers=(%u, %u, %u, %u),\n"
" prodvers=(%u, %u, %u, %u),\n"
" mask=0x3f,\n"
" flags=0x0,\n"
" OS=0x40004,\n"
" fileType=0x1,\n"
" subtype=0x0,\n"
" date=(0, 0)\n"
" ),\n"
" kids=[\n"
" StringFileInfo([\n"
" StringTable(\n"
" u'040904B0',\n"
" [StringStruct(u'CompanyName', u''),\n"
" StringStruct(u'FileDescription', u''),\n"
" StringStruct(u'FileVersion', u'%s'),\n"
" StringStruct(u'InternalName', u''),\n"
" StringStruct(u'LegalCopyright', u''),\n"
" StringStruct(u'OriginalFilename', u''),\n"
" StringStruct(u'ProductName', u''),\n"
" StringStruct(u'ProductVersion', u'%s'),\n"
" StringStruct(u'Comments', u''),\n"
" StringStruct(u'LegalTrademarks', u''),\n"
" ])\n"
" ]),\n"
" VarFileInfo([VarStruct(u'Translation', [0x0409, 1200])])\n"
" ]\n"
")\n",
fv1, fv2, fv3, fv4,
pv1, pv2, pv3, pv4,
szFileVer,
szProdVer);
fclose(fp);
return TRUE;
}
// 向 .spec 文件注入或更新 version= 参数
// 若已有 version= 则更新其值,否则在 name= 参数后新增一行
static BOOL InjectVersionIntoSpec(LPCTSTR lpSpecPath)
{
if (!PathFileExists(lpSpecPath))
{
_tprintf(_T("错误: spec 文件不存在 %s\n"), lpSpecPath);
return FALSE;
}
CFile myFile;
CFileException fileEx;
if (!myFile.Open(lpSpecPath, CFile::modeReadWrite, &fileEx))
{
_tprintf(_T("错误: 无法打开 spec 文件 %s\n"), lpSpecPath);
return FALSE;
}
DWORD dwLen = (DWORD)myFile.GetLength();
std::vector<char> buf(dwLen + 1, 0);
myFile.Read(buf.data(), dwLen);
myFile.Close();
std::string strContent(buf.data(), dwLen);
const std::string strNewValue = "version='version_info.txt'";
bool bFound = false;
std::string::size_type nPos = 0;
while ((nPos = strContent.find("version=", nPos)) != std::string::npos)
{
// 确保 version= 是独立参数(前驱字符为空白或换行,而非字母/数字)
bool bIsParam = (nPos == 0
|| strContent[nPos - 1] == '\n'
|| strContent[nPos - 1] == '\r'
|| strContent[nPos - 1] == ' '
|| strContent[nPos - 1] == '\t');
if (bIsParam)
{
// 获取行首(保留缩进)
std::string::size_type nLineStart = strContent.rfind('\n', nPos);
nLineStart = (nLineStart == std::string::npos) ? 0 : nLineStart + 1;
std::string strIndent = strContent.substr(nLineStart, nPos - nLineStart);
// 获取行末
std::string::size_type nLineEnd = strContent.find('\n', nPos);
if (nLineEnd == std::string::npos) nLineEnd = strContent.size();
// 替换整行(保留缩进,统一加尾随逗号)
std::string strNewLine = strIndent + strNewValue + ",";
strContent.replace(nLineStart, nLineEnd - nLineStart, strNewLine);
_tprintf(_T("成功: 已更新 spec 文件 version 字段 -> version_info.txt\n"));
bFound = true;
break;
}
nPos += 8; // len("version=")
}
if (!bFound)
{
// 未找到 version=,在 name= 参数后插入新行
std::string::size_type nNamePos = strContent.find("name=");
if (nNamePos != std::string::npos)
{
// 获取 name= 行的缩进
std::string::size_type nLineStart = strContent.rfind('\n', nNamePos);
nLineStart = (nLineStart == std::string::npos) ? 0 : nLineStart + 1;
std::string strIndent = strContent.substr(nLineStart, nNamePos - nLineStart);
// 找到 name= 行末
std::string::size_type nLineEnd = strContent.find('\n', nNamePos);
if (nLineEnd == std::string::npos) nLineEnd = strContent.size() - 1;
// 在 name= 行后插入 version=
std::string strInsert = "\n" + strIndent + strNewValue + ",";
strContent.insert(nLineEnd, strInsert);
_tprintf(_T("成功: 已向 spec 文件新增 version 字段 -> version_info.txt\n"));
}
else
{
_tprintf(_T("警告: spec 文件中未找到 version= 或 name= 参数,跳过版本信息注入。\n"));
}
}
// 写回文件
if (!myFile.Open(lpSpecPath, CFile::modeCreate | CFile::modeWrite, &fileEx))
{
_tprintf(_T("错误: 无法写入 spec 文件 %s\n"), lpSpecPath);
return FALSE;
}
myFile.Write(strContent.c_str(), (UINT)strContent.size());
myFile.Close();
return TRUE;
}
// 从 .spec 文件读取 version= 参数的值(单/双引号括起的路径)
// 返回 FALSE 表示未找到、值为 None 或值为空
static BOOL ReadVersionPathFromSpec(LPCTSTR lpSpecPath, CString& strVersionPath)
{
strVersionPath.Empty();
if (!PathFileExists(lpSpecPath)) return FALSE;
CFile myFile;
CFileException fileEx;
if (!myFile.Open(lpSpecPath, CFile::modeRead, &fileEx)) return FALSE;
DWORD dwLen = (DWORD)myFile.GetLength();
std::vector<char> buf(dwLen + 1, 0);
myFile.Read(buf.data(), dwLen);
myFile.Close();
std::string strContent(buf.data(), dwLen);
std::string::size_type nPos = 0;
while ((nPos = strContent.find("version=", nPos)) != std::string::npos)
{
bool bIsParam = (nPos == 0
|| strContent[nPos - 1] == '\n'
|| strContent[nPos - 1] == '\r'
|| strContent[nPos - 1] == ' '
|| strContent[nPos - 1] == '\t');
if (bIsParam)
{
std::string::size_type nValStart = nPos + 8; // skip "version="
while (nValStart < strContent.size() && strContent[nValStart] == ' ')
nValStart++;
if (nValStart >= strContent.size()) break;
char cQuote = strContent[nValStart];
if (cQuote == '\'' || cQuote == '"')
{
std::string::size_type nValEnd = strContent.find(cQuote, nValStart + 1);
if (nValEnd != std::string::npos && nValEnd > nValStart + 1)
{
strVersionPath = strContent.substr(nValStart + 1, nValEnd - nValStart - 1).c_str();
return TRUE;
}
}
break; // version=None 或空值,视为无
}
nPos += 8;
}
return FALSE;
}
// 更新已有 version_info 文件的版本字段;若文件不存在则创建
static BOOL UpdateVersionInfoFile(LPCTSTR lpPath, LPCTSTR lpProductVersion, LPCTSTR lpFileVersion)
{
UINT pv1 = 0, pv2 = 0, pv3 = 0, pv4 = 0;
UINT fv1 = 0, fv2 = 0, fv3 = 0, fv4 = 0;
if (!ParseVersionTuple(lpProductVersion, pv1, pv2, pv3, pv4)) return FALSE;
if (!ParseVersionTuple(lpFileVersion, fv1, fv2, fv3, fv4)) return FALSE;
CStringA strProdVerA(lpProductVersion);
CStringA strFileVerA(lpFileVersion);
const char* szProdVer = strProdVerA;
const char* szFileVer = strFileVerA;
if (!PathFileExists(lpPath))
{
// 文件不存在,直接创建
FILE* fp = NULL;
_tfopen_s(&fp, lpPath, _T("wb"));
if (!fp) return FALSE;
fprintf(fp,
"# UTF-8\n"
"VSVersionInfo(\n"
" ffi=FixedFileInfo(\n"
" filevers=(%u, %u, %u, %u),\n"
" prodvers=(%u, %u, %u, %u),\n"
" mask=0x3f,\n"
" flags=0x0,\n"
" OS=0x40004,\n"
" fileType=0x1,\n"
" subtype=0x0,\n"
" date=(0, 0)\n"
" ),\n"
" kids=[\n"
" StringFileInfo([\n"
" StringTable(\n"
" u'040904B0',\n"
" [StringStruct(u'CompanyName', u''),\n"
" StringStruct(u'FileDescription', u''),\n"
" StringStruct(u'FileVersion', u'%s'),\n"
" StringStruct(u'InternalName', u''),\n"
" StringStruct(u'LegalCopyright', u''),\n"
" StringStruct(u'OriginalFilename', u''),\n"
" StringStruct(u'ProductName', u''),\n"
" StringStruct(u'ProductVersion', u'%s'),\n"
" StringStruct(u'Comments', u''),\n"
" StringStruct(u'LegalTrademarks', u''),\n"
" ])\n"
" ]),\n"
" VarFileInfo([VarStruct(u'Translation', [0x0409, 1200])])\n"
" ]\n"
")\n",
fv1, fv2, fv3, fv4,
pv1, pv2, pv3, pv4,
szFileVer,
szProdVer);
fclose(fp);
return TRUE;
}
// 读取现有文件并就地更新版本字段
CFile myFile;
CFileException fileEx;
if (!myFile.Open(lpPath, CFile::modeReadWrite, &fileEx)) return FALSE;
DWORD dwLen = (DWORD)myFile.GetLength();
std::vector<char> buf(dwLen + 1, 0);
myFile.Read(buf.data(), dwLen);
myFile.Close();
std::string s(buf.data(), dwLen);
char szNew[128];
// 更新 filevers 元组
{
const std::string key = "filevers=(";
auto p = s.find(key);
if (p != std::string::npos) {
auto pValStart = p + key.size();
auto pValEnd = s.find(')', pValStart);
if (pValEnd != std::string::npos) {
snprintf(szNew, sizeof(szNew), "%u, %u, %u, %u", fv1, fv2, fv3, fv4);
s.replace(pValStart, pValEnd - pValStart, szNew);
}
}
}
// 更新 prodvers 元组
{
const std::string key = "prodvers=(";
auto p = s.find(key);
if (p != std::string::npos) {
auto pValStart = p + key.size();
auto pValEnd = s.find(')', pValStart);
if (pValEnd != std::string::npos) {
snprintf(szNew, sizeof(szNew), "%u, %u, %u, %u", pv1, pv2, pv3, pv4);
s.replace(pValStart, pValEnd - pValStart, szNew);
}
}
}
// 更新 FileVersion 字符串值
{
const std::string key = "u'FileVersion', u'";
auto p = s.find(key);
if (p != std::string::npos) {
auto pValStart = p + key.size();
auto pValEnd = s.find('\'', pValStart);
if (pValEnd != std::string::npos)
s.replace(pValStart, pValEnd - pValStart, szFileVer);
}
}
// 更新 ProductVersion 字符串值
{
const std::string key = "u'ProductVersion', u'";
auto p = s.find(key);
if (p != std::string::npos) {
auto pValStart = p + key.size();
auto pValEnd = s.find('\'', pValStart);
if (pValEnd != std::string::npos)
s.replace(pValStart, pValEnd - pValStart, szProdVer);
}
}
if (!myFile.Open(lpPath, CFile::modeCreate | CFile::modeWrite, &fileEx)) return FALSE;
myFile.Write(s.c_str(), (UINT)s.size());
myFile.Close();
return TRUE;
}
int HandlePyinstallerBuildCommand(int argc, TCHAR* argv[])
{
#ifdef _DEBUG
Sleep(15000);
#endif
// 解析 pid内嵌在 argv[1] 中,如 "pyinstaller=5"
// "pyinstaller=" 为 12 个字符
UINT nPid = 0;
CString strPid = CString(argv[1]).Mid(12);
if (strPid.IsEmpty() || !TryParseUInt16(strPid, nPid))
{
_tprintf(_T("错误: pyinstaller= 后的 pid 无效:%s应为 0-65535 范围的整数。\n"), strPid.GetString());
return 41;
}
LPCTSTR lpRepoPath = NULL;
CString strExtraArgs;
BOOL bTestMode = FALSE;
int nRepoArgRet = ParsePyinstallerOptions(argc, argv, lpRepoPath, strExtraArgs, bTestMode);
if (nRepoArgRet != 0)
{
return nRepoArgRet;
}
CString strProductVersion;
CString strFileVersion;
VersionBuildErrorCodes errorCodes = { 42, 43, 44 };
int nVersionRet = BuildVersionsFromRepo(
lpRepoPath,
nPid,
errorCodes,
strProductVersion,
strFileVersion,
DEFAULT_MAJOR_WHEN_NO_TAG_FOR_PYINSTALLER,
DEFAULT_MINOR_WHEN_NO_TAG_FOR_PYINSTALLER);
if (nVersionRet != 0)
{
return nVersionRet;
}
if (bTestMode)
{
int nDot1 = strProductVersion.Find(_T('.'));
int nDot2 = (nDot1 >= 0) ? strProductVersion.Find(_T('.'), nDot1 + 1) : -1;
if (nDot2 > nDot1)
{
strProductVersion = strProductVersion.Left(nDot2 + 1) + _T("0.0");
}
_tprintf(_T("[test] 测试版本号: ProductVersion=%s\n"), strProductVersion.GetString());
}
// 检测是否为 spec 模式,若是则生成 version_info.txt 并注入/更新 spec
CString strFirstToken = ExtractFirstToken(strExtraArgs);
BOOL bIsSpec = (strFirstToken.GetLength() >= 5 && strFirstToken.Right(5).CompareNoCase(_T(".spec")) == 0);
if (bIsSpec)
{
// 解析 spec 文件完整路径
CString strSpecPath;
if (PathIsRelative(strFirstToken))
strSpecPath.Format(_T("%s\\%s"), lpRepoPath, strFirstToken.GetString());
else
strSpecPath = strFirstToken;
// 获取 spec 所在目录
TCHAR szSpecDir[MAX_PATH] = {};
_tcscpy_s(szSpecDir, strSpecPath.GetString());
PathRemoveFileSpec(szSpecDir);
// 检查 spec 是否已指定版本信息文件
CString strExistingVersionPath;
if (ReadVersionPathFromSpec(strSpecPath, strExistingVersionPath))
{
// spec 已有 version=,直接更新所指向的文件(不存在则创建)
CString strVersionInfoFullPath;
if (PathIsRelative(strExistingVersionPath))
strVersionInfoFullPath.Format(_T("%s\\%s"), szSpecDir, strExistingVersionPath.GetString());
else
strVersionInfoFullPath = strExistingVersionPath;
if (!UpdateVersionInfoFile(strVersionInfoFullPath, strProductVersion, strFileVersion))
{
_tprintf(_T("错误: 无法更新版本信息文件: %s。\n"), strVersionInfoFullPath.GetString());
return 45;
}
_tprintf(_T("版本信息文件已更新: %s\n"), strVersionInfoFullPath.GetString());
}
else
{
// spec 未指定 version=,生成 version_info.txt 并注入 spec
CString strVersionInfoPath;
if (!WriteVersionInfoFile(szSpecDir, strProductVersion, strFileVersion, strVersionInfoPath))
{
_tprintf(_T("错误: 无法生成 version_info.txt目录: %s\n"), szSpecDir);
return 45;
}
_tprintf(_T("版本信息文件已生成: %s\n"), strVersionInfoPath.GetString());
if (!InjectVersionIntoSpec(strSpecPath))
{
_tprintf(_T("错误: 无法修改 spec 文件: %s。\n"), strSpecPath.GetString());
return 46;
}
}
}
_tprintf(_T("PyInstaller 打包开始..\n"));
_tprintf(_T("ProductVersion=%s\n"), strProductVersion.GetString());
_tprintf(_T("FileVersion=%s\n"), strFileVersion.GetString());
CString strCmd;
strCmd.Format(_T("cmd /c python -m PyInstaller %s"),strExtraArgs.GetString());
CString strBuildResult = StartProcess(NULL, strCmd.GetBuffer(), lpRepoPath);
strCmd.ReleaseBuffer();
if (strBuildResult.IsEmpty())
{
_tprintf(_T("错误: PyInstaller 打包失败。\n"));
}
return 0;
}