Move AfterBuild target to a .targets file - msbuild

I have a AfterBuild target that I would like to use for multiple projects in a solution. Is there a way that I can put that target into a .targets file and reference the file in each project.
Below is what I tried which does not seem to work.
Project File:
<Import Project="..\debug.targets"/>
.Targets File:
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="AfterBuild">
<PropertyGroup>
<WebsiteDirectory>C:\Inetpub\wwwroot</WebsiteDirectory>
</PropertyGroup>
<ItemGroup>
<output Include=".\**\*.dll" Exclude=".\**\obj\**" />
<output Include=".\**\*.pdb" Exclude=".\**\obj\**" />
<output Include=".\**\*.svc" />
<output Include=".\**\*.xap" />
<output Include=".\**\*.aspx" />
<output Include=".\**\*.js" />
<output Include=".\**\*.config" />
</ItemGroup>
<PropertyGroup>
<VirtualDirectoryPath>$(WebsiteDirectory)\$(RootNamespace)</VirtualDirectoryPath>
</PropertyGroup>
<copy SourceFiles="#(output)" DestinationFiles="#(output->'$(VirtualDirectoryPath)\%(RecursiveDir)%(Filename)%(Extension)')" />
</Target>

Use this
<Import Project="$(MSBuildThisFileDirectory)\debug.targets"/>
$(MSBuildThisFile) = The current project file.
$(MSBuildThisFileDirectory) = The directory that contains current project file.
Relative paths in project files are difficult to use depending on what is invoking the project file. Using msbuild directly and the relative path will resolve to the project file. Use VS and the relative path will use the solution file as the base path.
Using $(MSBuildThisFileDirectory) will force the relative path to use a pre-determined beginning path. All you need to do is fill in the rest of the relative path.

What you are doing is fundamentally correct, but ensure that your Import statement is the last Import in the project file.
To verify that the target is being invoked correctly, run msbuild in diag mode from the command line and note the output regarding your target.
msbuild myproj.proj /v:diag

Related

Remove Files and Folders Copied From AfterBuild Target

I would like to avoid hard coding the dll and folder names in the AfterClean target, is there a dynamic way to do this? Ideally it would only delete the files and folders created by the Copy in the AfterBuild target.
I tried to simplify this by changing the DestinationFolder to include a subdirectory in the OutputPath. The AfterClean target would only have to remove that subdirectory at this point. However, some of the library's DLLImport paths don't take that subdirectory into consideration which results in a crash.
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="AfterBuild">
<ItemGroup>
<NativeLibs Include="$(MSBuildThisFileDirectory)..\lib\native\**\*.*" />
</ItemGroup>
<Copy SourceFiles="#(NativeLibs)" DestinationFolder="$(OutputPath)\%(RecursiveDir)" />
</Target>
<Target Name="AfterClean">
<Delete Files="$(OutputPath)\LumiAPI.dll" />
<Delete Files="$(OutputPath)\LumiCore.dll" />
<Delete Files="$(OutputPath)\LumiInOpAPI.dll" />
<RemoveDir Directories="$(OutputPath)\SPM" />
<RemoveDir Directories="$(OutputPath)\plugin" />
</Target>
</Project>
Project Structure:
src
ConsumingProject
ConsumingProject.csproj
ConsumingProject.sln
packages
my-project.5.7.0.12
build
lib
native
plugin
VenusDvc.dll
SPM
sSPM_1.bin
LumiAPI.dll
LumiCore.dll
LumiInOpAPI.dll
net45
my-project.5.7.0.12.nupkg
Essentially I want to delete all the files and folders that were copied from the native folder to the output of the project (ie LumiAPI.dll, LumiCore.dll, SPM (folder), eSPM_1.bin, etc). However I want it to be generic enough so that if I add another folder to the native directory, it will delete those folders/files as well.
Use a seperate target which lists input and output files, then use that list in both other targets. Note this uses the DestinationFiles attribute from the Copy task instead of DestinationFolders. And it might print some messages about non-existing directories being passed to RemoveDir because the top directory gets removed already before child directories.
update since you don't want to remove the root output directory as it still has files, figured applying the 'only remove output directory if it's empty' principle for any destination directory is probably the safest way to go. Credits go to the answer here.
<Target Name="GetMyOutputFiles">
<ItemGroup>
<NativeLibs Include="$(MSBuildThisFileDirectory)..\lib\native\**\*.*" />
<!--Now add some metadata: output dir and output file-->
<NativeLibs>
<DestinationDir>$(OutputPath)\%(RecursiveDir)</DestinationDir>
<Destination>$(OutputPath)\%(RecursiveDir)%(FileName)%(Extension)</Destination>
</NativeLibs>
</ItemGroup>
</Target>
<Target Name="AfterBuild" DependsOnTargets="GetMyOutputFiles">
<!--Copy one-to-one-->
<Copy SourceFiles="#(NativeLibs)" DestinationFiles="#(NativeLibs->'%(Destination)')" />
</Target>
<Target Name="AfterClean" DependsOnTargets="GetMyOutputFiles">
<Delete Files="#(NativeLibs->'%(Destination)')" />
<!--Find number of files left in each destination directory-->
<ItemGroup>
<NativeLibs>
<NumFiles>0</NumFiles>
<!--Condition is to avoid errors when e.g. running this target multiple times-->
<NumFiles Condition="Exists(%(DestinationDir))">$([System.IO.Directory]::GetFiles("%(DestinationDir)", "*", System.IO.SearchOption.AllDirectories).get_Length())</NumFiles>
</NativeLibs>
</ItemGroup>
<!--Only remove empty directories, use 'Distinct' to skip duplicate directories-->
<RemoveDir Directories="#(NativeLibs->'%(DestinationDir)'->Distinct())" Condition="%(NumFiles)=='0'" />
</Target>

