Show / Hide Table of Contents

MSBuild Traversal Projects with Solution Generation

The Microsoft.Build.Traversal MSBuild project SDK allows project tree owners the ability to define what projects should be built. Visual Studio solution files are more targeted for end-users and are not good for build systems. Additionally, large project trees usually have several Visual Studio solution files scoped to different parts of the tree.

In an enterprise-level build, you want to have a way to control what projects are a built in your hosted build system. Traversal projects allow you to define a set of projects at any level of your folder structure and can be built locally or in a hosted build environment.

To supplement traversal projects, the Xamarin.MSBuild.Sdk MSBuild project SDK adds a GenerateSolution target. This allows the traversal project to define the canonical build and generate solutions suitable for loading into IDEs - without having to maintain both the traversal project, solutions, or configuration mappings.

Defining Solutions

Solutions support mapping an outer-level solution configuration to inner-level project configurations for each project in the solution. This mapping is typically maintained in the IDE as the solution format is very tedious to update by hand. Because of this solutions often become out of sync when using non-solution-driven build definitions, such as traversal projects.

With Xamarin.MSBuild.Sdk's GenerateSolution target however, defining these mappings directly in the traversal project is easy, and even supports solution folders for nicer in-IDE project tree grouping.

A traversal project that generates a solution must import the SDK and must have at least one <SolutionConfiguration> item.

<Project Sdk="Microsoft.Build.Traversal">
  <Sdk Name="Xamarin.MSBuild.Sdk"/>

  <ItemGroup>
    <SolutionConfiguration Include="Debug"/>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="SomeProject.csproj"/>
  </ItemGroup>
</Project>

When msbuild /t:GenerateSolution is run against the traversal project, a solution will be generated that includes a Debug|Any CPU solution configuration that maps to building SomeProject.csproj setting /p:Configuration=Debug and /p:Platform=AnyCPU

The <SolutionConfiguration> MSBuild Item

Any number of these items may be defined in the traversal project. Its Include attribute defines the list of solution configurations and each should take the form of $(Configuration)|$(Platform) per standard solution syntax. Configuration and platform values are always case insensitive.

Note that the |$(Platform) component may be omitted which will imply Any CPU. Additionally, Any CPU and AnyCPU are treated as equal.

The following three <SolutionConfiguration> items are identical and would only result in a single actual solution configuration at the time of generation:

<ItemGroup>
  <SolutionConfiguration Include="Debug"/>
  <SolutionConfiguration Include="Debug|Any CPU"/>
  <SolutionConfiguration Include="Debug|anycpu"/>
</ItemGroup>

Each item may provide metadata that influences the configuration. These metadata properties will be passed to msbuild when evaluating the traversal project. Any number of properties may be provided as metadata. This is best demonstrated by example.

Multi-platform Build and Solution

<Project Sdk="Microsoft.Build.Traversal">
  <Sdk Name="Xamarin.MSBuild.Sdk"/>

  <!-- Set platform helper properties -->
  <PropertyGroup>
    <IsWindows>$([MSBuild]::IsOSPlatform('Windows'))</IsWindows>
    <IsMac>$([MSBuild]::IsOSPlatform('OSX'))</IsMac>
  </PropertyGroup>

  <ItemGroup>
    <SolutionConfiguration Include="macOS Debug">
      <!-- Define the Configuration and Platform to be passed to projects -->
      <Configuration>Debug</Configuration>
      <Platform>x64</Platform>

      <!-- Override the platform helper properties via /t:GenerateSolution -->
      <IsMac>true</IsMac>
      <IsWindows>false</IsWindows>
    </SolutionConfiguration>

    <SolutionConfiguration Include="Windows Debug">
      <!-- Define the Configuration and Platform to be passed to projects -->
      <Configuration>Debug</Configuration>

      <!-- Override the platform helper properties via /t:GenerateSolution -->
      <IsMac>false</IsMac>
      <IsWindows>true</IsWindows>
    </SolutionConfiguration>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="CrossPlatformProject.csproj"/>
  </ItemGroup>

  <ItemGroup Condition="$(IsMac)">
    <ProjectReference Include="MacOnlyProject.csproj"/>
  </ItemGroup>

  <ItemGroup Condition="$(IsWindows)">
    <ProjectReference Include="WindowsOnlyProject.csproj"/>
  </ItemGroup>
