CodeGeneration.Roslyn icon indicating copy to clipboard operation
CodeGeneration.Roslyn copied to clipboard

Handle P2P (same solution) generator projects nicely

Open amis92 opened this issue 6 years ago • 5 comments

Situation:

  • Solution
    • Generator project - targeting netcoreapp2.1 or netstandard2.0
    • Consumer project

Consumer has a P2P reference to the generator. But it might need to target a framework that is incompatible with Generators', e.g. netstandard1.0.

Technically, the generator runs only during build, so this dependency is unnecessary. But it signals MSBuild that Generator has to be built before Consumer, which is good.

How to make P2P work, but ignore target framework and don't reference the Generator's assembly, and still let Consumer's build know somehow where Generator's assembly is?

amis92 avatar Jun 19 '19 19:06 amis92

Links browsed during investigation:

  • https://blogs.msdn.microsoft.com/kirillosenkov/2015/04/04/how-to-have-a-project-reference-without-referencing-the-actual-binary/
  • https://stackoverflow.com/questions/46905637/how-to-f5-deploy-with-add-in-libraries-in-visual-studio/47132155#47132155
  • https://github.com/Microsoft/msbuild/issues/1916
  • https://github.com/microsoft/msbuild/blob/master/documentation/ProjectReference-Protocol.md
  • https://github.com/dotnet/sdk/issues/939
  • https://github.com/microsoft/msbuild/pull/2867
  • https://github.com/microsoft/msbuild/issues/2661
  • https://github.com/microsoft/msbuild/issues/3843
  • https://jaylee.org/archive/2019/01/25/create-a-buildreference-dependency-between-sdk-style-projects.html this seems to follow what we currently implement in Tests project to reference the Tool project
  • https://github.com/microsoft/msbuild/blob/master/documentation/wiki/ResolveAssemblyReference.md
  • https://riptutorial.com/msbuild/topic/9236/common-item-types--projectreference
  • https://github.com/dotnet/sdk/issues/2280#issuecomment-392815466

amis92 avatar Jun 19 '19 20:06 amis92

What we want:

  • a ProjectReference to Generator project that ignores TargetFramework of the Generator
  • a reference to the Generator's assembly

amis92 avatar Jun 19 '19 20:06 amis92

Yes, this is a tough problem that I've fought for better support for since NuGet essentially broke it years ago. One of those links above is a bug I filed. :smile:

The simplest solution though works, if you have a solution file. Just add a project dependency to the solution file between the two projects. That works so long as you build within VS or at the command line you build the solution. If you build an individual project at the command line, it doesn't work.

But even with that limitation, it works better than not having anything expressed at all due to all the issues you've found.

AArnott avatar Jun 20 '19 02:06 AArnott

Yes, this is a tough problem that I've fought for better support for since NuGet essentially broke it years ago. One of those links above is a bug I filed. 😄

That amused me as well. 😂

After burning some hours on that topic, I tried a half-baked solution that works

If we included such a Generator.targets file in BuildTime package:

<Project InitialTargets="PrepareGeneratorProjectReference">
  <Target Name="PrepareGeneratorProjectReference" BeforeTargets="AssignProjectConfiguration;BeforeResolveReferences" >
    <ItemGroup>
      <_GeneratorProjectReference Include="@(ProjectReference)"
                                  Condition=" '%(ProjectReference.Generator)' == 'true' " />
      <ProjectReference Remove="@(_GeneratorProjectReference)" />
      <ProjectReference Include="@(_GeneratorProjectReference)"
                        ReferenceOutputAssembly="false"
                        OutputItemType="ResolvedGeneratorReferencePaths"
                        SkipGetTargetFrameworkProperties="true"
                        UndefineProperties=";TargetFramework;RuntimeIdentifier"
                        PrivateAssets="all"/>
      <_GeneratorProjectReference Remove="@(_GeneratorProjectReference)" />
    </ItemGroup>
  </Target>
</Project>

Then a project reference like that:


    <ProjectReference Include="..\Generators\Generators.csproj"
                      ReferenceOutputAssembly="false"
                      Generator="true">

works as expected. Unfortunately, ReferenceOutputAssembly is still required because it's again probably some magic property (I have found no method to be able to drop it here and only set it in target). But it also gives us a nice list of generator assemblies: ResolvedGeneratorReferencePaths which we could use in BuildTime.targets.

@AArnott what do you think? Is it worth it, or not really?

Maybe just requiring user to do that themselves for in-solution generators is good-enough… It'd be three additional metadatas more, so not so bad.

amis92 avatar Jun 20 '19 23:06 amis92

Thinking about this some more.

Currently for 0.7 we're shipping with the requirement for end users to mangle their references manually, as per Readme.

Now, for future, I think patching up those references can be done outside of targets in a flat ItemGroup somewhere in .targets. We'd still require users to mark their generator references with OutputItemType="CodeGenerationRoslynPlugin", but in .Tool's build/.targets we could update those references with additional metadata to "fix up" referencing in TFM-incompatible targets.

  <ItemGroup>
    <ProjectReference Update="@(ProjectReference->WithMetadataValue('OutputItemType', 'CodeGenerationRoslynPlugin'))">
       <ReferenceOutputAssembly Condition=" '%(ReferenceOutputAssembly)' != '' ">false</ReferenceOutputAssembly>
       <SkipGetTargetFrameworkProperties Condition=" '%(SkipGetTargetFrameworkProperties)' != '' ">true</SkipGetTargetFrameworkProperties>
       <Private Condition=" '%(Private)' != '' ">false</Private>
    </ProjectReference>
  </ItemGroup>

This still doesn't fix the issue that you can't easily (via CLI or Visual Studio UI) add the ProjectReference at all if TFMs are incompatible - and for that I have no idea.

amis92 avatar Mar 31 '20 11:03 amis92