Thursday, January 15, 2015

MSBuild inline tasks: how to write .NET code which runs at compile time to automatize the build process

Recently, working on UniversalIDE, my current open source project, I thought that it would have been interesting to show the git commit ID in the title bar of the program when it's built from a code commit that doesn't represent a release (a nightly build).
But obviously I didn't want to hardcode the commit ID in the source code and change it each time before a new commit (this method is not only unhandy, but it's practically impossible to set up, because you don't know the commit ID before doing the commit itself); so I realized that the best (perhaps the only?) way to achieve this goal was to make the build script of the application to read the current commit ID from the file in .git/refs/heads/master, and put it in the source code before compiling.
After a bit of research I found that it's very simple to insert .NET code in a MSBuild script (which is the file that Visual Studio calls project file, extension .vbproj or .csproj), using MSBuild inline tasks.

These are the steps to follow to add a simple MSBuild inline task that runs before or after the compile process:

  • First of all open the build script: in Visual Studio use the Solution Explorer window to unload the project and to edit the project file (using the project's right-click menu).
  • Define your task(s), for example at the end of the file. Here is an example:
      <!-- replace "SetBuildInfo" with the name you want to give to your task -->
      <UsingTask TaskName="SetBuildInfo" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
        <ParameterGroup />
          <Using Namespace="System" />
          <Using Namespace="System.IO" />
          <Code Type="Fragment" Language="vb"><![CDATA[
              'replace the following example code with your code
              Dim code As String = My.Computer.FileSystem.ReadAllText("UniversalIDEbuildInfo.vb")
              My.Computer.FileSystem.WriteAllText("UniversalIDEbuildInfo_model.vb", code, False, Encoding.ASCII)
              Dim commitID As String = My.Computer.FileSystem.ReadAllText("..\.git\refs\heads\master").Substring(0, 6)
              Dim isNightly As Boolean = True
              For Each tagFile As String In My.Computer.FileSystem.GetFiles("..\.git\refs\tags")
                  If My.Computer.FileSystem.ReadAllText(tagFile).Substring(0, 6) = commitID Then
                      isNightly = False
                      Exit For
                  End If
              code = code.Replace("%IS_NIGHTLY%", isNightly.ToString())
              code = code.Replace("%COMMIT_ID%", commitID)
              My.Computer.FileSystem.WriteAllText("UniversalIDEbuildInfo.vb", code, False, Encoding.ASCII)
              'end of the code snippet
  • Scroll at the bottom of the file: there should be some commented lines which defines the BeforeBuild and AfterBuild tasks; un-comment one of them or both, depending when you want your task(s) to be executed, and call the task(s), using the name you gave at the previous step. Example:
      <Target Name="BeforeBuild">
        <SetBuildInfo />
  • Now save all and reload the project to run it. Note that if your solution has multiple projects and the edited script belongs to the startup project, Visual Studio may have set another project as startup one, so you need to manually re-set it.
You can see a full working example in the UniversalIDE git repository on Sourceforge. For further information, you may found useful the MSDN article about inline tasks.