powershell-wix.psm1

#Requires -Version 3.0
Function Copy-WixSourceFiles {
  [Cmdletbinding()]
  Param(
    [Parameter(Mandatory=$true,Position = 0)]  [string] $Source,
    [Parameter(Mandatory=$true,Position = 1)]  [string] $Destination,
    [Parameter(Mandatory=$false)]  [string[]] $Exclude
   )
   New-Item $Destination -ItemType directory -Force | Out-Null
   $objects = Get-ChildItem $Source -Force -Exclude $exclude
   foreach ($object in $objects) {
     if ($object.Attributes -contains 'Directory') {
       Copy-WixSourceFiles $object.Fullname (Join-path $Destination $object.Name) -Exclude $Exclude
     }
     else {
       Copy-Item $object.FullName $Destination
     }
   }
 }

Function Get-WixAbsolutePath ($Path){
  $Path = [System.IO.Path]::Combine( ((pwd).Path), ($Path) );
  $Path = [System.IO.Path]::GetFullPath($Path);
  return $Path;
}

Function Get-WixNeutralPath ($Path){
  if ($Env:OS -eq "Windows_NT") { Return $Path }
  # If we are not using Windows, assume we are using Wine and translate Unix paths
  else { return ($Path -replace "^/", "Z:\" -replace "/","\") }
}

Function ConvertTo-WixNeutralString ($Text) {
  $changes = New-Object System.Collections.Hashtable
  $changes.'ß' = 'ss'
  $changes.'Ä' = 'Ae'
  $changes.'ä' = 'ae'
  $changes.'Ü' = 'Ue'
  $changes.'ü' = 'ue'
  $changes.'Ö' = 'Oe'
  $changes.'ö' = 'oe'
  $changes.' ' = '_'
  $changes.'-' = '_'
  Foreach ($key in $changes.Keys) {
    $text = $text.Replace($key, $changes.$key)
  }
  $text
}

Function New-WixUid {
  function New-RandomHexByte {
    "{0:X2}" -f (Get-Random -Minimum 0 -Maximum 255)
  }
    New-Alias -Name nrhb -Value New-RandomHexByte
    ((nrhb),(nrhb),(nrhb),(nrhb),
    "-",
    (nrhb),(nrhb),
    "-",
    (nrhb),(nrhb),
    "-",
    (nrhb),(nrhb),
    "-",
    (nrhb),(nrhb),(nrhb),(nrhb),(nrhb),(nrhb)) -join ''
}

# .ExternalHelp powershell-wix-help.xml
Function Get-WixLocalConfig {
  [Cmdletbinding()]
  Param(
    [Parameter(Mandatory=$false)]  [string] $Path = (Get-Location).Path,
    [Parameter(Mandatory=$false)]  [switch] $ProductShortName,
    [Parameter(Mandatory=$false)]  [switch] $ProductName,
    [Parameter(Mandatory=$false)]  [switch] $ProductVersion,
    [Parameter(Mandatory=$false)]  [switch] $Manufacturer,
    [Parameter(Mandatory=$false)]  [switch] $HelpLink,
    [Parameter(Mandatory=$false)]  [switch] $AboutLink,
    [Parameter(Mandatory=$false)]  [switch] $UpgradeCodeX86,
    [Parameter(Mandatory=$false)]  [switch] $UpgradeCodeX64
  ) #end Param
  $file = Get-WixAbsolutePath((Join-Path $Path '.wix.json'))
  $leaf = Split-Path $Path -Leaf
  $defaults = @{'ProductShortName' = $leaf;
                'ProductName' = $leaf;
                'ProductVersion' = '1.0.0';
                'Manufacturer' = $leaf;
                'HelpLink' = "http://www.google.com/q=${leaf}";
                'AboutLink' = "http://www.google.com/q=${leaf}";
                'UpgradeCodeX86' = (New-WixUid);
                'UpgradeCodeX64' = (New-WixUid)}
  $settings = New-Object -TypeName PSCustomObject
  $readSettings = New-Object -TypeName PSCustomObject
  $params = $PSBoundParameters.GetEnumerator()|
            Where-Object {($_.Key -ne 'Path')}

  # Make sure we have persistent upgrade codes
  if (Test-Path $file){
    try {
      $readSettings = Get-Content -Raw $file | ConvertFrom-Json
    } catch {}
  }
  If (!$readSettings.UpgradeCodeX86 -or !$readSettings.UpgradeCodeX64){
    If (!$readSettings.UpgradeCodeX86){
      Add-Member -InputObject $readSettings -MemberType NoteProperty `
                     -Name UpgradeCodeX86 -Value (New-WixUid)
    }
    If (!$readSettings.UpgradeCode64){
      Add-Member -InputObject $readSettings -MemberType NoteProperty `
                     -Name UpgradeCodeX64 -Value (New-WixUid)
    }
    #$readsettings
    $null = (New-Item -ItemType Directory -Force -Path (Split-Path $file))
    $readSettings | ConvertTo-JSON | Out-File -Encoding utf8 $file
  }

  if (Test-Path $file){
    try {
      $readSettings = Get-Content -Raw $file | ConvertFrom-Json
    } catch {}
  }
  foreach ($parameter in $params){
    $setting = $parameter.Key.ToLower()
    $value = $parameter.Value
    if ($value){
      if ($readSettings.$setting) {
        Add-Member -InputObject $settings -MemberType NoteProperty `
                   -Name $setting -Value $readSettings.$setting
      }
      elseif ($defaults.$setting) {
        Add-Member -InputObject $settings -MemberType NoteProperty `
                   -Name $setting -Value $defaults.$setting}
      else {
       Add-Member -InputObject $settings -MemberType NoteProperty `
                  -Name $setting -Value (Read-Host "$setting")
      }
    }
  }
  if ($params.count -eq 0){
    foreach ($default in $defaults.GetEnumerator()){
      $setting = $default.Name
      $value = $default.Value
      Add-Member -InputObject $settings -MemberType NoteProperty `
                 -Name $setting -Value $value -Force
    }
    $readSettings.PSObject.Properties |
    foreach-object {
      $setting = $_.Name
      $value = $_.Value
      Add-Member -InputObject $settings -MemberType NoteProperty `
                 -Name $setting -Value $value -Force
    }
  }
  Return $settings
} #end Function Get-WixLocalConfig
Export-ModuleMember -Function Get-WixLocalConfig

# .ExternalHelp powershell-wix-help.xml
Function Set-WixLocalConfig {
  [Cmdletbinding()]
  Param(
    [Parameter(Mandatory=$false)]        [string]  $Path = (Get-Location).Path,
    [Parameter(Mandatory=$false)]        [switch]  $Replace,
    [Parameter(Mandatory=$true,
               Position=0,
               ValueFromPipeline=$true,
               ParameterSetName="Object")] [object]  $Settings,
    [Parameter(Mandatory=$false, ParameterSetName="Strings")][string] $ProductShortName,
    [Parameter(Mandatory=$false, ParameterSetName="Strings")][string] $ProductName,
    [Parameter(Mandatory=$false, ParameterSetName="Strings")][string] $ProductVersion,
    [Parameter(Mandatory=$false, ParameterSetName="Strings")][string] $Manufacturer,
    [Parameter(Mandatory=$false, ParameterSetName="Strings")][string] $HelpLink,
    [Parameter(Mandatory=$false, ParameterSetName="Strings")][string] $AboutLink,
    [Parameter(Mandatory=$false, ParameterSetName="Strings")][string] $UpgradeCodeX86,
    [Parameter(Mandatory=$false, ParameterSetName="Strings")][string] $UpgradeCodeX64
  ) #end Param
  $file = Get-WixAbsolutePath((Join-Path $Path '.wix.json'))
  If ($Settings){
    $newSettings = New-Object -TypeName PSCustomObject
      if (!$Replace){
      $readSettings = Get-WixLocalConfig -Path $Path
      $readSettings.PSObject.Properties |
      foreach-object {
        Add-Member -InputObject $newSettings -MemberType NoteProperty `
                    -Name $_.Name -Value $_.Value
      }
    }
    $Settings.PSObject.Properties |
    foreach-object {
      $setting = $_.Name
      $value = $_.Value
      Add-Member -InputObject $newSettings -MemberType NoteProperty `
                 -Name $setting -Value $value -Force

    }
    $null = (New-Item -ItemType Directory -Force -Path (Split-Path $file))
    $newSettings | ConvertTo-JSON | Out-File -Encoding utf8 $file
    Get-WixLocalConfig -Path $Path
  }
  else {
    $params = $PSBoundParameters.GetEnumerator()|
              Where-Object {($_.Key -ne 'Path' -and
                             $_.Key -ne 'Settings' -and
                             $_.Key -ne 'Replace')}
    $Settings = New-Object -TypeName PSCustomObject
    foreach ($parameter in $params){
      $setting = $parameter.Key
      $value = $parameter.Value
        if ($value){
          Add-Member -InputObject $Settings -MemberType NoteProperty `
                     -Name $setting -Value $value
        }
    }
    Set-WixLocalConfig -Path $Path -Settings $Settings -Replace:$Replace
  }
} #end Function Set-WixLocalConfig
Export-ModuleMember -Function Set-WixLocalConfig

# .ExternalHelp powershell-wix-help.xml
Function Start-WixBuild {
  [Cmdletbinding()]
  Param(
    [Parameter(Mandatory=$false,Position=0)]  [string] $Path = (Get-Location).Path,
    [Parameter(Mandatory=$false)]  [string[]] $Exclude = @('.git','.gitignore','*.msi'),
    [Parameter(Mandatory=$false)]  [string] $OutputFolder = (Get-Location).Path,
    [Parameter(Mandatory=$false)]  [string] $LicenseFile = "$Path\license.rtf",
    [Parameter(Mandatory=$false)]  [string] $IconFile = "$Path\icon.ico",
    [Parameter(Mandatory=$false)]  [string] $BannerFile = "$Path\banner.bmp",
    [Parameter(Mandatory=$false)]  [string] $DialogFile = "$Path\dialog.bmp",
    [Parameter(Mandatory=$false)]  [string] $ProductShortName = (Get-WiXLocalConfig -ProductShortName -Path $Path).ProductShortName,
    [Parameter(Mandatory=$false)]  [string] $ProductName = (Get-WiXLocalConfig -ProductName -Path $Path).ProductName,
    [Parameter(Mandatory=$false)]  [string] $ProductVersion = (Get-WiXLocalConfig -ProductVersion -Path $Path).ProductVersion,
    [Parameter(Mandatory=$false)]  [string] $Manufacturer = (Get-WiXLocalConfig -Manufacturer -Path $Path).Manufacturer,
    [Parameter(Mandatory=$false)]  [string] $HelpLink = (Get-WiXLocalConfig -HelpLink -Path $Path).HelpLink,
    [Parameter(Mandatory=$false)]  [string] $AboutLink = (Get-WiXLocalConfig -AboutLink -Path $Path).AboutLink,
    [Parameter(Mandatory=$false)]  [string] $UpgradeCodeX86 = (Get-WiXLocalConfig -UpgradeCodeX86 -Path $Path).UpgradeCodeX86,
    [Parameter(Mandatory=$false)]  [string] $UpgradeCodeX64 = (Get-WiXLocalConfig -UpgradeCodeX64 -Path $Path).UpgradeCodeX64,
    [Parameter(Mandatory=$false)]  [int]    $Increment = 3,
    [Parameter(Mandatory=$false)]  [switch] $NoX86,
    [Parameter(Mandatory=$false)]  [switch] $NoX64
  )
  # Increment version number if requested
  If ($Increment -gt 0) {
    $versionArray = $ProductVersion.split(".")
    If ($Increment -gt $versionArray.length) {
      $extraDigits = $Increment - $versionArray.length
      for ($i=0;$i -lt $extraDigits-1; $i++){
        $versionArray += "0"
      }
      $versionArray += "1"
    }
    else {
      $versionArray[$Increment - 1] = [string]([int]($versionArray[$Increment - 1]) + 1)
    }
    $NewProductVersion = $versionArray -Join "."
    Set-WixLocalConfig -ProductVersion $NewProductVersion -Path $Path | Out-Null
  }

  # MSI IDs
  $productId = ConvertTo-WixNeutralString($ProductShortName)

  # Date and time
  $timeStamp = (Get-Date -format yyyyMMddHHmmss)

  # WiX paths
  If ($Env:OS -eq "Windows_NT") {
    If ((Get-ChildItem -Path 'C:\Program Files*\WiX*\' -Filter heat.exe -Recurse)){
      $wixDir = Split-Path ((((Get-ChildItem -Path 'C:\Program Files (x86)\WiX*\' -Filter heat.exe -Recurse) | Select-Object FullName)[0]).FullName)
      $heatExe = Join-Path $wixDir "heat.exe"
      $candleExe = Join-Path $wixDir "candle.exe"
      $lightExe = Join-Path $wixDir "light.exe"
    }
    Else {
      Throw "Please install WiX Toolset"
      Return
    }

  }
  Else {
    If ((Get-ChildItem -Path ($env:PATH -split ':')  -Filter heat -Recurse)){
      $wixDir = Split-Path ((((Get-ChildItem -Path ($env:PATH -split ':')  -Filter heat -Recurse) | Select-Object FullName)[0]).FullName)
      $heatExe = Join-Path $wixDir "heat"
      $candleExe = Join-Path $wixDir "candle"
      $lightExe = Join-Path $wixDir "light"
    }
    Else {
      Throw "Please install WiX Toolset"
      Return
    }
  }


  # Other paths
  $thisModuleName = ConvertTo-WixNeutralString($MyInvocation.MyCommand.ModuleName)
  If ($Env:OS -eq "Windows_NT") { $tmpDirGlobalRoot = Join-Path $Env:TMP $thisModuleName }
  Else { $tmpDirGlobalRoot = Join-Path '/tmp' $thisModuleName }
  $tmpDirThisRoot = Join-Path $tmpDirGlobalRoot $productId
  $tmpDir = Join-Path $tmpDirThisRoot $timeStamp


  $varName = "var." + $productId
  $oldMsi = Join-Path $OutputFolder ($productID + '*' + ".msi")
  $cabFileName = $productId + ".msi"

  $moduleIconFile = Join-Path $PSScriptRoot "icon.ico"
  $moduleBannerFile = Join-Path $PSScriptRoot "banner.bmp"
  $moduleDialogFile = Join-Path $PSScriptRoot "dialog.bmp"

  $tmpIconFile = Join-Path $tmpDir "icon.ico"
  $tmpBannerFile = Join-Path $tmpDir "banner.bmp"
  $tmpDialogFile = Join-Path $tmpDir "dialog.bmp"

  # MSI IDs
  $productId = ConvertTo-WixNeutralString($ProductShortName)

  # Create tmp folder
  if (test-path $tmpDir) {
    Remove-Item $tmpDir -Recurse
  }
  New-Item $tmpDir -ItemType directory | Out-Null

  # Copy Files to tmp dir
  $tmpSourceDir = Join-Path $tmpDir "files"
  Copy-WixSourceFiles $Path $tmpSourceDir -Exclude $Exclude

  # Add license
  if (test-path $LicenseFile) {
    $templateLicenseFile = Get-WixNeutralPath $LicenseFile
    $licenseCmd = @"
<WixVariable Id="WixUILicenseRtf" Value="$templateLicenseFile"></WixVariable>
"@

  }
  # Add icon
  if (test-path $IconFile) {
     Copy-Item $IconFile $tmpIconFile
  }
  elseif (test-path $moduleIconFile){
    Copy-Item $moduleIconFile $tmpIconFile
  }
  if (test-path $tmpIconFile) {
    $templateIconFile = Get-WixNeutralPath $tmpIconFile
    $iconCmd = @"
<Icon Id="icon.ico" SourceFile="$templateIconFile"/>
<Property Id="ARPPRODUCTICON" Value="icon.ico" />
"@

  }
  # Add banner graphic
  if (test-path $BannerFile) {
     Copy-Item $BannerFile $tmpBannerFile
  }
  elseif (test-path $moduleBannerFile){
    Copy-Item $moduleBannerFile $tmpBannerFile
  }
  if (test-path $tmpBannerFile) {
    $templateBannerFile = Get-WixNeutralPath $tmpBannerFile
    $bannerCmd = @"
<WixVariable Id="WixUIBannerBmp" Value="$templateBannerFile"></WixVariable>
"@

  }
  # Add dialog graphic
  if (test-path $DialogFile) {
     Copy-Item $DialogFile $tmpDialogFile
  }
  elseif (test-path $moduleDialogFile){
    Copy-Item $moduleDialogFile $tmpDialogFile
  }
  if (test-path $tmpDialogFile) {
    $templateDialogFile = Get-WixNeutralPath $tmpDialogFile
    $dialogCmd = @"
<WixVariable Id="WixUIDialogBmp" Value="$templateDialogFile"></WixVariable>
"@

  }

  # Platform settings
  $platforms = @()

  $x86Settings = @{ 'arch' = 'x86';
                    'sysFolder' = 'SystemFolder';
                    'progfolder' = 'ProgramFilesFolder';
                    'upgradeCode' = $UpgradeCodeX86;
                    'productName' = "${ProductName} (x86)";
                    'outputMsi' = (Join-Path $OutputFolder ($productID + "_" + $ProductVersion + "_x86.msi"))}
  $x64Settings = @{ 'arch' = 'x64';
                    'sysFolder' = 'System64Folder';
                    'progfolder' = 'ProgramFiles64Folder';
                    'upgradeCode' = $UpgradeCodeX64;
                    'productName' = "${ProductName} (x64)";
                    'outputMsi' = (Join-Path $OutputFolder ($productID + "_" + $ProductVersion + "_x64.msi"))                    }

  If (!$Nox86) {
    $platforms += $x86Settings
  }
  If (!$Nox64) {
    $platforms += $x64Settings
  }

  # Remove existing MSIs
  # Remove-Item $oldMsi

  # Do the build
  foreach ($platform in $platforms) {
    $platformArch = $platform.arch
    $platformUpgradeCode = $platform.upgradeCode
    $platformSysFolder = $platform.sysFolder
    $platformProgFolder = $platform.progFolder
    $platformProductName = $platform.productName


    $modulesWxs = Join-Path $tmpDir "_modules${platformArch}.wxs"
    $productWxs = Join-Path $tmpDir ".wxs${platformArch}"
    $productWxs2 = Join-Path $tmpDir ".wxs${platformArch}_2"
    $modulesWixobj = Join-Path $tmpDir "_modules${platformArch}.wixobj"
    $productWixobj = Join-Path $tmpDir ".wixobj${platformArch}"
    $productWixobj2 = Join-Path $tmpDir ".wixobj${platformArch}_2"
    $productPdb = Join-Path $tmpDir ($productID + ".wizpdb${platformArch}")

    # Build XML
    $wixXml = [xml] @"
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
  <Product Id="*" Language="1033" Name="$platformProductName" Version="$ProductVersion"
           Manufacturer="$Manufacturer" UpgradeCode="$platformUpgradeCode" >
 
    <Package Id="*" Description="$platformProductName Installer"
             InstallPrivileges="elevated" Comments="$ProductShortName Installer"
             InstallerVersion="200" Compressed="yes" Platform="$platformArch">
    </Package>
    $iconCmd
    <Upgrade Id="$platformUpgradeCode">
      <!-- Detect any newer version of this product -->
      <UpgradeVersion Minimum="$ProductVersion" IncludeMinimum="no" OnlyDetect="yes"
                      Language="1033" Property="NEWPRODUCTFOUND" />
 
      <!-- Detect and remove any older version of this product -->
      <UpgradeVersion Maximum="$ProductVersion" IncludeMaximum="yes" OnlyDetect="no"
                      Language="1033" Property="OLDPRODUCTFOUND" />
    </Upgrade>
 
    <!-- Define a custom action -->
    <CustomAction Id="PreventDowngrading"
                  Error="Newer version already installed." />
 
    <InstallExecuteSequence>
      <!-- Prevent downgrading -->
      <Custom Action="PreventDowngrading" After="FindRelatedProducts">
        NEWPRODUCTFOUND
      </Custom>
      <RemoveExistingProducts After="InstallFinalize" />
    </InstallExecuteSequence>
 
    <InstallUISequence>
      <!-- Prevent downgrading -->
      <Custom Action="PreventDowngrading" After="FindRelatedProducts">
        NEWPRODUCTFOUND
      </Custom>
    </InstallUISequence>
 
    <Media Id="1" Cabinet="$cabFileName" EmbedCab="yes"></Media>
    $licenseCmd
    $bannerCmd
    $dialogCmd
    <Directory Id="TARGETDIR" Name="SourceDir">
      <Directory Id="$platformProgFolder" Name="$platformProgFolder">
        <Directory Id="WindowsPowerShell" Name="WindowsPowerShell">
          <Directory Id="INSTALLDIR" Name="Modules">
            <Directory Id="$ProductId" Name="$ProductShortName">
              <Directory Id="VERSIONDIR" Name="$ProductVersion">
              </Directory>
            </Directory>
          </Directory>
        </Directory>
        <Directory Id="PowerShell" Name="PowerShell">
          <Directory Id="INSTALLDIRCORE" Name="Modules">
            <Directory Id="${ProductId}_core" Name="$ProductShortName">
              <Directory Id="VERSIONDIRCORE" Name="$ProductVersion">
              </Directory>
            </Directory>
          </Directory>
        </Directory>
      </Directory>
    </Directory>
    <Property Id="ARPHELPLINK" Value="$HelpLink"></Property>
    <Property Id="ARPURLINFOABOUT" Value="$AboutLink"></Property>
    <Feature Id="$ProductId" Title="$ProductShortName" Level="1">
 
      <Feature Id="${ProductId}_windows" Title="${ProductShortName}_windows" Level="1"
               ConfigurableDirectory="INSTALLDIR">
        <ComponentGroupRef Id="VERSIONDIR">
        </ComponentGroupRef>
      </Feature>
 
      <Feature Id="${ProductId}_core" Title="${ProductShortName}_core" Level="1"
               ConfigurableDirectory="INSTALLDIRCORE">
        <ComponentGroupRef Id="VERSIONDIRCORE">
        </ComponentGroupRef>
        </Feature>
 
    </Feature>
    <UI></UI>
    <UIRef Id="WixUI_InstallDir"></UIRef>
    <Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR"></Property>
  </Product>
</Wix>
"@


    # Save XML and create productWxs
    $wixXml.Save($modulesWxs)
    &$heatExe dir $tmpSourceDir -nologo -sfrag -sw5151 -ag -srd -gg -dir $productId -out $productWxs -cg VERSIONDIR -dr VERSIONDIR | Out-Null
    &$heatExe dir $tmpSourceDir -nologo -sfrag -sw5151 -ag -srd -gg -dir ${productId}_core -out $productWxs2 -cg VERSIONDIRCORE -dr VERSIONDIRCORE | Out-Null

    # Produce wixobj files
    &$candleexe $modulesWxs -out $modulesWixobj | Out-Null
    &$candleexe $productWxs -out $productWixobj | Out-Null
    &$candleexe $productWxs2 -out $productWixobj2 | Out-Null
  }
  foreach ($platform in $platforms) {
    $platformArch = $platform.arch
    $modulesWixobj = Join-Path $tmpDir "_modules${platformArch}.wixobj"
    $productWixobj = Join-Path $tmpDir ".wixobj${platformArch}"
    $productWixobj2 = Join-Path $tmpDir ".wixobj${platformArch}_2"
    $platformOutputMsi = $platform.outputMsi

    # Produce the MSI file
    &$lightexe -sval -sw1076 -spdb -ext WixUIExtension -out $platformOutputMsi $modulesWixobj $productWixobj $productWixobj2 -b $tmpSourceDir -sice:ICE91 -sice:ICE69 -sice:ICE38 -sice:ICE57 -sice:ICE64 -sice:ICE204 -sice:ICE80 | Out-Null
  }
  # Remove tmp dir
  Remove-Item $tmpDir -Recurse -Force
} #end Start-WixBuild
Export-ModuleMember -Function Start-WixBuild