How does ResolveProjectReferences work? - msbuild

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.

Related

MSBuild: How to get custom generated files after regular build process to be treated as content build output

I'm using MSBuild SDK style projects with VS 2019. I'm trying to run a custom file generation tool which depends on the output of the build of the current project. The files should be treated as if it was regular content for which CopyToOutputDirectory is set. In dependent projects I expect the files to be part of the output directory as well. The solution I now have works, but not from clean builds, which is obviously not acceptable.
I currently have this in the project file:
<Target Name="Generation" AfterTargets="AfterBuild">
<Exec Command="GeneratedFiles" />
<ItemGroup>
<Content Include="$(TargetDir)\GeneratedFiles.*.xml">
<TargetPath>GeneratedFiles\%(Filename)%(Extension)</TargetPath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Target>
This works, but only for non-clean builds.
The reason the Generation target doesn't work is because the logic that performs the copies, based on the presence and value of the CopyToOutputDirectory metadata, runs as part of the 'CoreBuild'. The Generation target has AfterTargets="AfterBuild". It runs after the build (and the copying) has already been completed.
The Generation target doesn't work for the first build and it doesn't work for subsequent incremental builds either.
The question description says that the files are copied for an incremental build. While it may be true that the files are copied, they can't be getting copied because of the Generation target. Without seeing the complete project file, I assume there is another place in the project where the CopyToOutputDirectory metadata is being set for the files.
To have the Generation target run after compilation and before files are copied, the following can be used:
<Target Name="Generation" AfterTargets="AfterCompile" BeforeTargets="GetCopyFilesToOutputDirectoryItems">
The GetCopyFilesToOutputDirectoryItems is publicly documented and it runs before another publicly documented target named CopyFilesToOutputDirectory. The CopyFilesToOutputDirectory is defined with a DependsOnTargets attribute which runs a set of targets that perform the actually copying. This means that the file copies are completed before the CopyFilesToOutputDirectory target is completed.
To ensure the correct order, BeforeTargets="GetCopyFilesToOutputDirectoryItems" is used and not BeforeTargets="CopyFilesToOutputDirectory".
If the scenario were different and <Exec Command="GeneratedFiles" /> didn't depend on the compilation step, i.e. the GeneratedFiles command didn't use the assembly being created by the project, then the Generation target could occur even earlier, e.g.
<Target Name="Generation" BeforeTargets="BeforeBuild">
Update - Execute a Target if ProjectReference has a specific Project
This update is a response to discussion in the comments.
Let's say we have a FooApp project and a BarLib project and FooApp depends on BarLib and needs to copy arbitrary files from the BarLib project directory.
FooApp has a ProjectReference to BarLib, e.g.
<ItemGroup>
<ProjectReference Include="..\BarLib\BarLib.csproj" />
</ItemGroup>
ProjectReference is an ItemGroup and we can check if it includes BarLib.
<Target Name="CopyFilesFromBar" BeforeTargets="BeforeBuild">
<PropertyGroup>
<!-- The name of the project to look for -->
<BarProjectName>BarLib</BarProjectName>
<!-- Use batching to check the ItemGroup -->
<!-- Save the project's directory because we will need it later -->
<BarProjectDirectory Condition="'%(ProjectReference.Filename)' == '$(BarProjectName)'">%(ProjectReference.Directory)</BarProjectDirectory>
<!-- Set up a boolean that indicates if the project was found or not -->
<HasProjectRefToBar>false</HasProjectRefToBar>
<HasProjectRefToBar Condition="$(FooProjectDirectory) != ''">true</HasProjectRefToBar>
</PropertyGroup>
<!-- Copy if the project was found in ProjectReference -->
<Copy SourceFiles="$(TargetDir)$(BarProjectDirectory)\bin\GeneratedFiles\*.*" DestinationFolder="$(OutputPath)GeneratedFiles" Condition="$(HasProjectRefToBar)" />
</Target>
This target could be defined once in a Directory.Build.targets file and shared across a solution.
If the generated files (in BarLib in the example scenario) don't change based on Configuration and Platform, consider using an output path location that doesn't change as in the example - 'bin\GeneratedFiles'. This makes it much easier for consuming projects. Otherwise keep all the projects in sync with regards to using the same Configuration and Platform values and the same $(OutputPath).

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.

