How to run a task in MSBuild, only if files count are more than zero? - msbuild

I'm trying to run a command in MSBuild, only if the condition is true. What condition? I count the files of a specific extension and if there is no file, I want to run that task.
<Target Name="MakeSureProjectHasViewsAndPagesInIt" AfterTargets="PreBuildEvent">
<Exec Command="echo PROJECT HAS NO CSHTML FILE!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" Condition="dir *.cshtml /s" />
</Target>
I can't make the condition part work. I want to count all *.cshtml files in the current project's directory and subdirectories and if the result is zero, I want to run that command.
I'm stuck at how to write that condition. Can you help please?

You have to define a property with the count of the files
<PropertyGroup>
<CSHTMLCount>$([System.IO.Directory]::GetFiles('$(MSBuildProjectDirectory)', '*.cshtml').Length)</CSHTMLCount>
</PropertyGroup>
<Target Name="MakeSureProjectHasViewsAndPagesInIt" Condition="'$(CSHTMLCount)' == '0'" AfterTargets="PreBuildEvent">
<Exec Command="echo PROJECT HAS NO CSHTML FILE!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" />
</Target>

Related

Multiple values for AfterTargets in .proj file

One of my projects has Exec Copy command to copy a file from one directory to another. We are intermittently seeing an issue where one of the process is locking a file and MsBuild is trying to execute a copy command on the same file. So, we are facing the error that the file is in use by another process.
We are suspecting that this is because of two different elements executing in parallel. Unless the first one finishes we do not want the second one to execute. Can we have two values for AfterBuild attribute like below.
Below are the projects: Project1.proj and Project2.proj. I want to use the target defined in Project2 into Project1. Project1.proj has a reference added for Projet2.proj
Project1.proj
<Target Name="TestTarget" AfterTargets="Build;TestTargetAnother" > ..... ..... </Target>
Project2.proj
<Target Name="TestTargetAnother" AfterTargets="Build" > ..... ..... </Target>
We are suspecting that this is because of two different elements
executing in parallel. Unless the first one finishes we do not want
the second one to execute.
First, rename your two files as Project1.targets and Project2.targets.
The file with proj suffix cannot be used in other referenced files. Files with this suffix cannot transfer properties.
Modify Project1.targets like this:
<Project>
<PropertyGroup>
<Flag>false</Flag>
</PropertyGroup>
<Target Name="TestTarget" AfterTargets="Build;TestTargetAnother" Condition="'$(Flag)'=='false'">
<Exec Command="xxxxxxxxxxx"/>
<PropertyGroup>
<Flag>true</Flag>
</PropertyGroup>
</Target>
<Import Project="Project2.targets"/>
</Project>
Then, modify Project2.targets like this:
<Project>
<Target Name="TestTargetAnother" AfterTargets="Build" Condition="'$(Flag)'=='false'">
<Exec Command="xxxxxxxx"/>
<PropertyGroup>
<Flag>true</Flag>
</PropertyGroup>
</Target>
</Project>
After that, reference Project1.targets into your xxx.csproj file:
<Import Project="Project1.targets" />
So it will execute one of them and cancel the other.

Wix bootstrapper - Set version number in Bundle

