首页
/ RustDesk Windows MSI 安装包工程详解:从 preprocess.py 预处理到自定义动作体系

RustDesk Windows MSI 安装包工程详解:从 preprocess.py 预处理到自定义动作体系

2026-09-05 20:13:53作者:田桥桑Industrious

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 给出的官方构建流程只有两步:

  1. res/msi/ 目录下运行 python preprocess.py,可用 python preprocess.py -h 查看全部参数;
  2. 用 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-beta1.4.0.beta);
  • 版本必须匹配 X.Y.Z 形式;若只有三段,则追加第四段 revision:g_version = f"{g_version}.{args.revision_version}",并校验 revision 在 .NET Version 允许范围内(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="&lt;随机uuid4&gt;" Subdirectory="子目录(根目录时省略)">
  <File Source="相对路径" KeyPath="yes" Checksum="yes" />
</Component>

注释里解释了为什么组件 Id 不能是 Component_{idx} 这类顺序号:会触发 Error WIX0130 The primary key 'xxxx' is duplicated in table 'Directory'。主程序 exe 本身则手工定义在 RustDesk.wxsApp.exe 组件中,带固定的 Guid,并预留了 fire:FirewallException(被注释,改用自定义动作方案,见第六节)。

升级策略注入

gen_upgrade_info()Package/Fragments/Upgrades.wxs 写入一个 <Upgrade> 节点:

<Upgrade Id="&lt;每次随机uuid4&gt;">
  <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.01.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 下写入 BuildDateshare_rdpInstallLocationWindowsInstaller=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\rustdeskURL Protocol 值)及其打开命令;
  • 两个 RemoveLegacyUninstall64/32 组件:若旧包曾在 HKLM\...\Uninstall\RustDesk 留下 WindowsInstaller 标记,安装时删除遗留注册表项,保证“添加/删除程序”里只出现一条记录。

添加/删除程序(ARP)注册

preprocess.py 的 g_arpsystemcomponentpreprocess.py)定义了四项 ARP 属性:

  • ARPCOMMENTS → 本地化文案 !(loc.AR_Comment)
  • ARPCONTACTARPREADME → 项目主页;
  • 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):升级时 InstalledREMOVE 同时为真,但 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 客户端时才创建自启动服务;
  • LaunchAppNOT UILevel=2(静默安装不弹客户端)且非卸载;
  • LaunchAppTray:额外要求 LAUNCH_TRAY_APP(默认 Y,见 AddRemoveProperties.wxs)且非 outgoing 客户端;
  • STOP_SERVICE 属性默认 'Y',安装初始化时会由 SetPropertyServiceStop%ProgramData%\RustDesk\config\RustDesk2.tomlstop-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>.exeRuntimeBroker_rustdesk.exe,为替换被锁定的文件铺路)、SetPropertyIsServiceRunning(查询服务状态)、SetPropertyServiceStop(实际映射到 DllEntry SetPropertyFromConfig)、TryDeleteStartupShortcut
  • deferred + Impersonate=no(SYSTEM 权限延迟执行):RemoveRuntimeGeneratedFilesAddFirewallRules / RemoveFirewallRules(同一 DllEntry AddFirewallRules,用参数 1/0 区分增删)、CreateStartServiceTryStopDeleteServiceAddRegSoftwareSASGenerationRemoveAmyuniIddInstallPrinter / 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 的注释中):

  1. 防火墙不走 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 参数实现;
  2. 系统版本判断不用 IsWindows10OrGreater():打印机安装的版本条件写作 VersionNT >= 603(注释解释该 API 依赖可执行文件内嵌 manifest 且行为不稳),源码注释同时说明 Win8.1 上远程打印机经测试可用,故从 603(Win8.1)开始支持;
  3. 参数化:每个 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 清单,可以作为后续演进方向的权威记录:

  1. Start menu / Uninstall(开始菜单项与卸载项,代码中 App.StartMenuApp.StartMenu.ShortcutUninstall 组件已实现大部分,对应 RustDesk.wxs 的条件组件);
  2. custom options(自定义安装选项,MyInstallDirDlg 提供了目录选择);
  3. Custom client(定制客户端):firewall and tcp allow、Outgoing 方向、是否展示 license、仅创建服务(outgoing 场景)——--custom--conn-type 参数与第六节所述条件表达式即该方向的当前实现。

参考资料(README “Refs” 原列条目):

  • Windows Installer Portal(learn.microsoft.com 的 windows-installer-portal 页面):MSI 属性、条件、序列的官方文档入口;
  • wxs schema 文档(wixtoolset.org 的 wxs 参考):本工程所有 WXS 文件的标签依据;
  • WiX 官方仓库(github.com/wixtoolset/wix):WiX 源码与问题跟踪。

九、实操小结:构建一个 RustDesk 官方 msi

结合上文,最小可复现流程如下:

  1. 准备一个发行产物目录(其中包含 rustdesk.exe 与依赖文件),确保 rustdesk.exe --version 能输出 X.Y.Z 形式版本;
  2. res/msi/ 下执行 python preprocess.py -d <产物目录相对路径>(默认 -d ../../rustdesk);
  3. 用 Visual Studio 2022 打开 msi.sln,生成解决方案(先编译 CustomActions 工程,Package 工程的 BinaryRef="Custom_Actions_Dll" 引用其输出 $(var.CustomActions.TargetDir)$(var.CustomActions.TargetName).dll);
  4. msiexec /i package.msi /l*v install.log 安装并检查日志;
  5. 若做定制客户端:追加 --app-name "MyDesk" --custom,视需要 --conn-type outgoing--custom-arp '{...}',脚本会自动完成 GUID 重生成、品牌替换与 ARP 属性覆盖。

需要再次强调两个前提限制:整个流程依赖 Windows + Visual Studio 2022 环境(README 明确的编译工具链);预处理脚本会就地修改 Package/ 下的模板文件(WXS/WXL/RTF),在只读仓库中请只在本地工作副本中执行这些构建步骤。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384