MSBUILD Unable to force PackageLocation for Package target

I'm trying to build a deployment package for my web service from msbuild just like you can do in Visual Studio by right-clicking on the project file.
The package gets created fine and is put in the /obj/Release/Packages folder under my source directory for the project file.
You should be able to specify where that package is created by setting the PackageLocation property in a PropertyGroup inside the project file. However, that is not working for me. It still puts the package in /obj/Release/Packages under the source directory.
Here is the snippet from my project file:
<Import Project="$(SrcTreeRoot)\Build\TaskInit.Tasks" />
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" />
<Import Project="$(SrcTreeRoot)\Build\TaskOverrides.Tasks" />
<PropertyGroup>
<Platform>Any Cpu</Platform>
<Configuration>Dev</Configuration>
<PackageLocation>$(PackageOutputDir)\MatrixOnCdsService\MatrixOnCdsService.zip</PackageLocation>
<PackageAsSingleFile>True</PackageAsSingleFile>
<EnablePackageProcessLoggingAndAssert>True</EnablePackageProcessLoggingAndAssert>
<!--OutDir>$(PackageOutputDir)\MatrixOnCdsService\</OutDir-->
</PropertyGroup>
We are also using a MasterBuild.proj that has the following sections:
<PackageProject Include="..\Source\AnalysisSuite\MatrixOnCdsService\MatrixOnCdsService.csproj"/>
...
<Target Name="Package">
<MSBuild Projects="#(PackageProject)" Targets="Package" Properties="Platform=$(Platform);
Configuration=$(Configuration);
DeployOnBuild=true;
DeployTarget=Package;
PackageLocation=$(PackageLocation);"/>
</Target>
TaskInit.tasks is our own custom import file that contains the PackageOutputDir property that we use to tell the project where to put the package.
My question is why is the package still being placed in the /obj/Release/Packages folder even after specifying the PackageLocation?
Stick the property group you have in a target:
<Target Name="SetValues">
<PropertyGroup>
<Platform>Any Cpu</Platform>
<Configuration>Dev</Configuration>
<PackageLocation>$(PackageOutputDir)\MatrixOnCdsService\MatrixOnCdsService.zip</PackageLocation>
<PackageAsSingleFile>True</PackageAsSingleFile>
<EnablePackageProcessLoggingAndAssert>True</EnablePackageProcessLoggingAndAssert>
<!--OutDir>$(PackageOutputDir)\MatrixOnCdsService\</OutDir-->
</PropertyGroup>
</Target>
then add this as a DependsOnTarget for your Package Target and i think you will have your values passed.
e.g. <Target Name="Package" DependsOnTargets="SetValues">
You could do the following in your MasterBuild.proj.
<Target Name="Package">
<ConvertToAbsolutePath Paths="$(PackageOutputDir)">
<Output TaskParameter="AbsolutePaths" PropertyName="Source_Dir_Abs"/>
</ConvertToAbsolutePath>
<MSBuild Projects="#(PackageProject)" Targets="Package"
properties="Platform=$(Platform);
Configuration=$(Configuration);
DeployOnBuild=false;
DeployTarget=Package;
PackageLocation=$(Source_Dir_Abs)\$(PackageProjectName).zip;
PackageAsSingleFile=true;
ExcludeFilesFromDeployment=Web.config;
_PackageTempDir=$(PackageOutputDir)\temp;">
</MSBuild>
</Target>
Where you are calling msbuild you will need to add a property that will be used in $(PackageProjectName) by doing the following:
msbuild.exe /property:PackageProjectName=$project

MSBuild project file: Copy item to specific location in output directory

