msbuild Directory.build.props cascade per project? - msbuild

Executive Summary: I want to set properties in property groups based on conditions that are present only late in the build pipeline and am looking for a way to solve this earlier.
I have a fairly simple Directory.build.props file
<Project>
<PropertyGroup>
<MyMode>Default</MyMode>
</PropertyGroup>
<!-- This one overrides the default group above -->
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<MyMode>Changed to Debug</MyMode>
</PropertyGroup>
<!-- This one is not applied -->
<PropertyGroup Condition=" '$(TargetFrameworkVersion)' == 'v4.7.2' ">
<MyMode>Framework</MyMode>
</PropertyGroup>
<Target Name="Stats" AfterTargets="Build">
<Message Importance="High" Text="::::: Mode set to $(MyMode)" />
<Message Importance="High" Text="::::: Target Framework set to $(TargetFrameworkVersion)" />
</Target>
</Project>
And a simple project structure
E:.
│ Directory.build.props
│ MSBuild_Test.sln
│
├───ConsoleAppNet
│ App.config
│ ConsoleAppNet.csproj
│ Program.cs
│
└───MSBuild_Test
Class1.cs
LibStandard.csproj
LibStandard is a .net standard library, ConsoleAppNet is a .net framework project which also has a build dependency to LibStandard
When I execute the msbuild script above I get this output
LibStandard -> E:\temp\MSBuild_Test\MSBuild_Test\bin\Debug\netstandard2.0\LibStandard.dll
::::: Mode set to Changed to Debug
::::: Target Framework set to v2.0
ConsoleAppNet -> E:\temp\MSBuild_Test\ConsoleAppNet\bin\Debug\ConsoleAppNet.exe
::::: Mode set to Changed to Debug
::::: Target Framework set to v4.7.2
As you can see, the console output should have triggered the property group with the condition resulting in MyMode being Framework, but it did not work out. This one was never matched:
<PropertyGroup Condition=" '$(TargetFrameworkVersion)' == 'v4.7.2' ">
<MyMode>Framework</MyMode>
</PropertyGroup>
Is there a good way to apply PropertyGroups during load based on the condition above?
I am aware that I can place PropertyGroup overrides in a Target, e.g.:
<Target Name="TooLate" BeforeTargets="BeforeBuild" Condition=" '$(TargetFrameworkVersion' == 'v4.7.2' ">
<PropertyGroup >
<MyMode>Framework</MyMode>
</PropertyGroup>
</Target>
and it also gets executed correctly but at this point in time I cannot set important other variables.
My intention is to redirect output directories based on different conditions. When I set $(OutputPath) in a target, it is already too late. The project ignores this output for the entire build of this project:
<Target Name="TooLate" BeforeTargets="BeforeBuild" Condition=" '$(TargetFrameworkVersion)' == 'v4.7.2' ">
<PropertyGroup >
<OutputPath>New_Output_Directory</OutputPath>
</PropertyGroup>
</Target>
I can even echo the OutputPath variable and it points to the correct value but the build uses the old value and not redirecting the output.

High five me, I found the solution for all the coming up Samuels asking about the same issue.
Quick answer
At the time of import of the Directory.build.props no other properties (e.g TargetFramework) are already imported and will default to empty. This is why the checks on them fail. Use Directory.build.targets instead!
Directory.build.props imported very early, allowing you to set properties at the beginning
Directory.build.targets imported very late, allowing you to customize the build chain
Resources
Here are some very useful pages regarding msbuild
Explanation of available targets
How to customize your build
Explanation
Here is a quote from the paragraph on the customization page (so long the current documents are alive ...)
Import order
Directory.Build.props is imported very early in
Microsoft.Common.props, and properties defined later are unavailable
to it. So, avoid referring to properties that are not yet defined (and
will evaluate to empty).
Directory.Build.targets is imported from Microsoft.Common.targets
after importing .targets files from NuGet packages. So, it can
override properties and targets defined in most of the build logic,
but sometimes you may need to customize the project file after the
final import.
By reading this the implication is somewhat fuzzy about the targets but Directory.Build.targets is the best place to override properties and use conditional checks.

Related

Condition on "PropertyGroup" in Directory.build.props not working

