Creating Live, Test, Training, ... versions using the same source files.
In a nutshell: Create unique UpgradeCode for each installer and automagically define the first character of each Guid for each installer, leaving the remaining 31 unique.
Prerequisites
Assumptions
- WiX variables are used to define UpgradeCode, ProductName, InstallName.
- You already have a working installer. I wouldn't attempt this until you do.
- All your Components are kept in one file (Components.wxs). This process will work if you have multiple files, there will just be more work to do.
Directory Structure
- Setup.Library
- All wxs files (Components, Features, UI Dialogs, ...)
- Common.Config.wxi (ProductCode="*", ProductVersion, PlatformProgramFilesFolder, ...)
- Setup.Live (wixproj)
- Link all Setup.Library files using "Add Existing File" -> "Add As Link" (the little down arrow button right next to the Add button in Visual Studio)
- Config.wxi (has unique UpgradeCode, ProductName, InstallName, ...)
- Setup.Test, ...
- as per live but Config.wxi is configured for Test environment.
Process
- Create Setup.Library directory and move all your wxs and wxi files (except Config.wxi) from existing project.
- Create Setup.Live, Setup.Test, etc as per normal wixproj.
- Add BeforeBuild target in wixproj in Setup.Live, etc to perform MSBuild Community Task FileUpdate to modify Guids (I used A for Live, B for Test and C for training)
- Add AfterBuild target to revert Components.wxs Guids back to 0.
- Verify with Orca that each component in each MSI has the modified guid.
- Verify that original guids are restored.
- Verify that each MSI is installing (and upgrading) correct product and location.
Example 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>
Example 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>
Example 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">
...
Note: I would now suggest leaving the Guid attribute out of Component (equivalent of *
), using one file per component and setting the file as the keypath. This removes the need for calling ModifyComponentsGuids
and RevertComponentsGuids
targets shown below. This might not be possible for all your components though.
Example 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>
Final thoughts
- This process should also work for creating different installers for different merge modules (Live, Test, ... as features) for the same installer. I went with different installers as it seemed a safer option, there is more risk that someone might upgrade Live instead of Training if they're on the same box and you just use features for the different merge modules.
- If you use your MSI to perform upgrades as well as new installs i.e. the major upgrade only approach, and you save your installation location in the registry, remember to create a variable for the key name for each install.
- We also create variables in each Config.wxi to enable unique virtual directory names, application pools, database names, et cetera for each installer.
UPDATE 1: Auto-generating component Guids removes the need for calling FileUpdate task if you create component with Guid="*" for each file, setting the file as the keypath.
UPDATE 2: One of the issues we've come up against is if you don't auto-generate your component Guid's and the build fails, then the temp files need to be manually deleted.
UPDATE 3: Found a way to remove reliance on svn:externals and temporary file creation. This makes the build process more resilient (and is best option if you can't wildcard your Guids) and less brittle if there is a build failure in light or candle.
UPDATE 4: Support for Multiple Instances using instance transforms is in WiX 3.0+, definitely also worth a look.