RustDesk Windows MSI 安装包工程详解:从 preprocess.py 预处理到自定义动作体系
RustDesk 在 Windows 平台的正式安装包(.msi)由一个基于 WiX v4 的独立安装工程生成,该工程与主程序解耦,通过一个 Python 预处理脚本把版本、组件、注册表、升级策略等信息注入 WXS 模板,再由 Visual Studio 2022 编译出可分发的 msi 文件。本文以 res/msi/README.md 为核心骨架,完整讲解该 MSI 工程的构建步骤、preprocess.py 的全部参数与其注入机制、安装包内各 Fragment 的结构,以及服务创建、防火墙规则、远程打印机等 C++ 自定义动作的调用链,帮助你既能照文档构建出 msi,也能理解安装器在系统里“做了什么”。
一、工程定位:独立的 WiX v4 安装解决方案
MSI 工程位于仓库的 res/msi/ 目录,包含三个部分:
- msi.sln:Visual Studio 解决方案,用 Visual Studio 2022 编译;
- Package/:WiX v4 包定义(
Package.wxs+Includes.wxi+Components/、Fragments/、Language/、UI/子目录); - CustomActions/:C++ 自定义动作 DLL 工程(vcxproj),提供 16 个 DllEntry 供安装序列调用。
README 明确说明:该工程主要派生自开源项目 MediaPortal-2,因此目录组织(Components/Fragments/UI/Language 分层 + Python 预处理注入)沿用了 MediaPortal 的模板思路,RustDesk 在其上替换了产品相关的组件与自定义动作。工程使用 WiX v4 的 XML schema(http://wixtoolset.org/schemas/v4/wxs),而非传统的 WixToolset v3 命令行流程,编译依赖 VS 2022 内置的 Wix v4 MSBuild 支持。
二、构建步骤:preprocess.py 与解决方案编译
README 给出的官方构建流程只有两步:
- 在 res/msi/ 目录下运行
python preprocess.py,可用python preprocess.py -h查看全部参数; - 用 Visual Studio 2022 打开 msi.sln 并构建该解决方案。
构建完成后,安装测试时可用下面的命令记录完整安装日志:
msiexec /i package.msi /l*v install.log
其中 /l*v 表示把全部事件级别写入日志文件,这是排查 MSI 安装行为(属性、条件、自定义动作执行顺序)的标准手段。
关键点在于:先运行预处理脚本,再编译。脚本会修改 Package/ 下的多个 WXS/WXI/WXL 模板文件(在占位注释标签之间插入生成内容),同时把图标等资源拷贝到位;如果直接编译而不预处理,模板中的 $(var.Version)、$(var.UpgradeCode) 等变量将无值可解析。
三、preprocess.py 全参数解析
preprocess.py 的 argparse 定义暴露了以下参数(含默认值):
| 参数 | 默认值 | 作用 |
|---|---|---|
-d / --dist-dir |
../../rustdesk |
待安装的发行产物目录,脚本会把其中除主 exe 外的所有文件自动生成为 MSI 组件 |
--arp |
false |
已废弃;注释明确“native MSI ARP registration is always used” |
--custom-arp |
{} |
自定义 ARP(添加/删除程序)属性,JSON 形式,如 '{"Comments": {"msi": "ARPCOMMENTS", "v": "Remote control application."}}' |
-c / --custom |
false |
是否构建定制客户端(custom client),与品牌重命名流程配套 |
--conn-type |
空字符串 | 连接类型:incoming / outgoing;空表示双向(incoming-outgoing) |
--app-name |
RustDesk |
应用名,用于产品名、注册表根、快捷方式名等 |
-v / --version |
空 | 应用版本;缺省时从 dist_dir/<App>.exe --version 动态读取 |
--revision-version |
当前时间戳/60(整数) |
版本号第四段(revision),范围 0~2147483647 |
-m / --manufacturer |
Purslane Tech Pte. Ltd. |
厂商名 |
版本与构建日期如何确定
init_global_vars()(preprocess.py)的行为:
- 若未显式传
--version,脚本会执行<dist_dir>/<AppName>.exe --version读取版本,并把-替换为.(如1.4.0-beta→1.4.0.beta); - 版本必须匹配
X.Y.Z形式;若只有三段,则追加第四段 revision:g_version = f"{g_version}.{args.revision_version}",并校验 revision 在 .NETVersion允许范围内(0~2147483647); - 构建日期则读取
--build-date输出,必须匹配YYYY-MM-DD HH:MM格式。
生成的预变量与 UpgradeCode
gen_pre_vars() 向 Package/Includes.wxi 的 <!--$PreVarsStart$--> 与 <!--$PreVarsEnd$--> 标签之间写入一整套 <?define ... ?> 变量,供所有 WXS 通过 $(var.xxx) 引用:
<?define Version="1.4.0.12345678" ?>
<?define Manufacturer="Purslane Tech Pte. Ltd." ?>
<?define Product="RustDesk" ?>
<?define Description="RustDesk Installer" ?>
<?define ProductLower="rustdesk" ?>
<?define RegKeyRoot=".rustdesk" ?>
<?define RegKeyInstall=".\rustdesk\Install" ?>
<?define BuildDir="..\..\rustdesk" ?>
<?define BuildDate="2026-01-01 01:00" ?>
<!-- The UpgradeCode must be consistent for each product. ! -->
<?define UpgradeCode = "3f1c..." ?>
注意 UpgradeCode 的计算方式(preprocess.py):uuid.uuid5(uuid.NAMESPACE_OID, "<AppName>.exe")——基于应用名的确定性 UUID。这意味着同一产品名的 UpgradeCode 恒定,保证升级时能正确识别旧版本;而换应用名(定制客户端)会自动得到不同 UpgradeCode,不会误判为同一产品。
发行文件自动组件化
insert_components_between_tags()(preprocess.py)遍历 --dist-dir 下所有文件(跳过与主程序同名的 exe),为每个文件在 Package/Components/RustDesk.wxs 的 <!--$AutoComonentStart$--> / <!--$AutoComponentEnd$--> 之间生成:
<Component Guid="<随机uuid4>" Subdirectory="子目录(根目录时省略)">
<File Source="相对路径" KeyPath="yes" Checksum="yes" />
</Component>
注释里解释了为什么组件 Id 不能是 Component_{idx} 这类顺序号:会触发 Error WIX0130 The primary key 'xxxx' is duplicated in table 'Directory'。主程序 exe 本身则手工定义在 RustDesk.wxs 的 App.exe 组件中,带固定的 Guid,并预留了 fire:FirewallException(被注释,改用自定义动作方案,见第六节)。
升级策略注入
gen_upgrade_info() 向 Package/Fragments/Upgrades.wxs 写入一个 <Upgrade> 节点:
<Upgrade Id="<每次随机uuid4>">
<UpgradeVersion Property="OLD_VERSION_FOUND" Minimum="1.0.0" Maximum="1.99.99"
IncludeMinimum="yes" IncludeMaximum="yes" OnlyDetect="no" IgnoreRemoveFailure="yes"
MigrateFeatures="yes" />
</Upgrade>
区间取值为当前主版本号 1.0.0~1.99.99,即同一主版本内互相识别升级,跨主版本不互相迁移;配合 Package/Package.wxs 中 <MajorUpgrade ... Schedule="afterInstallInitialize" AllowSameVersionUpgrades="yes" /> 完成大版本升级语义。
定制客户端(custom client)流程
当 --app-name 不是 RustDesk 时(对应 --custom),主流程 preprocess.py 会额外执行品牌替换:
replace_component_guids_in_wxs():遍历Package/**\*.wxs,把所有Component ... Guid="..."重新生成为新的随机 UUID,避免与官方包组件 GUID 冲突;replace_app_name_in_langs():把Package/Language/*.wxl语言文件中的 "RustDesk" 全部替换为新应用名;replace_app_name_in_custom_actions():对CustomActions/*.cpp、*.h做\bRustDesk\b正则替换,但特意保留RustDesk v4 Printer Driver这一打印机驱动名不变;update_license_file():改写 Package/License.rtf,把 RustDesk/Purslane 字样替换为新品牌。
--conn-type outgoing 的语义体现在 gen_conn_type():只有非空时才在 Package/Fragments/AddRemoveProperties.wxs 的 <!--$CustomClientPropsStart$--> 标签间生成 <Property Id="CC_CONNECTION_TYPE" Value="outgoing" />。该属性后续被安装序列用作“纯客户端(只向外连接)”模式的总开关,跳过服务创建、托盘自启与 SAS 注册表写入(详见第五、六节),正对应 README TODO 中 “Custom client. firewall and tcp allow. Outgoing” 的设计意图。
四、安装包结构总览
Package.wxs:包级定义
Package/Package.wxs 是整个 msi 的入口,要点:
<Package ... Scope="perMachine">:整机安装,UpgradeCode="$(var.UpgradeCode)"直接来自预处理生成的变量;<Media Id="1" Cabinet="cab1.cab" EmbedCab="yes" CompressionLevel="high" />:单 Cabinet 内嵌、高压缩;- 自定义 UI 集
ui:WixUI Id="UI_MyInstallDialog" InstallDirectory="INSTALLFOLDER_INNER",由 Package/UI/ 下的MyInstallDlg.wxs等定义(安装目录可选,即 README 提到的 “custom options” 雏形); - 防重入保护:
<CustomAction Id="BlockSelfInstalledApp">在AppSearch之后执行,当检测到注册表中已存在 Windows Installer 标记(APP_WINDOWS_INSTALLER="#0"或 32 位对应值)时弹窗阻止; - 序列调整:
<InstallExecute After="RemoveExistingProducts" />保证升级时先移除旧包再执行新安装;<InstallValidate Condition="NOT Installed" />仅在首次安装时做校验; - 功能特性
Feature Id="App" Level="1" ... AllowAbsent="no"聚合了文件组件组与全部注册表组件(安装目录、默认图标、--play命令、URL 协议、InstallState、遗留清理项、开始菜单等)。
目录结构:INSTALLFOLDER_INNER 的四分支规范化
Package/Components/Folders.wxs 处理旧版本命令行的兼容:实际安装目录是 ProgramFiles6432Folder\<Product>(即 INSTALLFOLDER_INNER),而命令行仍接受 INSTALLFOLDER 属性。脚本通过四条 SetProperty 规则把外部传入值规范化成统一的 INSTALLFOLDER\<Product>\ 形态:已带 \Product\、已带 \Product 无尾斜杠、有尾斜杠但非 Product 目录、无尾斜杠且非 Product 目录——四种情况分别对应 SetInstallFolderInnerFromProductDir 等四个 Action。同时 CommonAppDataFolder 下建立 App.Data.Folder(对应 %ProgramData%\RustDesk,配置文件 RustDesk2.toml 的存放处)。
注册表写入:InstallState 与协议
Package/Components/Regs.wxs 定义了若干持久化组件:
Product.Registry.InstallState:在HKLM\Software\RustDesk\InstallState\RustDesk下写入BuildDate、share_rdp、InstallLocation、WindowsInstaller=1以及MsiProductCode(KeyPath),并ForceDeleteOnUninstall="yes"——该键同时是“当前是否 Windows Installer 安装”的判据;Product.Registry.DefaultIcon/CommandPlay:在HKCR\.rustdesk下注册默认图标与右键“播放”命令(<exe> --play "%1"),--play是 RustDesk 主程序直接拉起远程会话的命令行入口;Product.Registry.URLProtocol/Command:注册rustdesk://URL 协议(HKCR\rustdesk,URL Protocol值)及其打开命令;- 两个
RemoveLegacyUninstall64/32组件:若旧包曾在HKLM\...\Uninstall\RustDesk留下 WindowsInstaller 标记,安装时删除遗留注册表项,保证“添加/删除程序”里只出现一条记录。
添加/删除程序(ARP)注册
preprocess.py 的 g_arpsystemcomponent(preprocess.py)定义了四项 ARP 属性:
ARPCOMMENTS→ 本地化文案!(loc.AR_Comment);ARPCONTACT、ARPREADME→ 项目主页;ARPHELPLINK→ issue 入口。
gen_native_arp_properties() 把它们以 <Property> 形式注入 AddRemoveProperties.wxs 的 <!--$ArpStart$--> 标签间,即“原生 MSI ARP 注册”(--arp 参数弃用的原因)。--custom-arp 提供的 JSON 可覆盖其中任意项,但脚本明确拒绝 ARPSYSTEMCOMPONENT 被置为“隐藏组件”,以强制保持 msi 在“添加/删除程序”中可见。
五、README 知识:MSI 场景属性表
README 的 “Knowledge/properties” 一节给出判断安装场景的属性矩阵,这也是理解整个安装序列条件表达式的基础:
| Property Name | Install | Uninstall | Change | Repair | Upgrade |
|---|---|---|---|---|---|
| Installed | False | True | True | True | True |
| REINSTALL | False | False | False | True | False |
| UPGRADINGPRODUCTCODE | False | False | False | False | True |
| REMOVE | False | True | False | False | True |
对照 RustDesk.wxs 中的条件表达式,可以直接读懂每个动作的触发时机:
- 卸载判定
(Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE):升级时Installed与REMOVE同时为真,但UPGRADINGPRODUCTCODE也为真,故该表达式只在真正卸载时为真。README 引用的外部主题“设置自定义动作仅在卸载时运行”即讲此技巧; - 升级判定
UPGRADINGPRODUCTCODE为真:例如RemoveRuntimeGeneratedFiles的条件是Installed AND (REMOVE="ALL" OR UPGRADINGPRODUCTCODE)——卸载或升级时清理运行时生成文件(config、日志等),普通安装/修复不动; - 修复判定
REINSTALL为真:本表中 Repair 唯一为真的属性。
服务与托盘的启动逻辑
安装序列中与服务/客户端行为相关的条件:
CreateStartService:(NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE)) AND (NOT STOP_SERVICE='Y') AND (NOT CC_CONNECTION_TYPE="outgoing")——非卸载、配置未要求停服务、且非纯 outgoing 客户端时才创建自启动服务;LaunchApp:NOT UILevel=2(静默安装不弹客户端)且非卸载;LaunchAppTray:额外要求LAUNCH_TRAY_APP(默认Y,见 AddRemoveProperties.wxs)且非 outgoing 客户端;STOP_SERVICE属性默认'Y',安装初始化时会由SetPropertyServiceStop从%ProgramData%\RustDesk\config\RustDesk2.toml的stop-service键读取真实值(ReadConfig.cpp 实现),若用户配置了“不启动服务”,则跳过建服务并删除启动项快捷方式(TryDeleteStartupShortcut)。
六、自定义动作:16 个 DllEntry 的职责与执行时机
Package/Fragments/CustomActions.wxs 声明了二进制引用 Custom_Actions_Dll(来自 CustomActions/CustomActions.vcxproj 的构建输出)以及全部动作的 Impersonate/Execute/Return 属性。DLL 导出清单见 CustomActions.def。按 Execute 语义分两类:
- immediate + Impersonate=yes(用户权限立即执行):
CustomActionHello(演示用)、TerminateProcesses/TerminateBrokers(结束<Product>.exe与RuntimeBroker_rustdesk.exe,为替换被锁定的文件铺路)、SetPropertyIsServiceRunning(查询服务状态)、SetPropertyServiceStop(实际映射到 DllEntrySetPropertyFromConfig)、TryDeleteStartupShortcut; - deferred + Impersonate=no(SYSTEM 权限延迟执行):
RemoveRuntimeGeneratedFiles、AddFirewallRules/RemoveFirewallRules(同一 DllEntryAddFirewallRules,用参数1/0区分增删)、CreateStartService、TryStopDeleteService、AddRegSoftwareSASGeneration、RemoveAmyuniIdd、InstallPrinter/UninstallPrinter。
各动作的 C++ 实现与仓库文件对应关系:
| 动作 | 实现文件 | 说明 |
|---|---|---|
CreateStartService / TryStopDeleteService |
ServiceUtils.cpp | OpenSCManagerW + CreateServiceW(SERVICE_AUTO_START、LocalSystem)/ ControlService 停止后 DeleteService |
AddFirewallRules |
FirewallRules.cpp | 通过 INetFwPolicy2 COM 接口增删入/出站规则 |
InstallPrinter / UninstallPrinter / RemoveAmyuniIdd |
RemotePrinter.cpp | 安装/卸载 RustDesk v4 打印机驱动 |
SetPropertyFromConfig(读配置设属性) |
ReadConfig.cpp | 解析 stop-service 等 TOML 配置键 |
AddRegSoftwareSASGeneration 等辅助逻辑 |
CustomActions.cpp | 主实现文件,含服务状态查询等 |
值得注意的源码级细节(都在 RustDesk.wxs 的注释中):
- 防火墙不走 WiX 原生
fire:FirewallException:注释说明该节点在Outbound="Yes"时会报Error 0x80070057: failed to add app to the authorized apps list,所以改用自定义动作调用 COM 接口。这同时回应了 README TODO 中 “firewall and tcp allow. Outgoing” 一项——出站放行已通过AddFirewallRuleWithEdgeTraversal的 in/out 参数实现; - 系统版本判断不用
IsWindows10OrGreater():打印机安装的版本条件写作VersionNT >= 603(注释解释该 API 依赖可执行文件内嵌 manifest 且行为不稳),源码注释同时说明 Win8.1 上远程打印机经测试可用,故从 603(Win8.1)开始支持; - 参数化:每个 deferred 动作都配一个
*.SetParam立即动作(如CreateStartService.SetParam把$(var.Product);"[INSTALLFOLDER_INNER]$(var.Product).exe" --service"写入属性),符合“immediate 阶段设参、deferred 阶段消费”的 WiX 最佳实践。--service参数即 RustDesk 主程序进入服务模式(注册并启动 Windows 服务)的命令行入口。
七、对话框定制:Resources 目录与位图
README 的 “Usage” 一节说明:把自定义对话框位图放入 “Resources” 目录即可。脚本侧的机制在 gen_custom_dialog_bitmaps()(preprocess.py):
- 检查的位图名单固定为
['WixUIBannerBmp', 'WixUIDialogBmp', 'WixUIExclamationIco', 'WixUIInfoIco', 'WixUINewIco', 'WixUIUpIco']; - 对实际存在的
Package/Resources/<变量名>.bmp,在 Package.wxs 的<!--$CustomBitmapsStart$-->/<!--$CustomBitmapsEnd$-->标签间生成<WixVariable Id="..." Value="Resources\....bmp" />,即 WiX UI 官方的“定制对话框集”方式; - 应用图标则无条件由
prepare_resources()把仓库的 res/icon.ico 拷贝到Package/Resources/icon.ico,供<Icon Id="AppIcon">引用(快捷方式、ARP 图标均用它)。
因此定制步骤为:准备上述命名的 bmp → 放入 Package/Resources/ → 重新运行 python preprocess.py → 重新构建解决方案。
八、TODO 与参考资料
README 保留了工程自身的 TODO 清单,可以作为后续演进方向的权威记录:
- Start menu / Uninstall(开始菜单项与卸载项,代码中
App.StartMenu与App.StartMenu.ShortcutUninstall组件已实现大部分,对应 RustDesk.wxs 的条件组件); - custom options(自定义安装选项,
MyInstallDirDlg提供了目录选择); - Custom client(定制客户端):firewall and tcp allow、Outgoing 方向、是否展示 license、仅创建服务(outgoing 场景)——
--custom、--conn-type参数与第六节所述条件表达式即该方向的当前实现。
参考资料(README “Refs” 原列条目):
- Windows Installer Portal(
learn.microsoft.com的 windows-installer-portal 页面):MSI 属性、条件、序列的官方文档入口; wxsschema 文档(wixtoolset.org 的 wxs 参考):本工程所有 WXS 文件的标签依据;- WiX 官方仓库(github.com/wixtoolset/wix):WiX 源码与问题跟踪。
九、实操小结:构建一个 RustDesk 官方 msi
结合上文,最小可复现流程如下:
- 准备一个发行产物目录(其中包含
rustdesk.exe与依赖文件),确保rustdesk.exe --version能输出X.Y.Z形式版本; - 在 res/msi/ 下执行
python preprocess.py -d <产物目录相对路径>(默认-d ../../rustdesk); - 用 Visual Studio 2022 打开 msi.sln,生成解决方案(先编译 CustomActions 工程,
Package工程的BinaryRef="Custom_Actions_Dll"引用其输出$(var.CustomActions.TargetDir)$(var.CustomActions.TargetName).dll); - 用
msiexec /i package.msi /l*v install.log安装并检查日志; - 若做定制客户端:追加
--app-name "MyDesk" --custom,视需要--conn-type outgoing与--custom-arp '{...}',脚本会自动完成 GUID 重生成、品牌替换与 ARP 属性覆盖。
需要再次强调两个前提限制:整个流程依赖 Windows + Visual Studio 2022 环境(README 明确的编译工具链);预处理脚本会就地修改 Package/ 下的模板文件(WXS/WXL/RTF),在只读仓库中请只在本地工作副本中执行这些构建步骤。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00