MSBuild conditions based on imported properties - visual-studio

I have a solution that has multiple projects, and I'd like them all to be able to access a set of shared properties that have been defined once in a common file at the solution level.
This mostly works fine using the code below and I can use the imported properties in the BeforeBuild target, however the problem I'm having is that I can't use the imported properties in conditions.
So I have the following in a CommonSettings.targets file in the solution folder:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="CommonSettingsTarget">
<PropertyGroup>
<MyCustomProperty>Sample</MyCustomProperty>
</PropertyGroup>
</Target>
</Project>
In my project file I have:
<Project ToolsVersion="14.0" DefaultTargets="Build" InitialTargets="CommonSettingsTarget" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(SolutionDir)CommonSettings.targets" />
<ItemGroup>
<EmbeddedResource Include="EmbeddedDocument.txt" Condition=" '$(MyCustomProperty)' == 'Sample' " />
</ItemGroup>
<Target Name="BeforeBuild">
<Message Text="MyCustomProperty='$(MyCustomProperty)'" Importance="high" />
</Target>
</Project>
In the above, I can see my imported property displayed in the output window as "MyCustomProperty='Sample'" which is great, however when it's used as part of a condition (to optionally include an embedded resource), the condition is never satisfied.
Is there any way to make the imported properties work with conditions?

As your ItemGroup is not within a target, but the PropertyGroup is, the CommonSettingsTarget has not yet been executed when your condition is evaluated and thus MyCustomProperty has not yet been defined.
The Message task is called from within the BeforeBuild target which depends on CommonSettingsTarget and thus MyCustomProperty has been defined when you create the message.
Think of the Import as copying the imported project into your project file. The result would be something like this:
<Project ToolsVersion="14.0" DefaultTargets="Build" InitialTargets="CommonSettingsTarget" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="CommonSettingsTarget">
<PropertyGroup>
<MyCustomProperty>Sample</MyCustomProperty>
</PropertyGroup>
</Target>
<ItemGroup>
<EmbeddedResource Include="EmbeddedDocument.txt" Condition=" '$(MyCustomProperty)' == 'Sample' " />
</ItemGroup>
<Target Name="BeforeBuild">
<Message Text="MyCustomProperty='$(MyCustomProperty)'" Importance="high" />
</Target>
</Project>
This is what happens:
You define a target CommonSettingsTarget which will define MyCustomProperty when it is executed. Not now.
You define the ItemGroup and therefore evaluate the condition. It returns false, because MyCustomProperty has not yet been defined.
You define a target BeforeBuild.
You run the initial target, i.e. CommonSettingsTarget. Now MyCustomProperty is defined.
You run the default target which depends on BeforeBuild and thus runs BeforeBuild. There you evaluate MyCustomProperty which has been defined in step 4.
As a solution, remove the CommonSettingsTarget target and define the PropertyGroup as a child of the Project in CommonSettings.targets instead:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MyCustomProperty>Sample</MyCustomProperty>
</PropertyGroup>
</Project>
In your project file, you should remove the InitialTargets="CommonSettingsTarget" then.

Related

Return a dynamic property from a target in another project

How can i "return" a dynamic property that gets set in a target in another project?
a.msbuildproj:
...
<Target Name="A">
<PropertyGroup>
<PropA>a</PropA>
</PropertyGroup>
</Target>
...
b.msbuildproj:
...
<Target Name="A">
<MSBuild
Project="a.msbuildproj"
Target="A"/>
<Message Text="$(PropA)"/>
</Target>
...
b calls target A in a, A sets property PropA, i need the value of PropA in b.
Background: I have setup an (appx) packaging project to package a desktop application and an application project. In the application project, i have a target that generates a version number i also want to use/have access to in the packaging project.
I know you can't "return" a property from a target in another project, but how would i solve this with msbuild?
It seems impossible to return a property from a target in another project.
Is it feasible to print the value of PropA in a and call a's Target A in b?
a.msbuildproj
<Target Name="A">
<PropertyGroup>
<PropA>a</PropA>
</PropertyGroup>
<Message Text="$(PropA)"/>
</Target>
b.msbuildproj
<Target Name="A">
<MSBuild
Projects="a.msbuildproj"
Targets="A"/>
</Target>

