MSBuild $(version)? - msbuild

In our target DestinationFiles we wish to set path to contain the version number of the project file we use to build. Is there a variable we can use to get that value?
< Target Name="CopyOutput">
<ItemGroup>
<PackagedFiles Include="blah blah"/>
</ItemGroup>
<Copy SourceFiles="#(PackagedFiles)"
DestinationFiles="#(PackagedFiles->'\\Blah\SOME_VERSION_NUMBER_FROM_BUILD\$(Configuration)\%(RecursiveDir)%(Filename)%(Extension)')"/>
</Target>

Sure just use $(VARNAME) and call msbuild with /p:VARNAME=XXXX
For more details regarding using enviornment vars with msbuild, you can have a look at https://sbarnea.com/articles/easy-windows-build-versioning/

Related

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.

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>

Property scope using msbuild extension pack detokenise

Im trying to use the msbuild extensions pack to fix up the configuration of our app on deploy,
i want to be able to pass a property (ENV) which will load my environment specific config file to use with the detokeniser, and fix up my application configs.
Like this:
<UsingTask TaskName="MSBuild.ExtensionPack.FileSystem.Detokenise"
AssemblyFile=".\Tools\MSBuild Extension Pack 4.0.3.0\MSBuild.ExtensionPack.dll"/>
<Import Project=".\Environments\$(Env).properties"/>
<Target Name="Build" >
<ItemGroup>
<SourceTemplates Include=".\Templates\**\*.*"/>
</ItemGroup>
<RemoveDir Directories=".\Temp"/>
<MakeDir Directories=".\Temp"/>
<Message Text="#(SourceTemplates)"/>
<Copy SourceFiles="#(SourceTemplates)"
DestinationFolder=".\Temp\%(RecursiveDir)" />
<ItemGroup>
<TargetTemplates Include=".\Temp\**\*.*"/>
</ItemGroup>
<MSBuild.ExtensionPack.FileSystem.Detokenise
TaskAction="Detokenise"
TargetFiles="#(TargetTemplates)"/>
</Target>
So i call this using
msbuild Detokenise.msbuild /p:Env=Prod
Msbuild knows about my file and i have access to its properties, but when the detokeniser runs i get the error:
Detokenise Task Execution Completed [15:07:50]
C:\Source\1.2\Build\Detokenise.msbuild(27,3):
error : InvalidProjectFileException: The imported project "C:\Source\1.2\Build\Environments\.properties" was not found.
Confirm that the path in the <Import> declaration is correct, and that the file exists on disk.
C:\Source\1.2\Build\Detokenise.msbuild\r
C:\Source\1.2\Build\Detokenise.msbuild(27,3): error :
All works fine if i hard code it-
Any ideas how to solve this. I thought of doing some text replacement on the msbuild before i execute...
You could try to assign this parameter to a local property:
<PropertyGroup Condition="'$(Env)'=='Prod'">
<TargetEnv>Prod</TargetEnv>
</PropertyGroup>
<!-- add other environments as needed -->
<PropertyGroup Condition="'$(Env)'=='Test'">
<TargetEnv>Test</TargetEnv>
</PropertyGroup>
<Import Project=".\Environments\$(TargetEnv).properties"/>
You could also try to enclose your parameter value in quotes:
msbuild Detokenise.msbuild /p:"Env=Prod"
As is your problem can't be reproduced, so it may be a side effect of other parameters not shown in your sample code.
I've seen a number of other questions where a similar problems was happening:
Visual Studio Ignoring MSBuild file (csproj) Customizations

Checking if project has a specific target in MSBuild

Some of my .csproj project files have special target "AssembleJS" that merges all .js files included in project in one big file (i.e. webcontrols.csproj has target "AssembleJS" with output "webcontrols.js").
So if I have project parts.csproj
That has target AssembleJS.
References project webcontrols.csproj.
References utility project utils.csproj that does not have any JavaScript and does not have AssembleJS target.
I want target AssembleJS of parts.csproj execute AssembleJS in webcontrols.csproj (the same was as MSBuild works with standard Build target).
Something like
<MSBuild Project="#ReferencedProjects" Targets="AssembleJS"/>
does not work because utils.csproj does not have target AssembleJS.
Is there any way to filter #ReferencedProjects based on whether project has certain target?
Any other idea on how to handle this scenario?
You cannot do what you are requiring. But you might be able to acheive it with batching.
<Project xmlns=''>
<ItemGroup>
<ReferencedProjects Include="webcontrols.csproj">
<Type>Web</Type>
</ReferencedProjects>
<ReferencedProjects Include="utils.csproj">
<Type>NonWeb</Type>
</ReferencedProjects>
</ItemGroup>
<Target Name="BuildWebProjects">
<MSBuild Projects="#(ReferencedProjects)" Condition=" '%(ReferencedProjects.Type)' == 'Web' " />
</Target>
</Project>
Do a search for MSBuild Batching and find some results on sedodream.com for more
info.
Should I expand on this?