Compare commits

...

16 Commits

Author SHA1 Message Date
0766d09e27 非强制上传模式下,要求HEAD游离且指向Tag 2026-06-29 14:44:06 +08:00
6b0f602e57 1、加强注释
2、上传时,添加-f参数才会强制上传,否则需要根据当前Tag标签信息来判断是否需要上传。
2026-06-28 20:56:56 +08:00
ec7b51873c 修复上传时,中文乱码的问题。 2026-06-28 20:16:16 +08:00
685a2094f7 字符编码统一GBK 2026-06-28 19:32:57 +08:00
04904c2919 1、新增上传命令行参数:upload
2、上传参数关联函数调整与优化
2026-06-26 17:16:44 +08:00
46134343e3 变更版本号 2026-06-24 09:58:32 +08:00
5679433f7c 1、修复TryGetDetectedBranch问题
2、修复TryGetBranchCommitCountByDate在游离状态下获取分支失败的问题,应当获取远程分支,如:origin/master
但当浅克隆仓库,本地从未拉取过 origin/master,需要先执行:git fetch origin/master
3、更新md文件
2026-06-24 09:24:27 +08:00
71f99a55b4 添加对gitlab runner执行ci时游离分支的支持 2026-06-23 21:12:56 +08:00
a1f4ebd45a 修复inno setup中脚本的版本修改失败问题。 2026-06-03 18:33:18 +08:00
171fa4448d 返回release和dev分支bid值 2026-05-22 20:27:28 +08:00
c4d2431cb0 更新md文件说明 2026-05-21 20:21:51 +08:00
67d24d69f5 支持spec内置版本信息的处理 2026-05-21 20:18:38 +08:00
4a4c51d124 1、setup=n为独立命令行,不再使用-setup=n方式。
2、更新md说明
2026-05-21 17:30:46 +08:00
84d67d9709 修复pyinstaller打包后,版本信息不对的问题。 2026-05-20 20:16:27 +08:00
150486a29b 修复pyinstaller打包失败的问题 2026-05-20 19:41:31 +08:00
d36618d101 1、支持pyinstaller打包方式
2、将python打包其他参数都放入params中
2026-05-20 15:59:51 +08:00
32 changed files with 24890 additions and 463 deletions

View File

