MSBuild: Ignore targets that don't exist - msbuild

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>

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).

Pass property for single project on commandline when building a solution

When I use msbuild to build a solution containing multiple projects, can I pass a property on the command line in a way that the property will only be used for one of the projects?
That means, can I say that -p:Foo=42 shall be used for Project1, but not for Project2?
MSBuild cannot specify a property for one of the project by the solution file(msbuild xxx\xxx.sln) easily and only targets can be specified. Or you have to type multiple msbuild command line to specify to the related csproj file to enable that changed property like msbuild Project1.csproj -t:build -p:xxx=xxx, msbuild Project2.csproj -t:build. However, it is too complex and inconvenient.
So I recommend that you could use msbuild script to get what you want.
1) create a file called build.proj file:
<Project>
<ItemGroup>
<!--add all the projects from the solution and remove the any project you want to modify the foo property-->
<MostProjectFile Include="**\*.csproj;**\*.vcxproj" Exclude="**\Project1.csproj" />
<!--add any projects you want to modify the foo value-->
<SpecialProjectFile Include="**\Project1.csproj" />
</ItemGroup>
<Target Name="Build">
<!--build the most projects and remove the project which you want to change foo property-->
<MSBuild Projects="#(MostProjectFile)" Targets="Build" Properties="Configuration=Debug"/>
<!--build any projects that wants to modify the foo property separately-->
<MSBuild Projects="#(SpecialProjectFile)" Targets="Build" Properties="Configuration=Debug;Foo=42"/>
</Target>
</Project>
2) if you want to change the foo property again, you can just modify the build.proj based on your needs.
Run msbuild xxx\build.proj -t:Build to build it.

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.

structure solution outputpath by project

I'm building a visual studio solution with msbuild
msbuild.exe my.sln
This way it outputs everything to the output paths specified in each project (bin\ by default), but in this case I need all the output artefacts to be in different folder, used for packaging. If I run
msbuild.exe my.sln /p:OutputhPath=<someFolder>
Then all the artifacts will end up in the specified folder, but the structure will be flat. What I would like it to be, is:
\package
\project1
\project2
...
But I can't think of a good way to do this, without modifying individual project files (which is almost out of question). Any ideas? (msbuild 4.0, VS2010 - if that changes anything)
There is probably a better way, but one thing you could do is build in place with msbuild.exe my.sln, and then copy the outputs to your \package dir so you keep the hierarchy. It should be pretty simple to do. You can use this as a starting point:
<Target Name="Package">
<PropertyGroup>
<SourceFolder>$(MSBuildProjectDirectory)\src</SourceFolder>
<TargetFolder>$(MSBuildProjectDirectory)\package</TargetFolder>
</PropertyGroup>
<ItemGroup>
<FilesToCopy Include="$(SourceFolder)\**\bin\Debug\**\*.*" />
</ItemGroup>
<!-- Recursive copy w/o flattening folder structure: -->
<Copy
SourceFiles="#(FilesToCopy)"
DestinationFiles="#(FilesToCopy->'$(TargetFolder)\%(RecursiveDir)%(Filename)%(Extension)')"
/>
</Target>
You can also define a property to keep track of your build configuration, and replace the hardcoded bin\Debug with bin\$(BuildConfig).

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>