Anubis mkmsi 深度解析:用 msitools 将 yeet 构建产物打包为 Windows MSI 安装程序
2026/9/20 20:36:12 网站建设 项目流程
  • 后端
  • 网络安全

【免费下载链接】anubis

Weighs the soul of incoming HTTP requests to stop AI crawlers

项目地址:https://gitcode.com/gh_mirrors/anubis4/anubis
点击查看免费下载

mkmsi是 Anubis 仓库中一个专用的构建期工具:它把yeet打包流水线产出的 Windows zip 归档转换为可通过msiexec安装的 MSI 安装包,并在安装器中内置原生 Windows 服务注册、配置引导与升级处理。本文以 internal/cmd/mkmsi/README.md 为主线,结合工具全部源码与测试,讲清它的依赖陷阱、完整构建流水线、MSI 表级"外科手术"补丁、可复现性工程与配套的自动化验证。读完你将掌握这个工具从 zip 文件名解析到 MSI 交付的全链路工作原理,并能在自己的打包流程中复用它。

mkmsi 是什么:从 yeet zip 到 MSI 的桥梁

Anubis 的发布打包由yeet驱动(见 yeetfile.js),其中 Windows 平台会先构建出anubis-<version>-windows-<arch>.zip归档。mkmsi位于 internal/cmd/mkmsi/main.go,其职责正如源码首行注释所述:"turns the Windows zip that yeet builds into an MSI installer"。

它的核心工作方式非常直白:调用 GNOME msitools 工具链wixlwixl-heatmsibuildmsiinfo),以 run/windows/anubis.wxs 为安装包定义,把 zip 解包、补入配置模板、生成文档碎片,最终编译并修补出一个行为"得体"的 MSI。整个构建分为两大阶段:

  1. 准备阶段:解压 zip、生成配置文件与文档清单;
  2. 编译与修补阶段wixl编译.wxs生成 MSI,随后用msibuild对生成结果做多处表级修补——这些修补之所以必要,是因为 wixl 对.wxs中若干合法声明会静默丢弃或随机化,只有事后直接操作 MSI 数据库才能得到正确结果。

命令行接口

mkmsi暴露四个命令行参数(main.go):

参数必填默认值含义
--zipyeet 构建出的 Windows zip 路径
--out输出的 MSI 文件路径
--packaging-dirrun/windows存放anubis.wxs与配置模板的目录
--staging-dirvar/mkmsi暂存构建产物的工作目录(按架构分子目录,构建前会清空旧内容)

--zip--out缺一不可,缺失时程序直接以log.Fatal退出(main.go)。构建成功后,程序会把 MSI 的最终路径打印到标准输出,方便外层脚本(如 yeet)继续消费。

环境准备:msitools 依赖与版本陷阱

README 明确要求工具链版本为msitools 0.106 或更高,并把wixlwixl-heatmsibuildmsiinfo四个可执行文件放在PATH上。macOS 与 Linux 上推荐用 Homebrew 安装:

brew install msitools

为什么发行版自带的 msitools 通常不可用

README 特别警告了两类发行版陷阱:

  • 包拆分:Debian 与 Ubuntu 把这四个工具拆进两个包——msibuildmsiinfomsitools包中,而wixlwixl-heat在单独的wixl包中。只装其中一个包会导致mkmsi找不到工具而失败。
  • 版本过老:Ubuntu 24.04 自带的 msitools 是 0.103。该版本在遇到run/windows/anubis.wxs中的<Condition>元素时会直接中止,报错unhandled child Component node Condition。由于.wxs中的升级启动条件(WIX_UPGRADE_DETECTED)依赖这个元素,0.103 完全无法构建本项目。

工具缺失时的测试行为

有趣的是,mkmsi的测试对工具缺失采取了"本地跳过、CI 强制"的策略。msi_test.go 中requireTool在找不到wixl/wixl-heat/msiinfo/msibuild时跳过用例;而设置了环境变量MKMSI_REQUIRE_VERIFY=true的 CI 作业会把"跳过"升级为"失败",防止出现"声称验证了却什么都没验证"的假绿。这一机制源于一个历史教训:GitHub Actions 在每个 job 上都设置CI=true,如果直接拿CI做门禁,从不安装 msitools 的主 Go 工作流会在每个 PR 上误报失败。