How to create a MSBuild Target that will only run if necessary

I have been experimenting with MSBuild to create custom targets. I am currently attempting to add support for compiling file with the JAWS script compiler. This is what I have so far.
Scripts.props
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<DEVDOCS_DIR>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\..\..\..\DevDocs'))</DEVDOCS_DIR>
<BUILTIN_MASTER>$(DEVDOCS_DIR)\jsd\enu\builtin_master.jsd</BUILTIN_MASTER>
<TOOLBOX_DIR>$(DEVDOCS_DIR)\Toolbox</TOOLBOX_DIR>
<JAWS_VER>17.0</JAWS_VER>
<!-- If perl.exe is not in your path, set the following variable to the full path and file name of perl.exe.
Optionally you could set the PERL_EXE environment variable. -->
<PERL_EXE Condition="'$(PERL_EXE)'==''">perl.exe</PERL_EXE>
<!-- If scompile.exe is not in your path, set the following variable to the full path and file name of scompile.exe.
Optionally you could set the SCOMPILE_EXE environment variable. -->
<SCOMPILE_EXE Condition="'$(SCOMPILE_EXE)'==''">scompile.exe</SCOMPILE_EXE>
</PropertyGroup>
</Project>
Scripts.targets
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="GenerateBuiltinJSD">
<Exec Command=""$(PERL_EXE)" compilejsd.pl -v Jaws/$(JAWS_VER) -o "$(MSBuildProjectDirectory)\builtin.jsd" "$(BUILTIN_MASTER)"" WorkingDirectory="$(TOOLBOX_DIR)" />
</Target>
<Target Name="CompileScripts">
<Exec Command=""$(SCOMPILE_EXE)" /d "%(ScriptSourceFiles.FullPath)"" Outputs="%(ScriptSourceFiles.RootDir)%(ScriptSourceFiles.Directory)%(ScriptSourceFiles.Filename).jsb" WorkingDirectory="$(MSBuildProjectDirectory)" />
</Target>
<Target Name="Clean">
<ItemGroup>
<JSBFiles Include="$(MSBuildProjectDirectory)\*.jsb" />
</ItemGroup>
<Delete Files="#(JSBFiles)" />
</Target>
<Target Name="Build">
<CallTarget Targets="GenerateBuiltinJSD;CompileScripts" />
</Target>
</Project>
Scripts.vcxproj
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
</ItemGroup>
<ItemGroup>
<ScriptSourceFiles Include="Default.jss" />
</ItemGroup>
<ItemGroup>
<ScriptMessageFiles Include="Default.jsm" />
</ItemGroup>
<ItemGroup>
<ScriptHeaderFiles Include="HJConst.jsh" />
</ItemGroup>
<ItemGroup>
<ScriptDocumentationFiles Include="default.jsd" />
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{B2C4363D-D228-425D-AB04-38997EA229C0}</ProjectGuid>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<PlatformToolset>v120</PlatformToolset>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="PropertySheets">
<Import Project="Scripts.props" />
</ImportGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="$(MSBuildProjectDirectory)\Scripts.targets" />
</Project>
This works up to a point. The problem is that when I do a build, it rebuilds all the script files, even if the relevant source file has not changed.
My question is how can I modify this so that each file is only built if necessary?
The feature you are asking about is called Incremental Build, and hopefully it is supported by MSBuild utility. All you have to do is to specify inputs and outputs for a target you are planning to skip if no source files are changed.
To specify inputs and outputs for a target: Use the Inputs and Outputs attributes of the Target element. For example:
<Target Name="Build"
Inputs="#(CSFile)"
Outputs="hello.exe">
MSBuild can compare the timestamps of the input files with the timestamps of the output files and determine whether to skip, build, or partially rebuild a target.
To succeed in your task you have to properly generate the Inputs and Outputs of your target. This can be achieved by using special targets, which will populate the required items, or you can use the transformation explicitly:
<Target Name="CompileScripts"
Inputs="#(ScriptSourceFiles)"
Outputs="#(ScriptSourceFiles->'%(RootDir)%(Directory)%(FileName).jsb')">
<Exec Command=""$(SCOMPILE_EXE)" /d "%(ScriptSourceFiles.FullPath)"" Outputs="%(ScriptSourceFiles.RootDir)%(ScriptSourceFiles.Directory)%(ScriptSourceFiles.Filename).jsb" WorkingDirectory="$(MSBuildProjectDirectory)" />
</Target>
(Also note that instead of referencing %(RootDir) item metadata you should use $(OutDir) property, because it is more straightforward, but you should ensure that this property is specified and exists when the target is executed).