I've created a Directory.build.props file so I can set the C# language version in there.
But I also have Visual Basic Projects, so i wanted to limit the setting to C# projects.
<Project>
<PropertyGroup Condition="'$(ProjectExt)'=='.csproj'">
<LangVersion>7.2</LangVersion>
</PropertyGroup>
</Project>
But my project is not loading it / the UI is not displaying the language version 7.2.
I've tried to apply the same condition inside the csproj file, also not working.
<PropertyGroup>
<LangVersion Condition="'$(ProjectExt)'=='.csproj'">7.2</LangVersion>
</PropertyGroup>
However, this will work:
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Message Text="Condition working" Importance="high" Condition="'$(ProjectExt)'=='.csproj'"/>
</Target>
The build will output my message
Why is the condition not working on my LanguageVersion? Any Clues?
You will need to use a property to condition on that is available very early in the build. In your case, you should condition on MSBuildProjectExtension:
<PropertyGroup>
<LangVersion Condition="'$(MSBuildProjectExtension)'=='.csproj'">7.2</LangVersion>
</PropertyGroup>
See MSBuild reserved and well-known properties for the complete set of available properties.
ProjectExt is only defined late in the build definition and is therefore not available in Directory.Build.props, which is imported very early into the project.

MSBuild, OutputPath to a lib directory is not honoured

I spent hours now but I simply don't get it:
Why is a lib sub directory not honoured by the VS "fast up-to-date check"?
If a lib output dir for libraries is set, the solution is always rebuild - if changes have been made or not does not matter. If \lib sub dir is removed it works. Why?
Here is what I tested so far:
Refer to the next code snippet. That one works perfectly. If several dependent project are asked to build multiple times they actually build only once if no changes have been made. The Visual Studio FastUpToDateCheck kicks in.
But if you change the line
<OutputPath>$(SolutionDir)bin\$(Configuration)\$(Platform)</OutputPath>
to
<OutputPath>$(SolutionDir)bin\$(Configuration)\$(Platform)\lib\</OutputPath>
it constantly rebuilds. Any ideas why?
ComponentBuild.props located next to .sln file
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<IntermediateOutputPath>$(SolutionDir)obj\$(Configuration)\$(MSBuildProjectName)\</IntermediateOutputPath>
<UseCommonOutputDirectory>False</UseCommonOutputDirectory>
<DisableFastUpToDateCheck>false</DisableFastUpToDateCheck>
</PropertyGroup>
<PropertyGroup Condition=" '$(OutputType)' == 'Library' ">
<!-- To distinguish by \lib\ does not work, a rebuild is triggered since the up-to-date check fails -->
<!-- <OutputPath>$(SolutionDir)bin\$(Configuration)\$(Platform)\lib\</OutputPath> -->
<OutputPath>$(SolutionDir)bin\$(Configuration)\$(Platform)</OutputPath>
<OutDir>$(OutputPath)</OutDir>
</PropertyGroup>
<PropertyGroup Condition=" '$(OutputType)' == 'Exe' ">
<OutputPath>$(SolutionDir)bin\$(Configuration)\$(Platform)\</OutputPath>
<OutDir>$(OutputPath)</OutDir>
</PropertyGroup>
</Project>
The file is included in csproj files just before Import Microsoft.CSharp.targets:
.csproj file:
<!-- position of include is important, OutputType of project must be defined already -->
<Import Project="$(SolutionDir)ComponentBuild.props" Condition="Exists('$(SolutionDir)ComponentBuild.props')" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent>
</PostBuildEvent>
</PropertyGroup>
The behaviour becomes more weird, the more I test.
I created two simple library projects A and B. B depends on A. I added above mentioned import and the FastUpToDateCheck works.
After adding lib path to the library outputtype, it works when nothing else is changed. But when lib B project is cleaned, every subsequent builds do rebuild project B.
When adding lib path to the exe outputtype as well. The FastUpToDateCheck works again.
Then I removed the lib path again from output type exe, but the FastUpToDateCheck surprisingly still works - always. Even when cleaning the build, changing a class or deleting all obj and bin folders.
BUT as soon as I removed the lib path from the lib outputtype as well, i.e. I set back all to the original state, it FAILS. It rebuilds every time. The first line of the diagnostic output is
Project 'ClassLibrary1' is not up to date. Missing output file
'c:\Users\hg348\Documents\Visual Studio
2015\Projects\BuildTest\bin\Debug\AnyCPU\lib\ClassLibrary1.dll'
It still looks into lib path even though it isn't set any more.
I think there is some nasty caching involved.
Can someone please verify this?
Well, my tests as described above lead to the answer:
It is caching in Visual Studio (VS) which triggers the builds after changing the output path. After making changes to the outputpath and probably outdir as well, Visual Studio still looks in the old directory for its FastUpToDateCheck.
Closing and reopening the Solution helps already to clear the VS cache. In some cases it is necessary to delete the hidden file .suo in hidden folder .vs
This solves all problems stated in the sample file given in the question above.
My final import file looks like this:
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Note that VS caches settings, to be sure the FastUpToDateCheck works
* reopen the solution after
- changing properties
- after adding a platform config
- after adding references to projects
* close VS and remove the hidden file
<solution folder>\.vs\<solution name>\v14\.suo after changing IntermediateOutputPath,
(You have to enable "how hidden files" in windows file explorer)
* After updating App.config do a random change in any .cs source file too,
otherwise FastUpToDateCheck fails
-->
<PropertyGroup>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<IntermediateOutputPath>$(SolutionDir)obj\$(Configuration)\$(Platform)\$(MSBuildProjectName)\</IntermediateOutputPath>
<!-- if true, don't copy output files of referenced assemblies, since everything builds to the same folder. -->
<UseCommonOutputDirectory>true</UseCommonOutputDirectory>
<DisableFastUpToDateCheck>false</DisableFastUpToDateCheck>
</PropertyGroup>
<PropertyGroup Condition=" '$(OutputType)' == 'Library' ">
<OutputPath>$(SolutionDir)bin\$(Configuration)\$(Platform)\lib\</OutputPath>
<OutDir>$(OutputPath)</OutDir>
</PropertyGroup>
<PropertyGroup Condition=" '$(OutputType)' == 'Exe' ">
<OutputPath>$(SolutionDir)bin\$(Configuration)\$(Platform)\</OutputPath>
<OutDir>$(OutputPath)</OutDir>
</PropertyGroup>
<!-- sets "Copy Local" property of references to false on reopen of solution
don't copy output files of referenced assemblies, since everything builds to the same folder -->
<ItemDefinitionGroup>
<Reference>
<Private>False</Private>
</Reference>
<ProjectReference>
<Private>False</Private>
</ProjectReference>
</ItemDefinitionGroup>
</Project>

