-
-
Save KirillOsenkov/f20cb84d37a89b01db63f8aafe03f19b to your computer and use it in GitHub Desktop.
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<OutputType>Exe</OutputType> | |
<TargetFramework>net472</TargetFramework> | |
</PropertyGroup> | |
<PropertyGroup> | |
<GeneratedText><![CDATA[ | |
using System%3B | |
public class Hello$(TargetFramework) | |
{ | |
public void Print() | |
{ | |
Console.WriteLine("Hello $(TargetFramework)!")%3B | |
} | |
} | |
]]></GeneratedText> | |
</PropertyGroup> | |
<Target Name="AddGeneratedFile" BeforeTargets="BeforeCompile;CoreCompile" Inputs="$(MSBuildAllProjects)" Outputs="$(IntermediateOutputPath)GeneratedFile.cs"> | |
<PropertyGroup> | |
<GeneratedFilePath>$(IntermediateOutputPath)GeneratedFile.cs</GeneratedFilePath> | |
</PropertyGroup> | |
<ItemGroup> | |
<Compile Include="$(GeneratedFilePath)" /> | |
<FileWrites Include="$(GeneratedFilePath)" /> | |
</ItemGroup> | |
<WriteLinesToFile Lines="$(GeneratedText)" File="$(GeneratedFilePath)" WriteOnlyWhenDifferent="true" Overwrite="true" /> | |
</Target> | |
</Project> |
BeforeTargets
of the target should be BeforeCompile
, not CoreCompile
. This is important for Source Link to work correctly. See
dotnet/sourcelink#392 (comment)
Similar to what @jcansdale said, if you wanted to avoid duplication you could introduce another Target that runs before AddGeneratedFile that would ensure IntermediateOutputPath is available
Nope, at that time the $(IntermediateOutputPath) isn't set yet. It will be set in the Targets file (vs. the Props file), which gets imported at the end of the file logically.
I thought there was likely a reason you were doing that. When I tried extracting GeneratedFilePath
, it appeared to work but indeed the file is being generated in the wrong place (root of the project not the intermediate path). 😄
A few tweaks:
- Hook
BeforeCompile
as @tmat pointed out, to avoid getting between targets that need to know what's going into the compiler and the compiler itself (Source Link is just one such example). - Add generated files to the
@(FileWrites)
item, so they get included inClean
. - When reading properties in the target, any imported build logic can change them, so using
$(MSBuildAllProjects)
is usually more accurate than$(MSBuildThisFileFullPath)
(in this specific example that's not really a problem, but if you had$(Whatever)
in the generated output (or input to a task) it can be relevant.
BeforeCompile
is not called when XAML does its compilation passes (AFAIK), so if you just hook into that but not CoreCompile
, it will likely fail the build as soon as you add a Page
or any other WPF/UWP XAML file that requires compiling the temporary assembly...
@rainersigwald, @kzu makes a good point.
The GenerateAssemblyInfo
in the SDK is also hooked up before CoreCompile
specifically to make XAML work:
https://github.com/dotnet/sdk/blob/2eb6c546931b5bcb92cd3128b93932a980553ea1/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.GenerateAssemblyInfo.targets#L39-L49
Would it work if the target specified BeforeTargets="BeforeCompile;CoreCompile"
?
Yes, BeforeTargets="A;B"
means "before whichever of A
or B
runs first".
@rainersigwald then I think we should change the GenerateAssemblyInfo
to do that. @nguerrera
Thanks all!
Made the updates:
BeforeCompile;CoreCompile
Inputs="$(MSBuildAllProjects)"
<FileWrites Include="$(GeneratedFilePath)" />
-
public void Print()
should bestatic void Main()
because the project is anExe
. This makes it easier to test. 😉 -
This covers the case where the project file changes, but what if
$(UserName)
changes?
Inputs="$(MSBuildAllProjects)" Outputs="$(IntermediateOutputPath)GeneratedFile.cs"
Using something like this seems to work:
<Target Name="AddGeneratedFile" BeforeTargets="BeforeCompile;CoreCompile" Inputs="$(MSBuildAllProjects)" Outputs="$(IntermediateOutputPath)GeneratedFile.$(UserName).cs">
<PropertyGroup>
<GeneratedFilePath>$(IntermediateOutputPath)GeneratedFile.$(UserName).cs</GeneratedFilePath>
</PropertyGroup>
That way we have a unique output file for all $(UserName)
strings. Without this, even rebuilding the project doesn't appear to regenerate the file. 😕
- I wonder if
$(MSBuildAllProjects)
is overkill and we should simply use$(MSBuildThisFileFullPath)
? The project is pretty much self contained.
Let's not use $(UserName)
in the sample - we don't want our sample build to be non-deterministic!
Could you rename the sample file to AddGeneratedFile.csproj
?
People can then use this option at the top to open with Visual Studio.
The .csproj
can then be build in the Solution Explorer - Folder view
like this:
Unfortunately Visual Studio doesn't recognize .proj
files.
If there's a build error, you might need to dotnet restore
in the project folder. 😞
Let's not use $(UserName) in the sample - we don't want our sample build to be non-deterministic!
Aren't people most likely going to want to pass something that might change? Wouldn't it be better to show a strategy for dealing with this?
Making the build depend on the build environment or the state of the build machine is generally not a good practice. This means that your CI is not building the same thing that you're testing on your dev box and two CI machines might also be building different things. See https://en.wikipedia.org/wiki/Reproducible_builds
Much better example would be to read some information from a text file that's checked in the repository and generate C# code based on that information. In this case you can e.g. use that text file as an input that the task depends on.
Having extra inputs is a good idea, but it needs to be a different sample. This sample is about literally a one-liner that comes from somewhere that's not a file (think Git commit SHA for instance). UserName is just for illustrative purposes.
Then use something like $(TargetFramework)
to avoid reading env variable.
Having extra inputs is a good idea, but it needs to be a different sample. This sample is about literally a one-liner that comes from somewhere that's not a file (think Git commit SHA for instance). UserName is just for illustrative purposes.
A Git commit SHA is a good example. In that case, I think you would need something like this:
<Target Name="AddGeneratedFile" BeforeTargets="BeforeCompile;CoreCompile"
Inputs="$(MSBuildAllProjects)" Outputs="$(IntermediateOutputPath)GeneratedFile.$(GitCommitSHA).cs">
<PropertyGroup>
<GeneratedFilePath>$(IntermediateOutputPath)GeneratedFile.$(GitCommitSHA).cs</GeneratedFilePath>
</PropertyGroup>
If you don't include the $(GitCommitSHA)
in the path, the GeneratedFile.cs
will get stuck on the first commit.
or you could just remove the Inputs and Outputs so the target always runs
or you could just remove the Inputs and Outputs so the target always runs
Will this mean the project is always dirty or will it only run when other files are out of date?
Will this mean the project is always dirty or will it only run when other files are out of date?
The answer to that is complex.
MSBuild itself will always build all the targets in a project, possibly skipping them at the target level based on target inputs and outputs.
Visual Studio, however, does not always invoke MSBuild for a project. When VS is asked to build, it delegates that operation to each project's project system. That project system can then decide whether to report success (because it thinks the project is up to date) or actually invoke a build operation. C# projects use one of two project systems, both of which have the concept of a "fast up-to-date check". That is essentially a big list of all of the inputs to any target in the project and all of its outputs--if any input is newer than any output, the project system will invoke MSBuild, which will then build using its own target incrementality. There are documented extension points for extending project-system understanding of project inputs and outputs.
So for your question, if you'r talking about VS scenarios, the project won't always be considered out of date because it won't (by default) know about the input(s) to the nonincremental target.
possibly skipping them at the target level based on target inputs and outputs.
Would this explain why the following generates GeneratedFile.cs
the first time, but never regenerates it, even after a Rebuild
?
<Target ... Inputs="$(MSBuildAllProjects)" Outputs="$(IntermediateOutputPath)GeneratedFile.cs">
Does Rebuild
force the up-to-date check to return false, but MSBuild decides the target still doesn't need to execute because GeneratedFile.cs
was updated more recently than $(MSBuildAllProjects)
?
Rebuild
does Clean;Build
. I think in that case, because the generated file isn't added to @(FileWrites)
, it's not getting deleted/cleaned up on Clean
, so Rebuild
doesn't reset its state.
Incremental build for targets is unconfigurable: it's always on.
A log at detailed
or diagnostic
level should show the target being skipped as up to date.
Nope, at that time the $(IntermediateOutputPath) isn't set yet. It will be set in the Targets file (vs. the Props file), which gets imported at the end of the file logically. That's why I had to duplicate it, otherwise I would have to manually import Sdk.targets and set the property after that.