TeamCity building MSP files - wix

For background: I've got quite a nice TeamCity setup; containing a ci build and a release build which uses WiX to build my installers and patch all the version numbers. When I do a new release build, I'd like to automatically create MSP patches against a previous set of installers. I'm thinking either tagged RTM in TeamCity, or as a list of version numbers.
The approach I'm leaning towards is creating a separate config and getting the msi artifacts of all the previous builds that fit the criteria (tag or version number). Tag would seem a lot neater, but I can't see anything in the documentation about how you use it?
I've got a script to build the MSP patch, but it relies on a PCP file which needs to be edited in ORCA to describe the patch.
In terms of editing the PCP, is there anything else I can use other than the ORCA to edit? I've been looking at moving to the WiX method here: http://wix.sourceforge.net/manual-wix3/patch_building.htm which looks promising.
Does anyone know if you can access artifacts in TeamCity by Tag in the same or another build?
Does anyone have any other insights into automatically building/chaining MSP patch files in TeamCity?

You can build the .PCP file using the PatchCreation element in the WiX toolset. That will probably give you the necessary flexiblity necessary to create the customized .PCP files.
Sorry, don't use TeamCity.
Sorry, don't use TeamCity. :)

To add to Rob's answer:
#2. TeamCity can retrieve items by tag:
http://servername:8080/httpAuth/app/rest/buildTypes/id:bt13/builds?status=SUCCESS&tag=RTM
#3. I used the PatchCreation element (Rob suggested above) in the WiX toolset and he's right its flexible enough for this. Here is an outline of what I've built, it all seems to work quite well in testing,
The teamcity project has a number of build parameters, they are:
New version number - default as changeme, so if its not been changed it breaks the build.
Old version number - as above
New build repo - this is the buildtypeid, look at querystring for your project and it will have buildTypeId=btXX. The XX is the number that should be supplied here.
Old build repo - as above
The teamcity project has the following steps:
MSBuild runner to run build.msbuild (see below)
Run Candle on the patch.wxs to create a patch.wixobj file
Run Light on patch.wixobj to create a patch.pcp
Unpack new version (command: msiexec /q /a new.msi) -
Unpack old version (command: msiexec /q /a old.msi) - choose a different working dir
Create patch (command: msimsp -s patch.pcp p hotfix-%system.msiOldVersion%-%system.msiNewVersion%.msp -l patch.log
MSBuild to Create patch.pcp
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!--<Import Project="references\MSBuild.Community.Tasks.Targets"/>-->
<UsingTask AssemblyFile="references\MSBuild.Community.Tasks.dll" TaskName="WebDownload"/>
<UsingTask AssemblyFile="references\MSBuild.Community.Tasks.dll" TaskName="TemplateFile"/>
<Target Name="Build">
<!-- preconditions for build -->
<Error Condition="'$(msiOldVersion)' == 'changeme'" Text="Use run custom build, setting the client version of the msi"/>
<Error Condition="'$(msiOldVersion)' == ''" Text="Use run custom build, setting the client version of the msi"/>
<Error Condition="'$(msiNewVersion)' == 'changeme'" Text="Use run custom build, setting the new version of the msi"/>
<Error Condition="'$(msiNewVersion)' == ''" Text="Use run custom build, setting the new version of the msi"/>
<Message Text="Old Version: $(msiOldVersion)"/>
<Message Text="New version: $(msiNewVersion)"/>
<!-- download files from teamcity... -->
<WebDownload FileUri="http://server:8080/httpAuth/repository/download/bt$(msiOldRepo)/trunk/Path/bin/Release/en-us/installer-v-v.$(msiOldVersion).msi" UserName="download" Password="abcdefgh" FileName="downloads/oldversion.msi" />
<WebDownload FileUri="http://server:8080/httpAuth/repository/download/bt$(msiNewRepo)/trunk/Path/bin/Release/en-us/installer-v.$(msiNewVersion).msi" UserName="download" Password="abcdefgh" FileName="downloads/newversion.msi" />
<!-- fill in blanks in patch.wxs -->
<ItemGroup>
<Tokens Include="oldVersion">
<ReplacementValue>$(msiOldVersion)</ReplacementValue>
</Tokens>
<Tokens Include="newVersion">
<ReplacementValue>$(msiNewVersion)</ReplacementValue>
</Tokens>
</ItemGroup>
<TemplateFile Template="template.wxs" OutputFileName="patch.wxs" Tokens="#(Tokens)"/>
</Target>
Template.wxs used by MSBuild script
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<PatchCreation
Id="deadbeef-dead-beef-dead-beefdeadbeef"
CleanWorkingFolder="no"
OutputPath="patch.pcp"
WholeFilesOnly="no">
<PatchInformation
Description="Small Update Patch"
Comments="Small Update Patch"
Manufacturer="Your Manufacturer"/>
<PatchMetadata
AllowRemoval="yes"
Description="Hotfix"
ManufacturerName="Your Manufacturer"
MoreInfoURL="http://yourwebsite.com"
TargetProductName="Your Product Name"
Classification="Hotfix"
DisplayName="Hotfix - TBC"/>
<Family DiskId="5000"
MediaSrcProp="Sample"
Name="Sample"
SequenceStart="5000">
<UpgradeImage SourceFile="downloads\newunpack\newVersion.msi" Id="SampleUpgrade">
<TargetImage SourceFile="downloads\oldunpack\oldVersion.msi" Order="2"
Id="SampleTarget" IgnoreMissingFiles="no" />
</UpgradeImage>
</Family>
<PatchSequence PatchFamily="SamplePatchFamily"
Supersede="yes" />
</PatchCreation>
</Wix>

Related

How are we supposed to execute package build targets in the new world where nuget packages are consumed through msbuild PackageReference?

I am developing a suite of UI tests using Selenium. One of the run-time dependencies of this suite is the chromedriver.exe, which we are expected to consume through the Selenium.WebDriver.ChromeDriver NuGet package.
The old world
When this NuGet package is imported the following lines are injected into the csproj file:
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\Selenium.WebDriver.ChromeDriver.2.44.0\build\Selenium.WebDriver.ChromeDriver.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Selenium.WebDriver.ChromeDriver.2.44.0\build\Selenium.WebDriver.ChromeDriver.targets'))" />
</Target>
<Import Project="..\packages\Selenium.WebDriver.ChromeDriver.2.44.0\build\Selenium.WebDriver.ChromeDriver.targets" Condition="Exists('..\packages\Selenium.WebDriver.ChromeDriver.2.44.0\build\Selenium.WebDriver.ChromeDriver.targets')" />
And it is automatic by the Visual Studio. This covers our bases, making sure the build targets provided by the Selenium.WebDriver.ChromeDriver package are there at the time of the build and running them as needed. The logic inside the build targets file copies/publishes the chromedriver.exe to the right location.
All is green.
The new world.
I consume the same NuGet package as PackageReference in the csproj file. Cool. However, the build targets of that package are no longer executed. See https://github.com/NuGet/Home/issues/4013. Apparently, this is by design.
I could import the targets manually, but the problem is that I will have to hard code the location where the package is restored. It is no longer restored in the packages directory in the solution, but under my windows profile. But there is no property pointing to this location and hard coding it sucks.
So, here is the version that works for me and I hate it:
<PropertyGroup>
<MyPackagesPath>$(UserProfile)\.nuget\packages\</MyPackagesPath>
<SeleniumWebDriverChromeDriverTargets>$(MyPackagesPath)selenium.webdriver.chromedriver\2.44.0\build\Selenium.WebDriver.ChromeDriver.targets</SeleniumWebDriverChromeDriverTargets>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="2.44.0" />
</ItemGroup>
<Target Name="EnsureChromeDriver" AfterTargets="PrepareForRun">
<Error Text="chrome driver is missing!" Condition="!Exists('$(OutDir)chromedriver.exe')" />
</Target>
<Import Project="$(SeleniumWebDriverChromeDriverTargets)" Condition="Exists('$(SeleniumWebDriverChromeDriverTargets)') And '$(ExcludeRestorePackageImports)' == 'true'" />
Overall, the Sdk style projects are absolutely great, but this whole business of running targets from the packages is totally broken, even if it is by design.
What am I missing?
EDIT 1
So, here is the content of the generated obj\UITests.csproj.nuget.g.targets:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(NuGetPackageRoot)selenium.webdriver.chromedriver\2.44.0\build\Selenium.WebDriver.ChromeDriver.targets" Condition="Exists('$(NuGetPackageRoot)selenium.webdriver.chromedriver\2.44.0\build\Selenium.WebDriver.ChromeDriver.targets')" />
</ImportGroup>
</Project>
Notice the ImportGroup condition is '$(ExcludeRestorePackageImports)' != 'true'. Now, this condition is always false, because ExcludeRestorePackageImports seems to be hard coded to be true in
c:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\CommonExtensions\Microsoft\NuGet\NuGet.targets
Inspecting binary log confirms this. Plus https://github.com/NuGet/Home/issues/4013 was closed as WontFix.
Or am I still missing something?
If you are running Restore and other targets during the build, you may get unexpected results due to NuGet modifying xml files on disk or because MSBuild files imported by NuGet packages aren't imported correctly.

Include all dependencies using dotnet pack

Is there any way to force dotnet pack to include all referenced assemblies (all dependencies in project.json)?
I believe this is related:
https://github.com/dotnet/cli/issues/1290
https://github.com/dotnet/cli/issues/3959
As of 2020 there is no officially supported way to do this. However various people have come up with ways to achieve it, and the current best way is to install a NuGet package prepared by the amazing Teroneko. Then all you need to do is edit your .csproj to update all your project to be flagged with PrivateAssets="all", as per the package README.
If you are unable to install the aforementioned NuGet package, you can achieve the same effect by editing by editing your .csproj to include the following (once again, this was discovered by Teroneko - it's essentially what the NuGet package he created does):
<Project>
<PropertyGroup>
<TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage</TargetsForTfmSpecificBuildOutput>
</PropertyGroup>
<Target Name="CopyProjectReferencesToPackage" DependsOnTargets="BuildOnlySettings;ResolveReferences">
<ItemGroup>
<!-- Filter out unnecessary files -->
<_ReferenceCopyLocalPaths Include="#(ReferenceCopyLocalPaths->WithMetadataValue('ReferenceSourceTarget', 'ProjectReference')->WithMetadataValue('PrivateAssets', 'All'))"/>
</ItemGroup>
<!-- Print batches for debug purposes -->
<Message Text="Batch for .nupkg: ReferenceCopyLocalPaths = #(_ReferenceCopyLocalPaths), ReferenceCopyLocalPaths.DestinationSubDirectory = %(_ReferenceCopyLocalPaths.DestinationSubDirectory) Filename = %(_ReferenceCopyLocalPaths.Filename) Extension = %(_ReferenceCopyLocalPaths.Extension)" Importance="High" Condition="'#(_ReferenceCopyLocalPaths)' != ''" />
<ItemGroup>
<!-- Add file to package with consideration of sub folder. If empty, the root folder is chosen. -->
<BuildOutputInPackage Include="#(_ReferenceCopyLocalPaths)" TargetPath="%(_ReferenceCopyLocalPaths.DestinationSubDirectory)"/>
</ItemGroup>
</Target>
</Project>
As with the package, you then mark the depended-upon project reference(s) in your .csproj with PrivateAssets="all", and it Just Works(tm).
I was looking for this answer and was annoyed when I couldn't find an obvious one. The solution that worked best for me was to create a nuspec, add the list of DLLs I wanted in the nupkg to that spec and then build with dotnet pack. I created an easy sample and readme here - nuget sample app
Another solution to the problem is to create a custom .targets file to include in your projects. You can add some msbuild instructions to include the files that you need in the package. There is some documentation here on how to do it, here a short example
<PropertyGroup Condition="$(PackAsComponent) != ''">
<TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);CustomBuildOutput</TargetsForTfmSpecificBuildOutput>
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);CustomContentInPackage</TargetsForTfmSpecificContentInPackage>
</PropertyGroup>
<Target Name="CustomBuildOutput">
<ItemGroup>
<BuildOutputInPackage Include="$(OutputPath)*.dll" Exclude="$(TargetPath)" />
<BuildOutputInPackage Include="$(OutputPath)*.pdb" />
<BuildOutputInPackage Include="$(OutputPath)*.exe" Exclude="$(TargetPath)" />
</ItemGroup>
</Target>
<Target Name="CustomContentInPackage">
<ItemGroup>
<TfmSpecificPackageFile Include="abc.txt">
<PackagePath>mycontent/$(TargetFramework)</PackagePath>
</TfmSpecificPackageFile>
</ItemGroup>
</Target>
Basically I activate this when I set the PackAsComponent property in my project.
This preserve the "dotnet pack" functionality 100% without the need to specify any parameter.
As I've installed Octopus build tools on my build system I use octo pack to create the packages. Although this is basically the same thing as just calling good old nuget.exe.
https://octopus.com/docs/packaging-applications/create-packages/octopus-cli
I hope this will help you.
nuget pack yournuspecfile.nuspec -properties Configuration=Release -IncludeReferencedProjects
or your command whatever.