How to modify the output path in the PreBuildEvent or BeforeBuild

I am using the following code in the BeforeBuild event:
<XmlPeek XmlInputPath="SiteSettings.config" Query="appSettings/add[#key='cProjectNumber']/#value">
<Output TaskParameter="Result" ItemName="value" />
</XmlPeek>
<PropertyGroup>
<ProjectNumber>0#(value)</ProjectNumber>
</PropertyGroup>
which then gives me access to $(ProjectNumber) which will contain a project number like 1234. I then use this to include some project specific overrides on classes. This is working fine.
My issue is accessing ProjectNumber outside of the BeforeBuild event.
What I would like to do is to modify the <OutputPath>bin\</OutputPath> of the project so it goes into a project specific folder (so I know which builds are done). I tried <OutputPath>bin\$(ProjectNumber)\</OutputPath> but I assume my property is out of scope as it's empty.
Does anyone know of a way to achieve what I want?
EDIT: This is what I have tried with declaring a global var:
<Project ToolsVersion="4.0" DefaultTarget="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectNumber></ProjectNumber>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\$(ProjectNumber)\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<Target Name="BeforeBuild">
<XmlPeek XmlInputPath="SiteSettings.config" Query="appSettings/add[#key='cProjectNumber']/#value">
<Output TaskParameter="Result" ItemName="value" />
</XmlPeek>
<PropertyGroup>
<ProjectNumber>0#(value)</ProjectNumber>
</PropertyGroup>
<ItemGroup>
<Compile Include="Accounts\$(ProjectNumber)\Objects\MyObject.cs">
<SubType>Code</SubType>
</Compile>
</ItemGroup>
</Target>
<Target Name="AfterBuild">
<Message Text="Proj:$(ProjectNumber)" Importance="High" />
</Target>
</Project>
AfterBuild has the correct project number message printed but <OutputPath>bin\$(ProjectNumber)\</OutputPath> doesn't have the correct value.
You can set a Property in the Project Global level with the name ProjectNumber and empty value as a place holder. The the ProjectNumber Property value you set in the InitializeBuild target will set the global level property of ProjectNumber.
By looking at the sample below you can understand it better:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" InitialTargets="InitializeBuild" DefaultTargets="All" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectNumber></ProjectNumber>
</PropertyGroup>
<Target Name="All">
<Message Text="This is the value after I set it in InitializeBuild '$(ProjectNumber)'"/>
</Target>
<Target Name="BeforeBuild">
<Message Text="This is the value before I set it in InitializeBuild '$(ProjectNumber)'"/>
<PropertyGroup>
<ProjectNumber>12345678</ProjectNumber>
</PropertyGroup>
</Target>
</Project>
In this sample, we are setting Property ProjectNumber with empty value at Global level and then in InitializeBuild target, it is set with value 12345678 as InitializeBuild is part of the InitialTargets and then through DefaultTargets later, once it is accessed in target All it is having the value 12345678.
The output of the above code goes as below:
>msbuild mybuild.msbuild
Microsoft (R) Build Engine version 4.0.30319.17929
[Microsoft .NET Framework, version 4.0.30319.17929]
Copyright (C) Microsoft Corporation. All rights reserved.
Build started 9/25/2013 5:35:22 AM.
Project "D:\CodeBase\COLNEW\COL-BIS\dev\mybuild.msbuild" on node 1 (default targets).
InitializeBuild:
This is the value before I set it in InitializeBuild''
All:
This is the value after I set it in InitializeBuild'12345678'
Done Building Project "D:\CodeBase\COLNEW\COL-BIS\dev\mybuild.msbuild" (default targets).
So your issue can be solved in similar fashion.