How to include an item in BuiltProjectOutputGroup

I have a custom project system, that uses the standard net sdk targets.
During the build, I produce an extra zip file. I'd like this extra file to be included in an output group, so that when I query my projects output groups (from vs) it shows up.
My project file looks like this:
<Project Sdk="Microsoft.NET.Sdk">
... stuff
<ItemGroup>
<PackageReference Include="DnnVsProjectSystem.BuildTools" Version="0.0.5">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
</ItemGroup>
<Import Project="$(CustomProjectExtensionsPath)DnnVsProjectSystem.targets"/>
</Project>
Notice, I am using the "sdk" attribute, which is a fairly new feature of msbuild.
The PackageReference that you see, is a nuget package that imports a .props and a .targets which augment the build with some custom build tasks. These are the ones that produce the zip file.
I have drilled into the net sdk targets and found this:
<Target Name="AllProjectOutputGroups" DependsOnTargets="
BuiltProjectOutputGroup;
DebugSymbolsProjectOutputGroup;
DocumentationProjectOutputGroup;
SatelliteDllsProjectOutputGroup;
SourceFilesProjectOutputGroup;
ContentFilesProjectOutputGroup;
SGenFilesOutputGroup" />
<!--
This is the key output for the BuiltProjectOutputGroup and is meant to be read directly from the IDE.
Reading an item is faster than invoking a target.
-->
<ItemGroup Condition=" '$(OutputType)' != 'winmdobj' ">
<BuiltProjectOutputGroupKeyOutput Include="#(IntermediateAssembly->'%(FullPath)')">
<IsKeyOutput>true</IsKeyOutput>
<FinalOutputPath>$(TargetPath)</FinalOutputPath>
<TargetPath>$(TargetFileName)</TargetPath>
<COM2REG Condition="'$(RegisterForComInterop)'=='true' and '$(OutputType)'=='library'">true</COM2REG>
</BuiltProjectOutputGroupKeyOutput>
</ItemGroup>
<ItemGroup Condition=" '$(OutputType)' == 'winmdobj' ">
<WinMDExpOutputWindowsMetadataFileItem Include="$(_IntermediateWindowsMetadataPath)" Condition="'$(_IntermediateWindowsMetadataPath)' != ''" />
<BuiltProjectOutputGroupKeyOutput Include="#(WinMDExpOutputWindowsMetadataFileItem->'%(FullPath)')">
<IsKeyOutput>true</IsKeyOutput>
<FinalOutputPath>$(TargetPath)</FinalOutputPath>
<TargetPath>$(TargetFileName)</TargetPath>
</BuiltProjectOutputGroupKeyOutput>
</ItemGroup>
This appears to be the target that is called by VS, when it wants information about output groups.
The problem is, i am not sure how I can get my item included in one of those output groups, as If i just add the item to the item group, in my own targets - my targets are irrelevent at this point, as they are not included in this dependency chain.
I also can't override any of the targets, because, as i'm using the sdk attribute, it looks like the sdk targets will always be imported last, overwriting anything that I declare.
Any guidance much appreciated.
If your only concern is to hook into the target or its dependency chain, I suggest using msbuild's BeforeTargets functionality:
<Target Name="IncludeMyCustomOutputGroup" BeforeTargets="AllProjectOutputGroups" DependsOnTargets="ResolveMyCustomPropertiesAndItems">
<ItemGroup>
<!-- Assuming #(MyCustomOutput) items are generated by your ResolveMyCustomPropertiesAndItems target, or just add anything else -->
<BuiltProjectOutputGroupKeyOutput Include="#(MyCustomOutput->'%(FullPath)')">
<IsKeyOutput>true</IsKeyOutput>
<FinalOutputPath>$(TargetPath)</FinalOutputPath>
<TargetPath>$(TargetFileName)</TargetPath>
</BuiltProjectOutputGroupKeyOutput>
</ItemGroup>
</Target>

