我们已经使用WiX有一段时间了,尽管通常对于使用方便的抱怨很多,但情况还算不错。我正在寻找有关以下内容的有用建议:
- 设置一个WiX项目(布局、引用、文件模式)
- 将WiX集成到解决方案中,并构建/发布过程
- 为新安装和升级配置安装程序
- 任何你想分享的好的WiX技巧
我们已经使用WiX有一段时间了,尽管通常对于使用方便的抱怨很多,但情况还算不错。我正在寻找有关以下内容的有用建议:
Keep variables in a separate wxi
include file. Enables re-use, variables are faster to find and (if needed) allows for easier manipulation by an external tool.
Define Platform variables for x86 and x64 builds
<!-- Product name as you want it to appear in Add/Remove Programs-->
<?if $(var.Platform) = x64 ?>
<?define ProductName = "Product Name (64 bit)" ?>
<?define Win64 = "yes" ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?else ?>
<?define ProductName = "Product Name" ?>
<?define Win64 = "no" ?>
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
<?endif ?>
Store the installation location in the registry, enabling upgrades to find the correct location. For example, if a user sets custom install directory.
<Property Id="INSTALLLOCATION">
<RegistrySearch Id="RegistrySearch" Type="raw" Root="HKLM" Win64="$(var.Win64)"
Key="Software\Company\Product" Name="InstallLocation" />
</Property>
Note: WiX guru Rob Mensching has posted an excellent blog entry which goes into more detail and fixes an edge case when properties are set from the command line.
Examples using 1. 2. and 3.
<?include $(sys.CURRENTDIR)\Config.wxi?>
<Product ... >
<Package InstallerVersion="200" InstallPrivileges="elevated"
InstallScope="perMachine" Platform="$(var.Platform)"
Compressed="yes" Description="$(var.ProductName)" />
and
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="$(var.PlatformProgramFilesFolder)">
<Directory Id="INSTALLLOCATION" Name="$(var.InstallName)">
The simplest approach is always do major upgrades, since it allows both new installs and upgrades in the single MSI. UpgradeCode is fixed to a unique Guid and will never change, unless we don't want to upgrade existing product.
Note: In WiX 3.5 there is a new MajorUpgrade element which makes life even easier!
Creating an icon in Add/Remove Programs
<Icon Id="Company.ico" SourceFile="..\Tools\Company\Images\Company.ico" />
<Property Id="ARPPRODUCTICON" Value="Company.ico" />
<Property Id="ARPHELPLINK" Value="http://www.example.com/" />
On release builds we version our installers, copying the msi file to a deployment directory. An example of this using a wixproj target called from AfterBuild target:
<Target Name="CopyToDeploy" Condition="'$(Configuration)' == 'Release'">
<!-- Note we append AssemblyFileVersion, changing MSI file name only works with Major Upgrades -->
<Copy SourceFiles="$(OutputPath)$(OutputName).msi"
DestinationFiles="..\Deploy\Setup\$(OutputName) $(AssemblyFileVersion)_$(Platform).msi" />
</Target>
Use heat to harvest files with wildcard (*) Guid. Useful if you want to reuse WXS files across multiple projects (see my answer on multiple versions of the same product). For example, this batch file automatically harvests RoboHelp output.
@echo off
robocopy ..\WebHelp "%TEMP%\WebHelpTemp\WebHelp" /E /NP /PURGE /XD .svn
"%WIX%bin\heat" dir "%TEMP%\WebHelp" -nologo -sfrag -suid -ag -srd -dir WebHelp -out WebHelp.wxs -cg WebHelpComponent -dr INSTALLLOCATION -var var.WebDeploySourceDir
There's a bit going on, robocopy
is stripping out Subversion working copy metadata before harvesting; the -dr
root directory reference is set to our installation location rather than default TARGETDIR; -var
is used to create a variable to specify the source directory (web deployment output).
Easy way to include the product version in the welcome dialog title by using Strings.wxl for localization. (Credit: saschabeaumont. Added as this great tip is hidden in a comment)
<WixLocalization Culture="en-US" xmlns="http://schemas.microsoft.com/wix/2006/localization">
<String Id="WelcomeDlgTitle">{\WixUI_Font_Bigger}Welcome to the [ProductName] [ProductVersion] Setup Wizard</String>
</WixLocalization>
Save yourself some pain and follow Wim Coehen's advice of one component per file. This also allows you to leave out (or wild-card *
) the component GUID.
Rob Mensching has a neat way to quickly track down problems in MSI log files by searching for value 3
. Note the comments regarding internationalization.
When adding conditional features, it's more intuitive to set the default feature level to 0 (disabled) and then set the condition level to your desired value. If you set the default feature level >= 1, the condition level has to be 0 to disable it, meaning the condition logic has to be the opposite to what you'd expect, which can be confusing :)
<Feature Id="NewInstallFeature" Level="0" Description="New installation feature" Absent="allow">
<Condition Level="1">NOT UPGRADEFOUND</Condition>
</Feature>
<Feature Id="UpgradeFeature" Level="0" Description="Upgrade feature" Absent="allow">
<Condition Level="1">UPGRADEFOUND</Condition>
</Feature>
检查IIS是否已安装:
<Property Id="IIS_MAJOR_VERSION">
<RegistrySearch Id="CheckIISVersion" Root="HKLM" Key="SOFTWARE\Microsoft\InetStp" Name="MajorVersion" Type="raw" />
</Property>
<Condition Message="IIS must be installed">
Installed OR IIS_MAJOR_VERSION
</Condition>
如何检查Vista+操作系统上是否安装了IIS 6 Metabase兼容性?
<Property Id="IIS_METABASE_COMPAT">
<RegistrySearch Id="CheckIISMetabase" Root="HKLM" Key="SOFTWARE\Microsoft\InetStp\Components" Name="ADSICompatibility" Type="raw" />
</Property>
<Condition Message="IIS 6 Metabase Compatibility feature must be installed">
Installed OR ((VersionNT < 600) OR IIS_METABASE_COMPAT)
</Condition>
将所有 ID 放在不同的命名空间中
F.
开头,例如:F.Documentation、F.Binaries、F.SampleCode。C.
开头,例如:C.ChmFile、C.ReleaseNotes、C.LicenseFile、C.IniFile、C.Registry。CA.
开头,例如:CA.LaunchHelp、CA.UpdateReadyDlg、CA.SetPropertyX。Fi.
开头。Di.
开头。我发现这有助于更好地跟踪各种类别中的所有 ID。
非常好的问题。我希望能看到一些最佳实践。
我有很多要分发的文件,所以我将我的项目分成了几个wxs源文件。
我有一个顶级源文件,我称之为Product.wxs,它基本上包含了安装的结构,但不包括实际的组件。这个文件有几个部分:
<Product ...>
<Package ...>
<Media>...
<Condition>s ...
<Upgrade ..>
<Directory>
...
</Directory>
<Feature>
<ComponentGroupRef ... > A bunch of these that
</Feature>
<UI ...>
<Property...>
<Custom Actions...>
<Install Sequences....
</Package>
</Product>
其余的.wix文件由Fragments组成,其中包含ComponentGroups,在Product.wxs中的Feature标签中引用。我的项目包含一个很好的逻辑文件分组,我将其分发出去。
<Fragment>
<ComponentGroup>
<ComponentRef>
....
</ComponentGroup>
<DirectoryRef>
<Component... for each file
....
</DirectoryRef>
</Fragment>
这并不完美,我的面向对象感觉有点敏感,因为片段必须引用Product.wxs文件中的名称(例如DirectoryRef),但我发现将其维护在单个大源文件中更容易。
我很想听听对此的评论,或者如果有人有好的建议也欢迎!
在退出对话框中添加复选框,以启动应用程序或帮助文件。
...
<!-- CA to launch the exe after install -->
<CustomAction Id ="CA.StartAppOnExit"
FileKey ="YourAppExeId"
ExeCommand =""
Execute ="immediate"
Impersonate ="yes"
Return ="asyncNoWait" />
<!-- CA to launch the help file -->
<CustomAction Id ="CA.LaunchHelp"
Directory ="INSTALLDIR"
ExeCommand ='[WindowsFolder]hh.exe IirfGuide.chm'
Execute ="immediate"
Return ="asyncNoWait" />
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT"
Value="Launch MyApp when setup exits." />
<UI>
<Publish Dialog ="ExitDialog"
Control ="Finish"
Order ="1"
Event ="DoAction"
Value ="CA.StartAppOnExit">WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT</Publish>
</UI>
如果您按照这种方式操作,"标准"外观可能不太正确。复选框始终具有灰色背景,而对话框是白色的:
解决此问题的一种方法是指定自定义的ExitDialog,并使用位置不同的复选框。这种方法可以解决问题,但似乎需要花费很多工作来改变一个控件的颜色。另一种解决相同问题的方法是后处理生成的MSI文件,更改该特定CheckBox控件在Control表中的X、Y字段。JavaScript代码如下:
var msiOpenDatabaseModeTransact = 1;
var filespec = WScript.Arguments(0);
var installer = new ActiveXObject("WindowsInstaller.Installer");
var database = installer.OpenDatabase(filespec, msiOpenDatabaseModeTransact);
var sql = "UPDATE `Control` SET `Control`.`Height` = '18', `Control`.`Width` = '170'," +
" `Control`.`Y`='243', `Control`.`X`='10' " +
"WHERE `Control`.`Dialog_`='ExitDialog' AND " +
" `Control`.`Control`='OptionalCheckBox'";
var view = database.OpenView(sql);
view.Execute();
view.Close();
database.Commit();
在使用cscript.exe作为命令行脚本运行此代码(在使用light.exe生成MSI后)将产生一个外观更专业的ExitDialog。
alt text http://www.dizzymonkeydesign.com/blog/misc/adding-and-customizing-dlgs-in-wix-3/images/exit_dlg_2.gifWIXUI_EXITDIALOGOPTIONALCHECKBOX
替换为 <Publish>
内的 WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed
。 - Alexander Kojevnikov简而言之:为每个安装程序创建唯一的UpgradeCode,并自动定义每个安装程序的Guid的第一个字符,剩余31个字符保持唯一。
Config.wxi示例
<?xml version="1.0" encoding="utf-8"?>
<Include>
<!-- Upgrade code should not change unless you want to install
a new product and have the old product remain installed,
that is, both products existing as separate instances. -->
<?define UpgradeCode = "YOUR-GUID-HERE" ?>
<!-- Platform specific variables -->
<?if $(var.Platform) = x64 ?>
<!-- Product name as you want it to appear in Add/Remove Programs-->
<?define ProductName = "Foo 64 Bit [Live]" ?>
<?else ?>
<?define ProductName = "Foo [Live]" ?>
<?endif ?>
<!-- Directory name used as default installation location -->
<?define InstallName = "Foo [Live]" ?>
<!-- Registry key name used to store installation location -->
<?define InstallNameKey = "FooLive" ?>
<?define VDirName = "FooLive" ?>
<?define AppPoolName = "FooLiveAppPool" ?>
<?define DbName = "BlahBlahLive" ?>
</Include>
示例 Config.Common.wxi
<?xml version="1.0" encoding="utf-8"?>
<Include>
<!-- Auto-generate ProductCode for each build, release and upgrade -->
<?define ProductCode = "*" ?>
<!-- Note that 4th version (Revision) is ignored by Windows Installer -->
<?define ProductVersion = "1.0.0.0" ?>
<!-- Minimum version supported if product already installed and this is an upgrade -->
<!-- Note that 4th version (Revision) is ignored by Windows Installer -->
<?define MinimumUpgradeVersion = "0.0.0.0" ?>
<!-- Platform specific variables -->
<?if $(var.Platform) = x64 ?>
<?define Win64 = "yes" ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?else ?>
<?define Win64 = "no" ?>
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
<?endif ?>
<?define ProductManufacturer = "Foo Technologies"?>
<!-- Decimal Language ID (LCID) for the Product. Used for localization. -->
<?define ProductLanguage = "1033" ?>
<?define WebSiteName = "DefaultWebSite" ?>
<?define WebSitePort = "80" ?>
<?define DbServer = "(local)" ?>
</Include>
示例Components.wxs
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<!-- The pre-processor variable which allows the magic to happen :) -->
<?include $(sys.CURRENTDIR)\Config.wxi?>
<?include ..\Setup.Library\Config.Common.wxi?>
<Fragment Id="ComponentsFragment">
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="$(var.PlatformProgramFilesFolder)">
<Directory Id="INSTALLLOCATION" Name="$(var.InstallName)">
<Component Id="ProductComponent" Guid="0XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" KeyPath="yes">
...
注意:我现在建议在组件中省略Guid属性(相当于*
),每个组件使用一个文件,并将文件设置为关键路径。这样就不需要调用下面显示的ModifyComponentsGuids
和RevertComponentsGuids
目标。虽然这可能对您的所有组件都不可行。
示例 Setup.Live.wixproj
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />
<Target Name="BeforeBuild">
<CallTarget Targets="ModifyComponentsGuids" />
</Target>
<Target Name="AfterBuild">
<CallTarget Targets="RevertComponentsGuids" />
</Target>
<!-- Modify the first character of every Guid to create unique value for Live, Test and Training builds -->
<Target Name="ModifyComponentsGuids">
<FileUpdate Files="..\Setup.Library\Components.wxs" Regex="Guid="([a-f]|[A-F]|\d)" ReplacementText="Guid="A" />
</Target>
<!-- Revert the first character of every Guid back to initial value -->
<Target Name="RevertComponentsGuids">
<FileUpdate Files="..\Setup.Library\Components.wxs" Regex="Guid="([a-f]|[A-F]|\d)" ReplacementText="Guid="0" />
</Target>
最后的想法
更新1:自动生成组件Guid可以通过为每个文件创建带有Guid =“*”的组件并将文件设置为关键路径来消除调用FileUpdate任务的需要。
更新2:我们遇到的问题之一是,如果您没有自动生成组件Guid,并且生成失败,则需要手动删除临时文件。
更新3:找到了一种方法来消除对svn:externals和临时文件创建的依赖。如果您无法使用通配符匹配Guid,则这使生成过程更具弹性,并且在light或candle中出现构建故障时更加稳定。
更新4:WiX 3.0+支持使用实例转换进行多个实例,绝对值得一看。
msiexec /i Package.msi /l*v c:\Package.log
Package.msi是您的软件包名称,
c:\Package.log是您想要输出日志的地方。
使用JavaScript CustomActions因其易用性而变得如此简单
有人说JavaScript不适合用于MSI CustomActions。理由是:难以调试,难以使之可靠。但我并不同意。它不难调试,确实比C++容易。只是不同而已。我发现用JavaScript编写CustomActions非常简单,比使用C++要容易得多。速度也快得多。同样可靠。
只有一个缺点:JavaScript CustomActions可以通过Orca提取,而C/C++ CA需要反向工程。如果您认为您的安装程序是受保护的知识产权,那么您将希望避免使用脚本。
如果您使用脚本,您只需要从一些结构开始。以下内容可以帮助您入门。
JavaScript自定义操作的“模板”代码:
//
// CustomActions.js
//
// Template for WIX Custom Actions written in Javascript.
//
//
// Mon, 23 Nov 2009 10:54
//
// ===================================================================
// http://msdn.microsoft.com/en-us/library/sfw6660x(VS.85).aspx
var Buttons = {
OkOnly : 0,
OkCancel : 1,
AbortRetryIgnore : 2,
YesNoCancel : 3
};
var Icons = {
Critical : 16,
Question : 32,
Exclamation : 48,
Information : 64
};
var MsgKind = {
Error : 0x01000000,
Warning : 0x02000000,
User : 0x03000000,
Log : 0x04000000
};
// http://msdn.microsoft.com/en-us/library/aa371254(VS.85).aspx
var MsiActionStatus = {
None : 0,
Ok : 1, // success
Cancel : 2,
Abort : 3,
Retry : 4, // aka suspend?
Ignore : 5 // skip remaining actions; this is not an error.
};
function MyCustomActionInJavascript_CA() {
try {
LogMessage("Hello from MyCustomActionInJavascript");
// ...do work here...
LogMessage("Goodbye from MyCustomActionInJavascript");
}
catch (exc1) {
Session.Property("CA_EXCEPTION") = exc1.message ;
LogException(exc1);
return MsiActionStatus.Abort;
}
return MsiActionStatus.Ok;
}
// Pop a message box. also spool a message into the MSI log, if it is enabled.
function LogException(exc) {
var record = Session.Installer.CreateRecord(0);
record.StringData(0) = "CustomAction: Exception: 0x" + decimalToHexString(exc.number) + " : " + exc.message;
Session.Message(MsgKind.Error + Icons.Critical + Buttons.btnOkOnly, record);
}
// spool an informational message into the MSI log, if it is enabled.
function LogMessage(msg) {
var record = Session.Installer.CreateRecord(0);
record.StringData(0) = "CustomAction:: " + msg;
Session.Message(MsgKind.Log, record);
}
// http://msdn.microsoft.com/en-us/library/d5fk67ky(VS.85).aspx
var WindowStyle = {
Hidden : 0,
Minimized : 1,
Maximized : 2
};
// http://msdn.microsoft.com/en-us/library/314cz14s(v=VS.85).aspx
var OpenMode = {
ForReading : 1,
ForWriting : 2,
ForAppending : 8
};
// http://msdn.microsoft.com/en-us/library/a72y2t1c(v=VS.85).aspx
var SpecialFolders = {
WindowsFolder : 0,
SystemFolder : 1,
TemporaryFolder : 2
};
// Run a command via cmd.exe from within the MSI
function RunCmd(command)
{
var wshell = new ActiveXObject("WScript.Shell");
var fso = new ActiveXObject("Scripting.FileSystemObject");
var tmpdir = fso.GetSpecialFolder(SpecialFolders.TemporaryFolder);
var tmpFileName = fso.BuildPath(tmpdir, fso.GetTempName());
LogMessage("shell.Run("+command+")");
// use cmd.exe to redirect the output
var rc = wshell.Run("%comspec% /c " + command + "> " + tmpFileName, WindowStyle.Hidden, true);
LogMessage("shell.Run rc = " + rc);
// here, optionally parse the output of the command
if (parseOutput) {
var textStream = fso.OpenTextFile(tmpFileName, OpenMode.ForReading);
while (!textStream.AtEndOfStream) {
var oneLine = textStream.ReadLine();
var line = ParseOneLine(oneLine);
...
}
textStream.Close();
}
if (deleteOutput) {
fso.DeleteFile(tmpFileName);
}
return {
rc : rc,
outputfile : (deleteOutput) ? null : tmpFileName
};
}
接下来,使用类似以下的方式注册自定义操作:
<Fragment>
<Binary Id="IisScript_CA" SourceFile="CustomActions.js" />
<CustomAction Id="CA.MyCustomAction"
BinaryKey="IisScript_CA"
JScriptCall="MyCustomActionInJavascript_CA"
Execute="immediate"
Return="check" />
</Fragmemt>
当然可以插入多个JavaScript函数,以实现多个自定义操作。例如,我使用JavaScript对IIS进行了WMI查询,以获取现有网站列表,可以在其中安装ISAPI筛选器。然后使用此列表来填充稍后在UI序列中显示的列表框。非常简单。shell.Run()
方法调用appcmd.exe执行工作。容易。我很惊讶没有人提到使用T4在构建过程中生成WXS文件。我是通过Henry Lee在New Age Solutions的博客了解到这一点的。
基本上,你需要创建一个自定义MSBuild任务来执行T4模板,而该模板会在Wix项目编译之前输出WXS文件。这允许你(根据实现方式)自动包含从其他解决方案编译的所有程序集的输出(这意味着你不再需要每次添加新程序集时都编辑wxs文件)。
使用Heat.exe来处理庞大的安装程序并实现"史诗级的胜利"
扩展Si和Robert-P有关heat的回答。
翻译:
(使用heat避免手动输入项目中的单个文件,并自动化构建以实现整体更轻松的过程。)
WiX 2.0 Heat语法详解
对于更新的版本(与旧版本并没有太大区别,但有可能会有烦人的语法更改....),请从cmd.exe进入Heat所在目录,只需键入heat即可,但如果需要帮助新版本,我这里有一个示例。
将以下内容添加到Visual Studio 2010中的生成事件中。
(右键单击项目->属性->生成事件->前生成事件)
$(WIX)bin\heat.exe" dir "$(EnviromentVariable)" -cg GroupVariable -gg -scom -sreg -sfrag -
srd -dr INSTALLLOCATION -var env.LogicPath -out "$(FragmentDir)\FileName.wxs
-gg
运行heat时生成Guid(如上面的命令执行时)
-scom
不获取“COM文件”
-sreg
不获取“注册表文件”
-sfrag
不获取“片段”
-srd
不获取“根目录”
dir
dir表示您希望Heat查找的文件夹
"$(EnviromentVariable)"
您将在(Right click project, Go to properties)项目属性->生成部分中添加到预处理器变量中的变量名称(假定使用Visual Studio 2010)
示例: EnviromentVariable=C:\Project\bin\Debug;不要使用双引号,但以分号结尾
-cg GroupVariable
将被引用从创建的片段到主wxs文件中的ComponentGroup
FragmentDir
输出wxs片段将存储的片段目录
FileName.wxs
文件名
完整教程在这里,非常有帮助