MSBuild attach to Visual Studio Project Build

I am using YUICompressor.Net for minification. The .proj file executes from MSBuild and works fine.
The question is how do I attach the MSBuild action to the build of the main Project?
I know there are some "After Build" events, bud how do I point them to execute my additional MSBuild.
In case it's relevant this is how my MSBuild file looks like:
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/MsBuild/2003">
<UsingTask TaskName="CssCompressorTask" AssemblyFile="..\bin\Yahoo.Yui.Compressor.Build.MsBuild.dll" />
<UsingTask TaskName="JavaScriptCompressorTask" AssemblyFile="..\bin\Yahoo.Yui.Compressor.Build.MsBuild.dll" />
<Target Name="Minify">
<ItemGroup>
<CssFile_Common Include="../Styles/common.css"/>
<CssFile_Plugins_All Include="../Styles/plugins.all.css"/>
</ItemGroup>
<CssCompressorTask
SourceFiles="#(CssFile_Common)"
DeleteSourceFiles="false"
OutputFile="../Styles/common.min.css"
CompressionType="Standard"
LoggingType="Info"
PreserveComments="false"
LineBreakPosition="-1"
/>
<CssCompressorTask
SourceFiles="#(CssFile_Plugins_All)"
DeleteSourceFiles="false"
OutputFile="../Styles/plugins.all.min.css"
CompressionType="Standard"
LoggingType="Info"
PreserveComments="false"
LineBreakPosition="-1"
/>
</Target>
</Project>
Assuming your sample file is named Minify.proj you would simply need to put something like this at the bottom of your main project file:
<Import Project="Minify.proj" />
<Target Name="BeforeBuild" DependsOnTargets="Minify">
</Target>

MSBuild pre clean customization

I am working with Visual Studio 2010. I have directed project output to a specific folder which will contain all the DLLs and EXEs when built. However when I clean the solution, the folder is not getting cleaned, and the DLLs are still present in it.
Can anyone tell me how to handle the clean solution command to clear out the folders I want to clean? I tried working with MSBuild and handling the BeforeClean and AfterClean targets, but it did not provide the desired result.
The answer from Sergio should work but I think it could be cleaner to override the BeforeClean/AfterClean targets. These are hooks into the build/clean process provided by microsoft. When you do a clean, VS do call the targets : BeforeClean;Clean;AfterClean and by default the first and the last do nothing.
In one of your existing .csproj file you can add the following :
<Target Name="BeforeClean">
<!-- DO YOUR STUFF HERE -->
</Target>
You can add to your VS .sln file special target named let's say BuildCustomAction.csproj:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
</PropertyGroup>
<ItemGroup>
<CleanOutCatalogFiles Include="..\..\bin\$(Configuration)\**\*.dll">
<Visible>false</Visible>
</CleanOutCatalogFiles>
<CleanOutCatalogFiles Include="..\..\bin\$(Configuration)\**\*.exe">
<Visible>false</Visible>
</CleanOutCatalogFiles>
</ItemGroup>
<Target Name="Build">
</Target>
<Target Name="Rebuild"
DependsOnTargets="Clean;Build">
</Target>
<Target Name="Clean"
Condition="'#(CleanOutCatalogFiles)'!=''">
<Message Text="Cleaning Output Dlls and EXEs" Importance="high" />
<Delete Files="#(CleanOutCatalogFiles)" />
</Target>
</Project>
Place it everywhere you want and specify relative path to the output catalog for your binaries. Add in VS this project as existing. That's all. With this you can do own custom actions for three common actions in VS: Build, Rebuild, Clean.
There exists more complex way to customize build process using CustomBeforeMicrosoftCommonTargets and CustomAfterMicrosoftCommonTargets but it requires to be very good in MSBuild.
Hope this helps.

Resources