In the process of cleaning up the folder/file structure on a project I inherited, I'm running into a problem with organizing the required external libraries. I want to keep them in their own .\dll\ folder, but they're not being copied to the build directory properly. They should be in the root build directory, but they're being moved to a subfolder instead.
My .csproj file contains the following xml:
<Project>
<ItemGroup>
<None Include="dlls\libraryA.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
Then, on build, the libraryA.dll file is copied to the bin\Debug\dll\ folder, but I want it in the bin\Debug\ folder.
I tried this and msbuild always wants to copy the files using their directory path, but there is a workaround...
Edit the csproj file and after this line:
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
Add these lines:
<PropertyGroup>
<PrepareForRunDependsOn>$(PrepareForRunDependsOn);MyCopyFilesToOutputDirectory</PrepareForRunDependsOn>
</PropertyGroup>
<Target Name="MyCopyFilesToOutputDirectory">
<Copy SourceFiles="#(None)" DestinationFolder="$(OutDir)" />
</Target>
The copy of the output files happens in the PrepareForRun target. This adds your own target to the list of targets that are executed as part of PrepareForRun.
This example copies all items in the None item group. You could create your own item group (e.g. MyFiles) and do the copy on that item group if you have other "None" files you don't want copied. When I tried this I had to change the item group name by editing the csproj file directly. Visual Studio did not allow me to set the item group of a file from the UI, but after I edited the csproj and changed it, Visual Studio displayed my custom item group name correctly.
If you only want to change it for one file, it may be easier to use the property:
<None Include="dlls\libraryA.dll">
<Link>%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Including content files in .csproj that are outside the project cone
This approach works
If you need to force copy of a specific file/nuget package into an asp.net core project (2.2), add at the end of your csproj :
<!-- Force copy MathNet because we need it in compilation -->
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="Build">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\MathNet.Numerics.4.8.1\lib\netstandard2.0\MathNet.Numerics.dll')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MathNet.Numerics.4.8.1\lib\netstandard2.0\MathNet.Numerics.dll'))" />
</Target>
<ItemGroup>
<ContentWithTargetPath Include="..\packages\MathNet.Numerics.4.8.1\lib\netstandard2.0\MathNet.Numerics.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>MathNet.Numerics.dll</TargetPath>
</ContentWithTargetPath>
</ItemGroup>
In SDK-style csproj you can write something like:
<Target Name="CopyFilesTargetName" AfterTargets="Build">
<Copy SourceFiles="$(OutDir)\dlls\Some.dll;$(OutDir)\dlls\SomeOther.dll" DestinationFolder="$(OutDir)" />
</Target>
You can also use <Move instead of <Copy to move files

custom .targets file not working