与 yeet 集成:在打包流水线中调用 mkmsi

README 明确说明"这是设计给 yeet 在打包时运行的",并给出了 yeetfile.js 中的实际调用代码(仓库中已落地):

// Build MSI installers from the Windows zips that were just built. packages .flat() .filter((pkg) => pkg.includes("windows")) .filter((pkg) => pkg.endsWith(".zip")) .forEach((zip) => { const msiPath = zip.replace(/\.zip$/, ".msi"); $`go run ./internal/cmd/mkmsi --zip ${zip} --out ${msiPath}`; });

这段代码从 yeet 的packages列表中筛出所有 Windows zip,然后对每个 zip 运行go run ./internal/cmd/mkmsi,把输出路径从.zip换为.msi。也就是说,mkmsi 本身不需要预先编译成独立二进制,直接以go run方式随打包流程执行即可。go.mod位于仓库根目录,internal/cmd/mkmsi是该模块内的一个标准package main

构建流水线拆解:从 zip 文件名到 MSI

main.go中的build()函数完整串起了整条流水线(main.go):

  1. 从 zip 文件名解析版本与架构;
  2. 把版本转换为 MSI ProductVersion;
  3. 计算 ProductCode 与 PackageCode;
  4. 清空并重建按架构隔离的 staging 目录;
  5. 解压 zip;
  6. 生成配置模板(etc/anubis.envetc/anubis.yaml);
  7. 生成文档目录的 wxs 碎片;
  8. 调用wixl编译并做后处理修补。

第一步:从文件名解析版本与架构

zip 文件名必须匹配zipNameRe正则^anubis-(.+)-windows-(amd64|arm64)\.zip$(build.go)。versionFromZipName取中间的版本段,archFromZipNameamd64arm64。文件名不符合时返回ErrBadVersion——注意anubis-1.26.2.zip(缺少平台后缀)也会被拒绝,确保"看不懂就失败"而不是产出错误安装包。

第二步:版本号到 MSI ProductVersion 的转换

MSI 的 ProductVersion 要求形如major.minor.build.revision的四段纯数字,其中major 与 minor 上限 255,build 与 revision 上限 65535version.go中的msiVersion用锚定两端正则^v?(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-pre(?P<pre>\d+))?(?:-(?P<commits>\d+)-g[0-9a-fA-F]+)?(?:-dev)?$解析 yeet 的版本串(version.go),转换规则如下:

yeet 版本串示例含义转换结果
1.26.2正式发布1.26.2.0
v1.26.2带 v 前缀1.26.2.0
1.26.2-1-gec3cce8a-devdev 构建(落后 1 个提交)1.26.2.1
1.26.2-137-gdeadbeedev 构建(落后 137 个提交)1.26.2.137
1.27.0-pre1预发布1.27.0.0

关键细节:预发布编号(-preN)没有对应的 MSI 版本段。MSI 判断升级只比较 major.minor.build 三段,无法表达"1.27.0-pre1 低于 1.27.0"。因此预发布与正式版共享同一个 ProductVersion,只能靠不同的 ProductCode 区分——所以 anubis.wxs 中声明了AllowSameVersionUpgrades="yes",保证在 1.27.0 之上安装 1.27.0-pre1(或反之)仍会正确触发升级移除旧版本,而不是两个产品同时注册互相损坏。dev 构建的提交数则落入 revision 段(Windows 比较版本时忽略该段,所以 dev 构建与其基准发布在安装器眼里"版本相同")。

范围检查同样严格:major=256patch=65536、提交数超过 65535、以及任何一位数字溢出int都会返回ErrVersionOutOfRange1.26(两段)、1.26.2.4(四段)、1.27.0-rc1(不认识的预发布类型)、1.27.0-pre(预发布缺编号)等一律ErrBadVersion(见 version_test.go 的完整用例表)。正则刻意锚定两端,避免1.26.2.4被前缀匹配成1.26.2.0这种"看似合理的错误答案"。