Accessing project properties from an external msbuild targets file

I have a common compile.targets file that is used to build many different solutions. In that file I would like to check if any of the contained projects are using TypeScript, and if they are, verify that certain properties are set in those projects (i.e. TypeScriptNoImplicitAny). I currently build the solution like so:
<Target Name="my-compile-target">
<MSBuild Projects="%(SolutionFile.FullPath)"/>
</Target>
What I would like is to be able to create a task that is run for each .csproj in the solution, that has access to all the properties set in the .csproj.
Is this possible? One workaround I tried was using the BeforeTargets attribute along with the targets I know are used to compile TypeScript:
<Target Name="check-typescript-options" BeforeTargets="CompileTypeScript;CompileTypeScriptWithTSConfig">
<Message Condition="'$(TypeScriptToolsVersion)' != ''" Text="Tools Version is $(TypeScriptToolsVersion)"></Message>
<Message Condition="'$(TypeScriptToolsVersion)' == ''" Text="No tools version found"></Message>
</Target>
Unfortunately, MSBuild gives me a warning that those TypeScript targets do not exist, and thus check-typescript-options is ignored.
As you say you need to "run" in the context of the individual csproj files. To do this, given your setup I would set one of two properties
CustomAfterMicrosoftCSharpTargets or CustomAfterMicrosoftCommonTargets
The easiest way would be to set these like so
<Target Name="my-compile-target">
<MSBuild Projects="%(SolutionFile.FullPath)"
Properties="CustomAfterMicrosoftCSharpTargets=$(PathToCustomTargetProj);" />
</Target>
In this case you would have $(PathToCustomTargetProj) set to some file path which has targets which run within the csproj pipeline. You could set this to compile.targets and then your check-typescript-options will be called as the BeforeTargets will be evaluated and satisfied correctly.

How does ResolveProjectReferences work?