MSBuild task doesn't build project references

Assuming the latest version of MSBuild, let's say I have 3 projects ProjA, ProjB and ProjC. I have a custom target in A and B that copy the individual outputs (bin items) into a custom path $(CustomOutputPath) - this all works fine individually. ProjC also has a custom target but in addition to copying its files to $(CustomOutputPath), it also cleans up the output path first, then chains ProjA and ProjB so that essentially all 3 projects have their files in the custom output path.
Let's assume I cannot change this requirement.
My ProjC target looks something like this:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Contains groups and properties only like
CustomOutputPath and BuildProjects -->
<Import Project="SharedGroups.msbuild"/>
<Target Name="AfterBuild">
<!-- Removing old output -->
<RemoveDir Directories="$(CustomOutputPath)" />
<ItemGroup>
<!-- Arbitrary contents of this project -->
<FilesToCopy Include="**\*.*" />
</ItemGroup>
<!-- this works fine -->
<Copy SourceFiles="#(FilesToCopy)"
DestinationFolder="$(CustomOutputPath)"
OverwriteReadOnlyFiles="true" />
<!-- Once cleanup and copy is completed, I want to run all the other
projects builds which contain similar but specific copy tasks as
above, with no clean up. BuildProjects is an ItemGroup of all
the projects I want to build -->
<MSBuild Projects="#(BuildProjects)"
Properties="Configuration=$(Configuration); BuildProjectReferences=true"/>
</Target>
</Project>
The problem I'm having is that one of the projects I am trying to build in the last step is failing because it references another project in the solution, which is not being built as part of the BuildProjectReferences=true directive, so it can't find the DLL. If I build this dependency individually then the MSBuild task will work, but I don't want to have to build this project independently.
Why is my referenced project not being built and is there a better way to do this with MSBuild?
Note: I am open to other solutions - I have tried to make ProjA and ProjB references of ProjC (hence not needing the MSBuild task at the bottom of ProjC target) but then the cleanup step in C happens AFTER A and B copy their output out so that doesn't work.
Use /verbosity:detailed and redirect output to a file. Look through the verbiage to see what's happening with the ResolveProjectReferences target. You can also use /verbosity:diag and see details of why is skipped things etc. That might be useful since the Condition on the various tasks used there are pretty hairy.

MSBuild: Ignore targets that don't exist