Why is MSBuild trying to run the projects it's building?

I'm currently writing an msbuild script to build a solution I've been working on, as well as run its tests. On my development machine, this works as expected. However, when I try to run the same build script on our build server, I get several failures. I've tracked the source of the problem down to the fact that my build script appears to be trying to run the .exe file associated with my application. This line during the script execution tipped me off, since it doesn't run that command on my dev box:
MSIAuthoring:
Building MSI
"C:\Program Files (x86)\Jenkins\workspace\Test Build\BuildArtifacts\MsiBuildTool.exe" "/MBSBUILD:MsiBuildTool"
I'm fairly new to build scripting, but my understanding is that the build script shouldn't be trying to run my program unless I explicitly tell it to do so. Does anyone know what might be causing this?
For reference, here is my build script:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="RunTests"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<BuildArtifactsDir Include="BuildArtifacts\"/>
<SolutionFile Include="MsiBuildTool.sln"/>
<NUnitConsole Include="C:\Program Files (x86)\NUnit 2.6.4\bin\nunit-console.exe"/>
<UnitTestsDll Include="BuildArtifacts\MsiBuildToolUnitTests.dll"/>
<TestResultsPath Include="BuildArtifacts\TestResults.xml"/>
</ItemGroup>
<PropertyGroup>
<Configuration Condition="'$(Configuration)' == ''">Release</Configuration>
<Platform Condition="'$(Platform)' == ''">Any CPU</Platform>
</PropertyGroup>
<Target Name="Init" DependsOnTargets="Clean">
<MakeDir Directories="#(BuildArtifactsDir)"/>
</Target>
<Target Name="Clean">
<RemoveDir Directories="#(BuildArtifactsDir)"/>
</Target>
<Target Name ="Compile" DependsOnTargets="Init">
<MSBuild Projects="#(SolutionFile)"
Targets ="Build"
Properties ="OutDir=%(BuildArtifactsDir.FullPath);Configuration=$(Configuration);Platform=$(Platform)"/>
</Target>
<Target Name="RunTests" DependsOnTargets="Compile">
<Exec Command='"#(NUnitConsole)" #(UnitTestsDll) /xml=#(TestResultsPath)'/>
</Target>
</Project>
Update:
After some digging through the output, I found that "MSIAuthoring" step was the result of the Wix# library that I'm using. As described by this thread: https://wixsharp.codeplex.com/discussions/644609#
I disabled the MSIAuthoring step by removing this line in my .csproj files:
<Import Project="..\packages\WixSharp.1.0.22.3\build\WixSharp.targets" Condition="Exists('..\packages\WixSharp.1.0.22.3\build\WixSharp.targets')" />
You're building solution file, thus MSBuild will generate msbuild-xml script first and then will build it. To find why it's being called on build machine but not on your dev machine - follow this advice and obtain generated MSBuild scripts from your dev environment and your build server. Then compare it.
Also enable diagnostic logging (/verbosity:diag in the command line) as Lex Li advised, and you'll see detailed decisions why each target being run or not - grep logs for something like "Conditions A, B, C on target BuildMSI evaluated to False" and this will show you the difference between environments.
It might be some type of post-build script on one of the projects which builds MSI only if it's being run not on dev environment - check actual build script to find where it comes from. Also check that it's really related to your build script, and it's not an extra build step in your Jenkins build configuration.

