Who has not struggled to correctly manage multiple platform configurations in Visual Studio without ending to edit a solution file or tweak some msbuild files by hand? Recently, I decided to cleanup the antique SharpDX.sln in SharpDX that was starting to be a bit fat and not easy to manage. The build is not extremely bizarre there, but as it needs to cover the combinations of NetPlatform x OSPlatform x DirectXVersion x Debug/Release with around 40 projects (without the samples), it is an interesting case of study. It turns out that modifying the solution to make a clean multi-platform build was impossible without hacking msbuild in order to circumvent unfortunate designs found in Microsoft msbuild files (and later to found at work in Xamarin build files as well). In this post, we will go through the gotchas found, and we will see also why Visual Studio should really improve the configuration manager if they want to improve our developers experience.

Preliminaries

There are a couple of things to understand on how VisualStudio and msbuild are working with solution files and configuration. This is just a little overview about the key settings and how they affect your build. I found some good introduction about this in the post "Targeting Platforms in Visual Studio" worth a read.

If we look at a simple solution containing only a single project:


Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestConsole", "TestConsole\TestConsole.csproj", "{56849035-CEF7-446D-AF0A-51EE9DC1DDB7}"
EndProject
Global
 GlobalSection(SolutionConfigurationPlatforms) = preSolution
  Debug|Any CPU = Debug|Any CPU
  Release|Any CPU = Release|Any CPU
 EndGlobalSection
 GlobalSection(ProjectConfigurationPlatforms) = postSolution
  {56849035-CEF7-446D-AF0A-51EE9DC1DDB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
  {56849035-CEF7-446D-AF0A-51EE9DC1DDB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
  {56849035-CEF7-446D-AF0A-51EE9DC1DDB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
  {56849035-CEF7-446D-AF0A-51EE9DC1DDB7}.Release|Any CPU.Build.0 = Release|Any CPU
 EndGlobalSection
 GlobalSection(SolutionProperties) = preSolution
  HideSolutionNode = FALSE
 EndGlobalSection
EndGlobal

What we can see from the solution is that it defines:

In SolutionConfigurationPlatforms, the mapping between solution configuration/platforms to project configuration/platforms. When you read the line :
Debug|Any CPU = Debug|Any CPU

It means that the Solution configuration/platform Debug|Any CPU will map to the project configuration/platform Debug|Any CPU.

The project configuration and platform are the actual values that will be used when using later the properties Configuration and Platform in the msbuild proj (csproj...etc.) as we can see it used by the TestConsole.csproj above:

  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugSymbols>true</DebugSymbols>
The solution also defines in the section ProjectConfigurationPlatforms the projects that will be build for each solution configuration/platform, as well as a mapping to the actual project configuration/platform. In SharpDX, the configuration/platform in SharpDX.sln are configured like this:

GlobalSection(SolutionConfigurationPlatforms) = preSolution
  Debug|DIRECTX11_2 = Debug|DIRECTX11_2
  Debug|Net20 = Debug|Net20
  Debug|Net40 = Debug|Net40
  Debug|Win8 = Debug|Win8
  Debug|WP81 = Debug|WP81
  Debug|WP8-ARM = Debug|WP8-ARM
  Debug|WP8-x86 = Debug|WP8-x86
  Release|DIRECTX11_2 = Release|DIRECTX11_2
  Release|Net20 = Release|Net20
  Release|Net40 = Release|Net40
  Release|Win8 = Release|Win8
  Release|WP81 = Release|WP81
  Release|WP8-ARM = Release|WP8-ARM
  Release|WP8-x86 = Release|WP8-x86
EndGlobalSection
As you can see, we are just using different configuration/platforms in order to target multiple .NET framework, different DirectX version and specifics OSes. But surprisingly, if you are trying to use this kind of configuration in your solution, It will not work out of the box.

Problem #1: Where is the solution platform?


By default, Visual Studio settings in C# is hiding the solution platform. Instead, what you will get is only the solution configuration:



This is really annoying, because if someone just open your solution, It will not realize that there are actually different platforms. The solution will just select the first defined platform. In order to get back the solution platform selector in Visual Studio, you need to activate back the button by selecting on the right side of the solution configuration the drop-down button "Add or Select buttons":


While I understand the ergonomic original reasons for hiding this button, in the era of multiple platform development, this should no longer be hidden and the default should show it. I hope that Visual Studio will fix this in a future release.

Problem #2: Project Platform semantic


This is the problem that made the refactoring of the SharpDX.sln quite laborious to hack. On the surface, solution platforms look nice. They provide a way to organize your project to target multiple platforms/configurations from the same solution. On the backside, it is not working as expected, mainly because some msbuild files are interpreting the value of the project platform.

And this is where I would like to take the opportunity here to explain why project platforms should have no semantic values for Visual Studio or Xamarin build files. Project platforms should be considered as user defined platforms, they are a way to organize our project in whatever combinations and these semantics should be owned by the developer of the project.

Unfortunately, Visual Studio msbuild files don't allow to use a custom project platform because they are expecting some specific platforms. For example, if you are developing a Windows Store Apps, you will find that a Windows Store Apps project won't compile if the platform is different from "Any CPU/x86/x64//Win32/arm"!. This is hardcoded in the file C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v11.0\AppxPackage\Microsoft.AppxPackage.Targets line 1270 like this (Windows Phone platform and Xamarin are suffering the same problem):
<PropertyGroup>
 <_ProjectArchitectureOutput>Invalid</_ProjectArchitectureOutput>
 <_ProjectArchitectureOutput Condition="'$(Platform)' == 'AnyCPU'">neutral</_ProjectArchitectureOutput>
 <_ProjectArchitectureOutput Condition="'$(Platform)' == 'x86'">x86</_ProjectArchitectureOutput>
 <_ProjectArchitectureOutput Condition="'$(Platform)' == 'Win32'">x86</_ProjectArchitectureOutput>
 <_ProjectArchitectureOutput Condition="'$(Platform)' == 'x64'">x64</_ProjectArchitectureOutput>
 <_ProjectArchitectureOutput Condition="'$(Platform)' == 'arm'">arm</_ProjectArchitectureOutput>
</PropertyGroup>
Using directly the Platform from a core VisualStudio msbuild file is a mistake (same for Configuration, that is used in some Visual Studio msbuild targets), as it is forcing the original solution to use only these platforms. Instead, build files from Visual Studio should use a property that can be redefined by the project (like the property PlatformTarget that is used by the C# compiler). We should have a way to redefine the mapping in whatever way we would like. In other words, Solution platform and configurations should be fully owned by the developer of the solution. Their semantics are project specific and Visual Studio should allow us to define the remapping to a target platform (like AnyCPU) in our project like this:

  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|ThisIsMyConfig' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    ...
Fortunately, there is a hack to manage this, though it is not completely safe. By default, the properties Platform and Configuration are immutable in msbuild, because they are considered as global properties passed to msbuild, so they cannot be modified. But there is a way to override the platform "ThisIsMyConfig" to "AnyCPU" for some specific build (like WindowsStoreApps). In SharpDX, this is made possible by the target "SharpDXForcePlatform" as can be seen in this file. In order to work, the trick is:
  • Add a target that will be executed automatically whenever there is a build. This is done by declaring a msbuild project with the attribute InitialTargets="YourTargetToForcePlatform"
  • In the YourTargetToForcePlatform, we can override the Platform property programmatically (they are mutable only when using this trick from a target). In the following code, we are remapping the Platform Win8 to AnyCPU like this:
    
    <Target Name="SharpDXForcePlatform">
        <!--
    Windows 8 App Store => AnyCPU
    Windows Phone 8.1 => AnyCPU
    -->
        <CreateProperty Condition=" '$(Platform)' == 'Win8' or '$(Platform)' == 'WP81'" Value="AnyCPU">
          <Output
              TaskParameter="Value"
              PropertyName="Platform" />
        </CreateProperty>
    
This way, when the build start, the Platform property is correctly setup for the platform being compiled. Beware that the property Platform used outside a target (in property groups...etc.) is still linked to the original semantic which is actually good. But if a Visual Studio build is using the property Platform outside a target, this trick will not work.

So bottom line of this problem is that Visual Studio builds should really take care of this and avoid forcing any semantic for the configuration/platform. Without this, we are forced to use the hack described above or worst, to duplicate the solution (this was the case for SharpDX, which made the full build quite a pain).



Problem #3: The unwanted Mixed Platforms

When you are using custom platforms names, and you want to add a new project to your solution, you will most likely end-up with a new solution platform Mixed Platforms. This is really annoying when we are already dealing with multiple platforms, we don't want Visual Studio to add a useless platform. The solution is to remove it by hand in the .sln, but we should not have to do this. At worst Visual Studio should ask the developer "Do you really want to add a new mixed platform to your solution?", at best, remove this Mixed Platforms.


Problem #4: The Configuration Manager


When managing several platforms with several dozens of projects, the configuration manager is a real pain to use, and we are always forced to edit the sln by hand and perform some regexp replace on the file to cleanup it or to fix it.




There are lots of issues with the current Configuration Manager:
  • The window is not resizable ! If you have more than 12 projects in your solution, you are good to use the scrollview quite a lot.
  • It is not possible to have a global view of all your projects and which one is activated for which platforms...etc. Considering that you need to check (Debug AND Release) x number of platforms, and you have go around for a while by clicking, waiting, clicking, scrolling... a nightmare!
  • It is not possible to bulk edit your projects. You have to go though each single project, single click, dropdowns...etc. for each projects.
  • Switching configuration or platform is slow when you have lots of projects (or some custom .targets). I don't understand why Visual Studio seems to reevaluate all the projects, so it can take 2-3 seconds when switching the configuration/platform while everything should be already accessible from memory (both solution and projects)

vNext

Whoever has done some cross platform development (even just inside .NET, by targeting different .NET framework) with Visual Studio will most likely have struggled with the issues describe above.

With the rise of Xamarin more tightly integrated into Visual Studio, more development targeting all Windows eco-system (Windows Desktop, Windows AppStore, Windows Phone Store) and Android/iOS, all these issues should be really fixed to improve our productivity. Fingers crossed for VS2014 if someone at the Visual Studio team is reading this!

How do you manage these issues in your projects? Do you have any other ideas to improve the situation when targeting multiple platforms in Visual Studio?

PS: I will have to double check whether there is some uservoice or connect bugs for the issues described in this post. If you have any link already, I'm interested!