Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option to run target once per project in multi-targeting build #2781

Open
iskiselev opened this issue Dec 7, 2017 · 7 comments
Open

Option to run target once per project in multi-targeting build #2781

iskiselev opened this issue Dec 7, 2017 · 7 comments
Labels

Comments

@iskiselev
Copy link

I'm trying to generate some additional sources for multi-targeting project (with TargetFrameworks).
Sources will be the same for both TargetFramework. When I attach my target with BeforeTargets to BeforeBuild, it will be executed twice - and with /m switch it may fail as both will try to generate same file.
I've tried to solve it by moving it to BeforeTargets DispatchToInnerBuilds and target really executed only once at correct time with 1-project solution. When solution contains more than one project and they reference, build could be started from ResolveReference target of other project. In that way it will be run in parallel with my source-generating target, which is not what I want.

Is there any way to run target only once in multi-targeting project and still be sure, that all compile-related task will be executed only after it?

I could provide sample application if it help.

@Adam-S-Daniel
Copy link
Contributor

A sample application would be very helpful, as would reviewing the issue title and other aspects of your write-up for conformance with the Creating New Issues guidance at https://github.com/Microsoft/msbuild/wiki/Contributing-Code.

@iskiselev iskiselev changed the title Multi-targeting build run target once Option to run target once per project in multi-targeting build Dec 7, 2017
@iskiselev
Copy link
Author

iskiselev commented Dec 7, 2017

Looks like I was able to find a solution - but it looks over-complicated, so let me leave this issue open.
I have a target that generate c# sources from other file, to be a little bit more concrete, let's say we run XSD tool. C# source are not committed in repository, as they should be auto-generated in that case.
So, it looks something like:

  <ItemGroup>
    <None Include="Sample.xsd" />
    <Compile Remove="Sample.cs" /> <!--If it was included already, remove it to avoid duplicate -->
    <Compile Include="Sample.cs" />  <!--Include it always, even if files don't exist -->
      <DependentUpon>Sample.xsd</DependentUpon>
    </Compile>
  </ItemGroup>

  <!--It could be optional BeforeTargets="CompileDesignTime;BeforeBuild", to allow VS generate it on first project openning-->
  <Target Name="GenerateXsd" BeforeTargets="BeforeBuild" Inputs="Sample.xsd" Outputs="Sample.cs">
    <PropertyGroup>
      <GenerateXsd_Command>"$(SDK40ToolsPath)xsd.exe" "Sample.xsd" /nologo /c  </GenerateXsd_Command>
    </PropertyGroup>
    <Exec Command="$(GenerateXsd_Command)" ConsoleToMSBuild="true" />
  </Target>

It works great, until we enable multi-targeting with something like:

<TargetFrameworks>net40;netstandard2.0</TargetFrameworks>

Now BeforeTargets will be executed twice - for net40 build and netstandard2.0 build. It is problem, as now we have race condition when two xsd tries to generate same file and one of it could not access it, because other locks it.

My naive attempt to solve it was changing it to BeforeTargets="DispatchToInnerBuilds". It works great, while we have only one project, but if we try to build solution, which contains project, referencing us, our build could be executed from ResolveReference target of other project, which will start inner-loop build for our project - DispatchToInnerBuilds will be executed in parallel in this case, so we run compilation now before we was able to generate source.

Final working solution looks next:

  <!--It could be optional BeforeTargets="CompileDesignTime", to allow VS generate it on first project openning-->
  <Target Name="GenerateXsdInner" Inputs="Sample.xsd" Outputs="Sample.cs">
    <PropertyGroup>
      <GenerateXsd_Command>"$(SDK40ToolsPath)xsd.exe" "Sample.xsd" /nologo /c</GenerateXsd_Command>
    </PropertyGroup>
    <Exec Command="$(GenerateXsd_Command)" ConsoleToMSBuild="true" />
  </Target>
  <Target Name="GenerateXsd" BeforeTargets="DispatchToInnerBuilds;BeforeBuild">
    <!--TargetFramework=once is critical here, as it allow will not execute task from same project with same properties twice. 
    We need to unify TargetFramework between empty, net40 and netstandard2.0-->
    <MSBuild Projects="$(MSBuildProjectFile)" Targets="GenerateXsdInner" Properties="TargetFramework=once" />
  </Target>

