How to make an MSBuild Target that only runs once instead of once, before Targets that run once per framework in the TargetFrameworks tag?
Asked Answered
M

5

12

I have a code generator tool that I partially own, and now that csproj files can list multiple Target Frameworks in them and building builds all of them, I am trying to figure out how to make an MSBuild Target to do the code generation only once per running the build, no matter how many Target Frameworks are listed, and have the compiling for each of the Target Frameworks wait until the code generation has completed.

I currently have it conditional on a specific value of TargetFramework. For example, Condition="'netstandard2.0' == '$(TargetFramework)'".

This avoids the code generation tool being kicked off for each Target Framework at the same time and then getting access denied errors as the processes try to create/update the same files.

However, the other Target Frameworks go right to trying to compile without waiting for the code generation to complete, and fail without that code existing.

I would like to have my code generation happen only once each time a project is built and have the compiling for each Target Framework only begin once that has finished.

It needs to run each time a build is run, in case the inputs have changed and it generates different code.

Note: For the moment I am ignoring a case where #if FrameworkSpecificDefine is used to have different input code to the code generator, such that different Target Frameworks causes different output from the code generator. For now, I am considering the output of the code generator to be the same and valid across all Target Frameworks.

Update: After looking for a Target that happens before MSBuild splits into TargetFramework specific builds, that I could then hook to build before, I see this in Detailed Build Output in VS:

1>Target _SetBuildInnerTarget:
1>Target _ComputeTargetFrameworkItems:
1>Target DispatchToInnerBuilds:
1>  Using "MSBuild" task from assembly "Microsoft.Build.Tasks.Core, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a".
1>  Task "MSBuild"
1>    Additional Properties for project "ProjectA.csproj":
1>      TargetFramework=netstandard2.0
1>    Additional Properties for project "ProjectA.csproj":
1>      TargetFramework=net47

I then set my target as BeforeTargets="DispatchToInnerBuilds" and it runs before the individual builds that set TargetFramework specifically and seems to meet my needs exactly.

