Wix toolset 安装程序制作总结

这段时间为开发的应用制作安装程序,选用了免费的Wix toolset来制作,花了大半个星期的时间。这篇文章记录一下制作过程中遇到的一些典型问题,希望对遭遇同样问题的同学能有所帮助。

如何批量生成component

Wix的基本逻辑是把每个要安装的文件作为一个component,类似以下结构

 <Component Id="cmp0C572E8D2BFFC59B011F9362292F5902" Guid="{FF8E06CC-99CE-4818-9FAA-79B0DF282571}"  Win64="yes" >
    <File Id="filCB87CA23169B24DC40451965BA65034F" KeyPath="yes" Source="$(var.myapp.TargetDir)\Autofac.dll"   ProcessorArchitecture="x64" />
 </Component>

如果要安装的文件很多,手写的方式十分低效,Wix提供了一个工具来生成此内容,在Wix toolset的安装目录中,目录通常是C:\Program Files (x86)\WiX Toolset v3.11\bin,具体用法:

heat.exe dir "C:\project\myapp\bin\Release" -dr INSTALLFOLDER -cg ProductComponent -gg -scom -sreg -sfrag -out "C:\project\mySetupProject\myapp.wxs"

其中C:\project\myapp\bin\Release是你要安装的文件所在目录,heat会扫描目录下所有文件,包括子目录。生成的文件位置在C:\project\mySetupProject\myapp.wxs,也就是out参数指定的地方。这里mySetupProject是安装程序本身的工程。wix工程会扫描工程目录下所有wxs文件,一起进行编译链接。
注意生成后的File节点的Source属性可能不正确,需要替换成$(var.myapp.TargetDir),这里myapp是指你自己的应用程序的项目名。

如何生成64位安装程序

默认生成是32位的,如以下片段

<Directory Id="ProgramFilesFolder">
        <Directory Id="COMPANYFOLDER" Name="!(bind.property.Manufacturer)">
          <Directory Id="INSTALLFOLDER" Name="!(bind.property.ProductName)" />
        </Directory>
</Directory>

ProgramFilesFolder会指向C:\program files(x86)目录, 为了改成指向C:\program files目录,需要修改几处地方

  1. Package节点添加Platform属性
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" Platform="x64" />
  1. Component和File节点都标明64位属性
      <Component Id="cmp0C572E8D2BFFC59B011F9362292F5902" Guid="{FF8E06CC-99CE-4818-9FAA-79B0DF282571}"  Win64="yes" >
        <File Id="filCB87CA23169B24DC40451965BA65034F" KeyPath="yes" Source="$(var.myApp.TargetDir)\Autofac.dll"   ProcessorArchitecture="x64" />
      </Component>

3.目录指明目标文件夹

<Directory Id="ProgramFiles64Folder">
        <Directory Id="COMPANYFOLDER" Name="!(bind.property.Manufacturer)">
          <Directory Id="INSTALLFOLDER" Name="!(bind.property.ProductName)" />
        </Directory>
</Directory>

如何设定安装语言

首先创建语言文件strings_zh-cn.wxl,这个就是国际化对应的翻译文件,文件名后缀zh-cn对应不同语言,所有的界面文字都可以引用这个文件里的条目,引用方式为 !(loc.xxx),这里xxx就是文件中的Id

<?xml version="1.0" encoding="utf-8"?>
<WixLocalization Culture="zh-cn" Codepage="936"  xmlns="http://schemas.microsoft.com/wix/2006/localization">
  <String Id="Lang">2052</String>
  <String Id="Code">936</String>
  <String Id="WixUIBack" Overridable="yes">上一步(&amp;B)</String>
  <String Id="WixUINext" Overridable="yes">下一步(&amp;N)</String>
  <String Id="WixUICancel" Overridable="yes">取消</String>
  <String Id="WixUIFinish" Overridable="yes">完成(&amp;F)</String>
  <String Id="WixUIRetry" Overridable="yes">重试(&amp;R)</String>
  <String Id="WixUIIgnore" Overridable="yes">忽略(&amp;I)</String>
  <String Id="WixUIYes" Overridable="yes">是(&amp;Y)</String>
  <String Id="WixUINo" Overridable="yes">否(&amp;N)</String>
  <String Id="WixUIOK" Overridable="yes">确定</String>
  <String Id="WixUIPrint" Overridable="yes">打印(&amp;P)</String>