第三步:确定性 GUID 三件套

version.go中定义了三个 GUID 的生成策略:

  • UpgradeCode(固定值):92f5fc1f-f6f2-4bd1-bbef-79b8a46c665f,是 Anubis 家族的"族谱"标识,永远不允许改变——改了它,所有未来安装器都无法升级已有安装。
  • ProductCode:以 UpgradeCode 为命名空间、对version + "/" + arch做 UUIDv5 哈希(uuid.NewSHA1)。同一提交重建得到同一 GUID;不同版本、不同架构得到不同 GUID。架构参与哈希是必要的:amd64 与 arm64 包安装到同一 UpgradeCode 家族,若共享 ProductCode,Windows 会把它们当成同一个产品,装一个就静默卸载另一个。
  • PackageCode:同样以 UpgradeCode 为命名空间,但哈希输入是"packagecode/" + version + "/" + arch前缀串,与 ProductCode 的输入刻意区分,避免两个 GUID 撞车。这是对 MSI 惯例的有意偏离——标准做法要求每个物理 .msi 文件有唯一 PackageCode,但既然整套构建已可复现,再让 PackageCode 每次随机反而会误导人。

配置模板生成:安装器随附的etc目录

stageConfigTemplates(main.go)负责生成安装器携带的两个配置模板:

  • etc/anubis.env:直接复制 run/windows/anubis.env;
  • etc/anubis.yaml:把 data/botPolicies.yaml(默认策略)与 run/windows/logging.yaml(Windows 日志配置块)按顺序拼接而成。

这两个文件随后会被安装到%ProgramFiles%\Techaro\Anubis\etc,由安装时的自定义动作复制到数据目录。模板中的__ANUBIS_DATA_DIR__占位符是特意留的:数据目录通常在C:\ProgramData\Techaro\Anubis,但%ProgramData%可被重定位,模板不写死路径,由运行时引导逻辑(见下文)写入真实值。

concatFilesextractOne都做了同一件可复现性关键操作:把生成/解压文件的 mtime 恢复为 zip 条目的统一 mtime(build.go)。因为 wixl 构建的 CAB 归档会把每个文件的 mtime 嵌进去,若这些文件带着墙钟时间戳,即使输入 zip 完全一致,MSI 也会每次构建都不同。解压时还会做路径穿越防护(filepath.Clean+ 前缀校验),防止恶意 zip 条目写到 staging 目录之外。

文档碎片生成:对抗 wixl-heat 的随机目录 ID

zip 内含完整的docs文档树(yeet 从docs/docs复制而来),安装器要把它们装到C:\Program Files\Techaro\Anubis\docgenerateDocFragment(main.go)先把文档目录遍历出的文件列表排序,再通过 stdin 喂给wixl-heat,生成一个挂在DOCDIR目录引用下的doc-files.wxs碎片。

问题在于:wixl-heat 每次调用都会给目录随机生成dir<HEX>ID,即使输入树完全一致(源码注明是经验验证的结论),cmp<HEX>fil<HEX>是稳定的,只有目录 ID 随机。为此 docfragment.go 实现了rewriteDirectoryIDs:用encoding/xml流式解析生成的碎片,按目录嵌套顺序累积Name属性组成相对路径,再用sha256前 16 字节转大写十六进制,派生形如dir<32位大写HEX>的确定性 ID。选择路径派生而非顺序编号是刻意的——顺序编号会在树中任意增删文件时打乱所有目录编号,而路径派生只影响真正移动过的目录。同时该函数会剔除冗余的xmlns声明,避免与编码器自动生成的命名空间声明冲突产生非法 XML。

wixl 编译与五类后处理修补

