65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Visual Studio 和 WiX 工具集为业务应用程序创建多项和选择性配置安装程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (8投票s)

2014 年 10 月 14 日

CPOL

12分钟阅读

viewsIcon

37384

downloadIcon

1225

一个示例展示了如何创建 WiX 安装程序和一个引导程序包,用于部署具有多重选择性配置的业务应用程序。

引言

大多数业务应用程序的部署都特定于基于不同环境的配置。MSBuild 工具提供了使用 AfterBuild 任务直接编辑配置文件的方法。然而,这不适用于具有多重选择性配置的部署。此类部署的最佳方法可能需要满足以下要求:

  • 在 Visual Studio 项目中为各种部署环境(例如本地调试、开发、UAT 和生产)设置配置。
  • 转换环境特定配置文件中的 XML 项和值。
  • 在安装程序 UI 上为需要在安装期间手动更改的配置项提供编辑功能。
  • 启用批处理生成,为多重配置生成单独的 MSI 包文件。
  • 将多个 MSI 包捆绑到一个输出的 MSI 包文件中,并提供选择性配置选项。

本文档和下载的源代码展示了一个实现这些需求的示例安装程序应用程序。您可能需要对 WiX Toolset 有基本的了解和实践,才能更好地理解编码场景并运行示例应用程序。

示例应用程序中的 Visual Studio 解决方案和项目文件需要进行许多手动编辑任务。如果您要在自己的应用程序中执行相同的操作,请确保在进行任何编辑工作之前备份原始文件。

安装程序演示

示例安装程序已在安装了 Windows XP、Windows 7、Windows 8/8.1、Windows Server 2003 和 Windows Server 2008 R2 的计算机上进行了测试。当通过执行单一的合并 MSI 包文件 *SM.MultiConfigApp.SetupAll.msi* 运行示例安装程序时,将显示 WiX 引导程序应用程序的第一个窗口。

单击所需的配置/环境的单选按钮将进入 WiX 安装程序欢迎窗口。父级引导程序窗口仍在其后台运行。

单击 **下一步** 按钮将显示 *目标文件夹* 对话框。

然后是可编辑的 *数据库连接配置* 对话框。

根据安装过程的持续时间,将显示进度条对话框。如果没有错误,将显示 *完成* 对话框。

单击 **完成** 按钮将返回到引导程序窗口,并显示安装成功消息。

安装程序结构

安装程序有两个 Visual Studio 解决方案和一个独立的 MSI 包文件夹。

  1. *SM.MultiConfigApp* 解决方案包含一个简单的 Windows 窗体项目作为目标应用程序和一个 WiX 常规安装项目。

  2. *SM.MultiConfigApp.SetupAll* 解决方案包含三个项目:

    • *SM.MultiConfigApp.Bundle*:一个 WiX 引导程序项目,将多个 MSI 文件刻录成单个输出的 EXE 文件。它还设置配置模式选择的用户界面。

    • *SM.MultiConfigApp.SetupAll*:一个 WiX 常规项目,包装了引导程序并生成单个输出的 MSI 文件。由于政策或法规的要求,许多公司在应用程序部署时需要 MSI 包文件。

    • *SM.RemoveMsiWrapper*:一个控制台/窗口应用程序项目,可在目标应用程序安装完成或取消后自动删除引导程序包装器安装。因此,安装模拟了实际执行的是 MSI 包而不是包装器的过程。

  3. *MsiPackge*:一个文件夹,其中包含两个解决方案的输出 MSI 包文件。它为刻录多个单独配置的 MSI 文件提供了共享源文件位置。

配置模式

Visual Studio 在 **生成** 菜单下的 **配置管理器** 中提供了设置配置和平台的选项。对于示例应用程序中的所有项目,我保留了默认的 **Debug** 模式,并添加了 **dev**、**uat** 和 **prod**,如下所示。所有添加的配置模式的名称都为小写,以便于识别和集成到 MSI 文件名中。