</WixLocalization>

然后修改Package节点

<Package InstallerVersion="200" Compressed="yes" Languages="!(loc.Lang)" SummaryCodepage="!(loc.Code)" InstallScope="perMachine" Platform="x64" />

这里Languages和SummaryCodepage属性就引用的语言文件设定的值。

如何以管理员权限在安装过程中执行命令

我们的业务需求是安装完成后需要启动一个windows服务,虽然wix提供了服务安装,但由于我们的服务可以直接由命令安装,所以没使用wix的方式,而是在安装copy文件以后,以管理员权限执行一个命令。经过尝试与摸索,配置的方法如下:

  1. package节点指明权限
<Package InstallerVersion="200" Compressed="yes" Languages="!(loc.Lang)" SummaryCodepage="!(loc.Code)" InstallScope="perMachine" Platform="x64" InstallPrivileges="elevated" AdminImage="yes"/>

这里添加了InstallPrivileges和AdminImage属性

  1. 定义一个customaction
<CustomAction Id="regService" Execute="deferred" Return="check" Directory="INSTALLFOLDER" Impersonate="no" ExeCommand="cmd.exe /c &quot;myapp.exe install -servicename:myAppService  -username:[USERACCOUNT] -password:[PASSWD] -displayname:myAppService&quot;"/>

这里USERACCOUNT和PASSWD是用户在界面输入的参数,这个后面再介绍,这里的关键是Execute属性定义为deferred,Impersonate属性定义为no,然后用cmd去执行。

3.定义InstallExecuteSequence,指明执行顺序

    <InstallExecuteSequence>
      <Custom Action='regService' Before='InstallFinalize'>NOT Installed</Custom>
    </InstallExecuteSequence>

注意这里执行时机必须是Before InstallFinalize

如何在卸载过程中执行命令

卸载和安装过程的执行命令类似,我们的需求是取消后台服务,同样定义一个CustomAction

<CustomAction Id="unRegService" Execute="deferred" Return="check" Directory="INSTALLFOLDER" Impersonate="no" ExeCommand="cmd.exe /c &quot;myApp.exe uninstall -servicename:myAppService&quot;"/>

注意Execute和Impersonate属性,然后定义InstallExecuteSequence序列

    <InstallExecuteSequence>
      <Custom Action='regService' Before='InstallFinalize'>NOT Installed</Custom>
      <Custom Action='unRegService' Before='RemoveFiles'>REMOVE</Custom>
    </InstallExecuteSequence>

注意这里的触发时机是Before RemoveFiles,而且触发条件是REMOVE,也就是在卸载时才动作。

如何自定义界面并插入界面序列中

自定义界面的开发可以看看网络上的其他教程,这里说以下配置,我们的需求是在WixUI_FeatureTree模式下,license界面以后,跳到自定义的一个界面,用户输入一些环境信息,例如账号密码,点击下一步再进入组件选择的界面。也就是说将自定义界面插入原生的license和组件选择界面中间。

  1. 首先去github上找到wix toolset的源码,因为我们用的是WixUI_FeatureTree模式,就找到WixUI_FeatureTree.wxs这个文件,路径是src/ext/UIExtension/wixlib/WixUI_FeatureTree.wxs,然后将里面的<UI Id="WixUI_FeatureTree">节点拷贝到我们自己的工程中,放在Product节点下。

  2. 修改LicenseAgreementDlg界面的next按钮的指向

<Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="EnvInfoDialog">LicenseAccepted = "1"</Publish>

这里EnvInfoDialog是我们自己开发的界面

  1. 修改组件选择的界面的back按钮的指向
 <Publish Dialog="CustomizeDlg" Control="Back" Event="NewDialog" Value="EnvInfoDialog" Order="2">NOT Installed</Publish>

