12
votes

I'm setting up TeamCity (migrating from CruiseControl.NET) and I'm struggling to get it to perform incremental builds through MSBuild.

I've got a small .proj file which contains a basic build script to invoke a build of my solution with some parameters fed in from TeamCity. When I invoke the script manually, MSBuild's Incremental Build features kick in and skip the build entirely on subsequent runs.

When calling this script via Team City, the build log shows the output of a clean compile every time. I've observed the working directory during builds and can see the output from the previous build hasn't gone anywhere.

I also manually called the build script from that directory by remoting onto the server and running MSBuild from the command-prompt. Running it this way triggers the expected incremental builds after the first invocation.

Even when starting the build from the dashboard with no changes made, a complete rebuild occurs.

I can't pinpoint the cause, but something appears to be giving MSBuild the impression that it's getting new changes and causing it to perform a rebuild on every run. I can't see much in the TeamCity documentation that would explain this - my expectation is that if there are no changes in the source control system, it would not update the working folder.

Is TeamCity passing some parameter to the build process which triggers a rebuild? Can I view these parameters?


Having examined a detail MSBuild log (/v:d command-line switch), the reason a complete rebuild is occurring is due to the file .NETFramework,Version=v4.0.AssemblyAttributes.cs being updated in the <Agent>\temp\buildTmp directory on every build.

This file is normally found at %TMP%\.NETFramework,Version=v4.0.AssemblyAttributes.cs; TeamCity is changing the local temp directory environment variable to reference the agent's temp folder. Unfortunately, this file is created by the Microsoft.Common.targets part of the build process when absent. Deletion of the "temp" file before every build causes it to be created every build and is dynamically referenced in the build of every project file.

I need to find a way to prevent this file from being re-created on every build.

3

3 Answers

11
votes

A workaround for this problem is to customize the MSBuild process to set the path at which the "Target Framework Moniker Assembly Attributes" file (the proper name for the file mentioned in the question) will be created.

The TargetFrameworkMonikerAssemblyAttributesPath property is defined in Microsoft.Common.targets determines where the file should be created. By overriding this property, the location can be changed to use a different location.

Here's a script that can be used to achieve a suitable replacement:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<PropertyGroup>
    <PrepareForBuildDependsOn>
        $(PrepareForBuildDependsOn);
        _SetTargetFrameworkMonikerAssemblyAttributesPath
    </PrepareForBuildDependsOn>
</PropertyGroup>

<Target 
    Name="_SetTargetFrameworkMonikerAssemblyAttributesPath"
    Condition="'$(TEAMCITY_VERSION)' != ''">

    <PropertyGroup>
        <TargetFrameworkMonikerAssemblyAttributesDir
            Condition="'$(TargetFrameworkMonikerAssemblyAttributesDir)' == ''">
            $([MSBuild]::GetRegistryValue("HKEY_CURRENT_USER\Environment", "TMP"))
        </TargetFrameworkMonikerAssemblyAttributesDir>
        <TargetFrameworkMonikerAssemblyAttributesDir
            Condition="'$(TargetFrameworkMonikerAssemblyAttributesDir)' == ''">
            $([MSBuild]::GetRegistryValue("HKEY_CURRENT_USER\Environment", "TEMP"))
        </TargetFrameworkMonikerAssemblyAttributesDir>
        <TargetFrameworkMonikerAssemblyAttributesDir 
            Condition="'$(TargetFrameworkMonikerAssemblyAttributesDir)' == ''">
            $(USERPROFILE)
        </TargetFrameworkMonikerAssemblyAttributesDir>
        <TargetFrameworkMonikerAssemblyAttributesDir
            Condition="'$(TargetFrameworkMonikerAssemblyAttributesDir)' == ''">
            $([System.IO.Path]::Combine('$(WINDIR)', 'Temp'))
        </TargetFrameworkMonikerAssemblyAttributesDir>
        <TargetFrameworkMonikerAssemblyAttributesPath>
            $([System.IO.Path]::Combine('$(TargetFrameworkMonikerAssemblyAttributesDir)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)'))
        </TargetFrameworkMonikerAssemblyAttributesPath>
    </PropertyGroup>

    <Message Text="Target Framework Moniker Assembly Attributes path is &quot;$(TargetFrameworkMonikerAssemblyAttributesPath)&quot;" Importance="low" />

</Target>

The target is only executed when TEAMCITY_VERSION is specified as a property, which should be when the build is being executed by the TeamCity agent.

NOTE: The child elements of the PropertyGroup should each be on a single line. They have been spread over multiple lines to increase readability here, but the additional line-breaks cause the script to fail.

When the target runs, it tries to build a suitable path based on the user's environment variables as defined in the registry, first looking for TMP and TEMP, before falling back to the user's profile folder and finally the C:\Windows\Temp directory. This matches the order documented by System.Path.GetTempPath(), and should result in behaviour matching MSBuild execution outside of TeamCity.

This should be saved as a .targets file somewhere on the system and imported to the .csproj file of projects being built by the TeamCity server, using an <Import> element. I added the script under my MSBuild extensions directory (C:\Program Files\MSBuild\) and referenced it by adding the following import element:

<Import Project="$(MSBuildExtensionsPath)\TeamCity\TeamCity.Incremental.targets" />

The location/ordering of Import elements doesn't matter, but I suggest including it after the <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> which should appear in every .csproj file.

2
votes

Adjusting TargetFrameworkMonikerAssemblyAttributesPath works around this issue, as Paul Turner mentions. Rather than battle the High Magick in Microsoft's build system scripts, I added an environment variable to set TargetFrameworkMonikerAssemblyAttributesPath in TeamCity project parameters.

In TeamCity's project settings, I set env.TargetFrameworkMonikerAssemblyAttributesDir to %env.windir%\Temp.

1
votes

This issue is still present in TeamCity 2017.3.

I wanted to find an easier to track work-around for this than the one detailed by the accepted answer so I did the following:

  1. I checked in a copy of my .NETFramework,Version=v4.7.AssemblyAttributes.cs file to my VCS
  2. I added a new build step to the build configurations that are using MSBuild with the following properties:
    • Runner Type: Command Line
    • Step Name: CopyAssemblyAttributesFile
    • Run: Custom script
    • Custom Script: copy "%system.teamcity.build.workingDir%\<path_to_AssemblyAttributes.cs Dir>\." "%env.TEMP%\."

This will copy my version of the AssemblyAttributes file with the timestamp from the initial VCS checkout the first time,

Subsequently MSBuild seems to think it is the same file since the timestamp will remain consistent and will now properly perform incremental builds, which may be verified from the build log on the agent.