Solution1.sln contains two projects:
ProjectA.csproj
ProjectB.csproj
ProjectB has a custom target called "Foo". I want to run:
msbuild Solution1.sln /t:Foo
This will fail because ProjectA doesn't define the "Foo" target.
Is there a way to make the solution ignore the missing target? (E.g., do nothing if the target doesn't exist for a specific project) without modifying the SLN or project files?
There is a two-part solution if you don't want to edit the solution or project files and you're happy for it to work from MSBuild command-line but not from Visual Studio.
Firstly, the error you get when you run:
MSBuild Solution1.sln /t:Foo
Is not that ProjectA does not contain a Foo target but that the solution itself does not contain a Foo target. As #Jaykul suggests, setting the MSBuildEmitSolution environment variable will reveal the default targets contained within the solution metaproj.
Using the metaproj as inspiration you can introduce a new file "before.Solution1.sln.targets" next to the solution file (the file name pattern is important) with contents like this:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Foo">
<MSBuild Projects="#(ProjectReference)" Targets="Foo" BuildInParallel="True" Properties="CurrentSolutionConfigurationContents=$(CurrentSolutionConfigurationContents); SolutionDir=$(SolutionDir); SolutionExt=$(SolutionExt); SolutionFileName=$(SolutionFileName); SolutionName=$(SolutionName); SolutionPath=$(SolutionPath)" SkipNonexistentProjects="%(ProjectReference.SkipNonexistentProjects)" />
</Target>
</Project>
The MSBuild element is mostly just copied from the solution metaproj's Publish target. Adjust the target name and any other details to suit your scenario.
With this file in place, you'll now get the error that ProjectA does not contain the Foo target. ProjectB may or may not build anyway depending on inter-project dependencies.
So, secondly, to solve this problem we need to give every project an empty Foo target which is then overridden in projects that actually already contain one.
We do this by introducing another file, eg "EmptyFoo.targets" (name not important) that looks like this:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Foo" />
</Project>
And then we get every project to automatically import this targets file either by running MSBuild with an extra property, eg:
MSBuild Solution1.sln /t:Foo /p:CustomBeforeMicrosoftCommonTargets=c:\full_path_to\EmptyFoo.targets
Or include the CustomerBeforeMicrosoftCommonTargets property in the Properties attribute on the MSBuild element in the first targets file where you could optionally specify the full path relative to the $(SolutionDir) property.
However, if you're willing to run Foo in conjunction with any of the default solution targets (ie Build, Rebuild, Clean, or Publish) you could take some inspiration for how the Web Publishing Pipeline in MSBuild uses the DeployOnBuild property to call the Publish target on Web projects in a solution containing other project types that don't support publishing.
More info on the before.Solution1.sln.targets file here:
http://sedodream.com/2010/10/22/MSBuildExtendingTheSolutionBuild.aspx
You can target those by project name, like /t:project:target (might need quotes, I can't remember).
You can find all the generated targets by setting the environment variable MSBuildEmitSolution = 1 ... which causes msbuild to save to disk the temp .metaproj file which it generates for your solution. That file has all those targets defined in it, just open it up and take a look ;)
Maybe not the best answer but a reasonable hack.
msbuild ProjectA.csproj
msbuild ProjectB.csproj /t:Foo
When msbuild building solution - msbuild emits only limited set of targets into it's .metaproj file, and afaik - you can't build custom target through building sln file, you have to use original project1.csproj or custom build script.
Just for reference:
Use ContinueOnError when using MSBuildTask or -p:ContinueOnError=ErrorAndContinue when using (dotnet) msbuild
It may be in limited scenarios helpful: For example you have a list of .csproj files and want attach metadata only to specific project file items then you could write something like this:
<Target Name="UniqueTargetName" Condition="'$(PackAsExecutable)' == 'Package' Or '$(PackAsExecutable)' == 'Publish'" Outputs="#(_Hello)">
<ItemGroup>
<_Hello Include="$(MSBuildProjectFullPath)" />
</ItemGroup>
</Target>
<Target Name="BuildEachTargetFramework" DependsOnTargets="_GetTargetFrameworksOutput;AssignProjectConfiguration;_SplitProjectReferencesByFileExistence"
Condition="$(ExecutableProjectFullPath) != ''">
<Message Text="[$(MSBuildThisFilename)] Target BuildEachTargetFramework %(_MSBuildProjectReferenceExistent.Identity)" Importance="high" />
<MSBuild
Projects="%(ProjectReferenceWithConfiguration.Identity)"
Targets="UniqueTargetName"
ContinueOnError="true">
<Output TaskParameter="TargetOutputs" ItemName="_Hallo2" />
</MSBuild>
<Message Text="[$(MSBuildThisFilename)] ########### HELLO %(_Hallo2.Identity)" Importance="high" />
</Target>

Creating MSBuild target hooks

Can someone please point me to a reference about target hooks in MSBuild?
I'm looking for something that will let me define targets to run before and after a specified target. I know this can be done using the DependsOnTargets property but I've seen references to using target hooks and I'd like to explore that area.
Thanks,
Zain
A good list of built-in overridable build process hooks can be found here. For custom targets, the only thing I can think of is to use either the DependsOnTarget attribute (like you mentioned) or the BeforeTargets/AfterTargets attribute (like #Ritch Melton mentioned.) Be careful, the BeforeTargets/AfterTargets are only available in MSBuild 4.0
If you understand the idea behind DependsOnTargets then open up the Microsoft.Common.targets file in the .Net SDK directory (C:\Windows\Microsoft.NET\Framework\v3.5). That file defines the build process for the MSBuild task and .Net projects created by Visual Studio. Look for tags called BeforeXXXX, and AfterXXXX. BeforeBuild and AfterBuild are referenced in the default.csproj file - Snippet:
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>
There are others, like Clean, Rebuild, etc..
Define a Target (or Targets) to execute inside those Target elements, like this (Creates a directory, or list of directories based on the value in the Directories Property:
<Target Name="CreateDir">
<MakeDir Directories="D:\Dogs.txt"/>
</Target>
Then include those Targets in the BeforeXXX Target:
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="BeforeBuild" BeforeTargets="CreateDir">
</Target>
</Project>