@@ -7,12 +7,14 @@
#include "gitver_common.h" #include "gitver_common.h"
#include "gitver_cli.h" #include "gitver_cli.h"
#include "gitver_nuitka.h" #include "gitver_nuitka.h"
#include "gitver_pyinstaller.h"
#include "gitver_process.h" #include "gitver_process.h"
#include "gitver_rewrite.h" #include "gitver_rewrite.h"
#include "gitver_tag.h" #include "gitver_tag.h"
#include "gitver_types.h" #include "gitver_types.h"
#include "gitver_version.h" #include "gitver_version.h"
#include "gitver_setup.h" #include "gitver_setup.h"
#include "gitver_upload.h"
#ifdef _DEBUG #ifdef _DEBUG
#define new DEBUG_NEW #define new DEBUG_NEW
@@ -350,11 +352,14 @@ void PrintFullUsageExamples()
_T(" gitver (显示帮助后进入当前分支创建 tag 流程)\n") _T(" gitver (显示帮助后进入当前分支创建 tag 流程)\n")
_T(" gitver rewrite [PEType可选] [-f可选]\n") _T(" gitver rewrite [PEType可选] [-f可选]\n")
_T(" gitver setver=<pid> [repodir=<path>可选] [-test可选]\n") _T(" gitver setver=<pid> [repodir=<path>可选] [-test可选]\n")
_T(" gitver nuitkabuild=<pid> <mainPy> [repodir=<path>可选] [-test可选] [params=\"<nuitka参数>\"可选]\n") _T(" gitver nuitkabuild=<pid> [repodir=<path>可选] [-test可选] [params=\"<mainPy> <nuitka参数>\"可选]\n")
_T(" gitver nuitkapydbuild=<pid> <modulePy> [repodir=<path>可选] [-test可选] [params=\"<nuitka参数>\"可选]\n") _T(" gitver nuitkapydbuild=<pid> [repodir=<path>可选] [-test可选] [params=\"<modulePy> <nuitka参数>\"可选]\n")
_T(" gitver -setup=0|1 [pid] [repodir=<path>可选] [-test可选]\n") _T(" gitver pyinstaller=<pid> [repodir=<path>可选] [-test可选] [params=\"<mainPy|specFile> <pyinstaller参数>\"可选]\n")
_T(" -setup=0: 使用 Inno Setup 脚本 (setup.iss)\n") _T(" gitver setup=n [pid=m可选] [repodir=<path>可选] [-test可选]\n")
_T(" -setup=1: 使用 NSIS 脚本 (setup.nsh)\n") _T(" gitver upload username=<用户名> [file=<上传文件>] [info=<配置.txt>] [url=<PMC基址>]\n")
_T(" setup=0: 使用 Inno Setup 脚本 (setup.iss)\n")
_T(" setup=1: 使用 NSIS 脚本 (setup.nsi)\n")
_T(" 不指定 pid 时自动从 .rc / AssemblyInfo.cs / version_info.txt 读取版本\n")
_T("\n示例:\n") _T("\n示例:\n")
_T(" gitver\n") _T(" gitver\n")
_T(" gitver rewrite\n") _T(" gitver rewrite\n")
@@ -363,15 +368,23 @@ void PrintFullUsageExamples()
_T(" gitver setver=5 repodir=E:\\Code\\OTH\\gitver\n") _T(" gitver setver=5 repodir=E:\\Code\\OTH\\gitver\n")
_T(" gitver setver=5\n") _T(" gitver setver=5\n")
_T(" gitver setver=5 -test\n") _T(" gitver setver=5 -test\n")
_T(" gitver nuitkabuild=5 main.py\n") _T(" gitver nuitkabuild=5 params=\"main.py\"\n")
_T(" gitver nuitkabuild=5 main.py -test\n") _T(" gitver nuitkabuild=5 -test params=\"main.py\"\n")
_T(" gitver nuitkabuild=5 src\\app.py repodir=E:\\Code\\MyPyProj params=\"--standalone --output-dir=dist\"\n") _T(" gitver nuitkabuild=5 repodir=E:\\Code\\MyPyProj params=\"src\\\\app.py --standalone --output-dir=dist\"\n")
_T(" gitver nuitkapydbuild=5 module.py\n") _T(" gitver nuitkapydbuild=5 params=\"module.py\"\n")
_T(" gitver nuitkapydbuild=5 module.py -test\n") _T(" gitver nuitkapydbuild=5 -test params=\"module.py\"\n")
_T(" gitver nuitkapydbuild=5 src\\core.py repodir=E:\\Code\\MyPyProj params=\"--output-dir=dist\"\n") _T(" gitver nuitkapydbuild=5 repodir=E:\\Code\\MyPyProj params=\"src\\\\core.py --output-dir=dist\"\n")
_T(" gitver -setup=0 5\n") _T(" gitver pyinstaller=5 params=\"main.py\"\n")
_T(" gitver -setup=1 5 repodir=E:\\Code\\MyProj\n") _T(" gitver pyinstaller=5 -test params=\"main.py\"\n")
_T("params: 传递给 Nuitka 的额外参数,用双引号括起来,如 params=\"--standalone --output-dir=dist\"\n") _T(" gitver pyinstaller=5 repodir=E:\\Code\\MyPyProj params=\"src\\\\app.py --onefile --name=MyApp\"\n")
_T(" gitver pyinstaller=5 params=\"myapp.spec\"\n")
_T(" gitver pyinstaller=5 -test params=\"myapp.spec\"\n")
_T(" gitver pyinstaller=5 repodir=E:\\Code\\MyPyProj params=\"myapp.spec\"\n")
_T(" gitver setup=0\n")
_T(" gitver setup=0 pid=5\n")
_T(" gitver setup=1 pid=5 repodir=E:\\Code\\MyProj\n")
_T(" gitver upload username=admin\n")
_T("params: 传递给 Nuitka/PyInstaller 的参数(含入口文件),用双引号括起来,如 params=\"main.py --standalone --output-dir=dist\"\n")
_T("-test: 将产品版本号的 major 和 minor 都置为 0用于测试版本构建\n") _T("-test: 将产品版本号的 major 和 minor 都置为 0用于测试版本构建\n")
_T("无参数时会读取当前分支最近三次 tag并提示选择 major 加 1 或 minor 加 1然后创建新 tag。\n") _T("无参数时会读取当前分支最近三次 tag并提示选择 major 加 1 或 minor 加 1然后创建新 tag。\n")
_T("未找到当前分支 tag 时自动使用默认版本 1.0。\n"); _T("未找到当前分支 tag 时自动使用默认版本 1.0。\n");
@@ -386,8 +399,10 @@ void PrintShortCommandUsage()
{ {
_tprintf(_T("请使用gitver rewrite [PE类型可选]\n")); _tprintf(_T("请使用gitver rewrite [PE类型可选]\n"));
_tprintf(_T("请使用gitver setver=<pid> [repodir=<path>可选]\n")); _tprintf(_T("请使用gitver setver=<pid> [repodir=<path>可选]\n"));
_tprintf(_T("gitver nuitkabuild=<pid> <mainPy> [repodir=<path>可选] [params=\"<nuitka参数>\"可选]\n")); _tprintf(_T("gitver nuitkabuild=<pid> [repodir=<path>可选] [params=\"<mainPy> <nuitka参数>\"可选]\n"));
_tprintf(_T("gitver nuitkapydbuild=<pid> <modulePy> [repodir=<path>可选] [params=\"<nuitka参数>\"可选]\n")); _tprintf(_T("gitver nuitkapydbuild=<pid> [repodir=<path>可选] [params=\"<modulePy> <nuitka参数>\"可选]\n"));
_tprintf(_T("gitver pyinstaller=<pid> [repodir=<path>可选] [params=\"<mainPy|specFile> <pyinstaller参数>\"可选]\n"));
_tprintf(_T("gitver upload username=<用户名> [file=<上传文件>] [info=<配置.txt>] [url=<PMC基址>]\n"));
} }
/// <summary> /// <summary>
@@ -475,13 +490,41 @@ LPCTSTR GetExitCodeHint(int nRetCode)
case 35: case 35:
return _T("rewrite version file failed"); return _T("rewrite version file failed");
case 36: case 36:
return _T("setup: unsupported -setup value"); return _T("setup: unsupported setup value");
case 37: case 37:
return _T("setup: setup script not found"); return _T("setup: setup script not found");
case 38: case 38:
return _T("setup: modify setup script failed"); return _T("setup: modify setup script failed");
case 39: case 39:
return _T("setup: compiler not found"); return _T("setup: compiler not found");
case 40:
return _T("pyinstaller: missing args");
case 41:
return _T("pyinstaller: invalid pid");
case 42:
return _T("pyinstaller: get branch or bid failed");
case 43:
return _T("pyinstaller: get tag failed");
case 44:
return _T("pyinstaller: get today commit count failed");
case 45:
return _T("pyinstaller: write version_info.txt failed");
case 46:
return _T("pyinstaller: inject version into spec file failed");
case 47:
return _T("upload: missing required args");
case 48:
return _T("upload: file not found");
case 49:
return _T("upload: info invalid or env password missing");
case 50:
return _T("upload: login failed");
case 51:
return _T("upload: upload http failed");
case 52:
return _T("upload: version not found after upload");
case 53:
return _T("upload: network error");
default: default:
return _T(""); return _T("");
} }
@@ -580,6 +623,8 @@ int HandleSetVerCommand(int argc, TCHAR* argv[])
CString strProductVersion; CString strProductVersion;
CString strFileVersion; CString strFileVersion;
CString strBranch;
SYSTEMTIME stFileVersionDate = { 0 };
VersionBuildErrorCodes errorCodes = { 5, 6, 9 }; VersionBuildErrorCodes errorCodes = { 5, 6, 9 };
int nVersionRet = BuildVersionsFromRepo( int nVersionRet = BuildVersionsFromRepo(
lpRepoPath, lpRepoPath,
@@ -588,7 +633,9 @@ int HandleSetVerCommand(int argc, TCHAR* argv[])
strProductVersion, strProductVersion,
strFileVersion, strFileVersion,
DEFAULT_MAJOR_WHEN_NO_TAG, DEFAULT_MAJOR_WHEN_NO_TAG,
DEFAULT_MINOR_WHEN_NO_TAG); DEFAULT_MINOR_WHEN_NO_TAG,
&strBranch,
&stFileVersionDate);
if (nVersionRet != 0) if (nVersionRet != 0)
{ {
return nVersionRet; return nVersionRet;
@@ -608,39 +655,23 @@ int HandleSetVerCommand(int argc, TCHAR* argv[])
_tprintf(_T("ProductVersion=%s\n"), strProductVersion.GetString()); _tprintf(_T("ProductVersion=%s\n"), strProductVersion.GetString());
_tprintf(_T("FileVersion=%s\n"), strFileVersion.GetString()); _tprintf(_T("FileVersion=%s\n"), strFileVersion.GetString());
// 检查是否附带了 -setup=N 标志 CString strChangeLog;
int nSetupType = -1; if (!TryGetGitLogByBranchDate(lpRepoPath, strBranch, stFileVersionDate, strChangeLog))
for (int i = 2; i < argc; ++i)
{ {
CString strArg = argv[i]; strChangeLog = _T("无当日提交记录");
if (strArg.GetLength() > 7 && strArg.Left(7).CompareNoCase(_T("-setup=")) == 0)
{
UINT nVal = 0;
if (!TryParseUInt16(strArg.Mid(7), nVal) || (nVal != 0 && nVal != 1))
{
_tprintf(_T("错误: -setup 参数值 \"%s\" 不支持,仅支持 0Inno Setup或 1NSIS\n"),
strArg.Mid(7).GetString());
return 36;
}
nSetupType = (int)nVal;
break;
}
} }
if (nSetupType >= 0) TCHAR szInfoPath[MAX_PATH] = { 0 };
_tcscpy_s(szInfoPath, lpRepoPath);
PathAppend(szInfoPath, _T("upload_info.txt"));
if (!WriteUploadInfoVersions(szInfoPath, strProductVersion, strFileVersion, strChangeLog))
{ {
// -setup=N 存在:不回写版本信息到项目文件,只修改安装脚本并编译 _tprintf(_T("警告: 未能写入 upload_info.txt\n"));
if (!ExecuteSetupBuild(nSetupType, strProductVersion, strFileVersion))
{
return 38;
}
return 0;
} }
return RewriteSetVerByProjectType(lpRepoPath, strProductVersion, strFileVersion); return RewriteSetVerByProjectType(lpRepoPath, strProductVersion, strFileVersion);
} }
/// <summary>
/// <summary> /// <summary>
/// 准备C++重写内容。 /// 准备C++重写内容。
/// </summary> /// </summary>
@@ -822,6 +853,14 @@ int HandleRewriteCommand(int argc, TCHAR* argv[])
return 0; return 0;
} }
/// <summary>
/// 程序入口:解析命令行参数并分发到各子命令处理函数。
/// 无参数时进入交互式 tag 创建模式。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <param name="envp">环境变量数组。</param>
/// <returns>命令执行退出码0 表示成功。</returns>
int main(int argc, TCHAR* argv[], TCHAR* envp[]) int main(int argc, TCHAR* argv[], TCHAR* envp[])
{ {
int nRetCode = 0; int nRetCode = 0;
@@ -875,6 +914,9 @@ int main(int argc, TCHAR* argv[], TCHAR* envp[])
if (argc >= 2 && _tcsnicmp(argv[1], _T("setver="), 7) == 0) if (argc >= 2 && _tcsnicmp(argv[1], _T("setver="), 7) == 0)
{ {
#ifdef _DEBUG
Sleep(10000);
#endif
DWORD dwStartTick = LogCommandStart(_T("setver")); DWORD dwStartTick = LogCommandStart(_T("setver"));
int nCmdRet = HandleSetVerCommand(argc, argv); int nCmdRet = HandleSetVerCommand(argc, argv);
LogCommandEnd(_T("setver"), dwStartTick, nCmdRet); LogCommandEnd(_T("setver"), dwStartTick, nCmdRet);
@@ -891,12 +933,33 @@ int main(int argc, TCHAR* argv[], TCHAR* envp[])
return nCmdRet; return nCmdRet;
} }
if (argc >= 2 && _tcsnicmp(argv[1], _T("-setup="), 7) == 0) if (argc >= 2 && _tcsnicmp(argv[1], _T("pyinstaller="), 12) == 0)
{ {
DWORD dwStartTick = LogCommandStart(_T("-setup")); DWORD dwStartTick = LogCommandStart(_T("pyinstaller"));
int nCmdRet = HandlePyinstallerBuildCommand(argc, argv);
LogCommandEnd(_T("pyinstaller"), dwStartTick, nCmdRet);
PrintCommandFailedWithCode(_T("pyinstaller"), nCmdRet);
return nCmdRet;
}
if (argc >= 2 && _tcsnicmp(argv[1], _T("setup="), 6) == 0)
{
DWORD dwStartTick = LogCommandStart(_T("setup"));
int nCmdRet = HandleSetupCommand(argc, argv); int nCmdRet = HandleSetupCommand(argc, argv);
LogCommandEnd(_T("-setup"), dwStartTick, nCmdRet); LogCommandEnd(_T("setup"), dwStartTick, nCmdRet);
PrintCommandFailedWithCode(_T("-setup"), nCmdRet); PrintCommandFailedWithCode(_T("setup"), nCmdRet);
return nCmdRet;
}
if (argc >= 2 && _tcsicmp(argv[1], _T("upload")) == 0)
{
#ifdef _DEBUG
Sleep(10000);
#endif
DWORD dwStartTick = LogCommandStart(_T("upload"));
int nCmdRet = HandleUploadCommand(argc, argv);
LogCommandEnd(_T("upload"), dwStartTick, nCmdRet);
PrintCommandFailedWithCode(_T("upload"), nCmdRet);
return nCmdRet; return nCmdRet;
} }

Binary file not shown.

View File

@@ -77,7 +77,7 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<OutDir>..\..\..\..\bin\$(ProjectName)\</OutDir> <OutDir>..\..\..\..\bin\$(ProjectName)\</OutDir>
<IntDir>$(OutDir)$(Configuration)\</IntDir> <IntDir>$(OutDir)$(Configuration)\</IntDir>
<TargetName>$(ProjectName)d</TargetName> <TargetName>$(ProjectName)</TargetName>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<OutDir>..\..\..\..\bin\$(ProjectName)\</OutDir> <OutDir>..\..\..\..\bin\$(ProjectName)\</OutDir>
@@ -149,12 +149,15 @@
<ClInclude Include="GitVer_cli.h" /> <ClInclude Include="GitVer_cli.h" />
<ClInclude Include="GitVer_common.h" /> <ClInclude Include="GitVer_common.h" />
<ClInclude Include="GitVer_nuitka.h" /> <ClInclude Include="GitVer_nuitka.h" />
<ClInclude Include="GitVer_pyinstaller.h" />
<ClInclude Include="GitVer_process.h" /> <ClInclude Include="GitVer_process.h" />
<ClInclude Include="GitVer_rewrite.h" /> <ClInclude Include="GitVer_rewrite.h" />
<ClInclude Include="GitVer_setup.h" /> <ClInclude Include="GitVer_setup.h" />
<ClInclude Include="GitVer_upload.h" />
<ClInclude Include="GitVer_tag.h" /> <ClInclude Include="GitVer_tag.h" />
<ClInclude Include="GitVer_types.h" /> <ClInclude Include="GitVer_types.h" />
<ClInclude Include="GitVer_version.h" /> <ClInclude Include="GitVer_version.h" />
<ClInclude Include="httplib.h" />
<ClInclude Include="pch.h" /> <ClInclude Include="pch.h" />
<ClInclude Include="Resource.h" /> <ClInclude Include="Resource.h" />
<ClInclude Include="targetver.h" /> <ClInclude Include="targetver.h" />
@@ -164,9 +167,11 @@
<ClCompile Include="GitVer_cli.cpp" /> <ClCompile Include="GitVer_cli.cpp" />
<ClCompile Include="GitVer_common.cpp" /> <ClCompile Include="GitVer_common.cpp" />
<ClCompile Include="GitVer_nuitka.cpp" /> <ClCompile Include="GitVer_nuitka.cpp" />
<ClCompile Include="GitVer_pyinstaller.cpp" />
<ClCompile Include="GitVer_process.cpp" /> <ClCompile Include="GitVer_process.cpp" />
<ClCompile Include="GitVer_rewrite.cpp" /> <ClCompile Include="GitVer_rewrite.cpp" />
<ClCompile Include="GitVer_setup.cpp" /> <ClCompile Include="GitVer_setup.cpp" />
<ClCompile Include="GitVer_upload.cpp" />
<ClCompile Include="GitVer_tag.cpp" /> <ClCompile Include="GitVer_tag.cpp" />
<ClCompile Include="GitVer_version.cpp" /> <ClCompile Include="GitVer_version.cpp" />
<ClCompile Include="pch.cpp"> <ClCompile Include="pch.cpp">

View File

@@ -39,6 +39,9 @@
<ClInclude Include="GitVer_nuitka.h"> <ClInclude Include="GitVer_nuitka.h">
<Filter>头文件</Filter> <Filter>头文件</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="GitVer_pyinstaller.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="GitVer_process.h"> <ClInclude Include="GitVer_process.h">
<Filter>头文件</Filter> <Filter>头文件</Filter>
</ClInclude> </ClInclude>
@@ -54,6 +57,15 @@
<ClInclude Include="GitVer_version.h"> <ClInclude Include="GitVer_version.h">
<Filter>头文件</Filter> <Filter>头文件</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="GitVer_setup.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="GitVer_upload.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="httplib.h">
<Filter>头文件</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="GitVer.cpp"> <ClCompile Include="GitVer.cpp">
@@ -71,6 +83,9 @@
<ClCompile Include="GitVer_nuitka.cpp"> <ClCompile Include="GitVer_nuitka.cpp">
<Filter>源文件</Filter> <Filter>源文件</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="GitVer_pyinstaller.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="GitVer_process.cpp"> <ClCompile Include="GitVer_process.cpp">
<Filter>源文件</Filter> <Filter>源文件</Filter>
</ClCompile> </ClCompile>
@@ -83,6 +98,12 @@
<ClCompile Include="GitVer_version.cpp"> <ClCompile Include="GitVer_version.cpp">
<Filter>源文件</Filter> <Filter>源文件</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="GitVer_setup.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="GitVer_upload.cpp">
<Filter>源文件</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ResourceCompile Include="GitVer.rc"> <ResourceCompile Include="GitVer.rc">

View File

@@ -5,11 +5,24 @@
extern TCHAR g_szCurModuleDir[MAX_PATH]; extern TCHAR g_szCurModuleDir[MAX_PATH];
/// <summary>
/// 打印无效仓库路径的错误信息。
/// </summary>
/// <param name="lpRepoPath">无效的路径字符串。</param>
void PrintInvalidRepoPathError(LPCTSTR lpRepoPath) void PrintInvalidRepoPathError(LPCTSTR lpRepoPath)
{ {
_tprintf(_T("错误: 无效的仓库路径 %s\n"), lpRepoPath == NULL ? _T("<null>") : lpRepoPath); _tprintf(_T("错误: 无效的仓库路径 %s\n"), lpRepoPath == NULL ? _T("<null>") : lpRepoPath);
} }
/// <summary>
/// 解析 setver / nuitka / pyinstaller 等命令的 repodir= 与 -test 可选参数。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <param name="nStartIndex">开始解析的下标。</param>
/// <param name="lpRepoPath">输出:仓库路径,未指定时为当前模块目录。</param>
/// <param name="bTestMode">输出:是否启用测试模式。</param>
/// <returns>0 成功3 多余参数23 无效 repodir。</returns>
int ParseSetVerOptions(int argc, TCHAR* argv[], int nStartIndex, LPCTSTR& lpRepoPath, BOOL& bTestMode) int ParseSetVerOptions(int argc, TCHAR* argv[], int nStartIndex, LPCTSTR& lpRepoPath, BOOL& bTestMode)
{ {
lpRepoPath = NULL; lpRepoPath = NULL;
@@ -24,12 +37,6 @@ int ParseSetVerOptions(int argc, TCHAR* argv[], int nStartIndex, LPCTSTR& lpRepo
continue; continue;
} }
// -setup=N 由调用方单独处理,此处静默跳过
if (strArg.GetLength() > 7 && strArg.Left(7).CompareNoCase(_T("-setup=")) == 0)
{
continue;
}
if (strArg.GetLength() > 8 && strArg.Left(8).CompareNoCase(_T("repodir=")) == 0) if (strArg.GetLength() > 8 && strArg.Left(8).CompareNoCase(_T("repodir=")) == 0)
{ {
if (lpRepoPath != NULL) if (lpRepoPath != NULL)
@@ -59,6 +66,14 @@ int ParseSetVerOptions(int argc, TCHAR* argv[], int nStartIndex, LPCTSTR& lpRepo
return 0; return 0;
} }
/// <summary>
/// 解析 rewrite 命令的可选 PE 类型1=exe2=dll与 -f 强制标志。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组argv[0] 为 "rewrite")。</param>
/// <param name="nPEType">输出PE 类型,默认 1。</param>
/// <param name="bForceRewrite">输出:是否忽略重写失败。</param>
/// <returns>0 成功22 PE 类型无效或重复。</returns>
int ParseRewriteOptions(int argc, TCHAR* argv[], int& nPEType, BOOL& bForceRewrite) int ParseRewriteOptions(int argc, TCHAR* argv[], int& nPEType, BOOL& bForceRewrite)
{ {
nPEType = 1; nPEType = 1;

View File

@@ -1,5 +1,16 @@
#pragma once #pragma once
/// <summary>
/// 打印无效仓库路径的错误信息。
/// </summary>
void PrintInvalidRepoPathError(LPCTSTR lpRepoPath); void PrintInvalidRepoPathError(LPCTSTR lpRepoPath);
/// <summary>
/// 解析 setver 等命令的 repodir= 与 -test 可选参数。
/// </summary>
int ParseSetVerOptions(int argc, TCHAR* argv[], int nStartIndex, LPCTSTR& lpRepoPath, BOOL& bTestMode); int ParseSetVerOptions(int argc, TCHAR* argv[], int nStartIndex, LPCTSTR& lpRepoPath, BOOL& bTestMode);
/// <summary>
/// 解析 rewrite 命令的 PE 类型与 -f 强制标志。
/// </summary>
int ParseRewriteOptions(int argc, TCHAR* argv[], int& nPEType, BOOL& bForceRewrite); int ParseRewriteOptions(int argc, TCHAR* argv[], int& nPEType, BOOL& bForceRewrite);

View File

@@ -2,6 +2,12 @@
#include "gitver.h" #include "gitver.h"
#include "gitver_common.h" #include "gitver_common.h"
/// <summary>
/// 将命令行参数拼接为便于日志输出的字符串(含引号转义)。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <returns>空格分隔、已转义的参数字符串。</returns>
CString BuildArgsForLog(int argc, TCHAR* argv[]) CString BuildArgsForLog(int argc, TCHAR* argv[])
{ {
CString strArgs; CString strArgs;
@@ -16,6 +22,11 @@ CString BuildArgsForLog(int argc, TCHAR* argv[])
return strArgs; return strArgs;
} }
/// <summary>
/// 记录命令开始时间并输出开始日志。
/// </summary>
/// <param name="lpCommandName">命令名称。</param>
/// <returns>当前 GetTickCount 值,供 LogCommandEnd 计算耗时。</returns>
DWORD LogCommandStart(LPCTSTR lpCommandName) DWORD LogCommandStart(LPCTSTR lpCommandName)
{ {
DWORD dwStartTick = GetTickCount(); DWORD dwStartTick = GetTickCount();
@@ -23,6 +34,11 @@ DWORD LogCommandStart(LPCTSTR lpCommandName)
return dwStartTick; return dwStartTick;
} }
/// <summary>
/// 为命令行参数添加引号转义(含空格、制表符或双引号时加引号)。
/// </summary>
/// <param name="strArg">原始参数字符串。</param>
/// <returns>可直接用于命令行的参数字符串。</returns>
CString QuoteCmdArg(const CString& strArg) CString QuoteCmdArg(const CString& strArg)
{ {
if (strArg.Find(_T(' ')) == -1 && strArg.Find(_T('\t')) == -1 && strArg.Find(_T('"')) == -1) if (strArg.Find(_T(' ')) == -1 && strArg.Find(_T('\t')) == -1 && strArg.Find(_T('"')) == -1)
@@ -37,6 +53,12 @@ CString QuoteCmdArg(const CString& strArg)
return strQuoted; return strQuoted;
} }
/// <summary>
/// 尝试将字符串解析为 0-65535 范围内的无符号整数。
/// </summary>
/// <param name="strValue">待解析字符串。</param>
/// <param name="nValue">输出:解析结果。</param>
/// <returns>解析成功返回 TRUE。</returns>
BOOL TryParseUInt16(const CString& strValue, UINT& nValue) BOOL TryParseUInt16(const CString& strValue, UINT& nValue)
{ {
CString strTrimmed = strValue; CString strTrimmed = strValue;
@@ -64,6 +86,15 @@ BOOL TryParseUInt16(const CString& strValue, UINT& nValue)
return TRUE; return TRUE;
} }
/// <summary>
/// 解析 argv[nIndex] 为 UInt16失败时打印错误并返回指定错误码。
/// </summary>
/// <param name="argv">参数数组。</param>
/// <param name="nIndex">要解析的参数下标。</param>
/// <param name="lpArgName">参数名(用于错误提示)。</param>
/// <param name="nValue">输出:解析结果。</param>
/// <param name="nErrorCode">解析失败时的退出码。</param>
/// <returns>0 成功,否则返回 nErrorCode。</returns>
int ParseUInt16ArgOrError(TCHAR* argv[], int nIndex, LPCTSTR lpArgName, UINT& nValue, int nErrorCode) int ParseUInt16ArgOrError(TCHAR* argv[], int nIndex, LPCTSTR lpArgName, UINT& nValue, int nErrorCode)
{ {
if (!TryParseUInt16(argv[nIndex], nValue)) if (!TryParseUInt16(argv[nIndex], nValue))
@@ -75,6 +106,11 @@ int ParseUInt16ArgOrError(TCHAR* argv[], int nIndex, LPCTSTR lpArgName, UINT& nV
return 0; return 0;
} }
/// <summary>
/// 判断参数字符串是否像路径(含 \、/、: 或以 . 开头)。
/// </summary>
/// <param name="lpArg">待判断的参数字符串。</param>
/// <returns>像路径则返回 TRUE。</returns>
BOOL IsLikelyPathArg(LPCTSTR lpArg) BOOL IsLikelyPathArg(LPCTSTR lpArg)
{ {
if (lpArg == NULL) if (lpArg == NULL)

View File

@@ -1,8 +1,31 @@
#pragma once #pragma once
/// <summary>
/// 将命令行参数拼接为便于日志输出的字符串。
/// </summary>
CString BuildArgsForLog(int argc, TCHAR* argv[]); CString BuildArgsForLog(int argc, TCHAR* argv[]);
/// <summary>
/// 记录命令开始时间并输出开始日志,返回 tick 供计算耗时。
/// </summary>
DWORD LogCommandStart(LPCTSTR lpCommandName); DWORD LogCommandStart(LPCTSTR lpCommandName);
/// <summary>
/// 为命令行参数添加引号转义。
/// </summary>
CString QuoteCmdArg(const CString& strArg); CString QuoteCmdArg(const CString& strArg);
/// <summary>
/// 尝试将字符串解析为 0-65535 范围内的无符号整数。
/// </summary>
BOOL TryParseUInt16(const CString& strValue, UINT& nValue); BOOL TryParseUInt16(const CString& strValue, UINT& nValue);
/// <summary>
/// 解析 argv[nIndex] 为 UInt16失败时打印错误并返回指定错误码。
/// </summary>
int ParseUInt16ArgOrError(TCHAR* argv[], int nIndex, LPCTSTR lpArgName, UINT& nValue, int nErrorCode); int ParseUInt16ArgOrError(TCHAR* argv[], int nIndex, LPCTSTR lpArgName, UINT& nValue, int nErrorCode);
/// <summary>
/// 判断参数字符串是否像路径。
/// </summary>
BOOL IsLikelyPathArg(LPCTSTR lpArg); BOOL IsLikelyPathArg(LPCTSTR lpArg);

View File

@@ -8,18 +8,25 @@
extern TCHAR g_szCurModuleDir[MAX_PATH]; extern TCHAR g_szCurModuleDir[MAX_PATH];
void PrintCommandUsageAndExamples(LPCTSTR lpCommand, LPCTSTR lpRequiredArgs, LPCTSTR lpEntryExample);
const UINT DEFAULT_MAJOR_WHEN_NO_TAG_FOR_NUITKA = 1; const UINT DEFAULT_MAJOR_WHEN_NO_TAG_FOR_NUITKA = 1;
const UINT DEFAULT_MINOR_WHEN_NO_TAG_FOR_NUITKA = 0; const UINT DEFAULT_MINOR_WHEN_NO_TAG_FOR_NUITKA = 0;
/// <summary>
/// 解析 nuitkabuild / nuitkapydbuild 命令的 repodir=、params= 与 -test 参数。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <param name="lpRepoPath">输出:仓库路径。</param>
/// <param name="strExtraArgs">输出:传给 Nuitka 的额外参数。</param>
/// <param name="bTestMode">输出:是否测试模式。</param>
/// <returns>0 成功3 未知/重复参数23 无效 repodir。</returns>
static int ParseNuitkaOptions(int argc, TCHAR* argv[], LPCTSTR& lpRepoPath, CString& strExtraArgs, BOOL& bTestMode) static int ParseNuitkaOptions(int argc, TCHAR* argv[], LPCTSTR& lpRepoPath, CString& strExtraArgs, BOOL& bTestMode)
{ {
lpRepoPath = NULL; lpRepoPath = NULL;
strExtraArgs.Empty(); strExtraArgs.Empty();
bTestMode = FALSE; bTestMode = FALSE;
for (int i = 3; i < argc; ++i) for (int i = 2; i < argc; ++i)
{ {
CString strArg = argv[i]; CString strArg = argv[i];
@@ -73,6 +80,14 @@ static int ParseNuitkaOptions(int argc, TCHAR* argv[], LPCTSTR& lpRepoPath, CStr
return 0; return 0;
} }
/// <summary>
/// 执行 Nuitka 构建命令并输出版本信息与构建结果。
/// </summary>
/// <param name="lpStartMessage">构建开始提示语。</param>
/// <param name="strNuitkaCmd">完整 Nuitka 命令行。</param>
/// <param name="strProductVersion">产品版本号(仅日志)。</param>
/// <param name="strFileVersion">文件版本号(仅日志)。</param>
/// <param name="lpRepoPath">工作目录。</param>
static void RunNuitkaBuild(LPCTSTR lpStartMessage, CString& strNuitkaCmd, const CString& strProductVersion, const CString& strFileVersion, LPCTSTR lpRepoPath) static void RunNuitkaBuild(LPCTSTR lpStartMessage, CString& strNuitkaCmd, const CString& strProductVersion, const CString& strFileVersion, LPCTSTR lpRepoPath)
{ {
_tprintf(_T("%s\n"), lpStartMessage); _tprintf(_T("%s\n"), lpStartMessage);
@@ -88,6 +103,20 @@ static void RunNuitkaBuild(LPCTSTR lpStartMessage, CString& strNuitkaCmd, const
} }
} }
/// <summary>
/// nuitkabuild / nuitkapydbuild 命令的公共实现:解析 pid、生成版本、调用 Nuitka。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <param name="nCmdPrefixLen">argv[1] 中 "xxx=" 前缀长度。</param>
/// <param name="bBuildModule">TRUE 时使用 --module 模式。</param>
/// <param name="lpStartMessage">构建开始提示语。</param>
/// <param name="lpUsageName">命令名(用于错误提示)。</param>
/// <param name="nPidErrorCode">pid 无效时的退出码。</param>
/// <param name="nBidErrorCode">获取 bid 失败时的退出码。</param>
/// <param name="nTagErrorCode">获取 Tag 失败时的退出码。</param>
/// <param name="nCommitErrorCode">获取提交信息失败时的退出码。</param>
/// <returns>0 成功,否则为对应错误码。</returns>
static int HandleNuitkaBuildCommandCore( static int HandleNuitkaBuildCommandCore(
int argc, int argc,
TCHAR* argv[], TCHAR* argv[],
@@ -95,8 +124,6 @@ static int HandleNuitkaBuildCommandCore(
BOOL bBuildModule, BOOL bBuildModule,
LPCTSTR lpStartMessage, LPCTSTR lpStartMessage,
LPCTSTR lpUsageName, LPCTSTR lpUsageName,
LPCTSTR lpEntryArgName,
int nArgLackErrorCode,
int nPidErrorCode, int nPidErrorCode,
int nBidErrorCode, int nBidErrorCode,
int nTagErrorCode, int nTagErrorCode,
@@ -111,14 +138,6 @@ static int HandleNuitkaBuildCommandCore(
return nPidErrorCode; return nPidErrorCode;
} }
if (argc < 3)
{
_tprintf(_T("错误: 参数不足,缺少 %s。\n"), lpEntryArgName);
_tprintf(_T("用法gitver %s=<pid> <%s> [repodir=<path>可选] [-test可选] [params=\"<nuitka参数>\"\u53ef选]\n"), lpUsageName, lpEntryArgName);
return nArgLackErrorCode;
}
CString strMainOrModulePy = argv[2];
LPCTSTR lpRepoPath = NULL; LPCTSTR lpRepoPath = NULL;
CString strExtraArgs; CString strExtraArgs;
BOOL bTestMode = FALSE; BOOL bTestMode = FALSE;
@@ -159,26 +178,30 @@ static int HandleNuitkaBuildCommandCore(
if (bBuildModule) if (bBuildModule)
{ {
strNuitkaCmd.Format( strNuitkaCmd.Format(
_T("cmd /c python -m nuitka --module --windows-product-version=%s --windows-file-version=%s %s %s"), _T("cmd /c python -m nuitka --module --windows-product-version=%s --windows-file-version=%s %s"),
strProductVersion.GetString(), strProductVersion.GetString(),
strFileVersion.GetString(), strFileVersion.GetString(),
strExtraArgs.GetString(), strExtraArgs.GetString());
QuoteCmdArg(strMainOrModulePy).GetString());
} }
else else
{ {
strNuitkaCmd.Format( strNuitkaCmd.Format(
_T("cmd /c python -m nuitka --windows-product-version=%s --windows-file-version=%s %s %s"), _T("cmd /c python -m nuitka --windows-product-version=%s --windows-file-version=%s %s"),
strProductVersion.GetString(), strProductVersion.GetString(),
strFileVersion.GetString(), strFileVersion.GetString(),
strExtraArgs.GetString(), strExtraArgs.GetString());
QuoteCmdArg(strMainOrModulePy).GetString());
} }
RunNuitkaBuild(lpStartMessage, strNuitkaCmd, strProductVersion, strFileVersion, lpRepoPath); RunNuitkaBuild(lpStartMessage, strNuitkaCmd, strProductVersion, strFileVersion, lpRepoPath);
return 0; return 0;
} }
/// <summary>
/// 处理 nuitkabuild=pid 命令standalone 可执行文件打包。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <returns>0 成功17-21 等为各类错误码。</returns>
int HandleNuitkaBuildCommand(int argc, TCHAR* argv[]) int HandleNuitkaBuildCommand(int argc, TCHAR* argv[])
{ {
return HandleNuitkaBuildCommandCore( return HandleNuitkaBuildCommandCore(
@@ -188,14 +211,18 @@ int HandleNuitkaBuildCommand(int argc, TCHAR* argv[])
FALSE, FALSE,
_T("Nuitka 打包开始.."), _T("Nuitka 打包开始.."),
_T("nuitkabuild"), _T("nuitkabuild"),
_T("mainPy"),
17,
18, 18,
19, 19,
20, 20,
21); 21);
} }
/// <summary>
/// 处理 nuitkapydbuild=pid 命令Python 扩展模块(.pyd打包。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <returns>0 成功24-28 等为各类错误码。</returns>
int HandleNuitkaPydBuildCommand(int argc, TCHAR* argv[]) int HandleNuitkaPydBuildCommand(int argc, TCHAR* argv[])
{ {
return HandleNuitkaBuildCommandCore( return HandleNuitkaBuildCommandCore(
@@ -205,8 +232,6 @@ int HandleNuitkaPydBuildCommand(int argc, TCHAR* argv[])
TRUE, TRUE,
_T("Nuitka pyd 打包开始.."), _T("Nuitka pyd 打包开始.."),
_T("nuitkapydbuild"), _T("nuitkapydbuild"),
_T("modulePy"),
24,
25, 25,
26, 26,
27, 27,

View File

@@ -1,4 +1,11 @@
#pragma once #pragma once
/// <summary>
/// 处理 nuitkabuild=pid 命令standalone 可执行文件打包。
/// </summary>
int HandleNuitkaBuildCommand(int argc, TCHAR* argv[]); int HandleNuitkaBuildCommand(int argc, TCHAR* argv[]);
/// <summary>
/// 处理 nuitkapydbuild=pid 命令Python 扩展模块(.pyd打包。
/// </summary>
int HandleNuitkaPydBuildCommand(int argc, TCHAR* argv[]); int HandleNuitkaPydBuildCommand(int argc, TCHAR* argv[]);

View File

@@ -2,6 +2,13 @@
#include "gitver.h" #include "gitver.h"
#include "gitver_process.h" #include "gitver_process.h"
/// <summary>
/// 按顺序将文件中多处旧字符串替换为新字符串并写回。
/// </summary>
/// <param name="lpFile">目标文件路径。</param>
/// <param name="vtOldContent">待查找的旧内容列表。</param>
/// <param name="vtNewContent">对应的新内容列表。</param>
/// <returns>全部替换成功返回 TRUE。</returns>
BOOL ReplaceFileContent(LPCTSTR lpFile, std::vector<std::string>& vtOldContent, std::vector<std::string>& vtNewContent) BOOL ReplaceFileContent(LPCTSTR lpFile, std::vector<std::string>& vtOldContent, std::vector<std::string>& vtNewContent)
{ {
if (vtOldContent.size() != vtNewContent.size()) if (vtOldContent.size() != vtNewContent.size())
@@ -69,9 +76,74 @@ BOOL ReplaceFileContent(LPCTSTR lpFile, std::vector<std::string>& vtOldContent,
return TRUE; return TRUE;
} }
CString StartProcess(LPCTSTR program, LPCTSTR args, LPCTSTR lpCurrentDirectory) /// <summary>
/// 将子进程管道输出的原始字节按指定代码页解码为 CString。
/// </summary>
/// <param name="strRawOutput">管道原始输出字节。</param>
/// <param name="nOutputCodePage">解码代码页CP_UTF8 或 CP_ACP 等)。</param>
/// <returns>解码后的字符串。</returns>
static CString DecodeProcessOutput(const std::string& strRawOutput, UINT nOutputCodePage)
{ {
CString strValue = _T(""); if (strRawOutput.empty())
{
return CString();
}
if (nOutputCodePage == CP_UTF8)
{
int nWideSize = MultiByteToWideChar(CP_UTF8, 0, strRawOutput.data(), (int)strRawOutput.size(), NULL, 0);
if (nWideSize <= 0)
{
return CString();
}
CStringW strWide;
MultiByteToWideChar(CP_UTF8, 0, strRawOutput.data(), (int)strRawOutput.size(), strWide.GetBuffer(nWideSize), nWideSize);
strWide.ReleaseBuffer(nWideSize);
#ifdef UNICODE
return CString(strWide);
#else
int nGbkSize = WideCharToMultiByte(CP_ACP, 0, strWide, strWide.GetLength(), NULL, 0, NULL, NULL);
if (nGbkSize <= 0)
{
return CString();
}
std::string strGbk;
strGbk.resize(nGbkSize);
WideCharToMultiByte(CP_ACP, 0, strWide, strWide.GetLength(), &strGbk[0], nGbkSize, NULL, NULL);
return CString(strGbk.c_str(), (int)strGbk.size());
#endif
}
#ifdef UNICODE
int nSize = MultiByteToWideChar(nOutputCodePage, 0, strRawOutput.data(), (int)strRawOutput.size(), NULL, 0);
if (nSize <= 0)
{
return CString();
}
CString strValue;
MultiByteToWideChar(nOutputCodePage, 0, strRawOutput.data(), (int)strRawOutput.size(), strValue.GetBuffer(nSize), nSize);
strValue.ReleaseBuffer(nSize);
return strValue;
#else
return CString(strRawOutput.c_str(), (int)strRawOutput.size());
#endif
}
/// <summary>
/// 启动子进程并捕获 stdout/stderr按代码页解码后返回输出。
/// </summary>
/// <param name="program">可执行程序路径NULL 时使用 args 中的命令。</param>
/// <param name="args">命令行参数。</param>
/// <param name="lpCurrentDirectory">工作目录。</param>
/// <param name="nOutputCodePage">输出解码代码页,默认 CP_ACP。</param>
/// <returns>子进程的标准输出/错误合并文本。</returns>
CString StartProcess(LPCTSTR program, LPCTSTR args, LPCTSTR lpCurrentDirectory, UINT nOutputCodePage)
{
std::string strRawOutput;
const int MY_PIPE_BUFFER_SIZE = 8912; const int MY_PIPE_BUFFER_SIZE = 8912;
CString strMutableArgs = args == NULL ? _T("") : args; CString strMutableArgs = args == NULL ? _T("") : args;
LPTSTR lpArgs = strMutableArgs.IsEmpty() ? NULL : strMutableArgs.GetBuffer(); LPTSTR lpArgs = strMutableArgs.IsEmpty() ? NULL : strMutableArgs.GetBuffer();
@@ -128,12 +200,7 @@ CString StartProcess(LPCTSTR program, LPCTSTR args, LPCTSTR lpCurrentDirectory)
break; break;
} }
szPipeOut[dwStdLen] = '\0'; strRawOutput.append(szPipeOut, dwStdLen);
#ifdef UNICODE
strValue.AppendFormat(_T("%hs"), szPipeOut);
#else
strValue += szPipeOut;
#endif
} }
WaitForSingleObject(processInfo.hProcess, INFINITE); WaitForSingleObject(processInfo.hProcess, INFINITE);
@@ -160,6 +227,8 @@ CString StartProcess(LPCTSTR program, LPCTSTR args, LPCTSTR lpCurrentDirectory)
strMutableArgs.ReleaseBuffer(); strMutableArgs.ReleaseBuffer();
} }
CString strValue = DecodeProcessOutput(strRawOutput, nOutputCodePage);
_tprintf(_T("StartProcess program=%s args=%s cwd=%s output=%s\n"), _tprintf(_T("StartProcess program=%s args=%s cwd=%s output=%s\n"),
program == NULL ? _T("<null>") : program, program == NULL ? _T("<null>") : program,
args == NULL ? _T("<null>") : args, args == NULL ? _T("<null>") : args,

View File

@@ -1,4 +1,13 @@
#pragma once #pragma once
/// <summary>
/// 按顺序将文件中多处旧字符串替换为新字符串并写回。
/// </summary>
BOOL ReplaceFileContent(LPCTSTR lpFile, std::vector<std::string>& vtOldContent, std::vector<std::string>& vtNewContent); BOOL ReplaceFileContent(LPCTSTR lpFile, std::vector<std::string>& vtOldContent, std::vector<std::string>& vtNewContent);
CString StartProcess(LPCTSTR program, LPCTSTR args, LPCTSTR lpCurrentDirectory);
/// <summary>
/// 启动子进程并捕获 stdout/stderr。
/// nOutputCodePage 为管道原始字节的解码代码页,默认 CP_ACP按系统 ANSI/GBK 直接使用);
/// CP_UTF8 时先按 UTF-8 解码为宽字符MultiByte 工程再转为 GBK 存入 CString。
/// </summary>
CString StartProcess(LPCTSTR program, LPCTSTR args, LPCTSTR lpCurrentDirectory, UINT nOutputCodePage = CP_ACP);

View File

@@ -0,0 +1,786 @@
#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;
/// <summary>
/// 解析 pyinstaller 命令的 repodir=、params= 与 -test 参数。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <param name="lpRepoPath">输出:仓库路径。</param>
/// <param name="strExtraArgs">输出:传给 PyInstaller 的参数。</param>
/// <param name="bTestMode">输出:是否测试模式。</param>
/// <returns>0 成功3 未知/重复参数23 无效 repodir。</returns>
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;
}
/// <summary>
/// 解析版本字符串 "a.b.c.d" 为四个无符号整数分量。
/// </summary>
/// <param name="lpVer">版本字符串。</param>
/// <param name="v1">输出:第 1 段。</param>
/// <param name="v2">输出:第 2 段。</param>
/// <param name="v3">输出:第 3 段。</param>
/// <param name="v4">输出:第 4 段。</param>
/// <returns>格式正确返回 TRUE。</returns>
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;
}
/// <summary>
/// 提取 params 字符串中第一个 token支持双引号括起的路径
/// </summary>
/// <param name="strParams">完整 params 参数字符串。</param>
/// <returns>第一个 token空输入返回空字符串。</returns>
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);
}
/// <summary>
/// 在指定目录生成 PyInstaller VSVersionInfo 格式的 version_info.txt。
/// </summary>
/// <param name="lpDir">输出目录。</param>
/// <param name="lpProductVersion">产品版本号。</param>
/// <param name="lpFileVersion">文件版本号。</param>
/// <param name="strOutPath">输出:生成的文件完整路径。</param>
/// <returns>成功返回 TRUE。</returns>
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;
}
/// <summary>
/// 向 .spec 文件注入或更新 version='version_info.txt' 参数。
/// 已有 version= 则更新整行,否则在 name= 行后插入。
/// </summary>
/// <param name="lpSpecPath">.spec 文件路径。</param>
/// <returns>成功返回 TRUE。</returns>
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;
}
/// <summary>
/// 从 .spec 文件读取 version= 参数引号内的外部版本信息文件路径。
/// </summary>
/// <param name="lpSpecPath">.spec 文件路径。</param>
/// <param name="strVersionPath">输出version= 指向的路径。</param>
/// <returns>找到有效引号路径返回 TRUENone 或空值返回 FALSE。</returns>
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;
}
/// <summary>
/// 更新已有 version_info 文件的版本字段;文件不存在时按模板创建。
/// </summary>
/// <param name="lpPath">version_info.txt 或内联 spec 路径。</param>
/// <param name="lpProductVersion">产品版本号。</param>
/// <param name="lpFileVersion">文件版本号。</param>
/// <returns>成功返回 TRUE。</returns>
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);
}
else
{// 可能不带u
const std::string key = "'FileVersion', '";
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);
}
else
{//可能不带u
const std::string key = "'ProductVersion', '";
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;
}
/// <summary>
/// 检查 .spec 中 version= 是否为无引号的 Python 标识符(内联 VSVersionInfo 对象名)。
/// </summary>
/// <param name="lpSpecPath">.spec 文件路径。</param>
/// <returns>是内联版本块引用返回 TRUE。</returns>
static BOOL HasInlineVersionInfoBlock(LPCTSTR lpSpecPath)
{
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;
while (nValStart < strContent.size() && strContent[nValStart] == ' ')
nValStart++;
if (nValStart < strContent.size())
{
char c = strContent[nValStart];
// 无引号且以字母或下划线开头,视为 Python 标识符
if (c != '\'' && c != '"' && (isalpha((unsigned char)c) || c == '_'))
return TRUE;
}
break;
}
nPos += 8;
}
return FALSE;
}
/// <summary>
/// 替换 .spec 文件中 version= 引号内的内联版本号字符串。
/// </summary>
/// <param name="lpSpecPath">.spec 文件路径。</param>
/// <param name="lpNewValue">新版本号字符串。</param>
/// <returns>找到并替换返回 TRUE。</returns>
static BOOL UpdateVersionStringInSpec(LPCTSTR lpSpecPath, LPCTSTR lpNewValue)
{
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);
CStringA strNewValueA(lpNewValue);
std::string strNewValue(strNewValueA);
std::string::size_type nPos = 0;
bool bUpdated = false;
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())
{
char cQuote = strContent[nValStart];
if (cQuote == '\'' || cQuote == '"')
{
std::string::size_type nValEnd = strContent.find(cQuote, nValStart + 1);
if (nValEnd != std::string::npos)
{
strContent.replace(nValStart + 1, nValEnd - nValStart - 1, strNewValue);
bUpdated = true;
_tprintf(_T("成功: 已更新 spec 内联版本号 -> %s\n"), lpNewValue);
break;
}
}
}
break;
}
nPos += 8;
}
if (!bUpdated)
{
_tprintf(_T("错误: 未在 spec 文件中找到可替换的 version= 引号值。\n"));
return FALSE;
}
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;
}
/// <summary>
/// 处理 pyinstaller=pid 命令:生成版本、处理 spec/version_info、调用 PyInstaller 打包。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <returns>0 成功40-46 等为各类错误码。</returns>
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 (PathFileExists(strVersionInfoFullPath))
{
// 外部版本信息文件存在,直接更新
if (!UpdateVersionInfoFile(strVersionInfoFullPath, strProductVersion, strFileVersion))
{
_tprintf(_T("错误: 无法更新版本信息文件: %s。\n"), strVersionInfoFullPath.GetString());
return 45;
}
_tprintf(_T("版本信息文件已更新: %s\n"), strVersionInfoFullPath.GetString());
}
else
{
// version= 后的值不是有效的外部文件路径,视为 spec 内联版本号定义,直接修改 spec
_tprintf(_T("版本信息文件未找到(%s将修改 spec 内联版本号。\n"), strVersionInfoFullPath.GetString());
if (!UpdateVersionStringInSpec(strSpecPath, strFileVersion))
{
_tprintf(_T("错误: 无法修改 spec 文件内联版本号。\n"));
return 46;
}
}
}
else if (HasInlineVersionInfoBlock(strSpecPath))
{
// spec 中 version=<标识符>,标识符指向 spec 内定义的 VSVersionInfo 对象,直接更新 spec 文件中的版本字段
if (!UpdateVersionInfoFile(strSpecPath, strProductVersion, strFileVersion))
{
_tprintf(_T("错误: 无法更新 spec 内联版本块: %s。\n"), strSpecPath.GetString());
return 45;
}
_tprintf(_T("spec 内联版本块已更新: %s\n"), strSpecPath.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;
}

View File

@@ -0,0 +1,6 @@
#pragma once
/// <summary>
/// 处理 pyinstaller=pid 命令:生成版本信息并调用 PyInstaller 打包。
/// </summary>
int HandlePyinstallerBuildCommand(int argc, TCHAR* argv[]);

View File

@@ -14,6 +14,12 @@ LPCTSTR GetProjectCodeTypeName(int nCodeType);
/// <summary> /// <summary>
/// 替换指定范围内包含指定标记的整行内容。 /// 替换指定范围内包含指定标记的整行内容。
/// </summary> /// </summary>
/// <param name="strContent">文件内容(原地修改)。</param>
/// <param name="strToken">行内查找标记。</param>
/// <param name="strNewLine">替换后的整行文本。</param>
/// <param name="nRangeStart">搜索范围起始偏移。</param>
/// <param name="nRangeEnd">搜索范围结束偏移。</param>
/// <returns>找到并替换返回 TRUE。</returns>
BOOL ReplaceWholeLineByTokenInRange(std::string& strContent, const std::string& strToken, const std::string& strNewLine, size_t nRangeStart, size_t nRangeEnd) BOOL ReplaceWholeLineByTokenInRange(std::string& strContent, const std::string& strToken, const std::string& strNewLine, size_t nRangeStart, size_t nRangeEnd)
{ {
if (nRangeStart == std::string::npos || nRangeEnd == std::string::npos || nRangeStart >= nRangeEnd) if (nRangeStart == std::string::npos || nRangeEnd == std::string::npos || nRangeStart >= nRangeEnd)
@@ -54,11 +60,17 @@ BOOL ReplaceWholeLineByTokenInRange(std::string& strContent, const std::string&
return TRUE; return TRUE;
} }
/// <summary>
/// 在全文件范围内替换包含指定标记的整行。
/// </summary>
BOOL ReplaceWholeLineByToken(std::string& strContent, const std::string& strToken, const std::string& strNewLine) BOOL ReplaceWholeLineByToken(std::string& strContent, const std::string& strToken, const std::string& strNewLine)
{ {
return ReplaceWholeLineByTokenInRange(strContent, strToken, strNewLine, 0, strContent.size()); return ReplaceWholeLineByTokenInRange(strContent, strToken, strNewLine, 0, strContent.size());
} }
/// <summary>
/// 替换首个匹配行,并删除后续所有含同一标记的重复行。
/// </summary>
BOOL EnsureSingleLineByToken(std::string& strContent, const std::string& strToken, const std::string& strNewLine) BOOL EnsureSingleLineByToken(std::string& strContent, const std::string& strToken, const std::string& strNewLine)
{ {
std::string::size_type nPos = strContent.find(strToken); std::string::size_type nPos = strContent.find(strToken);
@@ -127,6 +139,9 @@ BOOL EnsureSingleLineByToken(std::string& strContent, const std::string& strToke
return TRUE; return TRUE;
} }
/// <summary>
/// 若存在含标记的行则替换并去重,否则在文件末尾追加新行。
/// </summary>
BOOL EnsureSingleLineByTokenOrAppend(std::string& strContent, const std::string& strToken, const std::string& strNewLine) BOOL EnsureSingleLineByTokenOrAppend(std::string& strContent, const std::string& strToken, const std::string& strNewLine)
{ {
if (strContent.find(strToken) != std::string::npos) if (strContent.find(strToken) != std::string::npos)
@@ -147,6 +162,13 @@ BOOL EnsureSingleLineByTokenOrAppend(std::string& strContent, const std::string&
return TRUE; return TRUE;
} }
/// <summary>
/// 定位 .rc 文件中 VS_VERSION_INFO VERSIONINFO 块的 BEGIN…END 字节范围。
/// </summary>
/// <param name="strContent">.rc 文件内容。</param>
/// <param name="nBlockStart">输出:块起始偏移。</param>
/// <param name="nBlockEnd">输出:块结束偏移(不含末尾换行之后)。</param>
/// <returns>找到完整块返回 TRUE。</returns>
BOOL FindVersionInfoBlockRange(const std::string& strContent, size_t& nBlockStart, size_t& nBlockEnd) BOOL FindVersionInfoBlockRange(const std::string& strContent, size_t& nBlockStart, size_t& nBlockEnd)
{ {
nBlockStart = std::string::npos; nBlockStart = std::string::npos;
@@ -210,6 +232,12 @@ BOOL FindVersionInfoBlockRange(const std::string& strContent, size_t& nBlockStar
return FALSE; return FALSE;
} }
/// <summary>
/// 检测文本文件 BOM 类型UTF-16 BOM 视为不支持。
/// </summary>
/// <param name="strData">文件原始字节。</param>
/// <param name="nBomLength">输出BOM 长度(无 BOM 时为 0。</param>
/// <returns>支持的编码返回 TRUEUTF-16 返回 FALSE。</returns>
BOOL DetectSupportedTextBom(const std::string& strData, size_t& nBomLength) BOOL DetectSupportedTextBom(const std::string& strData, size_t& nBomLength)
{ {
nBomLength = 0; nBomLength = 0;
@@ -236,6 +264,13 @@ BOOL DetectSupportedTextBom(const std::string& strData, size_t& nBomLength)
return TRUE; return TRUE;
} }
/// <summary>
/// 以 ANSI/UTF-8带 BOM 时保留前缀)读取文本文件。
/// </summary>
/// <param name="lpFile">文件路径。</param>
/// <param name="strContent">输出:不含 BOM 的正文。</param>
/// <param name="strBomPrefix">输出:原始 BOM 前缀,写回时需保留。</param>
/// <returns>读取成功返回 TRUE。</returns>
BOOL ReadTextFileAsAnsi(LPCTSTR lpFile, std::string& strContent, std::string& strBomPrefix) BOOL ReadTextFileAsAnsi(LPCTSTR lpFile, std::string& strContent, std::string& strBomPrefix)
{ {
strContent.clear(); strContent.clear();
@@ -284,6 +319,13 @@ BOOL ReadTextFileAsAnsi(LPCTSTR lpFile, std::string& strContent, std::string& st
return TRUE; return TRUE;
} }
/// <summary>
/// 将文本内容与 BOM 前缀写回文件。
/// </summary>
/// <param name="lpFile">文件路径。</param>
/// <param name="strContent">正文内容。</param>
/// <param name="strBomPrefix">BOM 前缀(可为空)。</param>
/// <returns>写入成功返回 TRUE。</returns>
BOOL WriteTextFileAsAnsi(LPCTSTR lpFile, const std::string& strContent, const std::string& strBomPrefix) BOOL WriteTextFileAsAnsi(LPCTSTR lpFile, const std::string& strContent, const std::string& strBomPrefix)
{ {
CFile myFile; CFile myFile;
@@ -303,6 +345,13 @@ BOOL WriteTextFileAsAnsi(LPCTSTR lpFile, const std::string& strContent, const st
return TRUE; return TRUE;
} }
/// <summary>
/// 将 ProductVersion / FileVersion 写入 C++ .rc 文件的 VERSIONINFO 块。
/// </summary>
/// <param name="strRcFile">.rc 文件路径。</param>
/// <param name="strProductVersion">产品版本号。</param>
/// <param name="strFileVersion">文件版本号。</param>
/// <returns>成功返回 TRUE。</returns>
BOOL RewriteSetVerToCppRc(const CString& strRcFile, const CString& strProductVersion, const CString& strFileVersion) BOOL RewriteSetVerToCppRc(const CString& strRcFile, const CString& strProductVersion, const CString& strFileVersion)
{ {
std::string strContent; std::string strContent;
@@ -356,6 +405,13 @@ BOOL RewriteSetVerToCppRc(const CString& strRcFile, const CString& strProductVer
return TRUE; return TRUE;
} }
/// <summary>
/// 将 ProductVersion / FileVersion 写入 C# AssemblyInfo.cs。
/// </summary>
/// <param name="strAssemblyInfoFile">AssemblyInfo.cs 路径。</param>
/// <param name="strProductVersion">产品版本号AssemblyVersion。</param>
/// <param name="strFileVersion">文件版本号AssemblyFileVersion。</param>
/// <returns>成功返回 TRUE。</returns>
BOOL RewriteSetVerToAssemblyInfo(const CString& strAssemblyInfoFile, const CString& strProductVersion, const CString& strFileVersion) BOOL RewriteSetVerToAssemblyInfo(const CString& strAssemblyInfoFile, const CString& strProductVersion, const CString& strFileVersion)
{ {
std::string strContent; std::string strContent;
@@ -392,6 +448,13 @@ BOOL RewriteSetVerToAssemblyInfo(const CString& strAssemblyInfoFile, const CStri
return TRUE; return TRUE;
} }
/// <summary>
/// 按项目类型C++ / C#)将版本号写入对应资源文件。
/// </summary>
/// <param name="lpRepoPath">项目根目录NULL 时使用当前模块目录。</param>
/// <param name="strProductVersion">产品版本号。</param>
/// <param name="strFileVersion">文件版本号。</param>
/// <returns>0 成功15 不支持的项目类型35 写版本失败。</returns>
int RewriteSetVerByProjectType(LPCTSTR lpRepoPath, const CString& strProductVersion, const CString& strFileVersion) int RewriteSetVerByProjectType(LPCTSTR lpRepoPath, const CString& strProductVersion, const CString& strFileVersion)
{ {
LPCTSTR lpBaseDir = lpRepoPath == NULL ? g_szCurModuleDir : lpRepoPath; LPCTSTR lpBaseDir = lpRepoPath == NULL ? g_szCurModuleDir : lpRepoPath;

View File

@@ -1,3 +1,6 @@
#pragma once #pragma once
/// <summary>
/// 按项目类型C++ / C#)将版本号写入对应资源文件。
/// </summary>
int RewriteSetVerByProjectType(LPCTSTR lpRepoPath, const CString& strProductVersion, const CString& strFileVersion); int RewriteSetVerByProjectType(LPCTSTR lpRepoPath, const CString& strProductVersion, const CString& strFileVersion);

View File

@@ -5,6 +5,7 @@
#include "gitver_cli.h" #include "gitver_cli.h"
#include "gitver_version.h" #include "gitver_version.h"
#include "gitver_process.h" #include "gitver_process.h"
#include "gitver_upload.h"
extern TCHAR g_szCurModuleDir[MAX_PATH]; extern TCHAR g_szCurModuleDir[MAX_PATH];
@@ -15,6 +16,9 @@ BOOL WriteTextFileAsAnsi(LPCTSTR lpFile, const std::string& strContent, const st
void PrintInvalidRepoPathError(LPCTSTR lpRepoPath); void PrintInvalidRepoPathError(LPCTSTR lpRepoPath);
int ParseSetVerOptions(int argc, TCHAR* argv[], int nStartIndex, LPCTSTR& lpRepoPath, BOOL& bTestMode); int ParseSetVerOptions(int argc, TCHAR* argv[], int nStartIndex, LPCTSTR& lpRepoPath, BOOL& bTestMode);
BOOL FindPreferredCppRcFileAt(LPCTSTR lpBaseDir, CString& strResFile);
BOOL FindPreferredAssemblyInfoFileAt(LPCTSTR lpBaseDir, CString& strResFile);
BOOL FindFirstFileByPatternRecursive(LPCTSTR lpBaseDir, LPCTSTR lpPattern, CString& strFoundPath);
static const UINT DEFAULT_MAJOR_WHEN_NO_TAG_FOR_SETUP = 1; static const UINT DEFAULT_MAJOR_WHEN_NO_TAG_FOR_SETUP = 1;
static const UINT DEFAULT_MINOR_WHEN_NO_TAG_FOR_SETUP = 0; static const UINT DEFAULT_MINOR_WHEN_NO_TAG_FOR_SETUP = 0;
@@ -27,9 +31,7 @@ static const UINT DEFAULT_MINOR_WHEN_NO_TAG_FOR_SETUP = 0;
/// 在字符串内容中查找首个包含 strToken 的整行, /// 在字符串内容中查找首个包含 strToken 的整行,
/// 用 strNewLine 替换该行(不含行尾换行符)。 /// 用 strNewLine 替换该行(不含行尾换行符)。
/// </summary> /// </summary>
static BOOL ReplaceLineByToken(std::string& strContent, static BOOL ReplaceLineByToken(std::string& strContent, const std::string& strToken, const std::string& strNewLine)
const std::string& strToken,
const std::string& strNewLine)
{ {
std::string::size_type nPos = strContent.find(strToken); std::string::size_type nPos = strContent.find(strToken);
if (nPos == std::string::npos) if (nPos == std::string::npos)
@@ -246,15 +248,269 @@ static CString FindNsisCompiler()
} }
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
// 公共执行函数(供 HandleSetupCommand 和 setver -setup=N 共用) // 从项目文件读取版本号(当 setup=n 未指定 pid 时使用)
// ─────────────────────────────────────────────
/// <summary>
/// 从 strContent 中查找 strToken 之后第一个双引号括起来的值。
/// </summary>
static CString ExtractQuotedValueAfterToken(const std::string& strContent, const std::string& strToken)
{
std::string::size_type nPos = strContent.find(strToken);
if (nPos == std::string::npos)
return CString();
std::string::size_type nStart = strContent.find('"', nPos + strToken.size());
if (nStart == std::string::npos)
return CString();
++nStart;
std::string::size_type nEnd = strContent.find('"', nStart);
if (nEnd == std::string::npos)
return CString();
return CString(strContent.substr(nStart, nEnd - nStart).c_str());
}
/// <summary>
/// 从 .rc / AssemblyInfo.cs / version_info.txt / .spec 文件中读取版本号字符串。
/// 优先顺序:.rc > AssemblyInfo.cs > version_info.txt/.spec。
/// </summary>
static BOOL ReadVersionFromProjectFiles(LPCTSTR lpBaseDir, CString& strVersion)
{
strVersion.Empty();
std::string strContent, strBomPrefix;
// 1. .rc 文件 -> VALUE "FileVersion", "x.x.x.x"
CString strRcFile;
if (FindPreferredCppRcFileAt(lpBaseDir, strRcFile))
{
if (ReadTextFileAsAnsi(strRcFile, strContent, strBomPrefix))
{
CString strVal = ExtractQuotedValueAfterToken(strContent, "VALUE \"FileVersion\",");
if (!strVal.IsEmpty())
{
strVersion = strVal;
_tprintf(_T("从 .rc 文件读取版本号: %s\n"), strVersion.GetString());
return TRUE;
}
}
}
// 2. AssemblyInfo.cs -> AssemblyFileVersion("x.x.x.x")
CString strAiFile;
if (FindPreferredAssemblyInfoFileAt(lpBaseDir, strAiFile))
{
if (ReadTextFileAsAnsi(strAiFile, strContent, strBomPrefix))
{
CString strVal = ExtractQuotedValueAfterToken(strContent, "AssemblyFileVersion(");
if (!strVal.IsEmpty())
{
strVersion = strVal;
_tprintf(_T("从 AssemblyInfo.cs 读取版本号: %s\n"), strVersion.GetString());
return TRUE;
}
}
}
// 3. version_info.txt / .spec -> filevers=(x, x, x, x)
CString strSpecFile;
CString strBaseWithSlash = lpBaseDir;
if (!strBaseWithSlash.IsEmpty() && strBaseWithSlash.Right(1) != _T("\\"))
strBaseWithSlash += _T("\\");
if (FindFirstFileByPatternRecursive(strBaseWithSlash, _T("version_info.txt"), strSpecFile) ||
FindFirstFileByPatternRecursive(strBaseWithSlash, _T("*.spec"), strSpecFile))
{
if (ReadTextFileAsAnsi(strSpecFile, strContent, strBomPrefix))
{
std::string::size_type nPos = strContent.find("filevers=(");
if (nPos != std::string::npos)
{
nPos += 10;
std::string::size_type nEnd = strContent.find(')', nPos);
if (nEnd != std::string::npos)
{
std::string strTuple = strContent.substr(nPos, nEnd - nPos);
std::string strClean;
for (char c : strTuple)
if (c != ' ') strClean += (c == ',' ? '.' : c);
strVersion = CString(strClean.c_str());
_tprintf(_T("从 version_info.txt/.spec 读取版本号: %s\n"), strVersion.GetString());
return TRUE;
}
}
}
}
return FALSE;
}
// ─────────────────────────────────────────────
// 解析编译输出中的安装包路径
// ─────────────────────────────────────────────
/// <summary>
/// 从文本中提取首个双引号括起来的路径;若无引号则返回原文本。
/// </summary>
/// <param name="strText">待解析的文本行。</param>
/// <returns>提取出的路径字符串。</returns>
static CString ExtractQuotedPath(const CString& strText)
{
int nStart = strText.Find(_T('"'));
if (nStart < 0)
{
return strText;
}
int nEnd = strText.Find(_T('"'), nStart + 1);
if (nEnd <= nStart)
{
return strText;
}
return strText.Mid(nStart + 1, nEnd - nStart - 1);
}
/// <summary>
/// 在指定目录中查找修改时间最新的 .exe 文件。
/// </summary>
/// <param name="lpDir">要搜索的目录路径。</param>
/// <param name="strExePath">输出:找到的最新 exe 完整路径。</param>
/// <returns>找到则返回 TRUE否则返回 FALSE。</returns>
static BOOL FindNewestExeInDirectory(LPCTSTR lpDir, CString& strExePath)
{
strExePath.Empty();
if (lpDir == NULL || lpDir[0] == _T('\0') || !PathIsDirectory(lpDir))
{
return FALSE;
}
CString strPattern;
strPattern.Format(_T("%s\\*.exe"), lpDir);
WIN32_FIND_DATA findData = { 0 };
HANDLE hFind = ::FindFirstFile(strPattern, &findData);
if (hFind == INVALID_HANDLE_VALUE)
{
return FALSE;
}
FILETIME ftNewest = { 0 };
BOOL bFound = FALSE;
do
{
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
continue;
}
if (!bFound || CompareFileTime(&findData.ftLastWriteTime, &ftNewest) > 0)
{
ftNewest = findData.ftLastWriteTime;
strExePath.Format(_T("%s\\%s"), lpDir, findData.cFileName);
bFound = TRUE;
}
} while (::FindNextFile(hFind, &findData));
::FindClose(hFind);
return bFound && PathFileExists(strExePath);
}
/// <summary>
/// 从 Inno Setup / NSIS 编译输出中解析安装包路径。
/// 优先匹配编译器输出的路径行,失败则回退到 Output 目录或脚本目录中最新的 exe。
/// </summary>
/// <param name="nSetupType">0=Inno Setup1=NSIS。</param>
/// <param name="strOutput">编译器标准输出。</param>
/// <param name="strScriptDir">安装脚本所在目录。</param>
/// <param name="strInstallerPath">输出:解析到的安装包路径。</param>
/// <returns>成功解析到有效路径则返回 TRUE。</returns>
static BOOL ParseInstallerPathFromBuildOutput(int nSetupType, const CString& strOutput, const CString& strScriptDir, CString& strInstallerPath)
{
strInstallerPath.Empty();
if (nSetupType == 0)
{
LPCTSTR lpMarker = _T("Resulting Setup program filename is:");
int nPos = strOutput.Find(lpMarker);
if (nPos >= 0)
{
CString strRest = strOutput.Mid(nPos + (int)_tcslen(lpMarker));
strRest.Trim();
int nLineBreak = strRest.Find(_T('\n'));
CString strFirst = (nLineBreak >= 0) ? strRest.Left(nLineBreak) : strRest;
strFirst.Trim();
if (!strFirst.IsEmpty())
{
strFirst = ExtractQuotedPath(strFirst);
if (PathFileExists(strFirst))
{
strInstallerPath = strFirst;
return TRUE;
}
}
if (nLineBreak >= 0)
{
CString strSecond = strRest.Mid(nLineBreak + 1);
strSecond.Trim();
strSecond = ExtractQuotedPath(strSecond);
if (PathFileExists(strSecond))
{
strInstallerPath = strSecond;
return TRUE;
}
}
}
}
else
{
int nPos = 0;
while ((nPos = strOutput.Find(_T("Output:"), nPos)) >= 0)
{
CString strRest = strOutput.Mid(nPos + 7);
int nLineBreak = strRest.Find(_T('\n'));
CString strLine = (nLineBreak >= 0) ? strRest.Left(nLineBreak) : strRest;
strLine.Trim();
strLine = ExtractQuotedPath(strLine);
if (!strLine.IsEmpty() && PathFileExists(strLine))
{
strInstallerPath = strLine;
return TRUE;
}
nPos += 7;
}
}
CString strOutputDir;
strOutputDir.Format(_T("%s\\Output"), strScriptDir.GetString());
if (FindNewestExeInDirectory(strOutputDir, strInstallerPath))
{
return TRUE;
}
return FindNewestExeInDirectory(strScriptDir, strInstallerPath);
}
// ─────────────────────────────────────────────
// 公共执行函数
// 不回写版本信息到项目文件,只修改安装脚本并调用编译器打包。 // 不回写版本信息到项目文件,只修改安装脚本并调用编译器打包。
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
BOOL ExecuteSetupBuild(int nSetupType, const CString& strProductVersion, const CString& strFileVersion) /// <summary>
/// 修改安装脚本版本号并调用 Inno Setup / NSIS 编译器打包。
/// 不写回 .rc / AssemblyInfo.cs 等项目文件。
/// </summary>
/// <param name="nSetupType">0=Inno Setup (.iss)1=NSIS (.nsi)。</param>
/// <param name="strProductVersion">产品版本号(仅日志用途)。</param>
/// <param name="strFileVersion">写入脚本的文件版本号。</param>
/// <param name="strInstallerPath">输出:编译生成的安装包路径(可能为空)。</param>
/// <returns>编译流程执行成功返回 TRUE脚本/编译器缺失或修改失败返回 FALSE。</returns>
BOOL ExecuteSetupBuild(int nSetupType, const CString& strProductVersion, const CString& strFileVersion, CString& strInstallerPath)
{ {
#ifdef _DEBUG strInstallerPath.Empty();
Sleep(1500);
#endif
// 查找安装脚本 // 查找安装脚本
CString strScriptPath; CString strScriptPath;
if (!FindSetupScript(nSetupType, strScriptPath)) if (!FindSetupScript(nSetupType, strScriptPath))
@@ -301,9 +557,7 @@ BOOL ExecuteSetupBuild(int nSetupType, const CString& strProductVersion, const C
// 构建并执行编译命令 // 构建并执行编译命令
CString strCmd; CString strCmd;
strCmd.Format(_T("cmd /c %s %s"), strCmd.Format(_T("cmd /c %s %s"), QuoteCmdArg(strCompiler).GetString(), QuoteCmdArg(strScriptPath).GetString());
QuoteCmdArg(strCompiler).GetString(),
QuoteCmdArg(strScriptPath).GetString());
_tprintf(_T("执行编译: %s\n"), strCmd.GetString()); _tprintf(_T("执行编译: %s\n"), strCmd.GetString());
CString strOutput = StartProcess(NULL, strCmd.GetBuffer(), strScriptDir.GetString()); CString strOutput = StartProcess(NULL, strCmd.GetBuffer(), strScriptDir.GetString());
@@ -313,56 +567,102 @@ BOOL ExecuteSetupBuild(int nSetupType, const CString& strProductVersion, const C
{ {
_tprintf(_T("警告: 编译器无输出,可能执行失败。\n")); _tprintf(_T("警告: 编译器无输出,可能执行失败。\n"));
} }
else if (!ParseInstallerPathFromBuildOutput(nSetupType, strOutput, strScriptDir, strInstallerPath))
{
_tprintf(_T("警告: 未能从编译输出中识别安装包路径。\n"));
}
else
{
_tprintf(_T("安装包路径: %s\n"), strInstallerPath.GetString());
}
return TRUE; return TRUE;
} }
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
// 独立命令入口argv[1] 形如 "-setup=0" // 独立命令入口argv[1] 形如 "setup=0"
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
/// <summary>
/// 处理 setup=n 命令:解析参数、确定版本号、编译安装包并更新 upload_info.txt。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组argv[1] 形如 setup=0 或 setup=1。</param>
/// <returns>0 成功36 setup 值无效38 脚本修改/编译失败;其他见 setver 相关退出码。</returns>
int HandleSetupCommand(int argc, TCHAR* argv[]) int HandleSetupCommand(int argc, TCHAR* argv[])
{ {
#ifdef _DEBUG // 解析 setup=n 中的 n
Sleep(15000);
#endif
// 解析 -setup=n 中的 n
CString strSetupArg = argv[1]; CString strSetupArg = argv[1];
CString strN = strSetupArg.Mid(7); // 跳过 "-setup=" CString strN = strSetupArg.Mid(6); // 跳过 "setup="
UINT nSetupType = UINT(-1); UINT nSetupType = UINT(-1);
if (!TryParseUInt16(strN, nSetupType) || (nSetupType != 0 && nSetupType != 1)) if (!TryParseUInt16(strN, nSetupType) || (nSetupType != 0 && nSetupType != 1))
{ {
_tprintf(_T("错误: -setup 参数值 \"%s\" 不支持,仅支持 0Inno Setup或 1NSIS\n"), _tprintf(_T("错误: setup 参数值 \"%s\" 不支持,仅支持 0Inno Setup或 1NSIS\n"),
strN.GetString()); strN.GetString());
return 36; return 36;
} }
if (argc < 3) // 解析可选的 pid=m、repodir=、-test
LPCTSTR lpRepoPath = NULL;
BOOL bTestMode = FALSE;
BOOL bHasPid = FALSE;
UINT nPid = 0;
for (int i = 2; i < argc; ++i)
{ {
_tprintf(_T("错误: 参数不足。\n")); CString strArg = argv[i];
_tprintf(_T("用法gitver -setup=0|1 [pid] [repodir=<path>可选]\n")); if (strArg.CompareNoCase(_T("-test")) == 0)
_tprintf(_T("示例gitver -setup=0 5\n")); {
_tprintf(_T("示例gitver -setup=1 5 repodir=E:\\Code\\MyProj\n")); 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() > 4 && strArg.Left(4).CompareNoCase(_T("pid=")) == 0)
{
if (bHasPid)
{
_tprintf(_T("错误: pid 参数重复: %s\n"), argv[i]);
return 3;
}
CString strPid = strArg.Mid(4);
if (!TryParseUInt16(strPid, nPid))
{
_tprintf(_T("错误: pid 参数值 \"%s\" 无效,应为 0-65535 范围整数。\n"), strPid.GetString());
return 4;
}
bHasPid = TRUE;
continue;
}
_tprintf(_T("错误: 多余的参数 %s\n"), argv[i]);
return 3; return 3;
} }
UINT nPid = 0; if (lpRepoPath == NULL)
int nArgRet = ParseUInt16ArgOrError(argv, 2, _T("pid"), nPid, 4);
if (nArgRet != 0)
{ {
return nArgRet; lpRepoPath = g_szCurModuleDir;
}
LPCTSTR lpRepoPath = NULL;
BOOL bTestMode = FALSE;
int nRepoArgRet = ParseSetVerOptions(argc, argv, 3, lpRepoPath, bTestMode);
if (nRepoArgRet != 0)
{
return nRepoArgRet;
} }
CString strFileVersion; CString strFileVersion;
CString strProductVersion; CString strProductVersion;
if (bHasPid)
{
// 从 git 仓库生成版本号
VersionBuildErrorCodes errorCodes = { 5, 6, 9 }; VersionBuildErrorCodes errorCodes = { 5, 6, 9 };
int nVersionRet = BuildVersionsFromRepo( int nVersionRet = BuildVersionsFromRepo(
lpRepoPath, lpRepoPath,
@@ -372,7 +672,6 @@ int HandleSetupCommand(int argc, TCHAR* argv[])
strFileVersion, strFileVersion,
DEFAULT_MAJOR_WHEN_NO_TAG_FOR_SETUP, DEFAULT_MAJOR_WHEN_NO_TAG_FOR_SETUP,
DEFAULT_MINOR_WHEN_NO_TAG_FOR_SETUP); DEFAULT_MINOR_WHEN_NO_TAG_FOR_SETUP);
if (nVersionRet != 0) if (nVersionRet != 0)
{ {
return nVersionRet; return nVersionRet;
@@ -391,12 +690,36 @@ int HandleSetupCommand(int argc, TCHAR* argv[])
_tprintf(_T("FileVersion=%s\n"), strFileVersion.GetString()); _tprintf(_T("FileVersion=%s\n"), strFileVersion.GetString());
_tprintf(_T("ProductVersion=%s\n"), strProductVersion.GetString()); _tprintf(_T("ProductVersion=%s\n"), strProductVersion.GetString());
}
else
{
// 从项目文件读取版本号
if (!ReadVersionFromProjectFiles(lpRepoPath, strFileVersion))
{
_tprintf(_T("错误: 未找到 .rc、AssemblyInfo.cs 或 version_info.txt/.spec 文件,无法读取版本号。\n"));
_tprintf(_T("请使用 pid=m 参数指定产品 ID 以从 git 仓库生成版本号。\n"));
return 35;
}
strProductVersion = strFileVersion;
}
// 不回写版本信息到项目文件,直接修改安装脚本并编译打包 // 修改安装脚本并编译打包
if (!ExecuteSetupBuild((int)nSetupType, strProductVersion, strFileVersion)) CString strInstallerPath;
if (!ExecuteSetupBuild((int)nSetupType, strProductVersion, strFileVersion, strInstallerPath))
{ {
return 38; return 38;
} }
if (!strInstallerPath.IsEmpty())
{
TCHAR szInfoPath[MAX_PATH] = { 0 };
_tcscpy_s(szInfoPath, lpRepoPath);
PathAppend(szInfoPath, _T("upload_info.txt"));
if (!WriteUploadInfoPackageFile(szInfoPath, strInstallerPath))
{
_tprintf(_T("警告: 未能写入 upload_info.txt\n"));
}
}
return 0; return 0;
} }

View File

@@ -1,16 +1,16 @@
#pragma once #pragma once
/// <summary> /// <summary>
/// 查找安装脚本、修改版本号并调用编译器打包。 /// 查找安装脚本、修改版本号并调用编译器打包。
/// nSetupType=0: Inno Setup (.iss)nSetupType=1: NSIS (.nsi)。 /// nSetupType=0: Inno Setup (.iss)nSetupType=1: NSIS (.nsi)。
/// 不写回项目版本文件(.rc / AssemblyInfo.cs /// 不写回项目版本文件(.rc / AssemblyInfo.cs
/// </summary> /// </summary>
/// <returns>TRUE 成功FALSE 失败(错误已打印)。</returns> /// <returns>TRUE 成功FALSE 失败(错误已打印)。</returns>
BOOL ExecuteSetupBuild(int nSetupType, const CString& strProductVersion, const CString& strFileVersion); BOOL ExecuteSetupBuild(int nSetupType, const CString& strProductVersion, const CString& strFileVersion, CString& strInstallerPath);
/// <summary> /// <summary>
/// 处理独立的 -setup=n 命令行入口。 /// 处理独立的 setup=n 命令行入口。
/// 退出码0 成功36 -setup 值不支持37 未找到脚本, /// 退出码0 成功36 setup 值不支持37 未找到脚本,
/// 38 修改脚本失败39 未找到编译器,其他同通用错误码。 /// 38 修改脚本失败39 未找到编译器,其他同通用错误码。
/// </summary> /// </summary>
int HandleSetupCommand(int argc, TCHAR* argv[]); int HandleSetupCommand(int argc, TCHAR* argv[]);

View File

@@ -10,6 +10,14 @@ extern TCHAR g_szCurModuleDir[MAX_PATH];
const UINT DEFAULT_MAJOR_WHEN_NO_TAG_FOR_TAG = 1; const UINT DEFAULT_MAJOR_WHEN_NO_TAG_FOR_TAG = 1;
const UINT DEFAULT_MINOR_WHEN_NO_TAG_FOR_TAG = 0; const UINT DEFAULT_MINOR_WHEN_NO_TAG_FOR_TAG = 0;
/// <summary>
/// 获取当前分支下最近 nMaxCount 个符合 major.minor 格式的 Tag。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">输出:当前分支名。</param>
/// <param name="vtTags">输出Tag 列表(按版本升序,取最后 nMaxCount 个)。</param>
/// <param name="nMaxCount">最多返回的 Tag 数量。</param>
/// <returns>成功返回 TRUE无 Tag 时 vtTags 为空亦视为成功)。</returns>
BOOL GetRecentMajorMinorTags(LPCTSTR lpRepoPath, CString& strBranch, std::vector<BranchTagInfo>& vtTags, int nMaxCount) BOOL GetRecentMajorMinorTags(LPCTSTR lpRepoPath, CString& strBranch, std::vector<BranchTagInfo>& vtTags, int nMaxCount)
{ {
vtTags.clear(); vtTags.clear();
@@ -73,6 +81,10 @@ BOOL GetRecentMajorMinorTags(LPCTSTR lpRepoPath, CString& strBranch, std::vector
return TRUE; return TRUE;
} }
/// <summary>
/// 交互式 Tag 创建流程:展示最近 Tag、选择 major+1 或 minor+1、创建并推送。
/// </summary>
/// <returns>0 成功30-34 交互/创建/验证失败36 推送失败。</returns>
int HandleCreateTagInteractive() int HandleCreateTagInteractive()
{ {
CString strBranch; CString strBranch;

View File

@@ -1,5 +1,8 @@
#pragma once #pragma once
/// <summary>
/// 分支 Tag 的 major/minor 解析结果。
/// </summary>
struct BranchTagInfo struct BranchTagInfo
{ {
CString strTag; CString strTag;
@@ -7,5 +10,12 @@ struct BranchTagInfo
UINT nMinor; UINT nMinor;
}; };
/// <summary>
/// 获取当前分支下最近 nMaxCount 个符合 major.minor 格式的 Tag。
/// </summary>
BOOL GetRecentMajorMinorTags(LPCTSTR lpRepoPath, CString& strBranch, std::vector<BranchTagInfo>& vtTags, int nMaxCount); BOOL GetRecentMajorMinorTags(LPCTSTR lpRepoPath, CString& strBranch, std::vector<BranchTagInfo>& vtTags, int nMaxCount);
/// <summary>
/// 交互式 Tag 创建流程:展示最近 Tag、选择递增方式、创建并推送。
/// </summary>
int HandleCreateTagInteractive(); int HandleCreateTagInteractive();

View File

@@ -1,5 +1,8 @@
#pragma once #pragma once
/// <summary>
/// 项目源码类型枚举。
/// </summary>
enum ProjectCodeType enum ProjectCodeType
{ {
PROJECT_UNKNOWN = 0, PROJECT_UNKNOWN = 0,

1238
GitVer/GitVer_upload.cpp Normal file

File diff suppressed because it is too large Load Diff

22
GitVer/GitVer_upload.h Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
/// <summary>
/// 处理 upload 命令:登录 PMC 并上传包文件。
/// 退出码0 成功47 缺少参数48 文件不存在49 info 无效或未设置同名环境变量密码,
/// 50 登录失败51 上传 HTTP 失败52 验证未找到版本53 网络错误54 Tag/分支校验失败。
/// </summary>
int HandleUploadCommand(int argc, TCHAR* argv[]);
/// <summary>
/// 更新 upload_info.txt 中的 product_version、file_version、file_change_log保留其他字段。
/// </summary>
BOOL WriteUploadInfoVersions(
LPCTSTR lpInfoFile,
LPCTSTR lpProductVersion,
LPCTSTR lpFileVersion,
LPCTSTR lpFileChangeLog);
/// <summary>
/// 更新 upload_info.txt 中的 file安装包路径保留其他字段。
/// </summary>
BOOL WriteUploadInfoPackageFile(LPCTSTR lpInfoFile, LPCTSTR lpPackageFile);

View File

@@ -4,6 +4,33 @@
#include "gitver_process.h" #include "gitver_process.h"
#include "gitver_version.h" #include "gitver_version.h"
/// <summary>
/// 检测 Git 仓库是否处于正常分支状态(非 HEAD 游离)。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strStatus">输出symbolic-ref HEAD 结果。</param>
/// <returns>在分支上返回 TRUE游离状态返回 FALSE。</returns>
BOOL TryGetGitStatus(LPCTSTR lpRepoPath, CString& strStatus)
{
strStatus = StartProcess(NULL, _T("cmd /c git symbolic-ref --quiet HEAD"), lpRepoPath);
strStatus.Replace(_T("\r"), _T(""));
strStatus.Trim();
if (strStatus.IsEmpty())
{// 游离状态
return FALSE;
}
// 正常状态;
return TRUE;
}
/// <summary>
/// 获取当前检出的本地分支名称。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">输出:分支名。</param>
/// <returns>成功返回 TRUE。</returns>
BOOL TryGetCurrentBranch(LPCTSTR lpRepoPath, CString& strBranch) BOOL TryGetCurrentBranch(LPCTSTR lpRepoPath, CString& strBranch)
{ {
_tprintf(_T("获取当前分支名称: \n")); _tprintf(_T("获取当前分支名称: \n"));
@@ -20,6 +47,59 @@ BOOL TryGetCurrentBranch(LPCTSTR lpRepoPath, CString& strBranch)
return TRUE; return TRUE;
} }
/// <summary>
/// 在 HEAD 游离时通过远程分支推断当前所属分支名origin/ 前缀已去除)。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">输出:推断的分支名。</param>
/// <returns>找到远程分支返回 TRUE。</returns>
BOOL TryGetDetectedBranch(LPCTSTR lpRepoPath, CString& strBranch)
{
// 远程仓库名必须以 origin 开头;
// "C:\Program Files\Git\bin\bash.exe" -c "git branch -r --contains HEAD | head -n1 | sed 's/.*origin\///'"
strBranch = StartProcess(NULL, _T("\"C:\\Program Files\\Git\\bin\\bash.exe\" -c \"git branch -r --contains HEAD | head -n1 | sed 's/.*origin\\///'\""), lpRepoPath);
//strBranch = StartProcess("C:\\Program Files\\Git\\bin\\bash.exe", _T("git branch -r --contains HEAD | head -n1 | sed 's/.*origin\\///'"), lpRepoPath);
strBranch.Replace(_T("\r"), _T(""));
strBranch.Replace(_T("\n"), _T(""));
strBranch.Trim();
if (strBranch.IsEmpty())
{
// 没有找到远程分支;
return FALSE;
}
return TRUE;
}
/// <summary>
/// 获取指向当前 HEAD 的 Tag 名称。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strTag">输出Tag 名(多个时取 git 输出首行)。</param>
/// <returns>存在 Tag 返回 TRUE。</returns>
BOOL TryGetDetectedTag(LPCTSTR lpRepoPath, CString& strTag)
{
strTag = StartProcess(NULL, _T("cmd /c git tag --points-at HEAD"), lpRepoPath);
strTag.Replace(_T("\r"), _T(""));
strTag.Replace(_T("\n"), _T(""));
strTag.Trim();
if (strTag.IsEmpty())
{
// 没有找到标签;
return FALSE;
}
return TRUE;
}
/// <summary>
/// 从分支名解析 bid分支 IDmain/master=0release=1dev=2其余取末段数字。
/// </summary>
/// <param name="strBranch">分支名称。</param>
/// <param name="nBid">输出:分支 bid。</param>
/// <returns>解析成功返回 TRUE。</returns>
BOOL TryGetBidFromBranch(const CString& strBranch, UINT& nBid) BOOL TryGetBidFromBranch(const CString& strBranch, UINT& nBid)
{ {
nBid = 0; nBid = 0;
@@ -34,6 +114,18 @@ BOOL TryGetBidFromBranch(const CString& strBranch, UINT& nBid)
return TRUE; return TRUE;
} }
if (strBranch.CompareNoCase(_T("release")) == 0)
{
nBid = 1;
return TRUE;
}
if (strBranch.CompareNoCase(_T("dev")) == 0)
{
nBid = 2;
return TRUE;
}
int nDotPos = strBranch.ReverseFind(_T('.')); int nDotPos = strBranch.ReverseFind(_T('.'));
if (nDotPos <= 0 || nDotPos >= strBranch.GetLength() - 1) if (nDotPos <= 0 || nDotPos >= strBranch.GetLength() - 1)
{ {
@@ -44,6 +136,14 @@ BOOL TryGetBidFromBranch(const CString& strBranch, UINT& nBid)
return TryParseUInt16(strNum, nBid); return TryParseUInt16(strNum, nBid);
} }
/// <summary>
/// 解析符合「分支名.major.minor」格式的 Tag提取 major 与 minor。
/// </summary>
/// <param name="strBranch">分支名称。</param>
/// <param name="strTag">Tag 名称。</param>
/// <param name="nMajor">输出:主版本号。</param>
/// <param name="nMinor">输出:次版本号。</param>
/// <returns>格式匹配且解析成功返回 TRUE。</returns>
BOOL TryParseTagMajorMinor(const CString& strBranch, const CString& strTag, UINT& nMajor, UINT& nMinor) BOOL TryParseTagMajorMinor(const CString& strBranch, const CString& strTag, UINT& nMajor, UINT& nMinor)
{ {
if (strBranch.IsEmpty()) if (strBranch.IsEmpty())
@@ -79,6 +179,15 @@ BOOL TryParseTagMajorMinor(const CString& strBranch, const CString& strTag, UINT
return TRUE; return TRUE;
} }
/// <summary>
/// 获取当前分支下版本号最大的 major.minor Tag按 v:refname 排序遍历)。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">分支名称。</param>
/// <param name="strTag">输出:最新 Tag 全名。</param>
/// <param name="nMajor">输出:主版本号。</param>
/// <param name="nMinor">输出:次版本号。</param>
/// <returns>找到匹配 Tag 返回 TRUE。</returns>
BOOL GetLastMajorMinorTag(LPCTSTR lpRepoPath, const CString& strBranch, CString& strTag, UINT& nMajor, UINT& nMinor) BOOL GetLastMajorMinorTag(LPCTSTR lpRepoPath, const CString& strBranch, CString& strTag, UINT& nMajor, UINT& nMinor)
{ {
strTag.Empty(); strTag.Empty();
@@ -122,6 +231,13 @@ BOOL GetLastMajorMinorTag(LPCTSTR lpRepoPath, const CString& strBranch, CString&
return !strTag.IsEmpty(); return !strTag.IsEmpty();
} }
/// <summary>
/// 将 SYSTEMTIME 转换为 FileVersion 中的 yy 与 mmdd 分量。
/// 月份 1-12 映射为 11-22mmdd = (month+10)*100 + day
/// </summary>
/// <param name="stLocal">本地时间。</param>
/// <param name="nYY">输出:年份后两位。</param>
/// <param name="nMMDD">输出:编码后的月日。</param>
void GetFileVersionDateFromSystemTime(const SYSTEMTIME& stLocal, UINT& nYY, UINT& nMMDD) void GetFileVersionDateFromSystemTime(const SYSTEMTIME& stLocal, UINT& nYY, UINT& nMMDD)
{ {
nYY = stLocal.wYear % 100; nYY = stLocal.wYear % 100;
@@ -129,6 +245,14 @@ void GetFileVersionDateFromSystemTime(const SYSTEMTIME& stLocal, UINT& nYY, UINT
nMMDD = (stLocal.wMonth + 10) * 100 + stLocal.wDay; nMMDD = (stLocal.wMonth + 10) * 100 + stLocal.wDay;
} }
/// <summary>
/// 统计指定日期内 origin/分支 的提交次数(用于 FileVersion 末段 id
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">分支名称。</param>
/// <param name="stDate">目标日期。</param>
/// <param name="nId">输出:当日提交数。</param>
/// <returns>成功返回 TRUE。</returns>
BOOL TryGetBranchCommitCountByDate(LPCTSTR lpRepoPath, const CString& strBranch, const SYSTEMTIME& stDate, UINT& nId) BOOL TryGetBranchCommitCountByDate(LPCTSTR lpRepoPath, const CString& strBranch, const SYSTEMTIME& stDate, UINT& nId)
{ {
nId = 0; nId = 0;
@@ -142,7 +266,7 @@ BOOL TryGetBranchCommitCountByDate(LPCTSTR lpRepoPath, const CString& strBranch,
CString strCmd; CString strCmd;
strCmd.Format( strCmd.Format(
_T("cmd /c git rev-list --count --since=\"%s 00:00:00\" --until=\"%s 23:59:59\" \"%s\""), _T("cmd /c git rev-list --count --since=\"%s 00:00:00\" --until=\"%s 23:59:59\" \"origin/%s\""),
strDate.GetString(), strDate.GetString(),
strDate.GetString(), strDate.GetString(),
strBranch.GetString()); strBranch.GetString());
@@ -167,6 +291,13 @@ BOOL TryGetBranchCommitCountByDate(LPCTSTR lpRepoPath, const CString& strBranch,
return TryParseUInt16(strCount, nId); return TryParseUInt16(strCount, nId);
} }
/// <summary>
/// 获取分支最近一次提交的日期git log -1 --date=short
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">分支名称。</param>
/// <param name="stDate">输出:提交日期。</param>
/// <returns>成功返回 TRUE。</returns>
BOOL TryGetLastBranchCommitDate(LPCTSTR lpRepoPath, const CString& strBranch, SYSTEMTIME& stDate) BOOL TryGetLastBranchCommitDate(LPCTSTR lpRepoPath, const CString& strBranch, SYSTEMTIME& stDate)
{ {
::ZeroMemory(&stDate, sizeof(stDate)); ::ZeroMemory(&stDate, sizeof(stDate));
@@ -209,7 +340,123 @@ BOOL TryGetLastBranchCommitDate(LPCTSTR lpRepoPath, const CString& strBranch, SY
return TRUE; return TRUE;
} }
BOOL ResolveFileVersionDateAndCommitCount(LPCTSTR lpRepoPath, const CString& strBranch, UINT& nYY, UINT& nMMDD, UINT& nId) /// <summary>
/// 获取 Tag 对应提交的日期git log -1 --date=short
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strTag">Tag 名称。</param>
/// <param name="stDate">输出:提交日期。</param>
/// <returns>成功返回 TRUE。</returns>
BOOL TryGetTagCommitDate(LPCTSTR lpRepoPath, const CString& strTag, SYSTEMTIME& stDate)
{
::ZeroMemory(&stDate, sizeof(stDate));
if (strTag.IsEmpty())
{
return FALSE;
}
CString strCmd;
strCmd.Format(_T("cmd /c git log -1 --date=short --format=%%cd \"%s\""), strTag.GetString());
CString strDate = StartProcess(NULL, strCmd.GetBuffer(), lpRepoPath);
strCmd.ReleaseBuffer();
if (strDate.IsEmpty())
{
return FALSE;
}
strDate.Replace(_T("\r"), _T(""));
strDate.Replace(_T("\n"), _T(""));
strDate.Trim();
if (strDate.IsEmpty() || strDate.Left(6).CompareNoCase(_T("fatal:")) == 0)
{
return FALSE;
}
unsigned int nYear = 0;
unsigned int nMonth = 0;
unsigned int nDay = 0;
if (_stscanf_s(strDate.GetString(), _T("%u-%u-%u"), &nYear, &nMonth, &nDay) != 3)
{
return FALSE;
}
stDate.wYear = static_cast<WORD>(nYear);
stDate.wMonth = static_cast<WORD>(nMonth);
stDate.wDay = static_cast<WORD>(nDay);
return TRUE;
}
/// <summary>
/// 基于 Tag 提交日期解析 FileVersion 的 yy、mmdd 与 id当日提交数
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">分支名称。</param>
/// <param name="strTag">Tag 名称。</param>
/// <param name="nYY">输出:年份后两位。</param>
/// <param name="nMMDD">输出:编码月日。</param>
/// <param name="nId">输出:提交序号。</param>
/// <param name="pOutDate">可选输出Tag 提交日期。</param>
/// <returns>成功返回 TRUE。</returns>
BOOL ResolveTagFileVersionDateAndCommitCount(LPCTSTR lpRepoPath, const CString& strBranch, const CString& strTag, UINT& nYY, UINT& nMMDD, UINT& nId, SYSTEMTIME* pOutDate)
{
nYY = 0;
nMMDD = 0;
nId = 0;
if (strTag.IsEmpty())
{
_tprintf(_T("错误: 标签名称为空\n"));
return FALSE;
}
SYSTEMTIME stTargetDate = { 0 };
if (!TryGetTagCommitDate(lpRepoPath, strTag, stTargetDate))
{
_tprintf(_T("错误: 获取标签 [%s] 对应提交日期失败\n"), strTag.GetString());
return FALSE;
}
UINT nCommitCount = 0;
if (!TryGetBranchCommitCountByDate(lpRepoPath, strBranch, stTargetDate, nCommitCount))
{
_tprintf(_T("错误: 获取 [%s] 在 [%04u-%02u-%02u] 的提交数量失败\n"),
strBranch.GetString(),
stTargetDate.wYear,
stTargetDate.wMonth,
stTargetDate.wDay);
return FALSE;
}
_tprintf(_T("获取标签提交信息成功: %04u-%02u-%02u, 提交数量: %u\n"),
stTargetDate.wYear,
stTargetDate.wMonth,
stTargetDate.wDay,
nCommitCount);
GetFileVersionDateFromSystemTime(stTargetDate, nYY, nMMDD);
nId = nCommitCount;
if (pOutDate != NULL)
{
*pOutDate = stTargetDate;
}
return TRUE;
}
/// <summary>
/// 基于当前日期(或最近提交日)解析 FileVersion 的 yy、mmdd 与 id。
/// 当天提交数为 0 时回退到最近一次提交日期。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">分支名称。</param>
/// <param name="nYY">输出:年份后两位。</param>
/// <param name="nMMDD">输出:编码月日。</param>
/// <param name="nId">输出:提交序号。</param>
/// <param name="pOutDate">可选输出:采用的日期。</param>
/// <returns>成功返回 TRUE。</returns>
BOOL ResolveFileVersionDateAndCommitCount(LPCTSTR lpRepoPath, const CString& strBranch, UINT& nYY, UINT& nMMDD, UINT& nId, SYSTEMTIME* pOutDate)
{ {
nYY = 0; nYY = 0;
nMMDD = 0; nMMDD = 0;
@@ -251,9 +498,73 @@ BOOL ResolveFileVersionDateAndCommitCount(LPCTSTR lpRepoPath, const CString& str
GetFileVersionDateFromSystemTime(stTargetDate, nYY, nMMDD); GetFileVersionDateFromSystemTime(stTargetDate, nYY, nMMDD);
nId = nTodayCommitCount; nId = nTodayCommitCount;
if (pOutDate != NULL)
{
*pOutDate = stTargetDate;
}
return TRUE; return TRUE;
} }
/// <summary>
/// 获取指定日期内 origin/分支 的提交摘要列表UTF-8 输出,一行一条)。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">分支名称。</param>
/// <param name="stDate">目标日期。</param>
/// <param name="strLog">输出:提交摘要,多行以换行分隔。</param>
/// <returns>成功返回 TRUE。</returns>
BOOL TryGetGitLogByBranchDate(LPCTSTR lpRepoPath, const CString& strBranch, const SYSTEMTIME& stDate, CString& strLog)
{
strLog.Empty();
if (strBranch.IsEmpty())
{
return FALSE;
}
CString strCmd;
strCmd.Format(
_T("cmd /c git log --reverse --pretty=format:%%s --since=\"%04u-%02u-%02u 00:00:00\" --until=\"%04u-%02u-%02u 23:59:59\" \"origin/%s\""),
stDate.wYear,
stDate.wMonth,
stDate.wDay,
stDate.wYear,
stDate.wMonth,
stDate.wDay,
strBranch.GetString());
CString strOutput = StartProcess(NULL, strCmd.GetBuffer(), lpRepoPath, CP_UTF8);
strCmd.ReleaseBuffer();
if (strOutput.IsEmpty())
{
return FALSE;
}
strOutput.Replace(_T("\r"), _T(""));
strOutput.Trim();
if (strOutput.IsEmpty() || strOutput.Left(6).CompareNoCase(_T("fatal:")) == 0)
{
return FALSE;
}
strLog = strOutput;
return TRUE;
}
/// <summary>
/// 从 Git 仓库信息构建 ProductVersion 与 FileVersion 字符串。
/// 正常分支ProductVersion = pid.bid.major.minorFileVersion = pid.yy.mmdd.id。
/// HEAD 游离时改用远程分支与 Tag 解析。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="nPid">产品 ID。</param>
/// <param name="errorCodes">各步骤失败时的退出码映射。</param>
/// <param name="strProductVersion">输出:产品版本号。</param>
/// <param name="strFileVersion">输出:文件版本号。</param>
/// <param name="nDefaultMajor">无 Tag 时的默认 major。</param>
/// <param name="nDefaultMinor">无 Tag 时的默认 minor。</param>
/// <param name="pOutBranch">可选输出:使用的分支名。</param>
/// <param name="pOutFileVersionDate">可选输出FileVersion 采用的日期。</param>
/// <returns>0 成功,否则返回 errorCodes 中对应错误码。</returns>
int BuildVersionsFromRepo( int BuildVersionsFromRepo(
LPCTSTR lpRepoPath, LPCTSTR lpRepoPath,
UINT nPid, UINT nPid,
@@ -261,7 +572,9 @@ int BuildVersionsFromRepo(
CString& strProductVersion, CString& strProductVersion,
CString& strFileVersion, CString& strFileVersion,
UINT nDefaultMajor, UINT nDefaultMajor,
UINT nDefaultMinor) UINT nDefaultMinor,
CString* pOutBranch,
SYSTEMTIME* pOutFileVersionDate)
{ {
_tprintf(_T("开始构建版本: 仓库路径=%s pid=%u 默认Tag(%u.%u)\n"), _tprintf(_T("开始构建版本: 仓库路径=%s pid=%u 默认Tag(%u.%u)\n"),
lpRepoPath == NULL ? _T("<null>") : lpRepoPath, lpRepoPath == NULL ? _T("<null>") : lpRepoPath,
@@ -270,7 +583,47 @@ int BuildVersionsFromRepo(
nDefaultMinor); nDefaultMinor);
UINT nBid = 0; UINT nBid = 0;
CString strStatus;
CString strBranch; CString strBranch;
UINT nMajor = 0, nMinor = 0;
UINT nYY = 0, nMMDD = 0, nId = 0;
if (!TryGetGitStatus(lpRepoPath, strStatus))
{
_tprintf(_T("警告: 当前处于 HEAD 游离状态\n"));
if (!TryGetDetectedBranch(lpRepoPath, strBranch))
{
_tprintf(_T("错误: 获取远程分支失败\n"));
return errorCodes.nStatusErrorCode;
}
CString strTag;
if (!TryGetDetectedTag(lpRepoPath, strTag))
{
_tprintf(_T("错误: 获取远程分支标签失败\n"));
return errorCodes.nStatusErrorCode;
}
// 处于游离状态,使用远程分支和标签来解析版本号;
if (TryParseTagMajorMinor(strBranch, strTag, nMajor, nMinor))
{
_tprintf(_T("游离标签版本: %s (major=%u minor=%u)\n"), strTag.GetString(), nMajor, nMinor);
}
if (!ResolveTagFileVersionDateAndCommitCount(lpRepoPath, strBranch, strTag, nYY, nMMDD, nId, pOutFileVersionDate))
{
_tprintf(_T("错误: 获取游离标签提交信息失败\n"));
return errorCodes.nCommitErrorCode;
}
if (pOutBranch != NULL)
{
*pOutBranch = strBranch;
}
_tprintf(_T("游离标签提交信息: yy=%u mmdd=%u id=%u\n"), nYY, nMMDD, nId);
}
else
{
if (!TryGetCurrentBranch(lpRepoPath, strBranch)) if (!TryGetCurrentBranch(lpRepoPath, strBranch))
{ {
_tprintf(_T("错误: 获取当前分支失败\n")); _tprintf(_T("错误: 获取当前分支失败\n"));
@@ -286,8 +639,6 @@ int BuildVersionsFromRepo(
_tprintf(_T("分支 bid: %u\n"), nBid); _tprintf(_T("分支 bid: %u\n"), nBid);
CString strLastTag; CString strLastTag;
UINT nMajor = 0;
UINT nMinor = 0;
if (!GetLastMajorMinorTag(lpRepoPath, strBranch, strLastTag, nMajor, nMinor)) if (!GetLastMajorMinorTag(lpRepoPath, strBranch, strLastTag, nMajor, nMinor))
{ {
nMajor = nDefaultMajor; nMajor = nDefaultMajor;
@@ -299,18 +650,23 @@ int BuildVersionsFromRepo(
_tprintf(_T("最近标签: %s (major=%u minor=%u)\n"), strLastTag.GetString(), nMajor, nMinor); _tprintf(_T("最近标签: %s (major=%u minor=%u)\n"), strLastTag.GetString(), nMajor, nMinor);
} }
UINT nYY = 0; if (!ResolveFileVersionDateAndCommitCount(lpRepoPath, strBranch, nYY, nMMDD, nId, pOutFileVersionDate))
UINT nMMDD = 0;
UINT nId = 0;
if (!ResolveFileVersionDateAndCommitCount(lpRepoPath, strBranch, nYY, nMMDD, nId))
{ {
_tprintf(_T("错误: 获取分支提交信息失败\n")); _tprintf(_T("错误: 获取分支提交信息失败\n"));
return errorCodes.nCommitErrorCode; return errorCodes.nCommitErrorCode;
} }
if (pOutBranch != NULL)
{
*pOutBranch = strBranch;
}
_tprintf(_T("分支提交信息: yy=%u mmdd=%u id=%u\n"), nYY, nMMDD, nId); _tprintf(_T("分支提交信息: yy=%u mmdd=%u id=%u\n"), nYY, nMMDD, nId);
}
strProductVersion.Format(_T("%u.%u.%u.%u"), nPid, nBid, nMajor, nMinor); strProductVersion.Format(_T("%u.%u.%u.%u"), nPid, nBid, nMajor, nMinor);
strFileVersion.Format(_T("%u.%u.%u.%u"), nPid, nYY, nMMDD, nId); strFileVersion.Format(_T("%u.%u.%u.%u"), nPid, nYY, nMMDD, nId);
_tprintf(_T("文件版本信息: ProductVersion=%s FileVersion=%s\n"), strProductVersion.GetString(), strFileVersion.GetString()); _tprintf(_T("文件版本信息: ProductVersion=%s FileVersion=%s\n"), strProductVersion.GetString(), strFileVersion.GetString());
return 0; return 0;
} }

View File

@@ -1,16 +1,86 @@
#pragma once #pragma once
/// <summary>
/// BuildVersionsFromRepo 各步骤失败时使用的退出码映射。
/// </summary>
struct VersionBuildErrorCodes struct VersionBuildErrorCodes
{ {
int nBidErrorCode; int nBidErrorCode;
int nTagErrorCode; int nTagErrorCode;
int nCommitErrorCode; int nCommitErrorCode;
int nStatusErrorCode;
}; };
/// <summary>
/// 检测 Git 仓库是否处于正常分支状态(非 HEAD 游离)。
/// </summary>
BOOL TryGetGitStatus(LPCTSTR lpRepoPath, CString& strStatus);
/// <summary>
/// 获取当前检出的本地分支名称。
/// </summary>
BOOL TryGetCurrentBranch(LPCTSTR lpRepoPath, CString& strBranch); BOOL TryGetCurrentBranch(LPCTSTR lpRepoPath, CString& strBranch);
/// <summary>
/// 在 HEAD 游离时推断当前所属远程分支名。
/// </summary>
BOOL TryGetDetectedBranch(LPCTSTR lpRepoPath, CString& strBranch);
/// <summary>
/// 获取指向当前 HEAD 的 Tag 名称。
/// </summary>
BOOL TryGetDetectedTag(LPCTSTR lpRepoPath, CString& strTag);
/// <summary>
/// 从分支名解析 bid分支 ID
/// </summary>
BOOL TryGetBidFromBranch(const CString& strBranch, UINT& nBid); BOOL TryGetBidFromBranch(const CString& strBranch, UINT& nBid);
/// <summary>
/// 解析符合「分支名.major.minor」格式的 Tag。
/// </summary>
BOOL TryParseTagMajorMinor(const CString& strBranch, const CString& strTag, UINT& nMajor, UINT& nMinor); BOOL TryParseTagMajorMinor(const CString& strBranch, const CString& strTag, UINT& nMajor, UINT& nMinor);
/// <summary>
/// 获取当前分支下版本号最大的 major.minor Tag。
/// </summary>
BOOL GetLastMajorMinorTag(LPCTSTR lpRepoPath, const CString& strBranch, CString& strTag, UINT& nMajor, UINT& nMinor); BOOL GetLastMajorMinorTag(LPCTSTR lpRepoPath, const CString& strBranch, CString& strTag, UINT& nMajor, UINT& nMinor);
/// <summary>
/// 基于 Tag 提交日期解析 FileVersion 的 yy、mmdd 与 id。
/// </summary>
BOOL ResolveTagFileVersionDateAndCommitCount(
LPCTSTR lpRepoPath,
const CString& strBranch,
const CString& strTag,
UINT& nYY,
UINT& nMMDD,
UINT& nId,
SYSTEMTIME* pOutDate = NULL);
/// <summary>
/// 基于当前或最近提交日解析 FileVersion 的 yy、mmdd 与 id。
/// </summary>
BOOL ResolveFileVersionDateAndCommitCount(
LPCTSTR lpRepoPath,
const CString& strBranch,
UINT& nYY,
UINT& nMMDD,
UINT& nId,
SYSTEMTIME* pOutDate = NULL);
/// <summary>
/// 获取指定日期内 origin/分支 的提交摘要列表。
/// </summary>
BOOL TryGetGitLogByBranchDate(
LPCTSTR lpRepoPath,
const CString& strBranch,
const SYSTEMTIME& stDate,
CString& strLog);
/// <summary>
/// 从 Git 仓库信息构建 ProductVersion 与 FileVersion 字符串。
/// </summary>
int BuildVersionsFromRepo( int BuildVersionsFromRepo(
LPCTSTR lpRepoPath, LPCTSTR lpRepoPath,
UINT nPid, UINT nPid,
@@ -18,4 +88,6 @@ int BuildVersionsFromRepo(
CString& strProductVersion, CString& strProductVersion,
CString& strFileVersion, CString& strFileVersion,
UINT nDefaultMajor, UINT nDefaultMajor,
UINT nDefaultMinor); UINT nDefaultMinor,
CString* pOutBranch = NULL,
SYSTEMTIME* pOutFileVersionDate = NULL);

View File

@@ -1,23 +1,23 @@
#pragma once #pragma once
#include "targetver.h" #include "targetver.h"
#include <stdio.h> #include <stdio.h>
#include <tchar.h> #include <tchar.h>
#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS // 部分 CString 构造函数将是显式的 #define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS // 部分 CString 构造函数将是显式的
#define _AFX_NO_MFC_CONTROLS_IN_DIALOGS // 移除对话框中的 MFC 控件支持 #define _AFX_NO_MFC_CONTROLS_IN_DIALOGS // 移除对话框中的 MFC 控件支持
#ifndef VC_EXTRALEAN #ifndef VC_EXTRALEAN
#define VC_EXTRALEAN // 从 Windows 头文件中排除极少使用的内容 #define VC_EXTRALEAN // 从 Windows 头文件中排除极少使用的内容
#endif #endif
#include <afx.h> #include <afx.h>
#include <afxwin.h> // MFC 核心组件和标准组件 #include <afxwin.h> // MFC 核心组件和标准组件
#include <afxext.h> // MFC 扩展 #include <afxext.h> // MFC 扩展
#ifndef _AFX_NO_OLE_SUPPORT #ifndef _AFX_NO_OLE_SUPPORT
#include <afxdtctl.h> // MFC 对 Internet Explorer 4 公共控件的支持 #include <afxdtctl.h> // MFC 对 Internet Explorer 4 公共控件的支持
#endif #endif
#ifndef _AFX_NO_AFXCMN_SUPPORT #ifndef _AFX_NO_AFXCMN_SUPPORT
#include <afxcmn.h> // MFC 对 Windows 公共控件的支持 #include <afxcmn.h> // MFC 对 Windows 公共控件的支持
#endif // _AFX_NO_AFXCMN_SUPPORT #endif // _AFX_NO_AFXCMN_SUPPORT
#include <iostream> #include <iostream>

20834
GitVer/httplib.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
// pch.cpp: 与预编译标头对应的源文件 // pch.cpp: 与预编译标头对应的源文件
#include "pch.h" #include "pch.h"
// 当使用预编译的头时,需要使用此源文件,编译才能成功。 // 当使用预编译的头时,需要使用此源文件,编译才能成功。

View File

@@ -1,13 +1,13 @@
// pch.h: 这是预编译标头文件。 // pch.h: 这是预编译标头文件。
// 下方列出的文件仅编译一次,提高了将来生成的生成性能。 // 下方列出的文件仅编译一次,提高了将来生成的生成性能。
// 这还将影响 IntelliSense 性能,包括代码完成和许多代码浏览功能。 // 这还将影响 IntelliSense 性能,包括代码完成和许多代码浏览功能。
// 但是,如果此处列出的文件中的任何一个在生成之间有更新,它们全部都将被重新编译。 // 但是,如果此处列出的文件中的任何一个在生成之间有更新,它们全部都将被重新编译。
// 请勿在此处添加要频繁更新的文件,这将使得性能优势无效。 // 请勿在此处添加要频繁更新的文件,这将使得性能优势无效。
#ifndef PCH_H #ifndef PCH_H
#define PCH_H #define PCH_H
// 添加要在此处预编译的标头 // 添加要在此处预编译的标头
#include "framework.h" #include "framework.h"
#endif //PCH_H #endif //PCH_H

View File

@@ -1,6 +1,6 @@
//{{NO_DEPENDENCIES}} //{{NO_DEPENDENCIES}}
// Microsoft Visual C++ 生成的包含文件。 // Microsoft Visual C++ 生成的包含文件。
// 供 GitVer.rc 使用 // 供 GitVer.rc 使用
// //
#define IDI_ICON_APP 100 #define IDI_ICON_APP 100
#define IDI_ICON1 101 #define IDI_ICON1 101

View File

@@ -1,6 +1,6 @@
#pragma once #pragma once
// // 包含 SDKDDKVer.h 可定义可用的最高版本的 Windows 平台。 // // 包含 SDKDDKVer.h 可定义可用的最高版本的 Windows 平台。
// 如果希望为之前的 Windows 平台构建应用程序,在包含 SDKDDKVer.h 之前请先包含 WinSDKVer.h 并 // 如果希望为之前的 Windows 平台构建应用程序,在包含 SDKDDKVer.h 之前请先包含 WinSDKVer.h 并
// 将 _WIN32_WINNT 宏设置为想要支持的平台。 // 将 _WIN32_WINNT 宏设置为想要支持的平台。
#include <SDKDDKVer.h> #include <SDKDDKVer.h>

519
README.md
View File

@@ -1,6 +1,7 @@
# GitVer # GitVer
命令行版本管理工具,从 Git 仓库读取 tag 自动生成并回写版本号。 命令行版本管理工具,从 Git 仓库读取 tag 自动生成并回写版本号。
必须安装Git for Windows且安装目录默认在C盘C:\Program Files\Git
--- ---
@@ -10,9 +11,11 @@
gitver gitver
gitver rewrite [PEType] [-f] gitver rewrite [PEType] [-f]
gitver setver=<pid> [repodir=<path>] [-test] [-setup=0|1] gitver setver=<pid> [repodir=<path>] [-test] [-setup=0|1]
gitver nuitkabuild=<pid> <mainPy> [repodir=<path>] [-test] [params="<nuitka参数>"] gitver nuitkabuild=<pid> [repodir=<path>] [-test] [params="<mainPy> <nuitka参数>"]
gitver nuitkapydbuild=<pid> <modulePy> [repodir=<path>] [-test] [params="<nuitka参数>"] gitver nuitkapydbuild=<pid> [repodir=<path>] [-test] [params="<modulePy> <nuitka参数>"]
gitver -setup=0|1 <pid> [repodir=<path>] [-test] gitver pyinstaller=<pid> [repodir=<path>] [-test] [params="<mainPy|specFile> <pyinstaller参数>"]
gitver setup=n [pid=m] [repodir=<path>] [-test]
gitver upload username=<用户名> [file=<上传文件>] [info=<配置.txt>] [url=<PMC基址>]
``` ```
**通用参数说明:** **通用参数说明:**
@@ -22,7 +25,7 @@ gitver -setup=0|1 <pid> [repodir=<path>] [-test]
| `pid` | 产品 ID整数范围 0-65535内嵌在命令名中`setver=5` | | `pid` | 产品 ID整数范围 0-65535内嵌在命令名中`setver=5` |
| `repodir=<path>` | Git 仓库目录,缺省使用当前运行目录 | | `repodir=<path>` | Git 仓库目录,缺省使用当前运行目录 |
| `-test` | 将产品版本号的 major/minor 置为 0测试版本构建 | | `-test` | 将产品版本号的 major/minor 置为 0测试版本构建 |
| `params="..."` | 传递给 Nuitka 的额外参数,用双引号括起来 | | `params="..."` | 传递给 Nuitka 或 PyInstaller 的参数(**含入口文件名**,用双引号括起来 |
**BID 规则:** **BID 规则:**
`main`/`master` 分支固定 `bid=0`;其他分支格式须为 `<描述>.<数字>`,自动读取末尾数字作为 `bid` `main`/`master` 分支固定 `bid=0`;其他分支格式须为 `<描述>.<数字>`,自动读取末尾数字作为 `bid`
@@ -93,7 +96,7 @@ gitver rewrite -f
从 Git 仓库读取 tag生成产品版本号与文件版本号并按源码类型自动回写。 从 Git 仓库读取 tag生成产品版本号与文件版本号并按源码类型自动回写。
``` ```
gitver setver=<pid> [repodir=<path>] [-test] [-setup=0|1] gitver setver=<pid> [repodir=<path>] [-test]
``` ```
**版本号生成规则:** **版本号生成规则:**
@@ -101,26 +104,12 @@ gitver setver=<pid> [repodir=<path>] [-test] [-setup=0|1]
- `ProductVersion = pid.bid.major.minor`(取当前分支前缀的最新 tag - `ProductVersion = pid.bid.major.minor`(取当前分支前缀的最新 tag
- `FileVersion = pid.yy.mmdd.id`id = 当天当前分支提交次数) - `FileVersion = pid.yy.mmdd.id`id = 当天当前分支提交次数)
**`-setup=N` 标志(打包模式):**
存在 `-setup=N` 时,**不回写**版本信息到项目文件,改为:
1. 在 exe 目录或上级目录查找安装脚本
2. 修改脚本中的版本号字段
3. 调用对应编译器打包
| 值 | 脚本文件 | 编译器 |
|---|---|---|
| `0` | `setup.iss` | Inno Setup`ISCC.exe` |
| `1` | `setup.nsi` | NSIS`makensis.exe` |
**示例:** **示例:**
``` ```
gitver setver=5 gitver setver=5
gitver setver=5 repodir=E:\Code\OTH\gitver gitver setver=5 repodir=E:\Code\OTH\gitver
gitver setver=5 -test gitver setver=5 -test
gitver setver=5 -setup=0
gitver setver=5 repodir=E:\Code\MyProj -setup=1
``` ```
**输出示例:** **输出示例:**
@@ -128,8 +117,21 @@ gitver setver=5 repodir=E:\Code\MyProj -setup=1
``` ```
ProductVersion=5.0.1920.11 ProductVersion=5.0.1920.11
FileVersion=5.26.0519.3 FileVersion=5.26.0519.3
已更新 upload_info.txt: E:\Code\MyProj\upload_info.txt
``` ```
**upload_info.txt 自动更新:**
`setver` 成功生成版本号后,会在仓库目录(`repodir`,缺省为当前目录)写入或更新 `upload_info.txt`
| 字段 | 来源 |
|---|---|
| `product_version` | 本次生成的 ProductVersion |
| `file_version` | 本次生成的 FileVersion |
| `file_change_log` | 文件版本对应日期的 git 提交说明(`origin/<分支>` 当天 log多条以换行分隔 |
`setup` 成功编译安装包后,还会写入 `file`(安装包路径)。文件中已有的其他字段(如 `package_name``package_type_id``url` 等)会保留不变。
**回写说明:** **回写说明:**
- **C++**:回写 `.rc` 文件中的 FILEVERSION、PRODUCTVERSION 及对应字符串字段 - **C++**:回写 `.rc` 文件中的 FILEVERSION、PRODUCTVERSION 及对应字符串字段
@@ -145,21 +147,21 @@ FileVersion=5.26.0519.3
生成版本号并调用 Nuitka 打包 Python 程序EXE 模式)。 生成版本号并调用 Nuitka 打包 Python 程序EXE 模式)。
``` ```
gitver nuitkabuild=<pid> <mainPy> [repodir=<path>] [-test] [params="<nuitka参数>"] gitver nuitkabuild=<pid> [repodir=<path>] [-test] [params="<mainPy> <nuitka参数>"]
``` ```
自动调用: 自动调用:
``` ```
python -m nuitka --windows-product-version=<版本> --windows-file-version=<版本> <extra> <mainPy> python -m nuitka --windows-product-version=<版本> --windows-file-version=<版本> <params>
``` ```
**示例:** **示例:**
``` ```
gitver nuitkabuild=5 main.py gitver nuitkabuild=5 params="main.py"
gitver nuitkabuild=5 main.py -test gitver nuitkabuild=5 -test params="main.py"
gitver nuitkabuild=5 src\app.py repodir=E:\Code\MyPyProj params="--standalone --output-dir=dist" gitver nuitkabuild=5 repodir=E:\Code\MyPyProj params="src\\app.py --standalone --output-dir=dist"
``` ```
**输出示例:** **输出示例:**
@@ -179,42 +181,381 @@ FileVersion=5.26.0519.3
生成版本号并调用 Nuitka 打包 Python 模块pyd/DLL 模式)。 生成版本号并调用 Nuitka 打包 Python 模块pyd/DLL 模式)。
``` ```
gitver nuitkapydbuild=<pid> <modulePy> [repodir=<path>] [-test] [params="<nuitka参数>"] gitver nuitkapydbuild=<pid> [repodir=<path>] [-test] [params="<modulePy> <nuitka参数>"]
``` ```
自动调用: 自动调用:
``` ```
python -m nuitka --module --windows-product-version=<版本> --windows-file-version=<版本> <extra> <modulePy> python -m nuitka --module --windows-product-version=<版本> --windows-file-version=<版本> <params>
``` ```
**示例:** **示例:**
``` ```
gitver nuitkapydbuild=5 module.py gitver nuitkapydbuild=5 params="module.py"
gitver nuitkapydbuild=5 module.py -test gitver nuitkapydbuild=5 -test params="module.py"
gitver nuitkapydbuild=5 src\core.py repodir=E:\Code\MyPyProj params="--output-dir=dist" gitver nuitkapydbuild=5 repodir=E:\Code\MyPyProj params="src\\core.py --output-dir=dist"
``` ```
> 需要当前环境已安装 Nuitka可通过 `python -m nuitka --version` 验证)。 > 需要当前环境已安装 Nuitka可通过 `python -m nuitka --version` 验证)。
--- ---
### `gitver -setup=0|1`(独立打包命令) ### `gitver pyinstaller=<pid>`
独立调用安装脚本打包,不依赖 `setver=` 命令 生成版本号并调用 PyInstaller 打包 Python 程序,支持直接传入 `.py` 入口脚本或 `.spec` 构建配置文件
``` ```
gitver -setup=0|1 <pid> [repodir=<path>] [-test] gitver pyinstaller=<pid> [repodir=<path>] [-test] [params="<mainPy|specFile> <pyinstaller参数>"]
``` ```
自动调用:
```
python -m PyInstaller <params>
```
版本号照常从 Git tag 计算并输出。在 **spec 模式**下gitver 会在调用 PyInstaller 前自动将版本号写入版本信息,具体行为取决于 spec 文件中 `version=` 字段的值:
| spec 中 `version=` 的值 | 行为 |
|---|---|
| `'version_info.txt'`(引号路径,文件存在) | 就地更新外部版本信息文件 |
| `'1.2.3.4'`(引号字符串,非有效文件路径) | 替换 spec 内该引号字符串为新版本号 |
| `version_info`无引号标识符spec 内定义的 VSVersionInfo 对象) | 就地更新 spec 文件内的版本字段 |
| 无 `version=` / `version=None` | 在 spec 目录生成 `version_info.txt` 并自动注入 `version='version_info.txt'` |
**脚本模式**`.py` 入口PyInstaller 不接受命令行版本参数,版本号仅输出到控制台,不嵌入 EXE。
**两种打包模式:**
| 模式 | 参数示例 | 说明 |
|---|---|---|
| 脚本模式 | `params="main.py"` | 由 PyInstaller 自动生成 spec 并打包 |
| spec 模式 | `params="myapp.spec"` | 使用已有 spec 文件,打包配置完全由 spec 控制gitver 在调用 PyInstaller 前**自动将版本号写入版本信息**(见下表) |
**示例:** **示例:**
``` ```
gitver -setup=0 5 gitver pyinstaller=5 params="main.py"
gitver -setup=1 5 repodir=E:\Code\MyProj gitver pyinstaller=5 -test params="main.py"
gitver pyinstaller=5 repodir=E:\Code\MyPyProj params="src\\app.py --onefile --name=MyApp"
gitver pyinstaller=5 params="myapp.spec"
gitver pyinstaller=5 -test params="myapp.spec"
gitver pyinstaller=5 repodir=E:\Code\MyPyProj params="myapp.spec"
``` ```
**输出示例:**
```
PyInstaller 打包开始.. (main.py)
ProductVersion=5.0.1920.11
FileVersion=5.26.0519.3
```
> 需要当前环境已安装 PyInstaller可通过 `python -m PyInstaller --version` 验证)。
---
### `gitver setup=n`(独立打包命令)
独立调用安装脚本打包,不依赖其他命令。
```
gitver setup=n [pid=m] [repodir=<path>] [-test]
```
| 参数 | 说明 |
|---|---|
| `n` | 安装脚本类型,`0`=Inno Setup`setup.iss``1`=NSIS`setup.nsi` |
| `pid=m` | 可选,产品 ID整数 0-65535不指定时自动从项目的 `.rc``AssemblyInfo.cs``.spec` 文件中读取版本信息 |
**示例:**
```
gitver setup=0
gitver setup=1
gitver setup=0 pid=5
gitver setup=1 pid=5 repodir=E:\Code\MyProj
```
**upload_info.txt 自动更新:**
`setup` 编译成功后,会在仓库目录(`repodir`,缺省为当前目录)写入或更新 `upload_info.txt``file` 字段(安装包完整路径)。路径从 ISCC/NSIS 编译输出中解析;若无法解析,则尝试脚本目录下 `Output\*.exe` 或脚本目录内最新 `.exe`
**输出示例:**
```
安装包路径: E:\Code\MyProj\Output\MyApp_Setup_5.26.0624.3.exe
已更新 upload_info.txt: E:\Code\MyProj\upload_info.txt
```
---
### `gitver upload`(上传包到 PMC
将构建产物上传到 PMC 包管理中心。需 PMC 服务已运行,且账号具有管理员权限。
```
gitver upload username=<用户名> [file=<上传文件>] [info=<配置.txt>] [url=<PMC基址>]
gitver upload username=<用户名> # 需在分支上且 HEAD 指向 Tag
gitver upload username=<用户名> -f # 游离 HEAD 指向 Tag 时强制上传
gitver upload -f username=<用户名> [file=<上传文件>] # -f 位置任意
```
| 参数 | 说明 |
|---|---|
| `username=` | PMC 登录账号(必填);登录密码从**同名环境变量**读取,如 `username=admin` 读取 `%admin%` |
| `file=` | 可选,要上传的包文件路径;省略时从 info 文件的 `file` 字段读取(通常由 `setup` 写入) |
| `info=` | 可选,上传配置信息文件路径,默认 `upload_info.txt`(见下方格式) |
| `url=` | 可选PMC 服务基址,默认 `http://127.0.0.1:8000` |
**info 配置文件格式**UTF-8 或 ANSI每行 `key=value``#` 开头为注释):
```ini
url=http://127.0.0.1:8000
package_name=daily-report
package_type_id=1
file=E:\dist\MyApp_Setup_5.26.0624.3.exe
product_version=5.0.1920.11
file_version=5.26.0624.3
file_change_log=修复导出 bug
description=日报工具
wiki_url=https://example.com/wiki
product_change_log=首次发布
```
| info 字段 | 必填 | 说明 |
|---|---|---|
| `package_name` | 是 | 包名称 |
| `package_type_id` | 是 | 包类型 ID整数 |
| `file` | 否* | 安装包路径;`upload` 省略 `file=` 时必填,通常由 `setup` 自动写入 |
| `product_version` | 是 | 产品版本号 |
| `file_version` | 是 | 文件版本号 |
| `file_change_log` | 是 | 文件版本更新说明 |
| `description` | 否 | 包描述 |
| `wiki_url` | 否 | Wiki 链接 |
| `product_change_log` | 否 | 产品版本更新说明 |
| `url` | 否 | PMC 基址,默认 `http://127.0.0.1:8000` |
上传流程:先 `POST /auth/login` 获取 Session`POST /admin/packages/upload`multipart/form-data最后通过 `/api/packages/latest` 验证版本是否写入。
HTTP 实现基于 [cpp-httplib](https://github.com/yhirose/cpp-httplib)[`GitVer/httplib.h`](GitVer/httplib.h)),默认支持 `http://`;若需 `https://` 须另行启用 OpenSSL 支持。
**示例:**
```powershell
# 先设置与 username 同名的环境变量作为登录密码
$env:admin = "your_admin_password"
```
```
gitver upload username=admin
gitver upload username=admin info=E:\upload_info.txt
gitver upload username=admin file=E:\dist\app.zip info=E:\upload_info.txt
gitver upload username=admin url=http://192.168.1.10:8000
```
---
## Nuitka 与 PyInstaller 常用参数速查
### Nuitka 常用参数
通过 `params="..."` 传递给 `gitver nuitkabuild=` / `nuitkapydbuild=`
| 参数 | 说明 |
|---|---|
| `--standalone` | 独立模式:将所有依赖打包进输出目录,可单独分发 |
| `--onefile` | 单文件模式:将独立包压缩为单个 EXE需配合 `--standalone` |
| `--output-dir=<dir>` | 指定输出目录,如 `--output-dir=dist` |
| `--windows-icon-from-ico=<ico>` | 设置 EXE 图标 |
| `--windows-company-name=<name>` | 设置公司名(写入文件属性) |
| `--windows-product-name=<name>` | 设置产品名 |
| `--enable-plugin=<plugin>` | 启用插件,如 `pyside6``tk-inter``numpy` |
| `--follow-imports` | 跟踪所有隐式导入(打包更完整,体积也更大) |
| `--nofollow-import-to=<mod>` | 排除指定模块不展开,如 `--nofollow-import-to=tests` |
| `--include-package=<pkg>` | 强制包含指定包 |
| `--include-data-files=<src>=<dst>` | 打包额外数据文件 |
| `--remove-output` | 打包前先清理上次输出 |
| `--lto=yes` | 启用链接时优化Release 构建推荐) |
| `--jobs=<n>` | 并行编译线程数,如 `--jobs=4` |
**典型用法示例:**
```bash
# 最小单文件 EXE
gitver nuitkabuild=5 params="main.py --standalone --onefile --output-dir=dist"
# 带图标、公司名的独立包
gitver nuitkabuild=5 params="main.py --standalone --windows-icon-from-ico=app.ico --windows-company-name=MyCompany --output-dir=dist"
# PySide6 GUI 应用
gitver nuitkabuild=5 params="main.py --standalone --enable-plugin=pyside6 --output-dir=dist"
# 打包 pyd 模块
gitver nuitkapydbuild=5 params="core.py --output-dir=dist"
# 异地仓库
gitver nuitkabuild=5 repodir=E:\Code\MyProj params="main.py --standalone --onefile --output-dir=dist"
```
---
### PyInstaller 常用参数
通过 `params="..."` 传递给 `gitver pyinstaller=`
| 参数 | 说明 |
|---|---|
| `--onefile` / `-F` | 单文件模式:打包成单个 EXE |
| `--onedir` / `-D` | 单目录模式(默认) |
| `--name=<name>` / `-n` | 指定输出文件名(不含后缀) |
| `--icon=<ico>` / `-i` | 设置 EXE 图标 |
| `--distpath=<dir>` | 指定输出目录,默认 `dist` |
| `--workpath=<dir>` | 中间文件目录,默认 `build` |
| `--specpath=<dir>` | spec 文件生成位置,默认当前目录 |
| `--noconsole` / `-w` | 不显示控制台窗口GUI 应用) |
| `--hidden-import=<mod>` | 手动指定隐式导入的模块 |
| `--add-data=<src>;<dst>` | 打包额外数据文件Windows 分隔符为 `;`Linux/macOS 为 `:` |
| `--version-file=<file>` | 从文件读入 Windows 版本信息嵌入 EXE |
| `--uac-admin` | 请求管理员权限运行 |
| `--clean` | 构建前清理缓存 |
| `--log-level=WARN` | 减少构建输出噪音,可选 `DEBUG``INFO``WARN``ERROR` |
**典型用法示例:**
```bash
# 最小单文件 EXE
gitver pyinstaller=5 params="main.py --onefile --name=MyApp"
# GUI 应用(关指控制台)
gitver pyinstaller=5 params="main.py --onefile --noconsole --icon=app.ico --name=MyApp"
# 包含资源文件
gitver pyinstaller=5 params="main.py --onefile --add-data=assets;assets --name=MyApp"
# 指定输出目录
gitver pyinstaller=5 params="main.py --onefile --distpath=dist --workpath=build --name=MyApp"
# 使用 spec 文件(配置均在 spec 中,不需要其他 params
gitver pyinstaller=5 params="myapp.spec"
# 使用 spec 并覆盖输出路径
gitver pyinstaller=5 params="myapp.spec --distpath=release"
# 异地仓库
gitver pyinstaller=5 repodir=E:\Code\MyProj params="main.py --onefile --name=MyApp"
```
**spec 文件嵌入版本信息(推荐方式):**
PyInstaller 不支持命令行版本参数。需将版本号嵌入 EXE 时,建议先用 `gitver setver=<pid>` 生成版本号,再在 spec 文件中设置 `version_file` 字段:
```python
# myapp.spec 片段
exe = EXE(
pyz,
...,
version='version_info.txt', # Windows 版本信息文件
name='MyApp',
)
```
**完整 spec 文件示例(`myapp.spec`**
```python
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(
['src\\main.py'], # 入口脚本
pathex=[],
binaries=[],
datas=[
('assets', 'assets'), # 额外资源目录:源路径, 目标路径
],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='MyApp',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False, # True=控制台程序False=GUI程序
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='assets\\app.ico', # 图标文件路径
version='version_info.txt', # Windows 版本信息文件
)
```
**配套的 `version_info.txt` 示例:**
```python
# UTF-8
# Windows EXE 版本信息文件
# 版本号格式major, minor, patch, build均为整数
VSVersionInfo(
ffi=FixedFileInfo(
filevers=(5, 0, 1920, 11), # FileVersion对应 FileVersion=5.0.1920.11
prodvers=(5, 0, 1920, 11), # ProductVersion
mask=0x3f,
flags=0x0,
OS=0x40004,
fileType=0x1, # 0x1=EXE, 0x2=DLL
subtype=0x0,
date=(0, 0),
),
kids=[
StringFileInfo([
StringTable(
'040904B0', # 语言 0409=英语, 04B0=Unicode
[
StringStruct('CompanyName', 'MyCompany'),
StringStruct('FileDescription', 'MyApp Application'),
StringStruct('FileVersion', '5.0.1920.11'),
StringStruct('InternalName', 'MyApp'),
StringStruct('LegalCopyright', 'Copyright (C) 2026 MyCompany'),
StringStruct('OriginalFilename', 'MyApp.exe'),
StringStruct('ProductName', 'MyApp'),
StringStruct('ProductVersion', '5.0.1920.11'),
]
)
]),
VarFileInfo([VarStruct('Translation', [0x0409, 0x04B0])])
]
)
```
> 在 spec 模式下gitver 会在调用 PyInstaller 前自动将计算出的版本号写入版本信息,无需手动维护 `version_info.txt`。
--- ---
## 错误码 ## 错误码
@@ -245,10 +586,24 @@ gitver -setup=1 5 repodir=E:\Code\MyProj
| 33 | 无参数模式下创建 tag 失败 | | 33 | 无参数模式下创建 tag 失败 |
| 34 | 无参数模式下创建 tag 后校验失败 | | 34 | 无参数模式下创建 tag 后校验失败 |
| 35 | 未找到可回写目标文件,或回写失败 | | 35 | 未找到可回写目标文件,或回写失败 |
| 36 | `-setup=` 值不支持(仅支持 0 和 1 | | 36 | `setup=` 值不支持(仅支持 0 和 1 |
| 37 | 未找到安装脚本setup.iss / setup.nsi | | 37 | 未找到安装脚本setup.iss / setup.nsi |
| 38 | 修改安装脚本失败 | | 38 | 修改安装脚本失败 |
| 39 | 未找到安装编译器ISCC.exe / makensis.exe | | 39 | 未找到安装编译器ISCC.exe / makensis.exe |
| 40 | `pyinstaller=` 参数不足(缺少 mainPy 或 specFile |
| 41 | `pyinstaller=` 的 pid 非法 |
| 42 | `pyinstaller=` 无法根据分支名计算 bid |
| 43 | `pyinstaller=` 读取 tag 失败 |
| 44 | `pyinstaller=` 获取当天分支提交次数失败 |
| 45 | `pyinstaller=` 版本信息文件无法生成或更新 |
| 46 | `pyinstaller=` spec 文件无法注入或修改版本信息 |
| 47 | `upload` 缺少必填参数username或 file 未指定且 info 中无 file 字段 |
| 48 | `upload` 上传文件不存在 |
| 49 | `upload` info 文件无效、缺少必填字段,或未设置与 username 同名的环境变量 |
| 50 | `upload` PMC 登录失败 |
| 51 | `upload` HTTP 上传请求失败 |
| 52 | `upload` 上传后未找到对应版本(校验失败或版本冲突) |
| 53 | `upload` 网络/HTTP 错误 |
--- ---
@@ -304,19 +659,46 @@ gitver setver=5 -test
### 验证 `nuitkabuild=` ### 验证 `nuitkabuild=`
```bash ```bash
gitver nuitkabuild=5 main.py gitver nuitkabuild=5 params="main.py"
# 预期: 调用 python -m nuitka注入版本号参数 # 预期: 调用 python -m nuitka注入版本号参数
gitver nuitkabuild=5 src\app.py params="--standalone --output-dir=dist" gitver nuitkabuild=5 params="src\\app.py --standalone --output-dir=dist"
# 预期: 额外参数传递给 Nuitka # 预期: 额外参数传递给 Nuitka
``` ```
### 验证 `-setup=` ### 验证 `pyinstaller=`
```bash
gitver pyinstaller=5 params="main.py"
# 预期: 调用 python -m PyInstaller main.py
# 输出 ProductVersion / FileVersion
gitver pyinstaller=5 params="myapp.spec"
# 预期: 调用 python -m PyInstaller myapp.specspec 模式)
gitver pyinstaller=5 params="main.py --onefile --name=MyApp"
# 预期: 额外参数传递给 PyInstaller
```
### 验证 `setup=`
```bash ```bash
# 确保 exe 目录或上级目录有 setup.iss # 确保 exe 目录或上级目录有 setup.iss
gitver setver=5 -setup=0 gitver setup=0
# 预期: 修改 setup.iss 中的版本号并调用 ISCC.exe 编译 # 预期: 从 .rc 或 AssemblyInfo.cs 读取版本号,修改 setup.iss 并调用 ISCC.exe 编译
gitver setup=0 pid=5
# 预期: 使用 pid=5 生成版本号,修改 setup.iss 并调用 ISCC.exe 编译
```
### 验证 upload
```powershell
# 前提PMC 运行于 http://127.0.0.1:8000且已有 admin 账号
$env:admin = "your_admin_password"
# 准备 upload_info.txt含 package_name、file 等字段,不含 password
gitver upload username=admin
# 预期: 读取当前目录 upload_info.txt返回码 0PMC 管理页可见新版本
``` ```
--- ---
@@ -339,29 +721,62 @@ gitver setver=5 -setup=0
| `gitver setver=5` | 0 | | `gitver setver=5` | 0 |
| `gitver setver=5 -test` | 0 | | `gitver setver=5 -test` | 0 |
| `gitver setver=5 repodir=E:\NotExists` | 23 | | `gitver setver=5 repodir=E:\NotExists` | 23 |
| `gitver setver=5 -setup=0` | 0 / 37 / 38 / 39 |
| `gitver setver=5 -setup=9` | 36 |
| `gitver setver=abc` | 4 | | `gitver setver=abc` | 4 |
### nuitkabuild= 组 ### nuitkabuild= 组
| 命令 | 预期返回码 | | 命令 | 预期返回码 |
|---|---| |---|---|
| `gitver nuitkabuild=5 main.py` | 0 / 19 / 21 | | `gitver nuitkabuild=5 params="main.py"` | 0 / 19 / 21 |
| `gitver nuitkabuild=5 main.py -test` | 0 | | `gitver nuitkabuild=5 -test params="main.py"` | 0 |
| `gitver nuitkabuild=5 main.py params="--standalone"` | 0 | | `gitver nuitkabuild=5 params="main.py --standalone"` | 0 |
| `gitver nuitkabuild=5 main.py repodir=E:\NotExists` | 23 | | `gitver nuitkabuild=5 repodir=E:\NotExists params="main.py"` | 23 |
| `gitver nuitkabuild=5` | 17 | | `gitver nuitkabuild=5` | 0 |
### nuitkapydbuild= 组 ### nuitkapydbuild= 组
| 命令 | 预期返回码 | | 命令 | 预期返回码 |
|---|---| |---|---|
| `gitver nuitkapydbuild=5 module.py` | 0 / 26 / 28 | | `gitver nuitkapydbuild=5 params="module.py"` | 0 / 26 / 28 |
| `gitver nuitkapydbuild=5 module.py -test` | 0 | | `gitver nuitkapydbuild=5 -test params="module.py"` | 0 |
| `gitver nuitkapydbuild=5 module.py params="--output-dir=dist"` | 0 | | `gitver nuitkapydbuild=5 params="module.py --output-dir=dist"` | 0 |
| `gitver nuitkapydbuild=5 module.py repodir=E:\NotExists` | 23 | | `gitver nuitkapydbuild=5 repodir=E:\NotExists params="module.py"` | 23 |
| `gitver nuitkapydbuild=5` | 24 | | `gitver nuitkapydbuild=5` | 0 |
### pyinstaller= 组
| 命令 | 预期返回码 |
|---|---|
| `gitver pyinstaller=5 params="main.py"` | 0 / 42 / 44 |
| `gitver pyinstaller=5 -test params="main.py"` | 0 |
| `gitver pyinstaller=5 params="main.py --onefile"` | 0 |
| `gitver pyinstaller=5 params="myapp.spec"` | 0 |
| `gitver pyinstaller=5 -test params="myapp.spec"` | 0 |
| `gitver pyinstaller=5 repodir=E:\NotExists params="main.py"` | 23 |
| `gitver pyinstaller=5` | 0 |
| `gitver pyinstaller=abc params="main.py"` | 41 |
### setup= 组
| 命令 | 预期返回码 |
|---|---|
| `gitver setup=0` | 0 / 37 / 38 / 39 |
| `gitver setup=1` | 0 / 37 / 38 / 39 |
| `gitver setup=0 pid=5` | 0 / 37 / 38 / 39 |
| `gitver setup=0 pid=5 repodir=E:\NotExists` | 23 |
| `gitver setup=9` | 36 |
| `gitver setup=0 pid=abc` | 4 |
### upload 组
| 命令 | 预期返回码 |
|---|---|
| `gitver upload username=admin` | 0 / 47 / 48 / 49 / 50 / 52 / 53 |
| `gitver upload username=admin info=E:\upload_info.txt` | 0 / 50 / 52 / 53 |
| `gitver upload username=admin file=E:\dist\app.zip info=E:\upload_info.txt` | 0 / 50 / 52 / 53 |
| `gitver upload username=admin file=E:\NotExists info=E:\upload_info.txt` | 48 |
| `gitver upload username=admin info=E:\NotExists` | 49 |
| `gitver upload` | 47 |
> **注意**如果打包时提示“未找到安装编译器ISCC.exe / makensis.exe请手动下载安装对应工具并将其可执行文件路径如 `ISCC.exe` 或 `makensis.exe` 所在目录)添加到系统环境变量 `PATH`。常见下载地址: > **注意**如果打包时提示“未找到安装编译器ISCC.exe / makensis.exe请手动下载安装对应工具并将其可执行文件路径如 `ISCC.exe` 或 `makensis.exe` 所在目录)添加到系统环境变量 `PATH`。常见下载地址:
> >