(Adding to the BuildDependsOn property doesn't seem to work any longer: Add a msbuild task that runs after building a .NET Core project in Visual Studio 2017 RC; I suspect that Microsoft.Common.targets gets evaluated later in the new csproj format, and any appending to properties that you do in the project file are overwritten by Microsoft.Common.targets.)

Must answered 10/10, 2017 at 20:50 Comment(3)
Would that be once or once every time you build?Wry
Can you inject your target in, say, CoreBuildDependsOn, or similar?Daria
There is a related issue regarding dotnet CLI on GitHub. The same problem occurs when Visual Studio is open in the background, but DispatchToInnerBuilds does not help in that case. As a workaround, I added an extra parameter from the command line. For further details, see my comment on that thread.Decor
G
16

On single target framework I only use BeforeTargets="PreBuildEvent":

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  <Target Name="GenerateVersionInfo" BeforeTargets="PreBuildEvent">
    <Exec Command="your custom command" />
  </Target>
</Project>

on multi target frameworks I use BeforeTargets="DispatchToInnerBuilds"

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
  </PropertyGroup>
  <Target Name="GenerateVersionInfo" BeforeTargets="DispatchToInnerBuilds">
    <Exec Command="your custom command" />
  </Target>
</Project>

So my custom command is only exeuted once before every build. If you use InitialTargets, the command is executed more often than only once! For example if you save your project!

Generally answered 1/2, 2019 at 11:46 Comment(3)
This answer is a life saver !!! - very useful for invoking native compilation (in managed interop scenario) one can compile the native code only once and share it among .netcore and net "classic"Backward
DispatchToInnerBuilds seems to only work for projects with TargetFrameworks (it may still contain only one item though) and only when running the build from Visual Studio. From the command line, dotnet publish requires to specify a single framework and DispatchToInnerBuilds does not trigger.Decor
Very helpful answer. I needed a single target for both situations. See my answer below for an example if you do not know if the project will be multi targeted..Upbuild
T
5

While some targets only run in the inner builds, e.g. when you use BeforeTargets="BeforeBuild", the outer build also defines the IsCrossTargetingBuild variable to indicate that the currently running build is the outer build which dispatches to the inner build and is the preferred way to condition targets.

So you can condition your target like Condition="'$(IsCrossTargetingBuild)' == 'true'" to make sure the target is only run for the outer build.

Titustityus answered 11/10, 2017 at 4:8 Comment(2)
For those interested, IsCrossTargetingBuild is defined in Microsoft.Managed.Before.targets as '$(TargetFrameworks)' != '' and '$(TargetFramework)' == ''Decor
Could you, please, elaborate more on your solution? Would the target still be BeforeTargets="BeforeBuild" plus the condition, or would it be BeforeTargets="DispatchToInnerBuilds" plus the condition or any other combination? What if the project is multi-targeting but the build is invoked only for one target framework? Can you propose a solution that would work no matter if the builds are started from an IDE or CLI?Career
U
3

Very helpful answers here, but my situation requires a generic target that can be called by projects that have single or multiple TargetFrameworks. I came up with a Singleton target that runs once per project, before the builds, regardless of the number of TargetFrameworks. Single run for all of these cases:

  • <TargetFrameworks>netstandard2.0;net4.6</TargetFrameworks>
  • <TargetFrameworks>netstandard2.0</TargetFrameworks>
  • <TargetFramework>netstandard2.0</TargetFramework>

The key to this solution is to set a property (named IsCrossTargetingProject in the example below) that allows the individual builds to know if they are being launched by DispatchToInnerBuilds, and thus should only run in the outer build.

I could have used an example, so I decided to post it here for anyone else in this situation.

Example:

<Project>
    <PropertyGroup>
        <IsCrossTargetingProject Condition="$(TargetFrameworks) != ''">true</IsCrossTargetingProject>
    </PropertyGroup>

    <!--Only used to display properties to understand what the Singleton Target is doing.  Not required-->
    <Target Name="DebuggingDisplayProperties" BeforeTargets="DispatchToInnerBuilds;BeforeBuild">
        <Message Text="      ######################################################## Display Properties for $(MSBuildProjectName) $(TargetFramework)." Importance="high" />
        <Message Text="      IsCrossTargetingBuild: '$(IsCrossTargetingBuild)'" Importance="high" />
        <Message Text="      IsCrossTargetingProject: '$(IsCrossTargetingProject)'" Importance="high" />        
        <Message Text="      Compare => '$(IsCrossTargetingProject)' == '$(IsCrossTargetingBuild)'" Importance="high" />        
    </Target>
    
    <Target Name="SingletonTarget" Condition="'$(IsCrossTargetingProject)' == '$(IsCrossTargetingBuild)'" BeforeTargets="DispatchToInnerBuilds;BeforeBuild" >
        <!--Begin sample target body--> 
        <PropertyGroup Condition="'$(IsCrossTargetingBuild)' == ''">
            <CrossTargetMessage>$(TargetFramework)</CrossTargetMessage>
        </PropertyGroup>
        <PropertyGroup Condition="'$(IsCrossTargetingBuild)' == 'true'">
            <CrossTargetMessage>Multi-Targeted</CrossTargetMessage>
        </PropertyGroup>
        <Message Text="      ######################################################## Singleton Target for $(MSBuildProjectName) - $(CrossTargetMessage)." Importance="high" />
        <!--End sample target body--> 
    </Target>
</Project>

I have it in a separate file, and import it with the following line in my main project file:

<Import Project="..\Common\Singleton.targets" />
Upbuild answered 14/12, 2021 at 23:47 Comment(0)
E
2
Condition="'$(TargetFrameworks)' == '' OR $(TargetFrameworks.EndsWith($(TargetFramework)))" 
Evidentiary answered 18/1, 2020 at 22:37 Comment(1)
I think this is actually the solution for singleton-PostBuildEvent instead of a PreBuildEvent (because it runs on the last TargetFramework instead of the first). Anyway, that was what I was looking for, when google sent me here. So thanks.Wage
S
-2

The other way is use GenerateNuspec when you want to change the nuget file.

You will find the Foo output once when you package.

Simons answered 21/11, 2019 at 2:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.