I want to profile and tweak our build hoping to save few seconds here and there. I was able to create a task that derives from ResolveAssemblyReferences and use it instead, but I'm having problems in understanding the following (from Microsoft.Common.targets):
<!--
============================================================
ResolveProjectReferences
Build referenced projects:
[IN]
#(NonVCProjectReference) - The list of non-VC project references.
[OUT]
#(_ResolvedProjectReferencePaths) - Paths to referenced projects.
============================================================
-->
<Target
Name="ResolveProjectReferences"
DependsOnTargets="SplitProjectReferencesByType;_SplitProjectReferencesByFileExistence">
<!--
When building this project from the IDE or when building a .SLN from the command-line,
just gather the referenced build outputs. The code that builds the .SLN will already have
built the project, so there's no need to do it again here.
The ContinueOnError setting is here so that, during project load, as
much information as possible will be passed to the compilers.
-->
<MSBuild
Projects="#(_MSBuildProjectReferenceExistent)"
Targets="GetTargetPath"
BuildInParallel="$(BuildInParallel)"
UnloadProjectsOnCompletion="$(UnloadProjectsOnCompletion)"
Properties="%(_MSBuildProjectReferenceExistent.SetConfiguration); %(_MSBuildProjectReferenceExistent.SetPlatform)"
Condition="'#(NonVCProjectReference)'!='' and ('$(BuildingSolutionFile)' == 'true' or '$(BuildingInsideVisualStudio)' == 'true' or '$(BuildProjectReferences)' != 'true') and '#(_MSBuildProjectReferenceExistent)' != ''"
ContinueOnError="!$(BuildingProject)">
<Output TaskParameter="TargetOutputs" ItemName="_ResolvedProjectReferencePaths"/>
</MSBuild>
<!--
Build referenced projects when building from the command line.
The $(ProjectReferenceBuildTargets) will normally be blank so that the project's default
target is used during a P2P reference. However if a custom build process requires that
the referenced project has a different target to build it can be specified.
-->
<MSBuild
Projects="#(_MSBuildProjectReferenceExistent)"
Targets="$(ProjectReferenceBuildTargets)"
BuildInParallel="$(BuildInParallel)"
UnloadProjectsOnCompletion="$(UnloadProjectsOnCompletion)"
Condition="'#(NonVCProjectReference)'!='' and '$(BuildingInsideVisualStudio)' != 'true' and '$(BuildingSolutionFile)' != 'true' and '$(BuildProjectReferences)' == 'true' and '#(_MSBuildProjectReferenceExistent)' != ''">
<Output TaskParameter="TargetOutputs" ItemName="_ResolvedProjectReferencePaths"/>
</MSBuild>
<!--
Get manifest items from the (non-exe) built project references (to feed them into ResolveNativeReference).
-->
<MSBuild
Projects="#(_MSBuildProjectReferenceExistent)"
Targets="GetNativeManifest"
BuildInParallel="$(BuildInParallel)"
UnloadProjectsOnCompletion="$(UnloadProjectsOnCompletion)"
Properties="%(_MSBuildProjectReferenceExistent.SetConfiguration); %(_MSBuildProjectReferenceExistent.SetPlatform)"
Condition="'#(NonVCProjectReference)'!='' and '$(BuildingProject)'=='true' and '#(_MSBuildProjectReferenceExistent)'!=''">
<Output TaskParameter="TargetOutputs" ItemName="NativeReference"/>
</MSBuild>
<!-- Issue a warning for each non-existent project. -->
<Warning
Text="The referenced project '%(_MSBuildProjectReferenceNonexistent.Identity)' does not exist."
Condition="'#(NonVCProjectReference)'!='' and '#(_MSBuildProjectReferenceNonexistent)'!=''"/>
</Target>
Some parameters are passed and some are returned, but where does the actual work happen? There isn't much on msdn - I've found Microsoft.Build.Tasks.ResolveProjectBase, but it's of not much use.
ResolveProjectReferences (at least the one you're pointing to) is a target that is used to resolve inter-project references by building them using the <MSBuild> task. This task takes a project file to build, as well as the names of one or more targets in the project that should be invoked as part of the build (it also takes other parameters, but you can ignore those for now).
Consider the following target:
<Target
Name="Build"
Returns="#(BuildOutput)">
<ItemGroup>
<BuildOutput Include="bin\Debug\Foo.exe" />
</ItemGroup>
</Target>
If you referenced a project containing this target, and wanted to resolve the "Foo" target's outputs, you would have a <ProjectReference> element in your project like so:
<ItemGroup>
<ProjectReference Include="..\SomeProject\SomeProject.proj">
<Targets>Build</Targets>
</ProjectReference>
</ItemGroup>
Note that, if "Build" is the default target for the referenced project, you could leave the "Targets" metadata off entirely. You can also specify multiple targets in the Targets metadata (a semicolon-delimited list).
So your ResolveProjectReferences target will come along and call the <MSBuild> task, passing it "..\SomeProject\SomeProject.proj" and asking it to build the "Build" target. Now, since the "Build" target specifies outputs via its Returns attribute (but the Outputs attribute will be used if the Returns attribute is not specified), these outputs will be harvested during the build, and returned at the <MSBuild> tasks's TargetOutputs parameter. They have several additional pieces of metadata added which enable you to segregate them by originating target. These include:
MSBuildSourceProjectFile - the referenced project whose build generated the output
MSBuildSourceTargetName - the name of the target whose build generated the output
If you're working inside a C# project, there are a bunch of other stages of reference resolution (including assembly resolution). Drop me a line if you want to know about these.