ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Anubis mkmsi 深度解析:用 msitools 将 yeet 构建产物打包为 Windows MSI 安装程序

Anubis mkmsi 深度解析:用 msitools 将 yeet 构建产物打包为 Windows MSI 安装程序 后端网络安全【免费下载链接】anubisWeighs 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 工具链wixl、wixl-heat、msibuild、msiinfo以 run/windows/anubis.wxs 为安装包定义把 zip 解包、补入配置模板、生成文档碎片最终编译并修补出一个行为得体的 MSI。整个构建分为两大阶段准备阶段解压 zip、生成配置文件与文档清单编译与修补阶段wixl编译.wxs生成 MSI随后用msibuild对生成结果做多处表级修补——这些修补之所以必要是因为 wixl 对.wxs中若干合法声明会静默丢弃或随机化只有事后直接操作 MSI 数据库才能得到正确结果。命令行接口mkmsi暴露四个命令行参数main.go参数必填默认值含义--zip是—yeet 构建出的 Windows zip 路径--out是—输出的 MSI 文件路径--packaging-dir否run/windows存放anubis.wxs与配置模板的目录--staging-dir否var/mkmsi暂存构建产物的工作目录按架构分子目录构建前会清空旧内容--zip与--out缺一不可缺失时程序直接以log.Fatal退出main.go。构建成功后程序会把 MSI 的最终路径打印到标准输出方便外层脚本如 yeet继续消费。环境准备msitools 依赖与版本陷阱README 明确要求工具链版本为msitools 0.106 或更高并把wixl、wixl-heat、msibuild、msiinfo四个可执行文件放在PATH上。macOS 与 Linux 上推荐用 Homebrew 安装brew install msitools为什么发行版自带的 msitools 通常不可用README 特别警告了两类发行版陷阱包拆分Debian 与 Ubuntu 把这四个工具拆进两个包——msibuild与msiinfo在msitools包中而wixl与wixl-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_VERIFYtrue的 CI 作业会把跳过升级为失败防止出现声称验证了却什么都没验证的假绿。这一机制源于一个历史教训GitHub Actions 在每个 job 上都设置CItrue如果直接拿CI做门禁从不安装 msitools 的主 Go 工作流会在每个 PR 上误报失败。与 yeet 集成在打包流水线中调用 mkmsiREADME 明确说明这是设计给 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 文件名到 MSImain.go中的build()函数完整串起了整条流水线main.go从 zip 文件名解析版本与架构把版本转换为 MSI ProductVersion计算 ProductCode 与 PackageCode清空并重建按架构隔离的 staging 目录解压 zip生成配置模板etc/anubis.env、etc/anubis.yaml生成文档目录的 wxs 碎片调用wixl编译并做后处理修补。第一步从文件名解析版本与架构zip 文件名必须匹配zipNameRe正则^anubis-(.)-windows-(amd64|arm64)\.zip$build.go。versionFromZipName取中间的版本段archFromZipName取amd64或arm64。文件名不符合时返回ErrBadVersion——注意anubis-1.26.2.zip缺少平台后缀也会被拒绝确保看不懂就失败而不是产出错误安装包。第二步版本号到 MSI ProductVersion 的转换MSI 的 ProductVersion 要求形如major.minor.build.revision的四段纯数字其中major 与 minor 上限 255build 与 revision 上限 65535。version.go中的msiVersion用锚定两端正则^v?(?Pmajor\d)\.(?Pminor\d)\.(?Ppatch\d)(?:-pre(?Ppre\d))?(?:-(?Pcommits\d)-g[0-9a-fA-F])?(?:-dev)?$解析 yeet 的版本串version.go转换规则如下yeet 版本串示例含义转换结果1.26.2正式发布1.26.2.0v1.26.2带 v 前缀1.26.2.01.26.2-1-gec3cce8a-devdev 构建落后 1 个提交1.26.2.11.26.2-137-gdeadbeedev 构建落后 137 个提交1.26.2.1371.27.0-pre1预发布1.27.0.0关键细节预发布编号-preN没有对应的 MSI 版本段。MSI 判断升级只比较 major.minor.build 三段无法表达1.27.0-pre1 低于 1.27.0。因此预发布与正式版共享同一个 ProductVersion只能靠不同的 ProductCode 区分——所以 anubis.wxs 中声明了AllowSameVersionUpgradesyes保证在 1.27.0 之上安装 1.27.0-pre1或反之仍会正确触发升级移除旧版本而不是两个产品同时注册互相损坏。dev 构建的提交数则落入 revision 段Windows 比较版本时忽略该段所以 dev 构建与其基准发布在安装器眼里版本相同。范围检查同样严格major256、patch65536、提交数超过 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 家族若共享 ProductCodeWindows 会把它们当成同一个产品装一个就静默卸载另一个。PackageCode同样以 UpgradeCode 为命名空间但哈希输入是packagecode/ version / arch前缀串与 ProductCode 的输入刻意区分避免两个 GUID 撞车。这是对 MSI 惯例的有意偏离——标准做法要求每个物理 .msi 文件有唯一 PackageCode但既然整套构建已可复现再让 PackageCode 每次随机反而会误导人。配置模板生成安装器随附的etc目录stageConfigTemplatesmain.go负责生成安装器携带的两个配置模板etc/anubis.env直接复制 run/windows/anubis.envetc/anubis.yaml把 data/botPolicies.yaml默认策略与 run/windows/logging.yamlWindows 日志配置块按顺序拼接而成。这两个文件随后会被安装到%ProgramFiles%\Techaro\Anubis\etc由安装时的自定义动作复制到数据目录。模板中的__ANUBIS_DATA_DIR__占位符是特意留的数据目录通常在C:\ProgramData\Techaro\Anubis但%ProgramData%可被重定位模板不写死路径由运行时引导逻辑见下文写入真实值。concatFiles与extractOne都做了同一件可复现性关键操作把生成/解压文件的 mtime 恢复为 zip 条目的统一 mtimebuild.go。因为 wixl 构建的 CAB 归档会把每个文件的 mtime 嵌进去若这些文件带着墙钟时间戳即使输入 zip 完全一致MSI 也会每次构建都不同。解压时还会做路径穿越防护filepath.Clean 前缀校验防止恶意 zip 条目写到 staging 目录之外。文档碎片生成对抗 wixl-heat 的随机目录 IDzip 内含完整的docs文档树yeet 从docs/docs复制而来安装器要把它们装到C:\Program Files\Techaro\Anubis\doc。generateDocFragmentmain.go先把文档目录遍历出的文件列表排序再通过 stdin 喂给wixl-heat生成一个挂在DOCDIR目录引用下的doc-files.wxs碎片。问题在于wixl-heat 每次调用都会给目录随机生成dirHEXID即使输入树完全一致源码注明是经验验证的结论cmpHEX和filHEX是稳定的只有目录 ID 随机。为此 docfragment.go 实现了rewriteDirectoryIDs用encoding/xml流式解析生成的碎片按目录嵌套顺序累积Name属性组成相对路径再用sha256前 16 字节转大写十六进制派生形如dir32位大写HEX的确定性 ID。选择路径派生而非顺序编号是刻意的——顺序编号会在树中任意增删文件时打乱所有目录编号而路径派生只影响真正移动过的目录。同时该函数会剔除冗余的xmlns声明避免与编码器自动生成的命名空间声明冲突产生非法 XML。wixl 编译与五类后处理修补runWixlmain.go调用wixl编译anubis.wxs与doc-files.wxs通过-D传入 SourceDir、DocSourceDir、Win64、Version、ProductCode、InstallerVersion 等变量。InstallerVersion 按架构区分amd64 用200Windows Installer 引擎的长期基线arm64 必须用500——因为 Arm64 包模板只在 Windows Installer 5.0 引入声明过低版本可能导致目标机上被以not supported by this processor type拒绝。编译完成后需要依次执行五类msibuild修补每一类都是 wixl 能力缺陷或非确定性的补偿1.checkWixlOutput先信 stderr别信退出码这是贯穿全程的纪律build.gowixl 对不认识的属性只是往 stderr 写 CRITICAL 警告然后照样以退出码 0 结束产出一个悄悄丢了属性的安装包。所以每次调用 wixl/wixl-heat 后都必须先检查 stderr 是否含CRITICAL命中即报ErrWixlDroppedAttribute。build_test.go中的TestCheckWixlOutput用捕获的样本 stderr 验证了这条纯字符串匹配逻辑。2.secureInstallDir让INSTALLDIR覆盖真正生效MSI 的安装过程会提升到系统上下文deferred 阶段届时命令行传入的INSTALLDIR...若不在SecureCustomProperties属性中就会被静默忽略。wixl 0.106 无法在.wxs中声明式完成这件事Property/Secureyes会被静默丢弃手写Property IdSecureCustomProperties又会与 wixl 为MajorUpgrade自动生成的行冲突、直接让构建中止。因此 secureInstallDir 在构建后用msibuild -q UPDATE把INSTALLDIR追加进SecureCustomProperties再用msiinfo export读回验证——写入后必须读回校验是本文件对msibuild同样不信任的体现。3.patchInstallerImages替换 WiX 默认安装界面图片MSI 的向导界面WixUI_Minimal默认带 WiX 的横幅与对话框图。真实 WiX 用WixVariable IdWixUIBannerBmp覆盖但 wixl 不支持另一条路是把 wixl 的 ext/ui 树 vendor 进仓库但那意味着把约 18 个 MS-RL 许可证的.wxs文件带进这个 MIT 项目。最终方案是直接用msibuild -a替换 MSI 中Binary.WixUI_Bmp_Banner与Binary.WixUI_Bmp_Dialog两个流main.go图片来自 run/windows/banner.bmp 与 run/windows/dialog.bmp。替换后仍用msiinfo extract把流抽回来比对字节长度确认补丁真的落盘。4.patchSummaryInfo修正 Template 与 PackageCodewixl 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.patchEnvironmentID与patchCustomActionSequence消除两个非确定性来源Environment 表主键wixl 0.106 无视.wxs里声明的Idenv_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文档树组件全部标记Win64yes仅支持 64 位。服务注册anubis.exe组件内嵌ServiceInstall以NT SERVICE\Anubis虚拟账户运行Startdemand手动启动、ErrorControlnormal、VitalyesServiceControl配置为卸载时停止并删除服务。虚拟账户由 Windows 在装服务时自动创建、卸载时自动删除无需管理密码。升级语义MajorUpgrade开启AllowSameVersionUpgrades另有cmp_svc_start_on_upgrade组件其ServiceControl的启动请求被ConditionWIX_UPGRADE_DETECTED门控——只有检测到旧版本存在时才在安装时启动服务。这是对一个真实缺陷的修复RemoveExistingProducts排在InstallServices之前升级会先删旧服务再建新服务若不显式请求启动升级会静默报告成功而让 Anubis 处于停止状态同时全新安装因还没有配置与签名密钥绝不能自动启动。PATH 写入cmp_path组件把[BINDIR]追加到系统PATH。自定义动作SetAnubisExePathimmediate把[BINDIR]anubis.exe写入属性→BootstrapConfigdeferred、Impersonateno执行--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标志。其工作内容把%ProgramFiles%\Techaro\Anubis\etc下的anubis.env与anubis.yaml复制到数据目录%ProgramData%\Techaro\AnubisProgramData环境变量未设置时拒绝猜测路径防止filepath.Join把空值拼成相对路径 Techaro\Anubis通过SetNamedSecurityInfo以SID 字节形式给NT SERVICE\Anubis授予数据目录的 Modify 权限读配置、写轮转日志、删除旧日志但不含 WRITE_DAC/WRITE_OWNER服务无法自我提权。这里刻意不 shell 出icacls——icacls 会把 SID 反查成账户名而引导发生在服务注册之前LSA 无法映射尚未注册的服务 SID会导致整个调用以错误 1332 失败把引导诊断写入anubis-bootstrap.log。原因是 msiexec 会丢弃自定义动作的全部日志输出失败时只留下晦涩的 Error 1603同时引导失败必须返回成功退出码否则同样会以 1603 掩盖真实错误。服务启动后早期 stderr 会被重定向到anubis-startup.log服务进程没有可用 stderr运行期日志则按logging.yaml写入anubis.log并自动轮转。管理员需在首次启动前设置ED25519_PRIVATE_KEY_HEX64 位十六进制否则 Anubis 每次重启都会重新生成随机签名密钥使所有已签发挑战失效run/windows/anubis.env。可复现性工程让同一提交产出 byte 级一致的 MSIREADME 坦言一个现实约束由于 msitools 与 MSI 格式的种种限制当前无法让 MSI 构建完全可复现该工具由 Claude Opus 5 作为正确性 fuzzer 生成用于确保产物在 Windows 上行为正常。但代码里为尽可能可复现做了大量工作上述修补大多同时服务于正确性与可复现性。TestMSIReproducibleBuildmsi_test.go把同一 zip 构建两次要求除两个已知残留外完全 byte 一致摘要信息流的时间戳Created与Last saved由 wixl 按墙钟写入而msibuild -s没有对应参数InstallExecuteSequence 表的物理行序SetAnubisExePath与BootstrapConfig行的存储位置不可移动实测与 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_VERIFYtrue的 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 INSTALLDIRD:\Anubis静默安装加上/qnmsiexec /i anubis-1.26.2-windows-amd64.msi /qn INSTALLDIRD:\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 上的标准安装体验。赞分享后端网络安全【免费下载链接】anubisWeighs the soul of incoming HTTP requests to stop AI crawlers项目地址https://gitcode.com/gh_mirrors/anubis4/anubis点击查看免费下载相关推荐构建 ILSpy Windows 安装程序MSI从发布产物到 WiX 打包全流程指南构建 ILSpy Windows 安装程序MSI从发布产物到 WiX 打包全流程指南 本指南以 ILSpy.Installer/README.md htt逆向工程开发工具桌面应用Ruffle Windows MSI 安装包构建指南使用 WiX 工具集为 ruffle_desktop 打包安装程序Ruffle Windows MSI 安装包构建指南使用 WiX 工具集为 ruffle_desktop 打包安装程序 本指南基于 Ruffle 仓库中的 d音视频PowerToys 本地化构建产物如何打包进 MSI 安装包PowerToys 本地化构建产物如何打包进 MSI 安装包 PowerToys 的界面文案通过 resx / resw 资源文件和 lcl 翻译文件管理流桌面应用开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表