Config transformation in WIX setup

I'm creating a MSI setup with WIX for my Web Application. This works correct. The only thing that I don't get to work is to enabling the config transformation of the standard web application publish method.
I understand that you can add the using tag for existing target files. I try'ed to add the TransformXml to the AfterBuild Target in the project file of the WIX installer but that doesn't work.
<TransformXml Source="Web.Config" Transform="Web.$(Configuration).config" Destination="Web.Config" />
Can someone help me?
I created a test project for this called WebApplicationWix
I didn't see any mention of TransformXml in your example project.
You need code similar to this:
<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="AfterCompile">
<CallTarget Targets="TransformWebConfiguration" Condition="Exists('web.$(Configuration).config')"/>
</Target>
<Target Name="TransformWebConfiguration">
<!-- Generate transformed web configuration -->
<TransformXml Source="web.config" Destination="web.transformed.config" Transform="web.$(Configuration).config" />
</Target>
A few things to note:
Check the path to Microsoft.Web.Publishing.Tasks.dll in the UsingTask element (change for your version of Visual Studio)
In your example, the source and destination were the same; you should make sure the destination is a different file so that you don't have file lock issues or overwrite the web.config you're trying to transform with the transformed one.
In Visual Studio 2010, there were file locking issues with TransformXml, so be careful of that if you're using 2010.