runWixl(main.go)调用wixl编译anubis.wxsdoc-files.wxs,通过-D传入 SourceDir、DocSourceDir、Win64、Version、ProductCode、InstallerVersion 等变量。InstallerVersion 按架构区分:amd64 用200(Windows Installer 引擎的长期基线),arm64 必须用500——因为 Arm64 包模板只在 Windows Installer 5.0 引入,声明过低版本可能导致目标机上被以"not supported by this processor type"拒绝。

编译完成后,需要依次执行五类msibuild修补,每一类都是 wixl 能力缺陷或非确定性的补偿:

1.checkWixlOutput:先信 stderr,别信退出码

这是贯穿全程的纪律(build.go):wixl 对不认识的属性只是往 stderr 写 CRITICAL 警告,然后照样以退出码 0 结束,产出一个悄悄丢了属性的安装包。所以每次调用 wixl/wixl-heat 后都必须先检查 stderr 是否含CRITICAL,命中即报ErrWixlDroppedAttributebuild_test.go中的TestCheckWixlOutput用捕获的样本 stderr 验证了这条纯字符串匹配逻辑。

2.secureInstallDir:让INSTALLDIR=覆盖真正生效

MSI 的安装过程会提升到系统上下文(deferred 阶段),届时命令行传入的INSTALLDIR=...若不在SecureCustomProperties属性中,就会被静默忽略。wixl 0.106 无法在.wxs中声明式完成这件事:Property/@Secure="yes"会被静默丢弃,手写<Property Id="SecureCustomProperties">又会与 wixl 为MajorUpgrade自动生成的行冲突、直接让构建中止。因此 secureInstallDir 在构建后用msibuild -q UPDATEINSTALLDIR追加进SecureCustomProperties,再用msiinfo export读回验证——"写入后必须读回校验"是本文件对msibuild同样不信任的体现。

3.patchInstallerImages:替换 WiX 默认安装界面图片

MSI 的向导界面(WixUI_Minimal)默认带 WiX 的横幅与对话框图。真实 WiX 用<WixVariable Id="WixUIBannerBmp">覆盖,但 wixl 不支持;另一条路是把 wixl 的 ext/ui 树 vendor 进仓库,但那意味着把约 18 个 MS-RL 许可证的.wxs文件带进这个 MIT 项目。最终方案是直接用msibuild -a替换 MSI 中Binary.WixUI_Bmp_BannerBinary.WixUI_Bmp_Dialog两个流(main.go),图片来自 run/windows/banner.bmp 与 run/windows/dialog.bmp。替换后仍用msiinfo extract把流抽回来比对字节长度,确认补丁真的落盘。

4.patchSummaryInfo:修正 Template 与 PackageCode

wixl 0.106 没有 arm64 目标(只认 intel/intel64/ia64),所以 arm64 包是先按 x64 编译、再把摘要信息流的 Template 从x64;1033修正为Arm64;1033。同时 PackageCode 会被 wixl 随机化,这里用msibuild -s统一写为前面算出的确定性 UUIDv5(大写加花括号格式)。

实现上有个精妙约束:msibuild -s每次调用会同时覆盖 Subject、Template、PackageCode 三个字段,所以全文件只保留这一个-s调用点,且每次都重申全部三个值,杜绝两个调用点互相覆盖的隐患。修正后同样用msiinfo suminfo读回 Template 与 "Revision number (UUID)" 校验。

5.patchEnvironmentIDpatchCustomActionSequence:消除两个非确定性来源

  • Environment 表主键:wixl 0.106 无视.wxs里声明的Id="env_path",给 PATH 行分配随机 GUID。MSI 的 SQL 方言拒绝 UPDATE 主键列(实测报 "failed to execute query"),所以 patchEnvironmentID 采用"DELETE 后按固定主键重新 INSERT"的标准绕法,SQL 字符串经sqlEscape转义单引号。
  • 自定义动作时序:patchCustomActionSequence 发现 wixl 对相对 Before/After 提示的解析不确定——同一份 byte 级一致的输入连跑八次,SetAnubisExePath有时排到 1402(紧随 RemoveExistingProducts),有时排到 4001(紧随 InstallFiles)。两种排法都满足.wxs约束,wixl 退出码毫无信号。这里把两个动作钉死在InstallFiles的 Sequence 值 +1/+2 上(InstallFiles的值是读回来的而非硬编码),且只修正值、不动行的物理存储位置——后者实测不可通过 msibuild 的 SQL 接口移动,而 Windows Installer 按 Sequence 值执行动作,行序差异纯属外观残留。