在添加了所需的配置模式进行生成后,可以从列表中删除默认的 **Release** 模式。这可以通过修改解决方案和项目文件来完成。

  1. 使用任何文本编辑器打开 *.sln* 文件,然后搜索并删除任何包含“Release|”的 `GlobalSection` 行。

    GlobalSection(SolutionConfigurationPlatforms) = preSolution
        - - -		
        Release|Any CPU = Release|Any CPU
        Release|Mixed Platforms = Release|Mixed Platforms
        Release|x86 = Release|x86
        - - -	
    EndGlobalSection
    GlobalSection(ProjectConfigurationPlatforms) = postSolution
        - - -	
        {5BF1BA2D-CEF2-4A8C-8669-96F894D5A04F}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {5BF1BA2D-CEF2-4A8C-8669-96F894D5A04F}.Release|Any CPU.Build.0 = Release|Any CPU
        {5BF1BA2D-CEF2-4A8C-8669-96F894D5A04F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
        {5BF1BA2D-CEF2-4A8C-8669-96F894D5A04F}.Release|Mixed Platforms.Build.0 = Release|Any CPU
        {5BF1BA2D-CEF2-4A8C-8669-96F894D5A04F}.Release|x86.ActiveCfg = Release|Any CPU
        - - -	
    EndGlobalSection            
            
  2. 使用任何文本编辑器打开 *.csproj* 文件(或在 Visual Studio 解决方案资源管理器中卸载项目并选择上下文菜单命令 **编辑** <project_name>),然后搜索并删除任何包含“Release|”的整个 `PropertyGroup` 节点。

    <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
        <PlatformTarget>AnyCPU</PlatformTarget>
        <DebugType>pdbonly</DebugType>
        <Optimize>true</Optimize>
        <OutputPath>bin\Release\</OutputPath>
        <DefineConstants>TRACE</DefineConstants>
        <ErrorReport>prompt</ErrorReport>
        <WarningLevel>4</WarningLevel>
    </PropertyGroup>        

配置文件

每个额外的配置模式都需要一个相应的配置文件,该文件依赖于基础配置文件(在本例中为 *App.config*)。可以通过命名约定 *App.*<configuration_name>*[configuration_name].config* 向项目添加新文件,例如 *App.prod.config*。要将这些文件作为子文件附加到 *App.config*,请打开项目文件(在本例中为 *SM.MultiConfigApp.csproj*)并手动更新包含这些已添加配置文件的 `ItemGroup` 节点。

更新前的 `ItemGroup` 节点

<ItemGroup>
   <None Include="App.config" />
   <None Include="App.dev.config" />
   <None Include="App.prod.config" />
   <None Include="App.uat.config" />    
</ItemGroup>

更新后的 `ItemGroup` 节点

<ItemGroup>
   <None Include="App.config">
      <SubType>Designer</SubType>
   </None>
   <None Include="App.dev.config">
      <DependentUpon>App.config</DependentUpon>
      <SubType>Designer</SubType>
   </None>
   <None Include="App.prod.config">
      <DependentUpon>App.config</DependentUpon>
      <SubType>Designer</SubType>
   </None>
   <None Include="App.uat.config">
      <DependentUpon>App.config</DependentUpon>
      <SubType>Designer</SubType>
   </None>    
</ItemGroup>

基础和依赖配置文件应在 Visual Studio 解决方案资源管理器中显示为一个组。

转换配置项

通过使用 MSBuild Extensions 的 TranformXml 任务,示例应用程序中的 *App.config* 文件包含基础本地调试环境所需的所有项。任何环境特定的配置文件仅保留该环境的不同项。生成过程将为特定环境的安装填充应用程序配置文件,其中包含相同的基本项,但会动态更新从环境特定文件中定义的项。

要启用 TranformXml 任务,需要在项目文件(在本例中为 *SM.MultiConfigApp.csproj*)的根 `Project` 节点下方添加以下 XML 代码:

<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v12.0\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="AfterCompile" Condition="exists('App.$(Configuration).config')">
    <!-- Generates the transformed App.config in the intermediate directory -->
    <TransformXml Source="App.config" Destination="$(IntermediateOutputPath)$(TargetFileName).config" Transform="App.$(Configuration).config" />
    <!-- Forces the build process to use the transformed configuration file -->
    <ItemGroup>
      <AppConfigWithTargetPath Remove="App.config" />
      <AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config">
        <TargetPath>$(TargetFileName).config</TargetPath>
      </AppConfigWithTargetPath>
    </ItemGroup>
</Target>

其中 `v12.0` 指示项目是使用 Visual Studio 2013 创建的。如果您使用的是 Visual Studio 2012 或 2010,请将其更改为 `v11.0` 或 `v10.0`。

然后,我们需要向环境特定配置文件中的节点添加 Transform 属性和所需的值。

本地调试环境的 *App.config* 基础配置文件示例

<configuration>
  <connectionStrings>
    <add name="AppDBConnection" connectionString="Data Source=AppDbSource;Initial Catalog=AppDb;Persist Security Info=True;User ID=Debug;Password=debug" providerName="System.Data.SqlClient"/>
  </connectionStrings>
  <appSettings>
    <add key="Environment" value="debug"/>       
    <add key="EmailFrom" value="debug.mail@mytest.com"/>
    <!--Not transformed-->
    <add key="EmailTo" value="appadmin@mytest.com"/>
  </appSettings>   
</configuration>

显示转换项的生产环境配置文件示例

<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <connectionStrings>
    <add name="AppDBConnection" connectionString="Data Source=AppDbSource;Initial Catalog=AppDb;Persist Security Info=True;User ID=prod;Password=prod" providerName="System.Data.SqlClient" xdt:Transform="SetAttributes(connectionString)" xdt:Locator="Match(name)"/>
  </connectionStrings>
  <appSettings>
    <add key="Environment" value="prod" xdt:Transform="SetAttributes" xdt:Locator="Match(key)" />
    <add key="EmailFrom" value="prod.mail@mytest.com" xdt:Transform="SetAttributes" xdt:Locator="Match(key)" />
  </appSettings>
</configuration>

在安装过程中编辑配置值

常规 WiX 安装程序项目的实现不是本文的重点。读者可以参考各处的 WiX 安装程序文档了解详情。我在此只讨论示例应用程序中使用的安装时配置更新功能。

已配置的项大部分应在部署的目标应用程序中为指定的环境完成。如果某些敏感配置值只能由部署团队在设置过程中更改,该怎么办?生产环境中数据库连接字符串的密码就是一个例子。WiX 安装程序提供功能和 UI,用于在 XML 文件复制到目标位置后修改其中的节点和值。下载的示例安装程序中使用以下步骤,基于 WiX UI 输入编辑连接字符串。

  1. 在 *App.prod.config* 文件中,*Data Source*、*User ID* 和 *Password* 的原始连接字符串中的值可以为空,或在设置过程中可编辑。

    <connectionStrings>
        <add name="AppDBConnection" connectionString="Data Source=AppDbSource;Initial Catalog=AppDb;Persist Security Info=True;User ID=tbd;Password=****" providerName="System.Data.SqlClient" xdt:Transform="SetAttributes(connectionString)" xdt:Locator="Match(name)"/>
    </connectionStrings>
    
  2. 在 *Variables.wxi* 文件中使用 WiX 安装程序变量设置 **Data Source**、**User ID** 和 **Password** 的默认值。

    <?define DbDataSource = "AppDbSource" ?>
    - - - 
    <?if $(var.Configuration) = prod?>  
    <?define DbUserId = "prod" ?>
    <?define DbPassword = "" ?>
    <?endif?>
    
  3. 所有变量都通过在 *Product.wxs* 文件中设置的属性进行访问。

    <Property Id="DB_DATASOURCE" Secure="yes">$(var.DbDataSource)</Property>
    <Property Id="DB_USERID" Secure="yes">$(var.DbUserId)</Property>
    <Property Id="DB_PASSWORD" Secure="yes">$(var.DbPassword)</Property>
    
  4. 添加一个用于 **Data Source**、**User ID** 和 **Password** 值输入字段的对话框。详情请参阅 *Dialogs.wxs* 文件。可编辑的文本框通过 `Control` 节点的 `Property` 属性获取值。

    <Control Id="DbDataSourceEdit" Type="Edit" Property="DB_DATASOURCE" />
    <Control Id="DbUserIdEdit" Type="Edit" Property="DB_USERID" />
    <Control Id="DbPasswordEdit" Type="Edit" Property="DB_PASSWORD" />
    
  5. 设置将由 UI 输入替换的输出 XML 配置属性值。

    <util:XmlConfig Id="AppDbConnString"
    File="[INSTALLFOLDER]$(var.SM.MultiConfigApp.TargetName)$(var.SM.MultiConfigApp.TargetExt).config"
    Action="create" ElementPath="/configuration/connectionStrings/add[\[]@name='AppDBConnection'[\]]"
    Name="connectionString"
    Node="value"
    Value="Data Source=[DB_DATASOURCE]; Initial Catalog=AppDB; Persist Security Info=True; User ID=[DB_USERID]; Password=[DB_PASSWORD]"
    On="install"
    Sequence="1" />
    

输出 MSI 文件的通用位置

输出的 MSI 文件通常在 bin 文件夹下的带有配置名称的文件夹中生成。这对于文件访问很不方便,特别是对于引导程序应用程序将文件捆绑到一个 MSI 包中。可以通过在项目文件的根 `Project` 节点下的 `AfterBuild Target` 中添加 `Copy` 任务,将为特定环境生成的单独 MSI 文件复制到一个通用位置。

<Target Name="AfterBuild">
    <Copy SourceFiles="$(OutputPath)$(OutputName).msi" 
        DestinationFiles="..\..\MsiPackage\$(OutputName).msi" />  
</Target> 

*MsiPackage* 文件夹与解决方案根文件夹平行,并在之前的屏幕截图中显示。

批处理生成

对于具有多重配置的 Visual Studio 解决方案生成,批处理生成选项是生成所有服务器部署应用程序输出文件的简单有效的方法。只需从 Visual Studio 的 **生成** 菜单中单击 **批处理生成…**,然后在 *批处理生成* 对话框窗口中选中列出的项目生成配置。

 

当您在 Visual Studio 中打开下载源代码中的 *SM.MultiConfigApp* 解决方案,并在 *批处理生成* 对话框窗口中单击 **生成** 或 **重新生成** 按钮时,dev、uat 和 prod 配置的三个 MSI 文件将分别位于 *MsiPackage* 文件夹中。如果只需要发送单独的 MSI 文件来部署应用程序到每个环境,这些文件可以直接使用,或者可以由 WiX 引导程序应用程序获取,生成后面几节中描述的捆绑输出安装程序。

使用 WiX 扩展引导程序应用程序

为了最大限度地减少编码工作,我没有创建自己的托管 WiX 引导程序应用程序。WiX Extended Bootstrapper Application (WiX Extended BA) 基本满足了我的需求。引导程序位于实际安装程序之上,并提供单选按钮选择选项。缺点是找不到可由托管 C# 代码调用的安装完成和取消事件处理程序来执行某些自定义操作。要在示例应用程序中使用 WiX Extended BA,需要执行以下步骤:

  • 在 Visual Studio 项目中添加对 *WixBalExtensionExt.dll* 的引用,并在 *Bundle.wxs* 文件中添加命名空间引用,具体操作按照文档说明进行。

  • 复制并将 *Bundle4Theme.xml* 重命名为 *SMBundleTheme.xml*。在 *SMBundleTheme.xml* 和 *Bundle.wxs* 文件中配置单选按钮的 XML 节点和值。

  • 在 *Bundle.wxs* 文件中,将所有配置的 MSI 包文件包含在 `Chain` 节点下,以进行必需的操作。

  • 通过 `InstallCondition` 属性将单独的 MSI 包绑定到单选按钮。例如:

     InstallCondition="RadioButton_prod"        

WiX 引导程序包的代码相当标准,此处不一一列出。您可以在 *SM.MultiConfigApp.Bundle* 项目和其他文档中查看文件以获取详细信息。

捆绑输出的包装器安装程序

WiX 引导程序项目始终输出一个 EXE 文件,该文件无法直接转换为 MSI 文件。如果需要 MSI 文件,可以创建一个额外的安装程序项目作为捆绑 EXE 文件的包装器。*SM.MultiConfigApp.SetupAll* 项目展示了此包装器活动。执行包装器 MSI 文件实际上会将捆绑 EXE 文件安装到临时目录,然后启动自定义操作来运行 EXE 文件。

<!--Set install destination directory-->
<Directory Id="TempFolder">
   <Directory Id="INSTALLFOLDER" Name="~_tmpdir"></Directory>
</Directory>

<!--Set files to be installed-->
<Component Id="BootStrapperComponent" Guid="{AAC1A55F-7C74-4C27-8665-E274D9CDFD83}">
   <File Id="File1" Source="$(var.SM.MultiConfigApp.Bundle.TargetPath)" />
</Component>

<!--Set custom action-->
<CustomAction Id="RunBsExe" FileKey="File1" Return="asyncNoWait" Execute="deferred" ExeCommand="" HideTarget="no" Impersonate="no" />

<!--Set CA running sequence-->
<InstallExecuteSequence>      
   <Custom Action="RunBsExe" After="InstallFiles">NOT Installed</Custom>     
</InstallExecuteSequence>

移除包装器安装程序

MSI 包装器会在目标计算机上创建额外的安装,这对于常规部署场景可能无法接受。为了自动卸载包装器,解决方案中添加了另一个应用程序 *SM.RemoveMsiWrapper*。它的主要任务是调用 *msiexec.exe* 并根据包装器的 GUID 静默卸载包装器应用程序 *SM.MultiConfigApp.SetupAll*。

Process proc = new Process();
proc.StartInfo = new ProcessStartInfo("msiexec.exe", "/x " + WRAPPER_GUID + " /qn " + logParam);
proc.Start();   

这样的 *SM.RemoveMsiWrapper* 项目需要解决以下问题:

  1. 无界面地自动运行。这可以通过创建控制台应用程序项目,然后在项目 **属性** > **应用程序** 页面上将 **输出类型** 设置为 `Windows 应用程序` 来解决。

    请注意,设置“proc.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden”只会隐藏在进程下运行的应用程序(在本例中为 *msiexec.exe*)的用户界面,而不会隐藏 *SM.RemoveMsiWrapper* 应用程序本身的控制台窗口。使用参数“/qn”时,*msiexec.exe* 卸载程序实际上是静默的,没有用户界面。

  2. 使用包装器应用程序的静态 GUID,以便卸载程序可以轻松找到并删除它。这通过在 *Product.wxs* 文件中将 *SM.MultiConfigApp.SetupAll* 项目的 `Product Id` 设置为显式 GUID 值来完成。

    <Product Id="{5029EB25-4652-4F70-9CFD-91F7F81858E9}" 
             Name="SM.MultiConfigApp.SetupAll.Wrapper"
             - - -  >         
       - - -
    </Product>
    
  3. 执行卸载程序。如果 WiX 扩展 BA 的完整和取消处理程序可用于由托管代码调用,则应该没有问题。幸运的是,WiX 扩展 BA 在执行过程中总是在用户的标准临时文件夹中创建一个日志文件。在示例应用程序中,此日志文件用于检测引导程序级别的安装完成或取消操作。读者可以查看 *RemoveMsiWrapper.Program.cs* 文件中的代码以了解处理逻辑,但这里是概要:

    • *RemoveMsiWrapper* 应用程序必须立即开始运行,紧随 WiX 扩展 BA *SM.MultiConfigApp.Bundle* 之前。因此,它可以找到包装器在特定时间戳创建的最新日志文件。这通过包装器应用程序 *SM.MultiConfigApp.SetupAll* 中的执行顺序指定。

      <InstallExecuteSequence>
      <!--Schedule removing MSI wrapper app to run first-->
      <Custom Action="RemoveWrap" After="InstallFiles">NOT Installed</Custom>
      <Custom Action="RunBsExe" After="RemoveWrap">NOT Installed</Custom>     
      </InstallExecuteSequence>
    • *RemoveMsiWrapper* 应用程序将继续运行,并以特定的时间间隔检查日志文件中的值。可以预设日志文件访问间隔和允许的总应用程序运行时间。在示例应用程序中,日志文件访问检查每 2 秒发生一次,应用程序允许运行 90 分钟。业务应用程序安装程序通常运行时间较短,不可能超出此限制。

    • WiX 扩展 BA 的日志文件流在 WiX 扩展 BA 执行期间实际上是保持打开状态的。*RemoveMsiWrapper* 应用程序对文件的任何访问都将被拒绝,直到 WiX 扩展 BA 在安装完成或取消时关闭日志文件。当 *RemoveMsiWrapper* 应用程序打开日志文件并找到文本“exit code:”时,它将启动包装器卸载程序。

    • 包装器卸载程序的操作也通过在 *msiexec.exe* 的“/l*”参数中指定值来记录到文本文件中。因此,包装器删除过程将被跟踪,尽管在目标应用程序安装过程中看不到关于包装器的任何内容。

  4. 由于 UAC 导致的包装器卸载程序权限问题。这导致在 Vista 和 Windows Server 2008 及更高版本的计算机上无法静默“以管理员身份运行”。因此,包装器无法被卸载。示例应用程序使用 *app.manifest* 方法来解决此问题。

    • 通过选择 **项目** > **添加** > **添加新项** > **Visual C# 项** > **应用程序清单文件**,将 *app.manifest* 文件添加到 *RemoveMsiWrapper* 项目。

    • 编辑 *app.manifest* 文件中的两行,如下所示:

      <assemblyIdentity version="1.0.0.0" name="SM.RemoveMsiWrapper"/>
        - - -
      <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
        - - - 
      
    •  在项目 **属性** 页面上,将 **应用程序** > **图标和清单** > **清单** 设置为 *app.manifest* 文件。

    现在,如果目标应用程序安装程序是由属于本地管理员组的用户执行的,则包装器卸载程序将自动静默地以管理员身份运行。

摘要

为业务应用程序制作一个具有多重和选择性配置的单一 MSI Windows 安装程序存在挑战。本文档和示例安装程序为该主题提供了实际解决方案。除了多重配置,还可以将此方法扩展到具有多重平台或其他多重选择性项场景的安装程序。

© . All rights reserved.