TFS 2010 build Server Wix. Output only MSI

I've managed to get builds working on my build server, but now the issue I have is that the output in the output folder contains all output from all projects, rather than just the output from the wix project(s).
Any idea how to change this?
TIA
If you are only interested in the msi as a build output then you could create a step in your team build to copy your installer files to another location, the following build target, added to your build project should help.
This overrides the target AfterDropBuild
<Target Name="AfterDropBuild">
<PropertyGroup>
<InstallerDir>$(DropLocation)\$(BuildNumber)\Installers</InstallerDir>
</PropertyGroup>
<Message Importance="low" Text="InstallerDir=$(InstallerDir)" />
<MakeDir Directories="$(InstallerDir)" Condition="!Exists('$(InstallerDir)')" />
<CreateItem Include="$(BinariesRoot)\**\*.msi">
<Output TaskParameter="Include" ItemName="InstallationFiles"/>
</CreateItem>
<Copy SourceFiles="#(InstallationFiles)"
DestinationFolder="$(InstallerDir)"/>
</Target>
I create different solution platforms { Application, Setup } and set my .NET projects to build with application and my wix to build with setup. Then I tell the build definition to build those two platforms in that order. The result is that TFS archives the .NET code in an application folder that looks like the deployed machine and the MSI in the setup folder.
Only downside is when you add new projects you have to select the platform to build in configuration manager. My developers don't seem annoyed by it though.