安装包本体:anubis.wxs 解剖

run/windows/anubis.wxs 是安装包定义的核心,可概括为以下几点:

  • 目录布局ProgramFiles64Folder\Techaro\Anubis下分bin(两个 exe)、etc(两个配置模板)、doc(文档树);组件全部标记Win64="yes",仅支持 64 位。
  • 服务注册anubis.exe组件内嵌ServiceInstall,以NT SERVICE\Anubis虚拟账户运行,Start="demand"(手动启动)、ErrorControl="normal"Vital="yes"ServiceControl配置为卸载时停止并删除服务。虚拟账户由 Windows 在装服务时自动创建、卸载时自动删除,无需管理密码。
  • 升级语义MajorUpgrade开启AllowSameVersionUpgrades;另有cmp_svc_start_on_upgrade组件,其ServiceControl的启动请求被Condition>WIX_UPGRADE_DETECTED门控——只有检测到旧版本存在时才在安装时启动服务。这是对一个真实缺陷的修复:RemoveExistingProducts排在InstallServices之前,升级会先删旧服务再建新服务,若不显式请求启动,升级会静默报告成功而让 Anubis 处于停止状态;同时全新安装因还没有配置与签名密钥,绝不能自动启动。
  • PATH 写入cmp_path组件把[BINDIR]追加到系统PATH
  • 自定义动作SetAnubisExePath(immediate,把[BINDIR]anubis.exe写入属性)→BootstrapConfig(deferred、Impersonate="no",执行--windows-bootstrap-config,且仅在NOT REMOVE时运行)。deferred 动作读不到安装器属性,必须先由 immediate 动作把可执行路径固化到属性里。
  • 界面与许可WixUI_Minimal向导集,EULA 对话框显示 run/windows/License.rtf。

运行时配置引导:--windows-bootstrap-config

安装器在文件落盘后调用的BootstrapConfig自定义动作,对应 cmd/anubis/service_windows.go 中 Windows-only 的--windows-bootstrap-config标志。其工作内容:

  1. %ProgramFiles%\Techaro\Anubis\etc下的anubis.envanubis.yaml复制到数据目录%ProgramData%\Techaro\AnubisProgramData环境变量未设置时拒绝猜测路径,防止filepath.Join把空值拼成相对路径 "Techaro\Anubis");
  2. 通过SetNamedSecurityInfoSID 字节形式给NT SERVICE\Anubis授予数据目录的 Modify 权限(读配置、写轮转日志、删除旧日志,但不含 WRITE_DAC/WRITE_OWNER,服务无法自我提权)。这里刻意不 shell 出icacls——icacls 会把 SID 反查成账户名,而引导发生在服务注册之前,LSA 无法映射尚未注册的服务 SID,会导致整个调用以错误 1332 失败;
  3. 把引导诊断写入anubis-bootstrap.log。原因是 msiexec 会丢弃自定义动作的全部日志输出,失败时只留下晦涩的 "Error 1603";同时引导失败必须返回成功退出码,否则同样会以 1603 掩盖真实错误。

服务启动后,早期 stderr 会被重定向到anubis-startup.log(服务进程没有可用 stderr),运行期日志则按logging.yaml写入anubis.log并自动轮转。管理员需在首次启动前设置ED25519_PRIVATE_KEY_HEX(64 位十六进制),否则 Anubis 每次重启都会重新生成随机签名密钥,使所有已签发挑战失效(run/windows/anubis.env)。

可复现性工程:让同一提交产出 byte 级一致的 MSI