4.修改我们自己界面的back和Next按钮的指向

      <Publish Dialog="EnvInfoDialog" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg">1</Publish>
      <Publish Dialog="EnvInfoDialog" Control="Next" Event="NewDialog" Value="CustomizeDlg">1</Publish>

这就像修改双向链表,插入一个节点。学过数据结构的都明白。

如何传递变量给自定义行为

开发过程中遇到的一个问题就是我们需要根据用户输入修改配置文件,这是由一个CustomAction来执行修改动作的,其 Execute是"deferred"模式,界面上采集的用户输入以会话变量的形式存在,但在deferred模式下又无法直接传递给CustomAction使用。在查阅很多资料后,才找到了正确途径:

    <CustomAction Id="EnvInfoSaveAction.SetProperty" Return="check" Property="EnvInfoSaveAction" Value="USERACCOUNT=[USERACCOUNT];PASSWD=[PASSWD];ESHOST=[ESHOST]"/>
    <CustomAction Id="EnvInfoSaveAction" BinaryKey="CustomActionBinary" DllEntry="saveConfigInfo" Execute="deferred" Impersonate="no"/>

这里的关键就是要再定义一个CustomAction,其 Property属性是修改配置的CustomAction 的Id,然后Value属性是要传递的变量,以key=value的形式,用分号分隔。最后在CustomAction的实际代码中,用session.CustomActionData去获取变量的值。

    public class CustomActions
    {
        [CustomAction]
        public static ActionResult saveConfigInfo(Session session)
        {
            string account = session.CustomActionData["USERACCOUNT"];
            ... Do Something
        }
    }

如何自定义License声明

这个比较简单,提供一个License文件,格式为rtf的,再覆盖定义Wix变量即可

<WixVariable Id="WixUILicenseRtf" Value="License.rtf" />

如何Debug安装程序和卸载过程

有时候安装和卸载过程中会出错,那么这时候能看到log会方便排查问题,可以用msiexec命令来执行你的安装程序,同时提供日志文件参数

msiexec MyProduct.msi /L*V "%TEMP%\MyProduct.log"

安装以后,windows系统会把安装程序保持在自己的c:\windows\installer目录一份,卸载的时候执行这个程序,如果卸载的时候出问题,可以在此目录找到自己的安装程序,虽然被重命名了,但是可以看控制面板卸载时要求权限的确认框,这里会有具体文件信息。要生成卸载日志,可以用msiexec去执行这个msi文件,指定卸载参数和日志参数

msiexec /x xxxx.msi /L*V "%TEMP%\MyProduct.log"

如何解决无法卸载干净的问题

这个问题通常是在开发安装程序的过程中出现,多次安装以后,突然卸载不干净了,控制面板中卸载成功以后,文件却还遗留在系统中。通过查看log,发现有这样的提示

Disallowing uninstallation of component: {5C517030-1AA5-4018-82F2-A2D6F784EC04} since another client exists

意思是这个组件还被其他程序使用,所以不允许删除。其实都是开发过程中偶尔的安装错误导致系统认为还有别的客户端在使用,这些信息被存储在注册表中。严格说起来,不建议在开发环境中直接测试安装程序,一般都是找个干净的环境专门用于测试和还原。回来说问题的解决,在实验了数款清理工具和软件后,找到一个有效的powershell脚本,执行这个脚本即可清理注册表中的遗留数据,需要这里面的productName替换成安装工程product节点的Name属性值。

$productName = "myApp" # this should basically match against your previous
# installation path. Make sure that you don't mess with other components used
# by any other MSI package
$components = Get-ChildItem -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Components\
$count = 0
foreach ($c in $components)
{
foreach($p in $c.Property)
{
$propValue = (Get-ItemProperty "Registry::$($c.Name)" -Name "$($p)")."$($p)"
if ($propValue -match $productName)
{
Write-Output $propValue
$count++
Remove-Item "Registry::$($c.Name)" -Recurse
}
}
}
Write-Host "$($count) key(s) removed"
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342