Search code examples
c#azure-devopswixwindows-installer

Build MSI in Azure DevOps using Wix Toolkit


I have a Visual Studio solution consisting of two projects; MyProject and MyProject.Installer where the latter is a Wix Toolset project. I do not reference MyProject from the Wix project because I do not want the typical build output, but rather the stand-alone binaries (see pipeline).

I do a dotnet publish and output the files I want to \client relative to the MyProject.Installer wix project. In the Product.wxs file i reference these files:

<Component Id="MyProject.exe" Guid="b1211231-2e88-4679-b2d3-879e8a3f9353">
    <File Id="MyProject.exe" Source="client\MyProject.exe" KeyPath="yes"  />
</Component>

My Azure DevOps pipeline is as follows:

trigger:
- master

pool:
  vmImage: 'windows-latest'
variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'
steps:
- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'
- task: DotNetCoreCLI@2
  inputs:
    command: 'publish'
    publishWebProjects: false
    projects: '**/MyProject.csproj'
    arguments: '--output $(Build.ArtifactStagingDirectory)\client --configuration $(BuildConfiguration) --runtime win-x86 -p:PublishSingleFile=true -p:PublishTrimmed=true'
    zipAfterPublish: false
    modifyOutputPath: false
- task: MSBuild@1
  inputs:
    solution: '**/*.wixproj'
    configuration: '$(BuildConfiguration)'
    msbuildArguments: '/p:RunWixToolsOutOfProc=true'
- task: PublishBuildArtifacts@1
  displayName: 'Save artifacts'
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'drop'
    publishLocation: 'Container'

Output from pipeline run (successful):

"C:\Program Files\dotnet\dotnet.exe" publish D:\a\1\s\MyProject\MyProject.csproj --output D:\a\1\a\client --configuration Release --runtime win-x86 -p:PublishSingleFile=true -p:PublishTrimmed=true

And WIX (unsuccessful):

C:\Program Files (x86)\WiX Toolset v3.11\bin\Light.exe -out D:\a\1\s\MyProject.Installer\bin\Release\nb-NO\MyProject.Installer.msi -pdbout D:\a\1\s\MyProject.Installer\bin\Release\nb-NO\MyProject.Installer.wixpdb -cultures:nb-NO -ext "C:\Program Files (x86)\WiX Toolset v3.11\bin\\WixUIExtension.dll" -loc Product.nb-NO.wxl -contentsfile obj\Release\MyProject.Installer.wixproj.BindContentsFileListnb-NO.txt -outputsfile obj\Release\MyProject.Installer.wixproj.BindOutputsFileListnb-NO.txt -builtoutputsfile obj\Release\MyProject.Installer.wixproj.BindBuiltOutputsFileListnb-NO.txt -wixprojectfile D:\a\1\s\MyProject.Installer\MyProject.Installer.wixproj obj\Release\Product.wixobj
##[error]MyProject.Installer\Product.wxs(108,0): Error LGHT0103: The system cannot find the file 'client\MyProject.exe'.

It's clear that it cannot find the file to embed into the MSI, but I'm at a loss as to how to proceed.

Any advice on how to organize the projects, the output and configuring the pipeline is much appreciated.


Solution

  • My current solution:

    A Azure DevOps Git Repository consisting of one solution with two projects.

    • MyClient
    • MyClient.Installer

    The MyClient project has a local publish profile that publishes the build output to the wix project. This is the publish profile:

    <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
      <PropertyGroup>
        <Configuration>Release</Configuration>
        <Platform>Any CPU</Platform>
        <PublishDir>bin\Release\net5.0\publish\</PublishDir>
        <PublishProtocol>FileSystem</PublishProtocol>
        <TargetFramework>net5.0</TargetFramework>
        <SelfContained>true</SelfContained>
        <WebPublishMethod>FileSystem</WebPublishMethod>
        <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
        <LastUsedPlatform>Any CPU</LastUsedPlatform>
        <SiteUrlToLaunchAfterPublish />
        <LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
        <ExcludeApp_Data>False</ExcludeApp_Data>
        <RuntimeIdentifier>win-x86</RuntimeIdentifier>
        <ProjectGuid>xxxx-xxxx-xxxx</ProjectGuid>
        <publishUrl>..\MyClient.Installer\Client</publishUrl>
        <DeleteExistingFiles>True</DeleteExistingFiles>
        <PublishSingleFile>True</PublishSingleFile>
        <PublishTrimmed>False</PublishTrimmed>
      </PropertyGroup>
    </Project>
    

    This way I can reference the files when building the WIX project.

    My build pipeline in Azure DevOps:

    trigger:
    - master
    
    pool:
      vmImage: 'windows-latest'
    
    variables:
      solution: '**/*.sln'
      buildPlatform: 'Any CPU'
      buildConfiguration: 'Release'
    
    steps:
    
    - task: NuGetCommand@2
      inputs:
        restoreSolution: '$(solution)'
    - task: DotNetCoreCLI@2
      inputs:
        command: 'publish'
        publishWebProjects: false
        projects: '**/MyClient.csproj'
        arguments: '--output $(Build.ArtifactStagingDirectory)\client --configuration $(BuildConfiguration) --runtime win-x86 -p:PublishSingleFile=true -p:PublishTrimmed=true'
        zipAfterPublish: false
        modifyOutputPath: false
    - task: CopyFiles@2
      inputs:
        SourceFolder: 'MyClient.Installer'
        Contents: '**'
        TargetFolder: '$(Build.ArtifactStagingDirectory)'
    - task: PublishBuildArtifacts@1
      displayName: 'Save artifacts'
      inputs:
        PathtoPublish: '$(Build.ArtifactStagingDirectory)'
        ArtifactName: 'drop'
        publishLocation: 'Container'
    

    Here I publish the files, and I copy the WIX project to the artifact folder as well. Now the artifact contains the wix project and the published output - which is saved to the \client folder, just as it was locally.

    Having the artifact available I can now create separate MSI's for different environments, adding config transformations prior to building the MSI's.