README 坦言一个现实约束:由于 msitools 与 MSI 格式的种种限制,当前无法让 MSI 构建完全可复现(该工具由 Claude Opus 5 作为正确性 fuzzer 生成,用于确保产物在 Windows 上行为正常)。但代码里为"尽可能可复现"做了大量工作,上述修补大多同时服务于正确性与可复现性。TestMSIReproducibleBuild(msi_test.go)把同一 zip 构建两次,要求除两个已知残留外完全 byte 一致:

  • 摘要信息流的时间戳CreatedLast saved由 wixl 按墙钟写入,而msibuild -s没有对应参数;
  • InstallExecuteSequence 表的物理行序SetAnubisExePathBootstrapConfig行的存储位置不可移动(实测与 Action 字符串绑定),但执行顺序由 Sequence 值决定,行为无影响。

其余所有表与流(含 CAB 归档)都必须是精确字节相等。该测试先做字节级比较,不一致时才逐表、逐流比对以定位差异来源。

质量保障:测试矩阵与验证门禁

internal/cmd/mkmsi下三个测试文件构成完整验证体系:

  • version_test.gomsiVersion的约 20 组正反用例(含 pre 版本、dev 构建、范围溢出、畸形串),以及 ProductCode 的确定性、随版本/预发布/架构变化、拒绝坏版本四组断言;
  • build_test.goversionFromZipName解析与checkWixlOutput的 CRITICAL 识别;
  • msi_test.go:真实调用 wixl 链构建 MSI 后的表级断言,覆盖服务注册(ServiceInstall/ServiceControl)、PATH 写入、自定义动作、WixUI_Minimal对话框集、INSTALLDIR 安全属性、arm64/amd64 的 Template 与 InstallerVersion 差异、安装器图片字节长度、License.rtf 与 LICENSE 同步、升级才启动服务、双构建可复现等。

这些集成测试需要 msitools 与 yeet 产出的 zip,因此在本地自动跳过(requireTool/findZip),仅当MKMSI_REQUIRE_VERIFY=true的 CI 作业(如 package-builds-stable/unstable 工作流的 "Verify MSI contents" 步骤)中才强制执行,构成"发布包必须过、日常开发不阻塞"的门禁。

实际使用:安装、自定义路径与静默部署

最终 MSI 供管理员在 Windows Server 上使用(docs/docs/admin/environments/windows.mdx),默认安装到C:\Program Files\Techaro\Anubis并注册名为Anubis的服务。由于 MSI 没有图形化选目录界面,指定安装位置需通过INSTALLDIR属性(该属性被secureInstallDir补丁保证在提权阶段仍然生效):

msiexec /i anubis-1.26.2-windows-amd64.msi INSTALLDIR="D:\Anubis"

静默安装加上/qn

msiexec /i anubis-1.26.2-windows-amd64.msi /qn INSTALLDIR="D:\Anubis"

注意INSTALLDIR只影响程序文件位置,配置始终写入%ProgramData%\Techaro\Anubis。首次安装不会自动启动服务(等待管理员配置策略与签名密钥),配置完成后用Start-Service Anubis启动、Set-Service Anubis -StartupType Automatic设置开机自启;升级会短暂停服换新再重启,且不保留启动类型,升级后需重新执行Set-Service

小结

mkmsi的价值不在于"能生成 MSI"这一结果,而在于它把 MSI 生态中一系列隐蔽陷阱显式化了:wixl 静默丢属性、随机化目录 ID 与 GUID、非确定性的动作时序、INSTALLDIR提权丢失、虚拟账户 ACL 前置授权、msiexec 吞日志等,每个都以"构建后读回校验 + 表级修补 + 可复现测试"三重手段兜底。如果你的项目也在用 msitools 从 zip 生成 Windows 安装包,这份代码是一个值得逐行研读的参考实现;而 Anubis 用户则可以直接把构建产物作为 Windows Server 上的标准安装体验。

  • 后端
  • 网络安全

【免费下载链接】anubis

Weighs the soul of incoming HTTP requests to stop AI crawlers

项目地址:https://gitcode.com/gh_mirrors/anubis4/anubis
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询