Compare commits

...

8 Commits

Author SHA1 Message Date
4c51a7f8d5 优化安装包脚本版本信息为文件版本 2026-05-20 09:22:48 +08:00
Jeff
da6587502c 加强md描述 2026-05-19 18:26:34 +08:00
Jeff
4c6b5471b1 更新描述 2026-05-19 16:14:31 +08:00
Jeff
565575dbc5 更新md文件 2026-05-19 16:03:33 +08:00
Jeff
b2b508f520 调协字符编码 2026-05-19 15:38:12 +08:00
Jeff
abcbc88399 去除-f参数,同时添加md文件进行说明 2026-05-19 15:25:29 +08:00
Jeff
63891dab14 1、setver 5改为setver=5
2、新增-setupd=n命令行参数,n=0表示使用inno setup,n=1表示使用nsis脚本,并自动设置脚本的版本号和自动运行脚本。
2026-05-19 13:58:59 +08:00
Jeff
9eaec59bfc 新增命令行参数:-test,用于生成测试版本的产品版本信息,如pid.bid.0.0 2026-05-19 10:46:54 +08:00
10 changed files with 1072 additions and 212 deletions

View File

@@ -1,4 +1,4 @@
// GitVer.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
// GitVer.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include "pch.h"
@@ -12,12 +12,13 @@
#include "gitver_tag.h"
#include "gitver_types.h"
#include "gitver_version.h"
#include "gitver_setup.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// 唯一的应用程序对象
// 唯一的应用程序对象
CWinApp theApp;
@@ -32,18 +33,13 @@ extern TCHAR g_szFolderName[MAX_PATH] = { 0 };
const UINT DEFAULT_MAJOR_WHEN_NO_TAG = 1;
const UINT DEFAULT_MINOR_WHEN_NO_TAG = 0;
#ifdef DEBUG
#define RC_DIR _T("F:\\Code\\HS\\SHWS_dev\\SHWS\\")
#define RC_NAME _T("SHWS")
#endif
int HandleSetVerCommand(int argc, TCHAR* argv[]);
/// <summary>
/// 将宽字符字符串转换为ANSI字符串。
/// 将宽字符字符串转换为ANSI字符串。
/// </summary>
/// <param name="lpText">要转换的宽字符字符串。</param>
/// <returns>转换后的ANSI字符串。</returns>
/// <param name="lpText">要转换的宽字符字符串。</param>
/// <returns>转换后的ANSI字符串。</returns>
std::string ToAnsiString(LPCTSTR lpText)
{
if (lpText == NULL)
@@ -68,11 +64,11 @@ std::string ToAnsiString(LPCTSTR lpText)
}
/// <summary>
/// 检查指定目录下是否存在与模式匹配的文件。
/// 检查指定目录下是否存在与模式匹配的文件。
/// </summary>
/// <param name="lpBaseDir">要搜索的基目录。</param>
/// <param name="lpPattern">要匹配的文件模式。</param>
/// <returns>如果存在匹配的文件则返回TRUE否则返回FALSE。</returns>
/// <param name="lpBaseDir">要搜索的基目录。</param>
/// <param name="lpPattern">要匹配的文件模式。</param>
/// <returns>如果存在匹配的文件则返回TRUE否则返回FALSE。</returns>
BOOL HasMatchingFile(LPCTSTR lpBaseDir, LPCTSTR lpPattern)
{
CString strSearchPath;
@@ -90,12 +86,12 @@ BOOL HasMatchingFile(LPCTSTR lpBaseDir, LPCTSTR lpPattern)
}
/// <summary>
/// 递归查找与模式匹配的第一个文件。
/// 递归查找与模式匹配的第一个文件。
/// </summary>
/// <param name="lpBaseDir">要搜索的基目录。</param>
/// <param name="lpPattern">要匹配的文件模式。</param>
/// <param name="strFoundPath">找到的文件的完整路径。</param>
/// <returns>如果找到匹配的文件则返回TRUE否则返回FALSE。</returns>
/// <param name="lpBaseDir">要搜索的基目录。</param>
/// <param name="lpPattern">要匹配的文件模式。</param>
/// <param name="strFoundPath">找到的文件的完整路径。</param>
/// <returns>如果找到匹配的文件则返回TRUE否则返回FALSE。</returns>
BOOL FindFirstFileByPatternRecursive(LPCTSTR lpBaseDir, LPCTSTR lpPattern, CString& strFoundPath)
{
strFoundPath.Empty();
@@ -154,12 +150,12 @@ BOOL FindFirstFileByPatternRecursive(LPCTSTR lpBaseDir, LPCTSTR lpPattern, CStri
}
/// <summary>
/// 构建相对于仓库的路径。
/// 构建相对于仓库的路径。
/// </summary>
/// <param name="lpBaseDir">基目录。</param>
/// <param name="lpFullPath">完整路径。</param>
/// <param name="strRelativePath">相对路径。</param>
/// <returns>如果成功构建相对路径则返回TRUE否则返回FALSE。</returns>
/// <param name="lpBaseDir">基目录。</param>
/// <param name="lpFullPath">完整路径。</param>
/// <param name="strRelativePath">相对路径。</param>
/// <returns>如果成功构建相对路径则返回TRUE否则返回FALSE。</returns>
BOOL BuildRepoRelativePath(LPCTSTR lpBaseDir, LPCTSTR lpFullPath, CString& strRelativePath)
{
strRelativePath.Empty();
@@ -195,10 +191,10 @@ BOOL BuildRepoRelativePath(LPCTSTR lpBaseDir, LPCTSTR lpFullPath, CString& strRe
}
/// <summary>
/// 获取文件名(不包含扩展名)。
/// 获取文件名(不包含扩展名)。
/// </summary>
/// <param name="lpFilePath">文件路径。</param>
/// <returns>不包含扩展名的文件名。</returns>
/// <param name="lpFilePath">文件路径。</param>
/// <returns>不包含扩展名的文件名。</returns>
CString GetFileNameWithoutExtension(LPCTSTR lpFilePath)
{
if (lpFilePath == NULL)
@@ -217,11 +213,11 @@ CString GetFileNameWithoutExtension(LPCTSTR lpFilePath)
}
/// <summary>
/// 查找首选的C++资源文件(.rc
/// 查找首选的C++资源文件(.rc
/// </summary>
/// <param name="lpBaseDir">要搜索的基目录。</param>
/// <param name="strResFile">找到的资源文件的完整路径。</param>
/// <returns>如果找到首选的资源文件则返回TRUE否则返回FALSE。</returns>
/// <param name="lpBaseDir">要搜索的基目录。</param>
/// <param name="strResFile">找到的资源文件的完整路径。</param>
/// <returns>如果找到首选的资源文件则返回TRUE否则返回FALSE。</returns>
BOOL FindPreferredCppRcFileAt(LPCTSTR lpBaseDir, CString& strResFile)
{
strResFile.Empty();
@@ -259,11 +255,11 @@ BOOL FindPreferredCppRcFileAt(LPCTSTR lpBaseDir, CString& strResFile)
}
/// <summary>
/// 查找首选的AssemblyInfo.cs文件。
/// 查找首选的AssemblyInfo.cs文件。
/// </summary>
/// <param name="lpBaseDir">要搜索的基目录。</param>
/// <param name="strResFile">找到的AssemblyInfo.cs文件的完整路径。</param>
/// <returns>如果找到首选的AssemblyInfo.cs文件则返回TRUE否则返回FALSE。</returns>
/// <param name="lpBaseDir">要搜索的基目录。</param>
/// <param name="strResFile">找到的AssemblyInfo.cs文件的完整路径。</param>
/// <returns>如果找到首选的AssemblyInfo.cs文件则返回TRUE否则返回FALSE。</returns>
BOOL FindPreferredAssemblyInfoFileAt(LPCTSTR lpBaseDir, CString& strResFile)
{
strResFile.Empty();
@@ -289,13 +285,13 @@ BOOL FindPreferredAssemblyInfoFileAt(LPCTSTR lpBaseDir, CString& strResFile)
}
/// <summary>
/// 检测项目的源码类型。
/// 检测项目的源码类型。
/// </summary>
/// <param name="lpBaseDir">要检测的基目录。</param>
/// <returns>返回项目的源码类型枚举值。</returns>
/// <param name="lpBaseDir">要检测的基目录。</param>
/// <returns>返回项目的源码类型枚举值。</returns>
int DetectProjectCodeType(LPCTSTR lpBaseDir)
{
_tprintf(_T("检测项目源码类型:%s\n"), lpBaseDir);
_tprintf(_T("检测项目源码类型:%s\n"), lpBaseDir);
if (PathFileExists(CString(lpBaseDir) + _T("\\Properties\\AssemblyInfo.cs"))
|| HasMatchingFile(lpBaseDir, _T("*.csproj"))
|| HasMatchingFile(lpBaseDir, _T("*.cs")))
@@ -325,10 +321,10 @@ int DetectProjectCodeType(LPCTSTR lpBaseDir)
}
/// <summary>
/// 获取项目源码类型的名称。
/// 获取项目源码类型的名称。
/// </summary>
/// <param name="nCodeType">项目源码类型枚举值。</param>
/// <returns>项目源码类型的名称。</returns>
/// <param name="nCodeType">项目源码类型枚举值。</param>
/// <returns>项目源码类型的名称。</returns>
LPCTSTR GetProjectCodeTypeName(int nCodeType)
{
switch (nCodeType)
@@ -345,72 +341,78 @@ LPCTSTR GetProjectCodeTypeName(int nCodeType)
}
/// <summary>
/// 打印完整的使用示例。
/// 打印完整的使用示例。
/// </summary>
void PrintFullUsageExamples()
{
LPCTSTR lpUsageText =
_T("用法:\n")
_T(" gitver (显示帮助后进入当前分支创建 tag 流程)\n")
_T(" gitver rewrite [PEType可选]\n")
_T(" gitver setver [pid] [repoPath可选]\n")
_T(" gitver nuitkabuild [pid] [mainPy] [repoPath可选] [nuitka额外参数可选]\n")
_T(" gitver nuitkapydbuild [pid] [modulePy] [repoPath可选] [nuitka额外参数可选]\n")
_T("\n示例:\n")
_T("用法:\n")
_T(" gitver (显示帮助后进入当前分支创建 tag 流程)\n")
_T(" gitver rewrite [PEType可选] [-f可选]\n")
_T(" gitver setver=<pid> [repodir=<path>可选] [-test可选]\n")
_T(" gitver nuitkabuild=<pid> <mainPy> [repodir=<path>可选] [-test可选] [params=\"<nuitka参数>\"可选]\n")
_T(" gitver nuitkapydbuild=<pid> <modulePy> [repodir=<path>可选] [-test可选] [params=\"<nuitka参数>\"可选]\n")
_T(" gitver -setup=0|1 [pid] [repodir=<path>可选] [-test可选]\n")
_T(" -setup=0: 使用 Inno Setup 脚本 (setup.iss)\n")
_T(" -setup=1: 使用 NSIS 脚本 (setup.nsh)\n")
_T("\n示例:\n")
_T(" gitver\n")
_T(" gitver rewrite\n")
_T(" gitver rewrite 2\n")
_T(" gitver rewrite -f\n")
_T(" gitver setver 5 E:\\Code\\OTH\\gitver\n")
_T(" gitver setver 5\n")
_T(" gitver setver 5 -f\n")
_T(" gitver nuitkabuild 5 main.py\n")
_T(" gitver nuitkabuild 5 main.py -f\n")
_T(" gitver nuitkabuild 5 src\\app.py E:\\Code\\MyPyProj --standalone --output-dir=dist\n")
_T(" gitver nuitkabuild 5 main.py --standalone -f\n")
_T(" gitver nuitkapydbuild 5 module.py\n")
_T(" gitver nuitkapydbuild 5 module.py -f\n")
_T(" gitver nuitkapydbuild 5 src\\core.py E:\\Code\\MyPyProj --output-dir=dist\n")
_T("\n说明: nuitkabuild/nuitkapydbuild 中,-f 在额外参数开始前表示 gitver 选项;进入额外参数后会透传给 Nuitka。\n")
_T("无参数时会读取当前分支最近三次 tag并提示选择 major 加 1 或 minor 加 1然后创建新 tag\n");
_T(" gitver setver=5 repodir=E:\\Code\\OTH\\gitver\n")
_T(" gitver setver=5\n")
_T(" gitver setver=5 -test\n")
_T(" gitver nuitkabuild=5 main.py\n")
_T(" gitver nuitkabuild=5 main.py -test\n")
_T(" gitver nuitkabuild=5 src\\app.py repodir=E:\\Code\\MyPyProj params=\"--standalone --output-dir=dist\"\n")
_T(" gitver nuitkapydbuild=5 module.py\n")
_T(" gitver nuitkapydbuild=5 module.py -test\n")
_T(" gitver nuitkapydbuild=5 src\\core.py repodir=E:\\Code\\MyPyProj params=\"--output-dir=dist\"\n")
_T(" gitver -setup=0 5\n")
_T(" gitver -setup=1 5 repodir=E:\\Code\\MyProj\n")
_T("params: 传递给 Nuitka 的额外参数,用双引号括起来,如 params=\"--standalone --output-dir=dist\"\n")
_T("-test: 将产品版本号的 major 和 minor 都置为 0用于测试版本构建\n")
_T("无参数时会读取当前分支最近三次 tag并提示选择 major 加 1 或 minor 加 1然后创建新 tag。\n")
_T("未找到当前分支 tag 时自动使用默认版本 1.0。\n");
_tprintf(_T("%s"), lpUsageText);
}
/// <summary>
/// 打印命令简要用法(用于未知命令等场景)。
/// 打印命令简要用法(用于未知命令等场景)。
/// </summary>
void PrintShortCommandUsage()
{
_tprintf(_T("请使用gitver rewrite [PE类型可选]\n"));
_tprintf(_T("请使用gitver setver [pid] [repoPath可选]\n"));
_tprintf(_T("gitver nuitkabuild [pid] [mainPy] [repoPath可选] [nuitka额外参数可选]\n"));
_tprintf(_T("gitver nuitkapydbuild [pid] [modulePy] [repoPath可选] [nuitka额外参数可选]\n"));
_tprintf(_T("请使用gitver rewrite [PE类型可选]\n"));
_tprintf(_T("请使用gitver setver=<pid> [repodir=<path>可选]\n"));
_tprintf(_T("gitver nuitkabuild=<pid> <mainPy> [repodir=<path>可选] [params=\"<nuitka参数>\"可选]\n"));
_tprintf(_T("gitver nuitkapydbuild=<pid> <modulePy> [repodir=<path>可选] [params=\"<nuitka参数>\"可选]\n"));
}
/// <summary>
/// 打印命令参数不足时的标准用法与示例。
/// 打印命令参数不足时的标准用法与示例。
/// </summary>
/// <param name="lpCommandName">命令名称。</param>
/// <param name="lpRequiredArgs">必选参数占位文本。</param>
/// <param name="lpExampleRequiredArgs">示例中的必选参数。</param>
/// <param name="lpCommandName">命令名称。</param>
/// <param name="lpRequiredArgs">必选参数占位文本。</param>
/// <param name="lpExampleRequiredArgs">示例中的必选参数。</param>
void PrintCommandUsageAndExamples(LPCTSTR lpCommandName, LPCTSTR lpRequiredArgs, LPCTSTR lpExampleRequiredArgs)
{
_tprintf(_T("用法gitver %s %s [repoPath可选]\n"), lpCommandName, lpRequiredArgs);
_tprintf(_T("用法gitver %s %s [repodir=<path>可选]\n"), lpCommandName, lpRequiredArgs);
if (lpExampleRequiredArgs != NULL && _tcslen(lpExampleRequiredArgs) > 0)
{
_tprintf(_T("示例gitver %s 5 %s E:\\Code\\OTH\\gitver\n"), lpCommandName, lpExampleRequiredArgs);
_tprintf(_T("示例gitver %s 5 %s\n"), lpCommandName, lpExampleRequiredArgs);
_tprintf(_T("示例gitver %s 5 %s repodir=E:\\Code\\OTH\\gitver\n"), lpCommandName, lpExampleRequiredArgs);
_tprintf(_T("示例gitver %s 5 %s\n"), lpCommandName, lpExampleRequiredArgs);
}
else
{
_tprintf(_T("示例gitver %s 5 E:\\Code\\OTH\\gitver\n"), lpCommandName);
_tprintf(_T("示例gitver %s 5\n"), lpCommandName);
_tprintf(_T("示例gitver %s 5 repodir=E:\\Code\\OTH\\gitver\n"), lpCommandName);
_tprintf(_T("示例gitver %s 5\n"), lpCommandName);
}
}
/// <summary>
/// 获取退出码的简要说明。
/// 获取退出码的简要说明。
/// </summary>
LPCTSTR GetExitCodeHint(int nRetCode)
{
@@ -472,13 +474,21 @@ LPCTSTR GetExitCodeHint(int nRetCode)
return _T("interactive: verify tag failed");
case 35:
return _T("rewrite version file failed");
case 36:
return _T("setup: unsupported -setup value");
case 37:
return _T("setup: setup script not found");
case 38:
return _T("setup: modify setup script failed");
case 39:
return _T("setup: compiler not found");
default:
return _T("");
}
}
/// <summary>
/// 在命令执行返回非零时输出统一错误日志。
/// 在命令执行返回非零时输出统一错误日志。
/// </summary>
void PrintCommandFailedWithCode(LPCTSTR lpCommandName, int nRetCode)
{
@@ -497,18 +507,18 @@ void PrintCommandFailedWithCode(LPCTSTR lpCommandName, int nRetCode)
}
/// <summary>
/// 输出命令执行结束日志(含耗时和退出码)。
/// 输出命令执行结束日志(含耗时和退出码)。
/// </summary>
void LogCommandEnd(LPCTSTR lpCommandName, DWORD dwStartTick, int nRetCode)
{
DWORD dwElapsed = GetTickCount() - dwStartTick;
_tprintf(_T("命令结束: %s耗时 %u ms退出码 %d\n"), lpCommandName, dwElapsed, nRetCode);
_tprintf(_T("命令结束: %s耗时 %u ms退出码 %d\n"), lpCommandName, dwElapsed, nRetCode);
}
/// <summary>
/// 获取当前模块的目录信息。
/// 获取当前模块的目录信息。
/// </summary>
/// <returns>返回值。</returns>
/// <returns>返回值。</returns>
void GetDirInfo()
{
TCHAR szDrive[_MAX_DRIVE] = { 0 };
@@ -540,42 +550,29 @@ void GetDirInfo()
_stprintf_s(g_szFolderName, _T("%s"), strVal.GetString());
}
#ifdef _DEBUG
_stprintf_s(g_szFolderName, RC_NAME);
_stprintf_s(g_szCurModuleDir, RC_DIR);
#endif
SetCurrentDirectory(g_szCurModuleDir);
}
/// <summary>
/// 处理设置版本命令。
/// 处理设置版本命令。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <returns>返回值。</returns>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <returns>返回值。</returns>
int HandleSetVerCommand(int argc, TCHAR* argv[])
{
if (argc < 3)
{
_tprintf(_T("错误: 参数不足。\n"));
PrintCommandUsageAndExamples(_T("setver"), _T("[pid]"), _T(""));
_tprintf(_T("参数说明:\n"));
_tprintf(_T(" pid: 产品ID必填范围0-65535。\n"));
_tprintf(_T(" repoPath: 仓库路径,可选,默认使用当前目录。\n"));
return 3;
}
// 解析 "setver=N" 中的 pid
CString strPid = CString(argv[1]).Mid(7); // 跳过 "setver="
UINT nPid = 0;
int nArgRet = ParseUInt16ArgOrError(argv, 2, _T("pid"), nPid, 4);
if (nArgRet != 0)
if (strPid.IsEmpty() || !TryParseUInt16(strPid, nPid))
{
return nArgRet;
_tprintf(_T("错误: setver= 后的 pid 无效:%s应为 0-65535 范围的整数。\n"), strPid.GetString());
return 4;
}
LPCTSTR lpRepoPath = NULL;
BOOL bUseDefaultTagWhenMissing = FALSE;
int nRepoArgRet = ParseSetVerOptions(argc, argv, lpRepoPath, bUseDefaultTagWhenMissing);
BOOL bTestMode = FALSE;
int nRepoArgRet = ParseSetVerOptions(argc, argv, 2, lpRepoPath, bTestMode);
if (nRepoArgRet != 0)
{
return nRepoArgRet;
@@ -590,7 +587,6 @@ int HandleSetVerCommand(int argc, TCHAR* argv[])
errorCodes,
strProductVersion,
strFileVersion,
bUseDefaultTagWhenMissing,
DEFAULT_MAJOR_WHEN_NO_TAG,
DEFAULT_MINOR_WHEN_NO_TAG);
if (nVersionRet != 0)
@@ -598,21 +594,61 @@ int HandleSetVerCommand(int argc, TCHAR* argv[])
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("[测试版本] 已将 major/minor 置零: ProductVersion=%s\n"), strProductVersion.GetString());
}
_tprintf(_T("ProductVersion=%s\n"), strProductVersion.GetString());
_tprintf(_T("FileVersion=%s\n"), strFileVersion.GetString());
// 检查是否附带了 -setup=N 标志
int nSetupType = -1;
for (int i = 2; i < argc; ++i)
{
CString strArg = argv[i];
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)
{
// -setup=N 存在:不回写版本信息到项目文件,只修改安装脚本并编译
if (!ExecuteSetupBuild(nSetupType, strProductVersion, strFileVersion))
{
return 38;
}
return 0;
}
return RewriteSetVerByProjectType(lpRepoPath, strProductVersion, strFileVersion);
}
/// <summary>
/// <summary>
/// 准备C++重写内容。
/// 准备C++重写内容。
/// </summary>
/// <param name="strCommitId">提交ID。</param>
/// <param name="nPEType">PE类型。</param>
/// <param name="strResFile">资源文件路径。</param>
/// <param name="vtOldContent">旧内容向量。</param>
/// <param name="vtNewContent">新内容向量。</param>
/// <param name="strCommitId">提交ID。</param>
/// <param name="nPEType">PE类型。</param>
/// <param name="strResFile">资源文件路径。</param>
/// <param name="vtOldContent">旧内容向量。</param>
/// <param name="vtNewContent">新内容向量。</param>
/// </summary>
void PrepareCppRewriteContent(const CString& strCommitId, int nPEType, CString& strResFile, std::vector<std::string>& vtOldContent, std::vector<std::string>& vtNewContent)
{
@@ -620,7 +656,7 @@ void PrepareCppRewriteContent(const CString& strCommitId, int nPEType, CString&
if (!FindPreferredCppRcFileAt(g_szCurModuleDir, strResFile))
{
_tprintf(_T("错误: 未找到C++ .rc资源文件。\n"));
_tprintf(_T("错误: 未找到C++ .rc资源文件。\n"));
return;
}
@@ -659,7 +695,7 @@ void PrepareCppRewriteContent(const CString& strCommitId, int nPEType, CString&
vtNewContent.push_back(ToAnsiString(szValue));
_tprintf(_T("Rewrite new token: %s\n"), szValue);
#if 0
_stprintf_s(szValue, "VALUE \"FileDescription\", \"TODO: <文件说明>\"");
_stprintf_s(szValue, "VALUE \"FileDescription\", \"TODO: <文件说明>\"");
vtOldContent.push_back(szValue);
_stprintf_s(szValue, "VALUE \"FileDescription\", \"%s\"", strCommitId.GetString());
@@ -669,18 +705,18 @@ void PrepareCppRewriteContent(const CString& strCommitId, int nPEType, CString&
}
/// <summary>
/// 准备C#重写内容。
/// 准备C#重写内容。
/// </summary>
/// <param name="strCommitId">提交ID。</param>
/// <param name="strResFile">资源文件路径。</param>
/// <param name="vtOldContent">旧内容向量。</param>
/// <param name="vtNewContent">新内容向量。</param>
/// <param name="strCommitId">提交ID。</param>
/// <param name="strResFile">资源文件路径。</param>
/// <param name="vtOldContent">旧内容向量。</param>
/// <param name="vtNewContent">新内容向量。</param>
void PrepareCSharpRewriteContent(const CString& strCommitId, CString& strResFile, std::vector<std::string>& vtOldContent, std::vector<std::string>& vtNewContent)
{
TCHAR szValue[MAX_PATH] = { 0 };
if (!FindPreferredAssemblyInfoFileAt(g_szCurModuleDir, strResFile))
{
_tprintf(_T("错误: 未找到首选的 AssemblyInfo 文件。\n"));
_tprintf(_T("错误: 未找到首选的 AssemblyInfo 文件。\n"));
return;
}
@@ -707,11 +743,11 @@ void PrepareCSharpRewriteContent(const CString& strCommitId, CString& strResFile
}
/// <summary>
/// 处理重写命令。
/// 处理重写命令。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <returns>返回值。</returns>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <returns>返回值。</returns>
int HandleRewriteCommand(int argc, TCHAR* argv[])
{
int nCodeType = DetectProjectCodeType(g_szCurModuleDir);
@@ -726,23 +762,19 @@ int HandleRewriteCommand(int argc, TCHAR* argv[])
if (nCodeType == PROJECT_UNKNOWN)
{
_tprintf(_T("错误: 未知项目类型。\n"));
_tprintf(_T("错误: 未知项目类型。\n"));
return 15;
}
_tprintf(_T("项目类型: %s\n"), GetProjectCodeTypeName(nCodeType));
_tprintf(_T("项目类型: %s\n"), GetProjectCodeTypeName(nCodeType));
#ifdef _DEBUG
CString strValue = StartProcess(NULL, _T("cmd /c git rev-parse --short HEAD"), RC_DIR);
#else
CString strValue = StartProcess(NULL, _T("cmd /c git rev-parse --short HEAD"), NULL);
#endif
strValue.Replace(_T("\n"), _T(""));
strValue.Replace(_T("\r"), _T(""));
strValue.Trim();
if (strValue.IsEmpty())
{
_tprintf(_T("错误: 获取提交ID失败。\n"));
_tprintf(_T("错误: 获取提交ID失败。\n"));
return 2;
}
@@ -760,7 +792,7 @@ int HandleRewriteCommand(int argc, TCHAR* argv[])
}
else if (nCodeType == PROJECT_PYTHON)
{
_tprintf(_T("错误: 不支持的项目类型: Python。\n"));
_tprintf(_T("错误: 不支持的项目类型: Python。\n"));
return 16;
}
@@ -768,11 +800,11 @@ int HandleRewriteCommand(int argc, TCHAR* argv[])
{
if (bForceRewrite)
{
_tprintf(_T("警告: 资源文件路径为空,已按 -f 忽略本次重写失败。\n"));
_tprintf(_T("警告: 资源文件路径为空,已按 -f 忽略本次重写失败。\n"));
return 0;
}
_tprintf(_T("错误: 资源文件路径为空。\n"));
_tprintf(_T("错误: 资源文件路径为空。\n"));
return 35;
}
@@ -780,7 +812,7 @@ int HandleRewriteCommand(int argc, TCHAR* argv[])
{
if (bForceRewrite)
{
_tprintf(_T("警告: 替换文件内容失败,已按 -f 忽略本次重写失败。\n"));
_tprintf(_T("警告: 替换文件内容失败,已按 -f 忽略本次重写失败。\n"));
return 0;
}
@@ -794,7 +826,7 @@ int main(int argc, TCHAR* argv[], TCHAR* envp[])
{
int nRetCode = 0;
// 添加图标;
// 添加图标;
HWND hwnd = GetForegroundWindow();
SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)LoadIcon(NULL, MAKEINTRESOURCE(IDI_ICON_APP)));
HMODULE hModule = ::GetModuleHandle(NULL);
@@ -802,28 +834,28 @@ int main(int argc, TCHAR* argv[], TCHAR* envp[])
GetDirInfo();
if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0))
{
_tprintf(_T("错误MFC 初始化失败。\n"));
_tprintf(_T("错误MFC 初始化失败。\n"));
nRetCode = 1;
}
else
{
CString strArgs = BuildArgsForLog(argc, argv);
_tprintf(_T("命令参数: %s\n"), strArgs.GetString());
_tprintf(_T("命令参数: %s\n"), strArgs.GetString());
if (argc <= 1)
{
_tprintf(_T("进入交互式标签创建模式。\n"));
_tprintf(_T("进入交互式标签创建模式。\n"));
PrintFullUsageExamples();
DWORD dwStartTick = LogCommandStart(_T("interactive"));
int nCmdRet = HandleCreateTagInteractive();
LogCommandEnd(_T("interactive"), dwStartTick, nCmdRet);
PrintCommandFailedWithCode(_T("interactive"), nCmdRet);
// 暂停便于查看输出日志Windows 控制台)
// 暂停便于查看输出日志Windows 控制台)
system("pause");
return nCmdRet;
}
if (argc >= 2 && _tcsicmp(argv[1], _T("nuitkabuild")) == 0)
if (argc >= 2 && _tcsnicmp(argv[1], _T("nuitkabuild="), 12) == 0)
{
DWORD dwStartTick = LogCommandStart(_T("nuitkabuild"));
int nCmdRet = HandleNuitkaBuildCommand(argc, argv);
@@ -832,7 +864,7 @@ int main(int argc, TCHAR* argv[], TCHAR* envp[])
return nCmdRet;
}
if (argc >= 2 && _tcsicmp(argv[1], _T("nuitkapydbuild")) == 0)
if (argc >= 2 && _tcsnicmp(argv[1], _T("nuitkapydbuild="), 15) == 0)
{
DWORD dwStartTick = LogCommandStart(_T("nuitkapydbuild"));
int nCmdRet = HandleNuitkaPydBuildCommand(argc, argv);
@@ -841,7 +873,7 @@ int main(int argc, TCHAR* argv[], TCHAR* envp[])
return nCmdRet;
}
if (argc >= 2 && _tcsicmp(argv[1], _T("setver")) == 0)
if (argc >= 2 && _tcsnicmp(argv[1], _T("setver="), 7) == 0)
{
DWORD dwStartTick = LogCommandStart(_T("setver"));
int nCmdRet = HandleSetVerCommand(argc, argv);
@@ -859,7 +891,16 @@ int main(int argc, TCHAR* argv[], TCHAR* envp[])
return nCmdRet;
}
_tprintf(_T("错误:未知命令:%s\n"), argv[1]);
if (argc >= 2 && _tcsnicmp(argv[1], _T("-setup="), 7) == 0)
{
DWORD dwStartTick = LogCommandStart(_T("-setup"));
int nCmdRet = HandleSetupCommand(argc, argv);
LogCommandEnd(_T("-setup"), dwStartTick, nCmdRet);
PrintCommandFailedWithCode(_T("-setup"), nCmdRet);
return nCmdRet;
}
_tprintf(_T("错误:未知命令:%s\n"), argv[1]);
PrintShortCommandUsage();
return 3;

View File

@@ -151,6 +151,7 @@
<ClInclude Include="GitVer_nuitka.h" />
<ClInclude Include="GitVer_process.h" />
<ClInclude Include="GitVer_rewrite.h" />
<ClInclude Include="GitVer_setup.h" />
<ClInclude Include="GitVer_tag.h" />
<ClInclude Include="GitVer_types.h" />
<ClInclude Include="GitVer_version.h" />
@@ -165,6 +166,7 @@
<ClCompile Include="GitVer_nuitka.cpp" />
<ClCompile Include="GitVer_process.cpp" />
<ClCompile Include="GitVer_rewrite.cpp" />
<ClCompile Include="GitVer_setup.cpp" />
<ClCompile Include="GitVer_tag.cpp" />
<ClCompile Include="GitVer_version.cpp" />
<ClCompile Include="pch.cpp">

View File

@@ -10,28 +10,40 @@ void PrintInvalidRepoPathError(LPCTSTR lpRepoPath)
_tprintf(_T("错误: 无效的仓库路径 %s\n"), lpRepoPath == NULL ? _T("<null>") : lpRepoPath);
}
int ParseSetVerOptions(int argc, TCHAR* argv[], LPCTSTR& lpRepoPath, BOOL& bUseDefaultTagWhenMissing)
int ParseSetVerOptions(int argc, TCHAR* argv[], int nStartIndex, LPCTSTR& lpRepoPath, BOOL& bTestMode)
{
lpRepoPath = NULL;
bUseDefaultTagWhenMissing = FALSE;
bTestMode = FALSE;
for (int i = 3; i < argc; ++i)
for (int i = nStartIndex; i < argc; ++i)
{
CString strArg = argv[i];
if (strArg.CompareNoCase(_T("-f")) == 0)
if (strArg.CompareNoCase(_T("-test")) == 0)
{
bUseDefaultTagWhenMissing = TRUE;
bTestMode = TRUE;
continue;
}
if (lpRepoPath == NULL)
// -setup=N 由调用方单独处理,此处静默跳过
if (strArg.GetLength() > 7 && strArg.Left(7).CompareNoCase(_T("-setup=")) == 0)
{
if (!PathIsDirectory(argv[i]))
continue;
}
if (strArg.GetLength() > 8 && strArg.Left(8).CompareNoCase(_T("repodir=")) == 0)
{
PrintInvalidRepoPathError(argv[i]);
if (lpRepoPath != NULL)
{
_tprintf(_T("错误: repodir 参数重复: %s\n"), argv[i]);
return 23;
}
lpRepoPath = argv[i];
LPCTSTR lpPath = argv[i] + 8;
if (!PathIsDirectory(lpPath))
{
PrintInvalidRepoPathError(lpPath);
return 23;
}
lpRepoPath = lpPath;
continue;
}

View File

@@ -1,5 +1,5 @@
#pragma once
void PrintInvalidRepoPathError(LPCTSTR lpRepoPath);
int ParseSetVerOptions(int argc, TCHAR* argv[], LPCTSTR& lpRepoPath, BOOL& bUseDefaultTagWhenMissing);
int ParseSetVerOptions(int argc, TCHAR* argv[], int nStartIndex, LPCTSTR& lpRepoPath, BOOL& bTestMode);
int ParseRewriteOptions(int argc, TCHAR* argv[], int& nPEType, BOOL& bForceRewrite);

View File

@@ -13,45 +13,56 @@ void PrintCommandUsageAndExamples(LPCTSTR lpCommand, LPCTSTR lpRequiredArgs, LPC
const UINT DEFAULT_MAJOR_WHEN_NO_TAG_FOR_NUITKA = 1;
const UINT DEFAULT_MINOR_WHEN_NO_TAG_FOR_NUITKA = 0;
static int ParseNuitkaOptions(int argc, TCHAR* argv[], LPCTSTR& lpRepoPath, BOOL& bUseDefaultTagWhenMissing, CString& strExtraArgs)
static int ParseNuitkaOptions(int argc, TCHAR* argv[], LPCTSTR& lpRepoPath, CString& strExtraArgs, BOOL& bTestMode)
{
lpRepoPath = NULL;
bUseDefaultTagWhenMissing = FALSE;
strExtraArgs.Empty();
BOOL bExtraArgsStarted = FALSE;
bTestMode = FALSE;
for (int i = 4; i < argc; ++i)
for (int i = 3; i < argc; ++i)
{
CString strArg = argv[i];
BOOL bOptionStyleArg = !strArg.IsEmpty() && strArg.Left(1) == _T("-");
// 仅在额外参数尚未开始时,将 -f 解释为 gitver 选项;
// 一旦进入额外参数阶段,-f 应透传给 Nuitka。
if (!bExtraArgsStarted && strArg.CompareNoCase(_T("-f")) == 0)
if (strArg.CompareNoCase(_T("-test")) == 0)
{
bUseDefaultTagWhenMissing = TRUE;
bTestMode = TRUE;
continue;
}
if (!bExtraArgsStarted && lpRepoPath == NULL && !bOptionStyleArg && PathIsDirectory(argv[i]))
if (strArg.GetLength() > 8 && strArg.Left(8).CompareNoCase(_T("repodir=")) == 0)
{
lpRepoPath = argv[i];
continue;
}
if (!bExtraArgsStarted && lpRepoPath == NULL && !bOptionStyleArg && IsLikelyPathArg(argv[i]))
if (lpRepoPath != NULL)
{
PrintInvalidRepoPathError(argv[i]);
_tprintf(_T("错误: repodir 参数重复: %s\n"), argv[i]);
return 23;
}
LPCTSTR lpPath = argv[i] + 8;
if (!PathIsDirectory(lpPath))
{
PrintInvalidRepoPathError(lpPath);
return 23;
}
lpRepoPath = lpPath;
continue;
}
bExtraArgsStarted = TRUE;
if (strArg.GetLength() > 7 && strArg.Left(7).CompareNoCase(_T("params=")) == 0)
{
if (!strExtraArgs.IsEmpty())
{
strExtraArgs += _T(" ");
_tprintf(_T("错误: params 参数重复。\n"));
return 3;
}
strExtraArgs += QuoteCmdArg(argv[i]);
CString strVal = strArg.Mid(7);
// 去掉外层双引号(如 params="--standalone"
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)
@@ -80,6 +91,7 @@ static void RunNuitkaBuild(LPCTSTR lpStartMessage, CString& strNuitkaCmd, const
static int HandleNuitkaBuildCommandCore(
int argc,
TCHAR* argv[],
int nCmdPrefixLen,
BOOL bBuildModule,
LPCTSTR lpStartMessage,
LPCTSTR lpUsageName,
@@ -90,27 +102,27 @@ static int HandleNuitkaBuildCommandCore(
int nTagErrorCode,
int nCommitErrorCode)
{
if (argc < 4)
// 解析 pid内嵌在 argv[1] 中,如 "nuitkabuild=5"
CString strPid = CString(argv[1]).Mid(nCmdPrefixLen);
UINT nPid = 0;
if (strPid.IsEmpty() || !TryParseUInt16(strPid, nPid))
{
_tprintf(_T("错误: 参数不足。\n"));
CString strRequiredArgs;
strRequiredArgs.Format(_T("[pid] [%s]"), lpEntryArgName);
PrintCommandUsageAndExamples(lpUsageName, strRequiredArgs.GetString(), lpEntryArgName);
_tprintf(_T("错误: %s= 后的 pid 无效:%s应为 0-65535 范围的整数。\n"), lpUsageName, strPid.GetString());
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;
}
UINT nPid = 0;
int nArgRet = ParseUInt16ArgOrError(argv, 2, _T("pid"), nPid, nPidErrorCode);
if (nArgRet != 0)
{
return nArgRet;
}
CString strMainOrModulePy = argv[3];
CString strMainOrModulePy = argv[2];
LPCTSTR lpRepoPath = NULL;
BOOL bUseDefaultTagWhenMissing = FALSE;
CString strExtraArgs;
int nRepoArgRet = ParseNuitkaOptions(argc, argv, lpRepoPath, bUseDefaultTagWhenMissing, strExtraArgs);
BOOL bTestMode = FALSE;
int nRepoArgRet = ParseNuitkaOptions(argc, argv, lpRepoPath, strExtraArgs, bTestMode);
if (nRepoArgRet != 0)
{
return nRepoArgRet;
@@ -125,7 +137,6 @@ static int HandleNuitkaBuildCommandCore(
errorCodes,
strProductVersion,
strFileVersion,
bUseDefaultTagWhenMissing,
DEFAULT_MAJOR_WHEN_NO_TAG_FOR_NUITKA,
DEFAULT_MINOR_WHEN_NO_TAG_FOR_NUITKA);
if (nVersionRet != 0)
@@ -133,6 +144,17 @@ static int HandleNuitkaBuildCommandCore(
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());
}
CString strNuitkaCmd;
if (bBuildModule)
{
@@ -162,6 +184,7 @@ int HandleNuitkaBuildCommand(int argc, TCHAR* argv[])
return HandleNuitkaBuildCommandCore(
argc,
argv,
12,
FALSE,
_T("Nuitka 打包开始.."),
_T("nuitkabuild"),
@@ -178,6 +201,7 @@ int HandleNuitkaPydBuildCommand(int argc, TCHAR* argv[])
return HandleNuitkaBuildCommandCore(
argc,
argv,
15,
TRUE,
_T("Nuitka pyd 打包开始.."),
_T("nuitkapydbuild"),

402
GitVer/GitVer_setup.cpp Normal file
View File

@@ -0,0 +1,402 @@
#include "pch.h"
#include "gitver.h"
#include "gitver_setup.h"
#include "gitver_common.h"
#include "gitver_cli.h"
#include "gitver_version.h"
#include "gitver_process.h"
extern TCHAR g_szCurModuleDir[MAX_PATH];
// 来自 gitver_rewrite.cpp / GitVer.cpp 的跨单元函数声明
std::string ToAnsiString(LPCTSTR lpText);
BOOL ReadTextFileAsAnsi(LPCTSTR lpFile, std::string& strContent, std::string& strBomPrefix);
BOOL WriteTextFileAsAnsi(LPCTSTR lpFile, const std::string& strContent, const std::string& strBomPrefix);
void PrintInvalidRepoPathError(LPCTSTR lpRepoPath);
int ParseSetVerOptions(int argc, TCHAR* argv[], int nStartIndex, LPCTSTR& lpRepoPath, BOOL& bTestMode);
static const UINT DEFAULT_MAJOR_WHEN_NO_TAG_FOR_SETUP = 1;
static const UINT DEFAULT_MINOR_WHEN_NO_TAG_FOR_SETUP = 0;
// ─────────────────────────────────────────────
// 内部工具:行级文本替换
// ─────────────────────────────────────────────
/// <summary>
/// 在字符串内容中查找首个包含 strToken 的整行,
/// 用 strNewLine 替换该行(不含行尾换行符)。
/// </summary>
static BOOL ReplaceLineByToken(std::string& strContent,
const std::string& strToken,
const std::string& strNewLine)
{
std::string::size_type nPos = strContent.find(strToken);
if (nPos == std::string::npos)
{
return FALSE;
}
std::string::size_type nLineStart = strContent.rfind('\n', nPos);
nLineStart = (nLineStart == std::string::npos) ? 0 : (nLineStart + 1);
std::string::size_type nLineEnd = strContent.find('\n', nPos);
if (nLineEnd == std::string::npos)
{
nLineEnd = strContent.size();
}
// 去掉可能存在的 \r
std::string::size_type nReplEnd = nLineEnd;
if (nReplEnd > nLineStart && strContent[nReplEnd - 1] == '\r')
{
--nReplEnd;
}
strContent.replace(nLineStart, nReplEnd - nLineStart, strNewLine);
return TRUE;
}
// ─────────────────────────────────────────────
// 查找安装脚本
// ─────────────────────────────────────────────
/// <summary>
/// 在 exe 所在目录及其上级目录中查找安装脚本。
/// nSetupType=0 查找 setup.issnSetupType=1 查找 setup.nsi。
/// </summary>
static BOOL FindSetupScript(int nSetupType, CString& strScriptPath)
{
strScriptPath.Empty();
LPCTSTR lpFileName = (nSetupType == 0) ? _T("setup.iss") : _T("setup.nsi");
// 当前 exe 目录(无末尾反斜杠)
CString strExeDir = g_szCurModuleDir;
// 尝试 exe 目录
CString strCandidate;
strCandidate.Format(_T("%s\\%s"), strExeDir.GetString(), lpFileName);
if (PathFileExists(strCandidate))
{
strScriptPath = strCandidate;
return TRUE;
}
// 尝试上级目录
int nSlash = strExeDir.ReverseFind(_T('\\'));
if (nSlash > 0)
{
CString strParentDir = strExeDir.Left(nSlash);
strCandidate.Format(_T("%s\\%s"), strParentDir.GetString(), lpFileName);
if (PathFileExists(strCandidate))
{
strScriptPath = strCandidate;
return TRUE;
}
}
return FALSE;
}
// ─────────────────────────────────────────────
// 修改 Inno Setup 脚本
// ─────────────────────────────────────────────
/// <summary>
/// 修改 .iss 脚本中的 AppVersion= 行。
/// </summary>
static BOOL ModifyIssScript(const CString& strScriptPath, const CString& strFileVersion)
{
std::string strContent;
std::string strBomPrefix;
if (!ReadTextFileAsAnsi(strScriptPath, strContent, strBomPrefix))
{
_tprintf(_T("错误: 读取 .iss 文件失败: %s\n"), strScriptPath.GetString());
return FALSE;
}
const std::string strToken = "#define MyAppVersion ";
if (strContent.find(strToken) == std::string::npos)
{
_tprintf(_T("错误: .iss 文件中未找到 #define MyAppVersion 行: %s\n"), strScriptPath.GetString());
return FALSE;
}
std::string strNewLine = "#define MyAppVersion \"" + ToAnsiString(strFileVersion.GetString()) + "\"";
if (!ReplaceLineByToken(strContent, strToken, strNewLine))
{
_tprintf(_T("错误: 替换 .iss #define MyAppVersion 行失败。\n"));
return FALSE;
}
if (!WriteTextFileAsAnsi(strScriptPath, strContent, strBomPrefix))
{
_tprintf(_T("错误: 写入 .iss 文件失败: %s\n"), strScriptPath.GetString());
return FALSE;
}
_tprintf(_T("成功: 已更新 .iss #define MyAppVersion \"%s\"\n"), strFileVersion.GetString());
return TRUE;
}
// ─────────────────────────────────────────────
// 修改 NSIS 脚本
// ─────────────────────────────────────────────
/// <summary>
/// 修改 .nsh 脚本中的 !define PRODUCT_VERSION 行。
/// </summary>
static BOOL ModifyNshScript(const CString& strScriptPath, const CString& strFileVersion)
{
std::string strContent;
std::string strBomPrefix;
if (!ReadTextFileAsAnsi(strScriptPath, strContent, strBomPrefix))
{
_tprintf(_T("错误: 读取 .nsh 文件失败: %s\n"), strScriptPath.GetString());
return FALSE;
}
const std::string strToken = "!define PRODUCT_VERSION";
if (strContent.find(strToken) == std::string::npos)
{
_tprintf(_T("错误: .nsh 文件中未找到 !define PRODUCT_VERSION 行: %s\n"), strScriptPath.GetString());
return FALSE;
}
std::string strNewLine = "!define PRODUCT_VERSION \"" + ToAnsiString(strFileVersion.GetString()) + "\"";
if (!ReplaceLineByToken(strContent, strToken, strNewLine))
{
_tprintf(_T("错误: 替换 .nsh PRODUCT_VERSION 行失败。\n"));
return FALSE;
}
if (!WriteTextFileAsAnsi(strScriptPath, strContent, strBomPrefix))
{
_tprintf(_T("错误: 写入 .nsh 文件失败: %s\n"), strScriptPath.GetString());
return FALSE;
}
_tprintf(_T("成功: 已更新 .nsh PRODUCT_VERSION=%s\n"), strFileVersion.GetString());
return TRUE;
}
// ─────────────────────────────────────────────
// 查找编译器
// ─────────────────────────────────────────────
/// <summary>
/// 查找 Inno Setup 编译器 (ISCC.exe)。
/// 先查 PATH再尝试常见安装路径。
/// </summary>
static CString FindInnoSetupCompiler()
{
TCHAR szPath[MAX_PATH] = { 0 };
if (::SearchPath(NULL, _T("ISCC.exe"), NULL, MAX_PATH, szPath, NULL) > 0)
{
return CString(szPath);
}
static LPCTSTR lpCandidates[] =
{
_T("C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe"),
_T("C:\\Program Files\\Inno Setup 6\\ISCC.exe"),
_T("C:\\Program Files (x86)\\Inno Setup 5\\ISCC.exe"),
_T("C:\\Program Files\\Inno Setup 5\\ISCC.exe"),
};
for (int i = 0; i < _countof(lpCandidates); ++i)
{
if (PathFileExists(lpCandidates[i]))
{
return CString(lpCandidates[i]);
}
}
return CString();
}
/// <summary>
/// 查找 NSIS 编译器 (makensis.exe)。
/// 先查 PATH再尝试常见安装路径。
/// </summary>
static CString FindNsisCompiler()
{
TCHAR szPath[MAX_PATH] = { 0 };
if (::SearchPath(NULL, _T("makensis.exe"), NULL, MAX_PATH, szPath, NULL) > 0)
{
return CString(szPath);
}
static LPCTSTR lpCandidates[] =
{
_T("C:\\Program Files (x86)\\NSIS\\makensis.exe"),
_T("C:\\Program Files\\NSIS\\makensis.exe"),
};
for (int i = 0; i < _countof(lpCandidates); ++i)
{
if (PathFileExists(lpCandidates[i]))
{
return CString(lpCandidates[i]);
}
}
return CString();
}
// ─────────────────────────────────────────────
// 公共执行函数(供 HandleSetupCommand 和 setver -setup=N 共用)
// 不回写版本信息到项目文件,只修改安装脚本并调用编译器打包。
// ─────────────────────────────────────────────
BOOL ExecuteSetupBuild(int nSetupType, const CString& strProductVersion, const CString& strFileVersion)
{
#ifdef _DEBUG
Sleep(1500);
#endif
// 查找安装脚本
CString strScriptPath;
if (!FindSetupScript(nSetupType, strScriptPath))
{
LPCTSTR lpScriptName = (nSetupType == 0) ? _T("setup.iss") : _T("setup.nsi");
_tprintf(_T("错误: 未在 exe 目录或上级目录中找到 %s。\n"), lpScriptName);
return FALSE;
}
_tprintf(_T("找到安装脚本: %s\n"), strScriptPath.GetString());
// 修改脚本中的版本号(不写回 .rc / AssemblyInfo.cs
BOOL bModifyOk = (nSetupType == 0)
? ModifyIssScript(strScriptPath, strFileVersion)
: ModifyNshScript(strScriptPath, strFileVersion);
if (!bModifyOk)
{
_tprintf(_T("错误: 修改安装脚本版本信息失败。\n"));
return FALSE;
}
// 查找编译器
CString strCompiler = (nSetupType == 0)
? FindInnoSetupCompiler()
: FindNsisCompiler();
if (strCompiler.IsEmpty())
{
LPCTSTR lpCompilerName = (nSetupType == 0)
? _T("ISCC.exe (Inno Setup)")
: _T("makensis.exe (NSIS)");
_tprintf(_T("错误: 未找到安装包编译器 %s请确认已安装并加入 PATH。\n"), lpCompilerName);
return FALSE;
}
_tprintf(_T("找到编译器: %s\n"), strCompiler.GetString());
// 取脚本所在目录作为工作目录
CString strScriptDir = strScriptPath;
int nSlash = strScriptDir.ReverseFind(_T('\\'));
if (nSlash >= 0)
{
strScriptDir = strScriptDir.Left(nSlash);
}
// 构建并执行编译命令
CString strCmd;
strCmd.Format(_T("cmd /c %s %s"),
QuoteCmdArg(strCompiler).GetString(),
QuoteCmdArg(strScriptPath).GetString());
_tprintf(_T("执行编译: %s\n"), strCmd.GetString());
CString strOutput = StartProcess(NULL, strCmd.GetBuffer(), strScriptDir.GetString());
strCmd.ReleaseBuffer();
if (strOutput.IsEmpty())
{
_tprintf(_T("警告: 编译器无输出,可能执行失败。\n"));
}
return TRUE;
}
// ─────────────────────────────────────────────
// 独立命令入口argv[1] 形如 "-setup=0"
// ─────────────────────────────────────────────
int HandleSetupCommand(int argc, TCHAR* argv[])
{
#ifdef _DEBUG
Sleep(15000);
#endif
// 解析 -setup=n 中的 n
CString strSetupArg = argv[1];
CString strN = strSetupArg.Mid(7); // 跳过 "-setup="
UINT nSetupType = UINT(-1);
if (!TryParseUInt16(strN, nSetupType) || (nSetupType != 0 && nSetupType != 1))
{
_tprintf(_T("错误: -setup 参数值 \"%s\" 不支持,仅支持 0Inno Setup或 1NSIS\n"),
strN.GetString());
return 36;
}
if (argc < 3)
{
_tprintf(_T("错误: 参数不足。\n"));
_tprintf(_T("用法gitver -setup=0|1 [pid] [repodir=<path>可选]\n"));
_tprintf(_T("示例gitver -setup=0 5\n"));
_tprintf(_T("示例gitver -setup=1 5 repodir=E:\\Code\\MyProj\n"));
return 3;
}
UINT nPid = 0;
int nArgRet = ParseUInt16ArgOrError(argv, 2, _T("pid"), nPid, 4);
if (nArgRet != 0)
{
return nArgRet;
}
LPCTSTR lpRepoPath = NULL;
BOOL bTestMode = FALSE;
int nRepoArgRet = ParseSetVerOptions(argc, argv, 3, lpRepoPath, bTestMode);
if (nRepoArgRet != 0)
{
return nRepoArgRet;
}
CString strFileVersion;
CString strProductVersion;
VersionBuildErrorCodes errorCodes = { 5, 6, 9 };
int nVersionRet = BuildVersionsFromRepo(
lpRepoPath,
nPid,
errorCodes,
strProductVersion,
strFileVersion,
DEFAULT_MAJOR_WHEN_NO_TAG_FOR_SETUP,
DEFAULT_MINOR_WHEN_NO_TAG_FOR_SETUP);
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("[测试版本] 已将 major/minor 置零: ProductVersion=%s\n"), strProductVersion.GetString());
}
_tprintf(_T("FileVersion=%s\n"), strFileVersion.GetString());
_tprintf(_T("ProductVersion=%s\n"), strProductVersion.GetString());
// 不回写版本信息到项目文件,直接修改安装脚本并编译打包
if (!ExecuteSetupBuild((int)nSetupType, strProductVersion, strFileVersion))
{
return 38;
}
return 0;
}

16
GitVer/GitVer_setup.h Normal file
View File

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

View File

@@ -6,7 +6,7 @@
BOOL TryGetCurrentBranch(LPCTSTR lpRepoPath, CString& strBranch)
{
_tprintf(_T("获取当前分支名称\n"));
_tprintf(_T("获取当前分支名称: \n"));
strBranch = StartProcess(NULL, _T("cmd /c git rev-parse --abbrev-ref HEAD"), lpRepoPath);
strBranch.Replace(_T("\r"), _T(""));
strBranch.Replace(_T("\n"), _T(""));
@@ -125,7 +125,7 @@ BOOL GetLastMajorMinorTag(LPCTSTR lpRepoPath, const CString& strBranch, CString&
void GetFileVersionDateFromSystemTime(const SYSTEMTIME& stLocal, UINT& nYY, UINT& nMMDD)
{
nYY = stLocal.wYear % 100;
// 为了让版本号对齐4位月份从1-12映射到11-22;
// 为版本号生成文件版本号,月份映射到1-12映射到11-22;
nMMDD = (stLocal.wMonth + 10) * 100 + stLocal.wDay;
}
@@ -227,26 +227,26 @@ BOOL ResolveFileVersionDateAndCommitCount(LPCTSTR lpRepoPath, const CString& str
if (nTodayCommitCount == 0)
{
_tprintf(_T("当天提交数量为0获取最一次提交日期\n"));
_tprintf(_T("警告: 当天提交数量为0尝试获取最一次提交日期\n"));
if (!TryGetLastBranchCommitDate(lpRepoPath, strBranch, stTargetDate))
{
_tprintf(_T("错误: 获取最一次提交日期失败\n"));
_tprintf(_T("错误: 获取最一次提交日期失败\n"));
return FALSE;
}
if (!TryGetBranchCommitCountByDate(lpRepoPath, strBranch, stTargetDate, nTodayCommitCount))
{
_tprintf(_T("错误: 获取[%s]提交数量失败\n"), strBranch.GetString());
_tprintf(_T("错误: 获取 [%s] 提交数量失败\n"), strBranch.GetString());
return FALSE;
}
_tprintf(_T("获取到最后一次提交日期: %04u-%02u-%02u, 提交数量: %u\n"),
_tprintf(_T("获取提交信息成功: %04u-%02u-%02u, 提交数量: %u\n"),
stTargetDate.wYear, stTargetDate.wMonth, stTargetDate.wDay, nTodayCommitCount);
}
else
{
_tprintf(_T("获取当天提交数量: %u\n"), nTodayCommitCount);
_tprintf(_T("获取当天提交数量成功: %u\n"), nTodayCommitCount);
}
GetFileVersionDateFromSystemTime(stTargetDate, nYY, nMMDD);
@@ -260,14 +260,12 @@ int BuildVersionsFromRepo(
const VersionBuildErrorCodes& errorCodes,
CString& strProductVersion,
CString& strFileVersion,
BOOL bUseDefaultTagWhenMissing,
UINT nDefaultMajor,
UINT nDefaultMinor)
{
_tprintf(_T("开始构建版本: 仓库路径=%s pid=%u 默认Tag(%s)=%u.%u\n"),
_tprintf(_T("开始构建版本: 仓库路径=%s pid=%u 默认Tag(%u.%u)\n"),
lpRepoPath == NULL ? _T("<null>") : lpRepoPath,
nPid,
bUseDefaultTagWhenMissing ? _T("启用") : _T("关闭"),
nDefaultMajor,
nDefaultMinor);
@@ -275,7 +273,7 @@ int BuildVersionsFromRepo(
CString strBranch;
if (!TryGetCurrentBranch(lpRepoPath, strBranch))
{
_tprintf(_T("错误: 获取当前分支失败\n"));
_tprintf(_T("错误: 获取当前分支失败\n"));
return errorCodes.nBidErrorCode;
}
_tprintf(_T("当前分支: %s\n"), strBranch.GetString());
@@ -292,19 +290,13 @@ int BuildVersionsFromRepo(
UINT nMinor = 0;
if (!GetLastMajorMinorTag(lpRepoPath, strBranch, strLastTag, nMajor, nMinor))
{
if (!bUseDefaultTagWhenMissing)
{
_tprintf(_T("错误: 获取标签失败\n"));
return errorCodes.nTagErrorCode;
}
nMajor = nDefaultMajor;
nMinor = nDefaultMinor;
_tprintf(_T("使用默认标签 %u %u\n"), nMajor, nMinor);
}
else
{
_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;

View File

@@ -17,6 +17,5 @@ int BuildVersionsFromRepo(
const VersionBuildErrorCodes& errorCodes,
CString& strProductVersion,
CString& strFileVersion,
BOOL bUseDefaultTagWhenMissing,
UINT nDefaultMajor,
UINT nDefaultMinor);

374
README.md
View File

@@ -1,2 +1,374 @@
# GitVer
# 命令行版本管理工具
命令行版本管理工具,从 Git 仓库读取 tag 自动生成并回写版本号。
---
## 用法总览
```
gitver
gitver rewrite [PEType] [-f]
gitver setver=<pid> [repodir=<path>] [-test] [-setup=0|1]
gitver nuitkabuild=<pid> <mainPy> [repodir=<path>] [-test] [params="<nuitka参数>"]
gitver nuitkapydbuild=<pid> <modulePy> [repodir=<path>] [-test] [params="<nuitka参数>"]
gitver -setup=0|1 <pid> [repodir=<path>] [-test]
```
**通用参数说明:**
| 参数 | 说明 |
|---|---|
| `pid` | 产品 ID整数范围 0-65535内嵌在命令名中`setver=5` |
| `repodir=<path>` | Git 仓库目录,缺省使用当前运行目录 |
| `-test` | 将产品版本号的 major/minor 置为 0测试版本构建 |
| `params="..."` | 传递给 Nuitka 的额外参数,用双引号括起来 |
**BID 规则:**
`main`/`master` 分支固定 `bid=0`;其他分支格式须为 `<描述>.<数字>`,自动读取末尾数字作为 `bid`
---
## 命令详解
### `gitver`(无参数交互模式)
自动读取当前分支最近三次符合 `<分支>.major.minor` 格式的 tag提示用户选择 major+1 或 minor+1二次确认后创建新 tag。
当前分支无有效 tag 时进入首发引导:提示输入默认 major/minor回车使用默认 1/0再创建首个 tag。
**交互示例(已有 tag**
```
当前分支: main最近三个 tag
main.1920.9
main.1920.10
main.1920.11
输入 1 → 创建 main.1921.0
输入 2 → 创建 main.1920.12
```
**交互示例(无 tag 首发):**
```
未找到符合规则的历史标签
输入默认 major回车=1: 2
输入默认 minor回车=0: 5
→ 创建 <分支>.2.5
```
---
### `gitver rewrite`
自动识别当前目录源码类型C++、C#、Python执行默认版本回写。
```
gitver rewrite [PEType] [-f]
```
| 参数 | 说明 |
|---|---|
| `PEType` | 可选,`1`=EXE默认`2`=DLL最多出现一次 |
| `-f` | 强制模式:未找到目标文件或回写失败仅提示,不返回错误码 35 |
**回写行为:**
- **C++**:递归查找首个 `.rc` 文件,回写 `FILEVERSION``PRODUCTVERSION``VALUE "FileVersion"``VALUE "ProductVersion"`(限定在 `VS_VERSION_INFO` 版本块内)
- **C#**:递归查找首个 `AssemblyInfo.cs`,回写 `AssemblyVersion``AssemblyFileVersion`
- **Python**:不执行回写,提示改用 `nuitkabuild=``nuitkapydbuild=`
**示例:**
```
gitver rewrite
gitver rewrite 2
gitver rewrite -f
```
---
### `gitver setver=<pid>`
从 Git 仓库读取 tag生成产品版本号与文件版本号并按源码类型自动回写。
```
gitver setver=<pid> [repodir=<path>] [-test] [-setup=0|1]
```
**版本号生成规则:**
- `ProductVersion = pid.bid.major.minor`(取当前分支前缀的最新 tag
- `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 repodir=E:\Code\OTH\gitver
gitver setver=5 -test
gitver setver=5 -setup=0
gitver setver=5 repodir=E:\Code\MyProj -setup=1
```
**输出示例:**
```
ProductVersion=5.0.1920.11
FileVersion=5.26.0519.3
```
**回写说明:**
- **C++**:回写 `.rc` 文件中的 FILEVERSION、PRODUCTVERSION 及对应字符串字段
- **C#**:回写 `AssemblyInfo.cs` 中的 AssemblyVersion、AssemblyFileVersion
- **Python**:仅输出版本号,不执行文件回写,提示改用 Nuitka 命令
- 编码:支持 ANSI/UTF-8保留 UTF-8 BOMUTF-16 文件会报错停止
- 当前分支无匹配 tag 时自动使用默认版本 `1.0`
---
### `gitver nuitkabuild=<pid>`
生成版本号并调用 Nuitka 打包 Python 程序EXE 模式)。
```
gitver nuitkabuild=<pid> <mainPy> [repodir=<path>] [-test] [params="<nuitka参数>"]
```
自动调用:
```
python -m nuitka --windows-product-version=<版本> --windows-file-version=<版本> <extra> <mainPy>
```
**示例:**
```
gitver nuitkabuild=5 main.py
gitver nuitkabuild=5 main.py -test
gitver nuitkabuild=5 src\app.py repodir=E:\Code\MyPyProj params="--standalone --output-dir=dist"
```
**输出示例:**
```
Nuitka 打包开始..
ProductVersion=5.0.1920.11
FileVersion=5.26.0519.3
```
> 需要当前环境已安装 Nuitka可通过 `python -m nuitka --version` 验证)。
---
### `gitver nuitkapydbuild=<pid>`
生成版本号并调用 Nuitka 打包 Python 模块pyd/DLL 模式)。
```
gitver nuitkapydbuild=<pid> <modulePy> [repodir=<path>] [-test] [params="<nuitka参数>"]
```
自动调用:
```
python -m nuitka --module --windows-product-version=<版本> --windows-file-version=<版本> <extra> <modulePy>
```
**示例:**
```
gitver nuitkapydbuild=5 module.py
gitver nuitkapydbuild=5 module.py -test
gitver nuitkapydbuild=5 src\core.py repodir=E:\Code\MyPyProj params="--output-dir=dist"
```
> 需要当前环境已安装 Nuitka可通过 `python -m nuitka --version` 验证)。
---
### `gitver -setup=0|1`(独立打包命令)
独立调用安装脚本打包,不依赖 `setver=` 命令。
```
gitver -setup=0|1 <pid> [repodir=<path>] [-test]
```
**示例:**
```
gitver -setup=0 5
gitver -setup=1 5 repodir=E:\Code\MyProj
```
---
## 错误码
| 码 | 含义 |
|---|---|
| 2 | 默认回写流程下未获取到 git 提交号 |
| 3 | 参数非法或未知参数 |
| 4 | `setver=` 的 pid 非法 |
| 5 | `setver=` 无法根据分支名计算 bid |
| 9 | `setver=` 获取当天分支提交次数失败 |
| 15 | 回写流程未识别源码类型 |
| 16 | 回写流程识别到 Python请改用 nuitka 命令 |
| 17 | `nuitkabuild=` 参数不足(缺少 mainPy |
| 18 | `nuitkabuild=` 的 pid 非法 |
| 19 | `nuitkabuild=` 无法根据分支名计算 bid |
| 21 | `nuitkabuild=` 获取当天分支提交次数失败 |
| 22 | `rewrite` 参数非法PEType 非法或重复) |
| 23 | `repodir=` 不是有效目录 |
| 24 | `nuitkapydbuild=` 参数不足(缺少 modulePy |
| 25 | `nuitkapydbuild=` 的 pid 非法 |
| 26 | `nuitkapydbuild=` 无法根据分支名计算 bid |
| 28 | `nuitkapydbuild=` 获取当天分支提交次数失败 |
| 29 | 无参数模式下无法获取当前分支 |
| 30 | 无参数模式下读取标签列表失败 |
| 31 | 无参数模式下未读取到用户输入 |
| 32 | 无参数模式下用户选择非法,或 major/minor 已达上限 |
| 33 | 无参数模式下创建 tag 失败 |
| 34 | 无参数模式下创建 tag 后校验失败 |
| 35 | 未找到可回写目标文件,或回写失败 |
| 36 | `-setup=` 值不支持(仅支持 0 和 1 |
| 37 | 未找到安装脚本setup.iss / setup.nsi |
| 38 | 修改安装脚本失败 |
| 39 | 未找到安装编译器ISCC.exe / makensis.exe |
---
## 最小自测
### 基础验证:`setver=`
```bash
# 初始化测试仓库
mkdir test_repo && cd test_repo
git init
echo test > README.txt
git add . && git commit -m "init"
git tag main.1920.10
git tag main.1920.11
# 验证版本生成
gitver setver=5
# 预期:
# ProductVersion=5.0.1920.11
# FileVersion=5.yy.mmdd.<id>
# 返回码: 0
```
### 验证无 tag 时自动使用默认版本
```bash
git checkout -b newbranch
gitver setver=5
# 预期: ProductVersion=5.<bid>.1.0(无 tag 自动回落默认 1.0
# 返回码: 0
```
### 验证分支 tag 隔离
```bash
git checkout -b feature.12
git tag feature.12.1930.7
git tag feature.12.1930.8
gitver setver=5
# 预期: ProductVersion=5.12.1930.8(忽略 main.* tag
# 返回码: 0
```
### 验证 `-test`
```bash
gitver setver=5 -test
# 预期: ProductVersion=5.0.0.0major/minor 置零)
# 返回码: 0
```
### 验证 `nuitkabuild=`
```bash
gitver nuitkabuild=5 main.py
# 预期: 调用 python -m nuitka注入版本号参数
gitver nuitkabuild=5 src\app.py params="--standalone --output-dir=dist"
# 预期: 额外参数传递给 Nuitka
```
### 验证 `-setup=`
```bash
# 确保 exe 目录或上级目录有 setup.iss
gitver setver=5 -setup=0
# 预期: 修改 setup.iss 中的版本号并调用 ISCC.exe 编译
```
---
## 回归清单
### rewrite 组
| 命令 | 预期返回码 |
|---|---|
| `gitver rewrite` | 0 / 15 / 16 / 35 |
| `gitver rewrite -f` | 0 / 15 / 16 |
| `gitver rewrite 2` | 0 / 15 / 35 |
| `gitver rewrite 1 2` | 22 |
### setver= 组
| 命令 | 预期返回码 |
|---|---|
| `gitver setver=5` | 0 |
| `gitver setver=5 -test` | 0 |
| `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 |
### nuitkabuild= 组
| 命令 | 预期返回码 |
|---|---|
| `gitver nuitkabuild=5 main.py` | 0 / 19 / 21 |
| `gitver nuitkabuild=5 main.py -test` | 0 |
| `gitver nuitkabuild=5 main.py params="--standalone"` | 0 |
| `gitver nuitkabuild=5 main.py repodir=E:\NotExists` | 23 |
| `gitver nuitkabuild=5` | 17 |
### nuitkapydbuild= 组
| 命令 | 预期返回码 |
|---|---|
| `gitver nuitkapydbuild=5 module.py` | 0 / 26 / 28 |
| `gitver nuitkapydbuild=5 module.py -test` | 0 |
| `gitver nuitkapydbuild=5 module.py params="--output-dir=dist"` | 0 |
| `gitver nuitkapydbuild=5 module.py repodir=E:\NotExists` | 23 |
| `gitver nuitkapydbuild=5` | 24 |
> **注意**如果打包时提示“未找到安装编译器ISCC.exe / makensis.exe请手动下载安装对应工具并将其可执行文件路径如 `ISCC.exe` 或 `makensis.exe` 所在目录)添加到系统环境变量 `PATH`。常见下载地址:
>
> - Inno Setup: https://jrsoftware.org/isinfo.php
> - NSIS: https://nsis.sourceforge.io/Download
>
> 添加方法:
> 1. 复制编译器安装目录路径(如 `C:\Program Files (x86)\Inno Setup 6`)。
> 2. 打开“系统属性”→“高级”→“环境变量”,在“系统变量”中找到 `PATH`,点击“编辑”,添加上述路径。
> 3. 重新打开命令行窗口后再运行打包命令。