</Project>

When the GenerateSolution target is run, a solution with a macOS Debug and a Windows Debug configuration will be generated.

Project Built in macOS Debug Built in Windows Debug
CrossPlatformProject.csproj ✓ (as Debug|x64) ✓ (as Debug|AnyCPU)
MacOnlyProject.csproj ✓ (as Debug|x64)
WindowsOnlyProject.csproj ✓ (as Debug|AnyCPU)

Additions to the <ProjectReference> MSBuild Item

The GenerateSolution target supports the following optional metadata properties:

Property Description Example Value
<Configuration> Overrides the value from <SolutionConfiguration> for this project only. AppStore
<Platform> Overrides the value from <SolutionConfiguration> for this project only. Arm64
<SolutionFolder> A relative virtual path for grouping this project in the solution. Either \ or / may be used as a path separator. A\B\C
<Project Sdk="Microsoft.Build.Traversal">
  <Sdk Name="Xamarin.MSBuild.Sdk"/>

  <ItemGroup>
    <ProjectReference Include="…">
      <SolutionFolder>Client Applications</SolutionFolder>
    </ProjectReference>
  </ItemGroup>

  …
</Project>

Additional Solution Generation Properties

Property Description
<GenerateSolutionAfterBuild> To avoid having to explicitly run the /t:GenerateSolution target, set the GenerateSolutionAfterBuild to true. Doing so will run the target automatically after a successful build.
<GenerateSolutionFilePath> Override the path to the solution to generate. If not specified, the solution will be generated in the same directory as the traversal project with the same name (e.g. path/to/traversal.proj → path/to/traversal.sln)
<Project Sdk="Microsoft.Build.Traversal">
  <Sdk Name="Xamarin.MSBuild.Sdk"/>

  <PropertyGroup>
    <GenerateSolutionAfterBuild>true</GenerateSolutionAfterBuild>
    <GenerateSolutionFilePath>Solutions\Foo.sln</GenerateSolutionFilePath>
  </PropertyGroup>

  …
</Project>

Rationale for Solution Generation

While this may change in the future, the solution generator specifically ignores any changes made to the solution itself (e.g. configuration changes introduced via the IDE or extra metadata the IDE may add to the solution).

This is because the canonical definition of the build should come from the traversal project itself. A solution is simply a scoped view of the project to drive the development experience in the IDE.

Always discard solution edits/changes made by the IDE

Only changes to solution files made as a result of running /t:GenerateSolution should be committed to the repository. Discard any edits/changes made by the IDE.

GUIDs

Solutions define the shape of the project tree based on the mapping of two GUIDs. In older MSBuild projects, each project had a unique <ProjectGuid> property. These GUIDs were reused to reference projects in the solution and map their configurations.

With the advent of SDK-style projects however, <ProjectGuid> is optional and discouraged. It is an artifact of the solution structure itself.

However, since a GUID is needed inside the solution, we have to provide one. If we were to use the .NET Guid.NewGuid method to create one, the solution would change on every single generation, because this method creates a version 4 random GUID.

/t:GenerateSolution solves this by using version 5 SHA-1 hashed GUIDs instead, which are stable: given a constant namespace GUID and a value (in this case, the path to the project file relative to the solution file), a version 5 GUID will always produce the same value and thus result in a solution file that does not change on each generation.

Note

If a project does happen to provide an explicit <ProjectGuid>, that value is used instead of creating a hashed GUID based on the path.

Example

using static Xamarin.GuidHelpers;

var itemGuid = GuidV5 (
  // constant namespace
  new Guid ("{17ad6350-380a-4d65-9b2c-aa44b5da8111}"),
  // path to project relative to solution with normalized separators
  @"path\to\project.csproj".Replace ('\\', '/')
);

// itemGuid will always be '{5984500c-0dbf-5c42-947b-c6674ccdbe30}'
  • Improve this Doc
Back to top Generated by DocFX