I'm interested, if I missed some way to make it simpler?

@kkm000
Copy link
Contributor

kkm000 commented Dec 10, 2017

When you are in the outer project, '$(TargetFramework)' == ''. In an inner project, it will be set (via the outer build, or if build directly as a dependency in VS, as you noted). Also. there is a variable set to 'true' in an inner build only, I do not remember the name, but I think it is called InnerBuild. Can this help you?

You can build the intermediate .cs in both inner and outer projects, but make sure to have up-to-date check via target's Inputs and Outputs. If the file was generated through the outer build, there will be no race in the inners as up-to-date check will skip the targets. If it is not up-to-date, then you are invoked on a single framework (e. g. msbuild /p:TargetFramework=net40), and it's safe to build. That is what I do.

I was totally wrong. There is no consistent way to hook up into the outer build. DispatchToInnerBuilds is currently your best bet, you are correct.

Another option, if the code generator is trivially quick and you treat the source as transient, output the file to the $(IntermediateOutputPath) (check spelling). This path includes the TFM, like obj\Release\net40\, so you avoid any races.

@lindexi
Copy link

lindexi commented Nov 21, 2019

How the pakcage nuget tagert?

@SimonCropp
Copy link
Contributor

Condition="'$(TargetFrameworks)' == '' OR $(TargetFrameworks.EndsWith($(TargetFramework)))" 

@SacredGeometer
Copy link

Condition="'$(TargetFrameworks)' == '' OR $(TargetFrameworks.EndsWith($(TargetFramework)))" 

This will avoid running the target altogether on the other frameworks, which can have side effects like an inner build for a framework racing ahead of the other framework generating the file and failing at a later target because the files don't exist yet.

What is really needed here is a target property that indicates that all inner builds should serialize on that target.

Scottj1s added a commit to microsoft/CsWinRT that referenced this issue Jun 15, 2020
…eAssemblyReferences (see also dotnet/msbuild#2781).  Since cswinrt.exe is moving to forked code gen based on TFM, will just adopt that now, and resolve build breaks.
Scottj1s added a commit to microsoft/CsWinRT that referenced this issue Jun 15, 2020
…eAssemblyReferences (see also dotnet/msbuild#2781).  Since cswinrt.exe is moving to forked code gen based on TFM, will just adopt that now, and resolve build breaks. (#315)
@chm-tm
Copy link

chm-tm commented Aug 3, 2021

I tried the work around given by @iskiselev, but it doesn't work when the targets are inside a NuGet or any NuGet contributes .targets or .props . This is since the NuGet targets won't be imported without matching TargetFramework.

This can be fixed by specifying an existing TargetFramework:

    <PropertyGroup>
      <FirstTargetFramework Condition=" '$(TargetFrameworks)' == '' ">$(TargetFramework)</FirstTargetFramework>
      <FirstTargetFramework Condition=" '$(FirstTargetFrameworks)' == '' ">$(TargetFrameworks.Split(';')[0])</FirstTargetFramework>
    </PropertyGroup>
    <MSBuild Projects="$(MSBuildProjectFile)" Targets="GenerateXsdInner" Properties="TargetFramework=$(FirstTargetFramework)" />

I also vote for providing a mechanism which is easier to use.

zachf496 added a commit to zachf496/C-sharp-WinRT-stack that referenced this issue Aug 23, 2022
…eAssemblyReferences (see also dotnet/msbuild#2781).  Since cswinrt.exe is moving to forked code gen based on TFM, will just adopt that now, and resolve build breaks. (#315)
@AR-May AR-May added the triaged label Feb 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

8 participants