I have created a custom .targets file as below (Just added all the common tasks required in myproj.vcxproj file to .targets file)
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- *******************************************************************************************
Common tasks
******************************************************************************************* -->
<Target Name="H1">
<Exec Command="del /F/Q #(S_PACK_H1)" />
<RemoveDir Directories="#(D_PACK_H1)" />
</Target>
<Target Name="H2">
<Exec Command="del /F/Q #(S_PACK_H2)" />
<RemoveDir Directories="#(D_PACK_H2)" />
</Target>
<Target Name="H11">
<Exec Command="del /F/Q #(S_PACK_H11)" />
<RemoveDir Directories="#(D_PACK_H11)" />
</Target>
</Project>
All the macros/arrays like S_PACK_H1, D_PACK_H11 are defined in myproj.vcxproj file after which I am importing this in myproj.vcxproj file as below
<Import Project="C:\Program Files\MSBuild\MyCompany\Mycustom.targets" />
when I use the below cmd
msbuild myproj.vcxproj /t:H11
it gives an error "error MSB4057: The target "H11" does not exist in the project"
but If I have the same list of tasks in .vcxproj file instead of .targets file then it works fine.
Can I define macros in .vcxproj file and use them in .targets file? Will MSBuild be able to get that definition/value? If not then how do I go about using/passing something defined in vxcproj file in .targets file?
Why is msbuild not able to see my task when it is in .targets file Vs .proj file? what else do I need to do?
There is no obvious reason for this not to work. Yes you can define targets in an imported file and they should be available, regardless of where the import occurs. If you are using MSBuild 4.0 (there is no ToolsVersion attribute on your .targets file above, so I'm not sure) then you can generate a fully preprocessed file, like this:
> msbuild mproj.vcxproj /pp
Look for the preprocessed file in the same folder. Open it up in a text editor and search for your imported content, it should all be there. If not, perhaps the preprocessed file can shed some light into what is going wrong.

How can I change AssemblyProduct, AssemblyTitle using MSBuild?

I have an MSBuild script which compiles my existing solution but I'd like to change some properties of one of the projects within the solution at compile-time, including but not limited to AssemblyProduct and AssemblyTitle.
Here's a snippet of my build script:
<Target Name="Compile" >
<MSBuild Projects="..\MySolution.sln"
Properties="Configuration=MyReleaseConfig;Platform=x86" />
</Target>
I've got one main executable and several DLLs that are compiled. I am aware of the MSBuild Extension Pack and I suspect it might help me to get to where I need to be, although I'm not sure how to proceed.
Can I selectively change AssemblyInfo properties at build time?
You're on the right track with the MSBuild Extension Pack.
I find the easiest way to conditionally generate the assembly details at build time is to add an "AssemblyVersion" target directly to my .csproj file(s) that require an updated AssemblyInfo file. You can add the target directly to each csproj file that requires an updated AssemblyInfo file, or as I prefer to do it, create a custom targets file with the AssemblyVersion target and have each csproj file include your custom targets file.
Either way you likely want to use the MSBuild Extension Pack or the MSBuild Community Tasks to use their respective AssemblyInfo task.
Here's some code from our build scripts:
<!-- Import the AssemblyInfo task -->
<Import Project="$(MSBuildCommunityTasksPath)\MSBuild.Community.Tasks.Targets"/>
<!-- Overriding the Microsoft.CSharp.targets target dependency chain -->
<!-- Call our custom AssemblyVersion target before build, even from VS -->
<PropertyGroup>
<BuildDependsOn>
AssemblyVersion;
$(BuildDependsOn)
</BuildDependsOn>
</PropertyGroup>
<ItemGroup>
<AssemblyVersionFiles Include="$(MSBuildProjectDirectory)\Properties\AssemblyInfo.cs"/>
</ItemGroup>
<Target Name="AssemblyVersion"
Inputs="#(AssemblyVersionFiles)"
Outputs="UpdatedAssemblyVersionFiles">
<Attrib Files="%(AssemblyVersionFiles.FullPath)"
Normal="true"/>
<AssemblyInfo
CodeLanguage="CS"
OutputFile="%(AssemblyVersionFiles.FullPath)"
AssemblyCompany="$(CompanyName)"
AssemblyCopyright="Copyright $(CompanyName), All rights reserved."
AssemblyVersion="$(Version)"
AssemblyFileVersion="$(Version)">
<Output TaskParameter="OutputFile"
ItemName="UpdatedAssemblyVersionFiles"/>
</AssemblyInfo>
</Target>
Sneal's answer was very helpful, but I'd like to show what I actually ended up doing. Instead of editing csproj files (there are several) I instead added tasks to my build script. Here's a snippet:
<PropertyGroup>
<ProductName>MyApp</ProductName>
<CompanyName>MyCompany</CompanyName>
<Major>1</Major>
<Minor>0</Minor>
<Build>0</Build>
<Revision>0</Revision>
</PropertyGroup>
<ItemGroup>
<AssemblyVersionFiles Include="..\MyMainProject\Properties\AssemblyInfo.cs"/>
</ItemGroup>
<Target Name="AssemblyVersionMAIN" Inputs="#(AssemblyVersionFiles)" Outputs="UpdatedAssemblyVersionFiles">
<Attrib Files="%(AssemblyVersionFiles.FullPath)" Normal="true"/>
<AssemblyInfo
CodeLanguage="CS"
OutputFile="%(AssemblyVersionFiles.FullPath)"
AssemblyProduct="$(ProductName)"
AssemblyTitle="$(ProductName)"
AssemblyCompany="$(CompanyName)"
AssemblyCopyright="© $(CompanyName) 2010"
AssemblyVersion="$(Major).$(Minor).$(Build).$(Revision)"
AssemblyFileVersion="$(Major).$(Minor).$(Build).$(Revision)"
AssemblyInformationalVersion="$(Major).$(Minor).$(Build).$(Revision)">
<Output TaskParameter="OutputFile" ItemName="UpdatedAssemblyVersionFiles"/>
</AssemblyInfo>
</Target>
<Target Name="Compile" DependsOnTargets="AssemblyVersionMAIN">
<MSBuild Projects="..\MySolution.sln"
Properties="Configuration=Release;Platform=x86;Optimize=true" />
</Target>
Then, I can override my variables from the command line, or a batch script, like so:
set MAJ=1
set MIN=2
set BLD=3
set REV=4
msbuild buildScript.xml /t:Compile /p:Major=%MAJ% /p:Minor=%MIN% /p:Build=%BLD% /p:Revision=%REV%
<Target Name="SetVersion">
<ItemGroup>
<AssemblyInfoFiles Include="$(TargetDir)\**\AssemblyInfo.cs"/>
</ItemGroup>
<Message Text="change the Version number for:"/>
<Message Text="%(AssemblyInfoFiles.FullPath)"/>
<MSbuild.ExtensionPack.Framework.AssemblyInfo
AssemblyInfoFiles="#(AssemblyInfoFiles)"
AssemblyTitle="newTitle"
AssemblyMajorVersion="2"
AssemblyMinorVersion="0"/>
</Target>