I've got a Wix installer that uses a bootstrapper to launch my msi file. I've done this by calling a batch file as a post build event in my wix project. This then calls candle and light manually and passes various variables into the Bundle.wxs file. This all works and generates the exe which calls my msi file..
However, I now want to pass the msi BuildVersion into the bundle file. In the wxs file that creates the msi I am using the BuildVersion that I have setup in the BeforeBuild section, using the BuildVersion=%(AssemblyVersion.Version).
I cannot access this variable no matter what I try, in order to pass it to my build_bootstrapper.bat file. I can however pass in hardcoded values. I am currently setting up my own AssemblyVersionNumber enviornment variable as you can see below in the AfterBuild section:
<AssemblyVersionNumber Condition="'$(AssemblyVersionNumber)' == ''">$(BuildVersion)</AssemblyVersionNumber>
but it is empty by the time it gets to my script file (even though it's populated if hardcoded). I've tried everything.
Does anybody have any ideas of how I can get the %(AssemblyVersion.Version); to my command file from the post build step?
Thanks in advance
<Target Name="BeforeBuild">
<GetAssemblyIdentity AssemblyFiles="..\..\App\AppThing\bin\Release\AppThing.exe">
<Output TaskParameter="Assemblies" ItemName="AssemblyVersion" />
</GetAssemblyIdentity>
<PropertyGroup>
<DefineConstants>BuildVersion=%(AssemblyVersion.Version);</DefineConstants>
</PropertyGroup>
</Target>
<Target Name="AfterBuild">
<PropertyGroup>
<DefineConstants>BuildVersion=%(AssemblyVersion.Version);</DefineConstants>
<AssemblyVersionNumber Condition="'$(AssemblyVersionNumber)' == ''">$(BuildVersion)</AssemblyVersionNumber>
</PropertyGroup>
</Target>
<PropertyGroup>
<PreBuildEvent>$(ProjectDir)scripts\copy_services.bat $(SolutionDir) $(ProjectDir)</PreBuildEvent>
</PropertyGroup>
<Target Name="AfterClean">
<Message Text="Cleaning wix files, TargetDir is: $(TargetDir)" Importance="High" ContinueOnError="true" />
<CreateItem Include="$(TargetDir)\**\*.*">
<Output TaskParameter="Include" ItemName="BinFilesDir" />
</CreateItem>
<Delete Files="#(BinFilesDir)" />
</Target>
<PropertyGroup>
<PostBuildEvent>$(ProjectDir)scripts\build_bootstrapper.bat $(ProjectDir) $(ConfigurationName) $(AssemblyVersionNumber)</PostBuildEvent>
</PropertyGroup>
$(BuildVersion) isn't set to anything.
You're setting define constants to "BuildVersion=%(AssemblyVersion.Version)" but never actually defining a MSBuild property called "BuildVersion" so the value of $(BuildVersion) is "".
Use %(AssemblyVersion.Version).
<AssemblyVersionNumber Condition="'$(AssemblyVersionNumber)' == ''">%(AssemblyVersion.Version)</AssemblyVersionNumber>

MSBuild afterbuild step to copy code into a temp folder

I'm trying to place my compiled web application into a temporary directory after it has been built.
I have the following but it doesn't seem to work. It drops and create directors but the msbuild task doesn't seem to copy the compile output into the obj/publish directory that i need it to?
<Target Name="AfterBuild">
<CallTarget Targets="Publish" />
</Target>
<Target Name="Publish">
<RemoveDir Directories="$(SolutionDir)AsycLearn\obj\publish\" ContinueOnError="true" />
<MakeDir Directories="$(SolutionDir)AsycLearn\obj\publish\"/>
<MSBuild Projects="AsycLearn.csproj" Targets="ResolveReferences;_CopyWebApplication" Properties="WebProjectOutputDir=$(SolutionDir) AsycLearn\obj\publish\;OutDir=$(SolutionDir) AsycLearn\obj\publish\bin\" />
</Target>
any ideas? Thanks
It looks like you need to quote your output paths because they contain spaces. Otherwise the value that gets read will be truncated at the first space. That will also terminate the parameter set that gets passed to any other tasks.
Properties="WebProjectOutputDir=$(SolutionDir) AsycLearn\obj\publish\;OutDir=$(SolutionDir) AsycLearn\obj\publish\bin\"
should be
Properties="WebProjectOutputDir="$(SolutionDir) AsycLearn\obj\publish\";OutDir="$(SolutionDir) AsycLearn\obj\publish\bin\""
The escape sequence " is needed because we want the resolved value to include quotes.

Issue with using MSBuild to build and copy all outputs to a common folder

We are trying to write a msbuild script that will build the solution and copy over all the compiled binaries and dependencies over to a specific output folder. While the build script that we have does build and copy over the binaries to a common folder, but we are not getting the dependencies copied.
This probably has to do with the way we have used the msbuild task to build the solution and we are accepting the targetoutputs of the task into an itemgroup and iterating over the item group to copy all the compiled dlls and exes over to a common folder. But this is not including the dependency dlls which gets placed into the individual bin folder of each project.
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ParentSolutionFile />
</PropertyGroup>
<ItemGroup>
<Assemblies Include="*.dll, *.exe" />
</ItemGroup>
<Target Name="BuildAll">
<CombinePath BasePath="$(MSBuildProjectDirectory)" Paths="Source\Solutions\xxx.sln">
<Output TaskParameter="CombinedPaths" PropertyName="ParentSolutionFile" />
</CombinePath>
<Message Text="$(ParentSolutionFile)" />
<MSBuild Projects="$(ParentSolutionFile)">
<Output TaskParameter="TargetOutputs" ItemName="Assemblies" />
</MSBuild>
<Message Text="%(Assemblies.Identity)" />
<Copy SourceFiles="%(Assemblies.Identity)" DestinationFolder="$(MSBuildProjectDirectory)\Binary" OverwriteReadOnlyFiles="True" SkipUnchangedFiles="True" />
</Target>
What will be the preferred way to copy over all the binaries along with the necessary dependencies to a common output folder?
Does not overriding OutputPath do the trick alone?
<MSBuild Projects="$(ParentSolutionFile)" Properties="OutputPath=$(MSBuildProjectDirectory)\Binary">
<Output TaskParameter="TargetOutputs" ItemName="Assemblies" />
</MSBuild>
And leave out the copy task alltogether?
The build process will place the final result in the directory represented by OutputPath - at least if you are building c# projects. For C/C++ the internal structure and variable names are completely different.
Thus, in theory, you could pass the OutputPath in the MsBuild-task that builds the solution.
<MsBuild Projects="$(ParentSolutionFile)"
Properties="OutputPath=$(MSBuildProjectDirectory)\Binary"/>
However, the csproj-files will overwrite that value unconditionally with the following code:
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\Debug\</OutputPath>
I have solved this by injecting my own build system in each and every csproj-file.
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\..\build\buildsystem.targets" />
The path is relative to the csproj-file. An absolute path is fine too, or a variable. The trick is to make it work on all dev machines as well as the build agents.
Now, in buildsystem.targets, simply redefine OutputPath as much as you like. Again, the trick is to ensure you get the same - or at least a well defined - location regardless of who builds it (dev, build agent) and regardless how the build was initiated (VS, command line).
A simple way of handling the differences is to import conditionally.
<Import Project="..\..\..\build\buildsystem.targets"
Condition="'$(BuildingInsideVisualStudio)'!='true'"/>
That will give you no changes if initiating the build from VS and whatever changes you code for if you build from command line.
--Jesper

How do I select all read-only files with msbuild?

I'm trying to write an MsBuild script to zip some files up. I need to select all of the read-only files recursively from a folder into an ItemGroup to add to the zip.
I'm using the community tasks Zip task, but am struggling with selecting files based on their attributes.
Is there anything around to do this out of the box, or do I need to write a custom task?
Thanks for you help.
You can use Property Functions (added to msbuild 4) to figure out if a file is read-only like so:
<ItemGroup>
<MyFiles Include="Testing\*.*" >
<ReadOnly Condition='1 == $([MSBuild]::BitwiseAnd(1, $([System.IO.File]::GetAttributes("%(Identity)"))))'>True</ReadOnly>
</MyFiles>
</ItemGroup>
<Target Name="Run" Outputs="%(MyFiles.Identity)">
<Message Text="%(MyFiles.Identity)" Condition="%(MyFiles.ReadOnly) != True"/>
<Message Text="%(MyFiles.Identity) ReadOnly" Condition="%(MyFiles.ReadOnly) == True" />
</Target>
Have you looked at the community build tasks site?
It has a zip task and an attribute change task - they should get you most of they way there.
This seems to do the job with a bit of dirty command line usage.
<Exec Command="dir .\RelPath\ToFolder\ToSearchIn /S /AR /B > readonlyfiles.temp.txt"/>
<ReadLinesFromFile File="readonlyfiles.temp.txt">
<Output TaskParameter="Lines" ItemName="ReadOnlyFiles"/>
</ReadLinesFromFile>
<Delete Files="readonlyfiles.temp.txt"/>
That gives absolute paths to the files.
To get relative paths, try something like this:
<Exec Command="dir .\RelPath\ToFolder\ToSearchIn /S /AR /B > readonlyfiles.temp.txt"/>
<FileUpdate Files="readonlyfiles.temp.txt"
Multiline="True"
Regex="^.*\\RelPath\\ToFolder\\ToSearchIn"
ReplacementText="RelPath\ToFolder\ToSearchIn"
/>
<ReadLinesFromFile File="readonlyfiles.temp.txt">
<Output TaskParameter="Lines" ItemName="ReadOnlyZipFiles"/>
</ReadLinesFromFile>
<Delete Files="readonlyfiles.temp.txt"/>