dotnet / sdk

Core functionality needed to create .NET Core projects, that is shared between Visual Studio and CLI
https://dot.net/core
MIT License
2.69k stars 1.06k forks source link

Generalize multi-pass build support #2790

Open ryantrem opened 5 years ago

ryantrem commented 5 years ago

There are already several standard build "dimensions," including Configuration, Platform, and TargetFramework. These dimensions and their potential values create a matrix of overall build configurations. I can specify which element of the matrix to use to configure the build by specifying values for each dimension (e.g. Debug|x86|net46). If I want to perform multiple build passes (multiple elements of the matrix), then I have a few options depending on the dimension(s) I want vary.

For TargetFramework, I can specify a set of TargetFrameworks and the dotnet sdk targets will perform multiple build passes, one for each TargetFramework specified.

For Configuration and Platform, I can use Visual Studio's Batch Build feature to select multiple Configurations and/or Platforms.

For my own custom "dimension," my only real option is multiple MSBuild invocations from the command line, passing in values for my properties (and this might be a lot of invocations depending on the number of dimensions and number of potential values per dimension).

It would be great if the multi-pass build support for TargetFrameworks was generalized into a mechanism that supported multi-pass builds across arbitrary build dimensions. I could imagine this perhaps being expressed as follows:

<!-- Define each build dimension. -->
<ItemGroup>
  <BuildDimension Condition="'$(Configuration)' == ''" Include="Configuration">
    <Values>Debug;Release</Values>
  </BuildDimension>
  <BuildDimension Condition="'$(Platform)' == ''" Include="Platform">
    <Values>x86;x64;arm;AnyCPU</Values>
  </BuildDimension>
  <BuildDimension Condition="'$(TargetFramework)' == ''" Include="TargetFramework">
    <Values>net46;netstandard2.0;uap10.0.14393</Values>
  </BuildDefinition>
</ItemGroup>

The idea would be that a specific value for a build dimension (e.g. Configuration=Debug) could be specified, otherwise multiple passes would be performed for that dimension. I could imagine this showing up in Visual Studio as dropdowns for each build dimension, similar to what we have today for Configuration and Platform. I would also imagine the default output path would be generated by simply combining the values of each dimension (e.g. Debug\x86\net46).

Usually, a build configuration matrix is sparse, so I'd also want to be able to ignore certain elements of the matrix (don't build them). I could imagine this perhaps being expressed as follows:

<PropertyGroup>
  <!-- Allow an early out of the inner build loop, which is effectively allowing for a sparse build dimension matrix. -->
  <BuildPassEnabled Condition="'$(Platform)' == 'arm' AND '$(TargetFramework)' != 'uap10.0.14393'">false</BuildPassEnabled>
</PropertyGroup>

Generalized build dimensions and multi-pass builds are particularly useful when you need to define your own custom build dimensions. For example, when writing code to be consumed by Unity 3D, I have the concept of Unity Players (runtimes) (e.g. Windows Store, Android, iOS, etc.). Depending on the target Player, I might want to include different source files, define different conditional compilation symbols, etc. Further, I might want to define a "compatibility graph" in the same way TargetFramework does (e.g. netstandard2.0 is "compatible with" net471, so a net471 project can consume a netstandard2.0 project). Taking my Unity 3D example further, different Unity Players support different .NET runtimes. For example, iOS and Android are based on .NET Framework, while Windows Store is based on UWP .NET Core). So ideally I'd want to be able to have UnityIOS and UnityAndroid be "compatible with" UnityNetFramework (for example). I could imagine this perhaps being expressed as follows:

My common props that define the build dimension

<ItemGroup>
  <BuildDimensionCompatibility Include="UnityEditor;UnityIOS;UnityAndroid">
    <CompatibleWith>UnityNetFramework</CompatibleWith>
  </BuildDimensionCompatibility>
</ItemGroup>

Project 1

<ItemGroup>
  <BuildDimension Condition="'$(UnityPlayer)' == ''" Include="UnityPlayer">
    <Values>UnityUAP;UnityNetFramework</Values>
  </BuildDimension>
</ItemGroup>

Project 2

<ItemGroup>
  <BuildDimension Condition="'$(UnityPlayer)' == ''" Include="UnityPlayer">
    <Values>UnityUAP;UnityEditor;UnityIOS;UnityAndroid</Values>
  </BuildDimension>
  <!-- This ProjectReference works because UnityEditor/UnityIOS/UnityAndroid are compatible with UnityNetFramework in the same way that net471 and netcore5 are compatible with netstandard2.0 -->
  <ProjectReference Include="Project1.csproj" />
</ItemGroup>

Finally, for all this to be effective, I'd want the same custom build dimension support and compatibility graph support to be part of NuGet (in terms of having build results from multiple build passes in the package and having the right assemblies/content being consumed by the consuming project).

I feel like a solution like this would drastically simplify scenarios where custom "build dimensions" are needed, which in my experience is fairly common, and almost always results in some hacky solution that only one or two people on the team understand.

ryantrem commented 5 years ago

Perhaps a generalization of the example I give above is just multi-platform development in general. For example, I may have some platform specific (say Windows, Android, and iOS) code, but still want to use netstandard2.0 for all platforms. For example, on Windows, I want to build against .netstandard2.0 and the Windows Runtime, while for Android I want to build against .netstandard2.0 and some Android specific APIs (perhaps the platform APIs provided via Xamarin). In this case, I would only be building against a single TargetFramework (netstandard2.0 for all platforms), but I'd still want multiple build passes - one per platform, with additional platform specific assembly references. This isn't really possible today without either having multiple projects, or overloading TargetFramework to mean "some implementation of .NET plus some other platform specific assemblies" (which conceptually is what UAP is - .NET Core + Windows Runtime).