After spending the whole day studying MsBuild: project files, items, properties, targets and tasks, I still cannot find a solution for my problem.
I aim to create a custom target in my .csproj to generate binary files from input text files using console program. The target does what it should do. But I want the generated files to be part of the project file as Content that should be copied (published) together with an application.
I ended up the the following project file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<MyTool>.\tools\mytool.exe</MyTool>
</PropertyGroup>
<Target Name="MyTarget" DependsOnTargets="Restore" BeforeTargets="Build;Publish"
Inputs="#(TxtFiles)"
Outputs="#(TxtFiles->'%(RootDir)%(Directory)%(Filename).bin')">
<Exec Command="$(MyTool) -o %(TxtFiles.RootDir)%(TxtFiles.Directory)%(TxtFiles.Filename).bin %(TxtFiles.Identity)"
Outputs="#(TxtFiles->'%(RootDir)%(Directory)%(Filename).bin')">
<Output TaskParameter="Outputs" ItemName="BinFiles" />
</Exec>
</Target>
<ItemGroup>
<TxtFiles Include="my_resources\*.txt" />
<Content Include="#(TxtFiles)">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Content>
<Content Include="#(BinFiles)">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<PublishState>Included</PublishState>
</Content>
</ItemGroup>
</Project>
Unfortunately - it doesn't populate item #(BinFiles).
If if declate it manually (as if calling the tool beforehand), it works:
<ItemGroup>
<Content Include="my_resources\*.bin">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PublishState>Included</PublishState>
</Content>
</ItemGroup>
I want to achieve the same behavior. Also as my target declares inputs and outputs, how whould it populate its outputs to be added as content?
Finally, the other question had a clue in the answer.
Using it managed to define the final project file which worked as intended. I understood that I do not need the target Outputs, but rather add them to the Contents item.
I ended up with the following:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<MyTool>.\tools\mytool.exe</MyTool>
</PropertyGroup>
<ItemGroup>
<Content Include="my_resources\**\*.txt">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Target Name="MyTarget" DependsOnTargets="Restore" BeforeTargets="BeforeBuild;PrepareForPublish">
<ItemGroup>
<TxtFiles Include="#(Content)" Condition="'%(Extension)' == '.txt'" />
</ItemGroup>
<Exec Command="$(MyTool) -o $([System.IO.Path]::ChangeExtension('%(TxtFiles.Fullpath)','.bin')) %(TxtFiles.Identity)"
Outputs="$([System.IO.Path]::ChangeExtension('%(TxtFiles.Fullpath)','.bin'))">
<Output TaskParameter="Outputs" ItemName="_BinFiles" />
</Exec>
<ItemGroup>
<Content Include="#(_BinFiles)">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PublishState>Included</PublishState>
</Content>
<BinFiles Include="#(Content)" Condition="'%(Extension)' == '.bin'" />
</ItemGroup>
</Target>
</Project>
Related
I am trying to add custom files to a specific NuGet package (basically I need all output files included in the NuGet package since it serves as a tool for Chocolatey).
After some searching, I found this potential fix:
<PropertyGroup>
<TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);GetToolsPackageFiles</TargetsForTfmSpecificBuildOutput>
</PropertyGroup>
<Target Name="GetToolsPackageFiles">
<ItemGroup>
<BuildOutputInPackage Include="$(OutputPath)\**\*.dll" />
<BuildOutputInPackage Include="$(OutputPath)\**\*.exe" />
</ItemGroup>
</Target>
Unfortunately, this won't work correctly for subdirectories, so I tried this:
<PropertyGroup>
<TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);GetToolsPackageFiles</TargetsForTfmSpecificBuildOutput>
</PropertyGroup>
<Target Name="GetToolsPackageFiles">
<ItemGroup>
<BuildOutputInPackage Include="$(OutputPath)\**\*.dll">
<TargetPath>$([MSBuild]::MakeRelative('$(OutputPath)', %(FullPath)))</TargetPath>
</BuildOutputInPackage>
<BuildOutputInPackage Include="$(OutputPath)\**\*.exe">
<TargetPath>$([MSBuild]::MakeRelative('$(OutputPath)', %(FullPath)))</TargetPath>
</BuildOutputInPackage>
</ItemGroup>
</Target>
According to the docs, I should be able to use %(FullPath), but I am getting this error:
error MSB4184: The expression "[MSBuild]::MakeRelative(C:\Sour
ce\RepositoryCleaner\output\Release\RepositoryCleaner\netcoreapp3.1\, '')" cannot be evaluated. Parameter "path" cannot have zero length.
[C:\Source\RepositoryCleaner\src\RepositoryCleaner\RepositoryCleaner.csproj]
Any idea why the well-known items don't seem to work in this scenario?
Got a fix by specifying the item group outside the target and then using that instead.
<PropertyGroup>
<TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);GetToolsPackageFiles</TargetsForTfmSpecificBuildOutput>
</PropertyGroup>
<ItemGroup>
<ToolDllFiles Include="$(OutputPath)\**\*.dll" />
<ToolExeFiles Include="$(OutputPath)\**\*.exe" />
</ItemGroup>
<Target Name="GetToolsPackageFiles">
<ItemGroup>
<BuildOutputInPackage Include="#(ToolDllFiles)">
<TargetPath>$([MSBuild]::MakeRelative('$(OutputPath)', %(ToolDllFiles.FullPath)))</TargetPath>
</BuildOutputInPackage>
<BuildOutputInPackage Include="#(ToolExeFiles)">
<TargetPath>$([MSBuild]::MakeRelative('$(OutputPath)', %(ToolExeFiles.FullPath)))</TargetPath>
</BuildOutputInPackage>
</ItemGroup>
</Target>
I have the following MSBuild script:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
...
<BuildDependsOn>
NSwag;
$(BuildDependsOn)
</BuildDependsOn>
<!--<AfterTransform>NSwag</AfterTransform>-->
</PropertyGroup>
<ItemGroup>
...
</ItemGroup>
<Target Name="NSwag" BeforeTargets="BeforeBuild">
<Message Text="Generating C# client code via NSwag" Importance="high" />
<!-- ISSUE HERE -->
<Copy SourceFiles="..\..\MyClient.cs" DestinationFiles="Gen\MyClient.cs" />
</Target>
</Project>
The Target "NSwag" above is going to be used for code generation tool. But to simplify things, I use here just a file copy command.
The issue is that the .cs files added within this Target are not visible in the MSBuild compilation:
The type or namespace name 'MyClient' does not exist in the namespace 'MyNamespace'
NOTE: The issue occurs only if the file didn't exist in the destination folder.
NOTE: I was trying to mangle with the following but with no success so far:
<Target Name="RemoveSourceCodeDuplicates" BeforeTargets="BeforeBuild;BeforeRebuild" DependsOnTargets="UpdateGeneratedFiles">
<RemoveDuplicates Inputs="#(Compile)">
<Output TaskParameter="Filtered" ItemName="Compile"/>
</RemoveDuplicates>
</Target>
and
<Target Name="UpdateGeneratedFiles" BeforeTargets="BeforeBuild;BeforeRebuild" DependsOnTargets="NSwag">
<ItemGroup>
<Compile Include="Gen\MyClient.cs" Condition="!Exists('Gen\MyClient.cs')" />
</ItemGroup>
</Target>
What am I missing here?
I think I found a workaround for that - check and include the files first (UpdateGeneratedFiles target), then generate them (NSwag target). See the script below:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
...
<BuildDependsOn>
NSwag;
$(BuildDependsOn)
</BuildDependsOn>
</PropertyGroup>
<Target Name="NSwag" BeforeTargets="BeforeBuild;BeforeRebuild"
DependsOnTargets="UpdateGeneratedFiles">
<Message Text="Generating C# client code via NSwag" Importance="high" />
<Copy SourceFiles="..\..\MyClient.cs" DestinationFiles="Gen\MyClient.cs" />
</Target>
<Target Name="UpdateGeneratedFiles" BeforeTargets="BeforeBuild;BeforeRebuild" >
<ItemGroup>
<Compile Include="Gen\MyClient.cs" Condition="!Exists('Gen\MyClient.cs')" />
</ItemGroup>
</Target>
</Project>
I am publishing an ASP.NET Core 1.1 application and I need to delete from the output a few folders (fr;nl;pt) created by a library (Fluent Validation):
<ItemGroup>
<FluentValidationExcludedCultures Include="fr;nl;pt">
<InProject>false</InProject>
</FluentValidationExcludedCultures>
</ItemGroup>
<Target Name="RemoveTranslationsAfterBuild" AfterTargets="AfterBuild">
<RemoveDir Directories="#(FluentValidationExcludedCultures->'$(OutputPath)%(Filename)')" />
</Target>
But this does not work and the folders are still copied ... Then I tried:
<ItemGroup>
<Content Include="fr" CopyToPublishDirectory="Never" />
<Content Include="nl" CopyToPublishDirectory="Never" />
<Content Include="pt" CopyToPublishDirectory="Never" />
</ItemGroup>
But this didn't work either ...
Does anyone has any idea how to make this work?
Try to edit your csproj file and add the following section for each of the directories that you do not want to include when publishing:
<ItemGroup>
<PublishFile Remove="directory\**" />
</ItemGroup>
Another solution that works for build/publish
<!-- Removes FluentValidation localization folders -->
<Target Name="AfterPackage" AfterTargets="CopyAllFilesToSingleFolderForPackage" />
<ItemGroup>
<FluentValidationExcludedCultures Include="cs;da;de;es;fa;fi;fr;it;ko;mk;nl;pl;pt;ru;sv;tr;zh-CN">
<InProject>false</InProject>
</FluentValidationExcludedCultures>
</ItemGroup>
<Target Name="FluentValidationRemoveTranslationsAfterBuild" AfterTargets="AfterBuild">
<RemoveDir Directories="#(FluentValidationExcludedCultures->'$(OutDir)%(Filename)')" />
</Target>
<Target Name="FluentValidationRemoveTranslationsAfterPackage" AfterTargets="AfterPublish">
<RemoveDir Directories="#(FluentValidationExcludedCultures->'$(OutDir)%(Filename)')" />
</Target>
I have an item with metadata I want to copy to perform some actions on and the result to occur in multiple locations,
for example, I want to copy the file to multiple locations:
<ItemGroup>
<MyItem Include="myFile.txt">
<Out>c:\blah;c:\test</Out>
</MyItem>
</ItemGroup>
how would I setup a target to create c:\blah and c:\test if they dont exist, then copy myFile.txt to c:\blah\myFile.txt and c:\test\myFile.txt
I also want to get the list of full output paths (c:\blah\myFile.txt and c:\test\myFile.txt) if I want to clean them during a clean.
If you dont want to change the structure of you ItemGroup, you need to handle that you have a nested ItemGroup (the MetaDataElement Out). Therefor you will need to batch the ItemGroup MyItem to the target and inside you can batch Out. I made a small example project:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="CopyFiles" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
<ItemGroup>
<MyItem Include="myFile.txt">
<Out>c:\blah;c:\test</Out>
</MyItem>
<MyItem Include="myFile2.txt">
<Out>c:\blah2;c:\test2</Out>
</MyItem>
</ItemGroup>
<Target Name="CopyFiles"
Inputs="%(MyItem.Identity)"
Outputs="%(MyItem.Identity)\ignore_this.msg">
<PropertyGroup>
<File>%(MyItem.Identity)</File>
</PropertyGroup>
<ItemGroup>
<Folders Include="%(MyItem.Out)" />
</ItemGroup>
<Message Text="%(Folders.Identity)\$(File)" />
</Target>
</Project>
The Output will be:
Project "D:\TEMP\test.proj" on node 1 (default targets).
CopyFiles:
c:\blah\myFile.txt
c:\test\myFile.txt
CopyFiles:
c:\blah2\myFile2.txt
c:\test2\myFile2.txt
Done Building Project "D:\TEMP\test.proj" (default targets).
Build succeeded.
0 Warning(s)
0 Error(s)
What you want to do is a concept called MSBuild Batching.
It allows you to divide item lists into different batches and pass each of those batches into a task separately.
<Target Name="CopyFiles">
<ItemGroup Label="MyFolders">
<Folder Include="c:\blah" />
<Folder Include="C:\test" />
</ItemGroup>
<Copy SourceFiles="myFile.txt" DestinationFolder="%(Folder.Identity)\">
<Output TaskParameter="CopiedFiles" ItemName="FilesCopy" />
</Copy>
</Target>
How about this:
<Target Name="CopyFiles">
<!--The item(s)-->
<ItemGroup>
<MyItem Include="myFile.txt"/>
</ItemGroup>
<!--The destinations-->
<ItemGroup>
<MyDestination Include="c:\blah"/>
<MyDestination Include="c:\test"/>
</ItemGroup>
<!--The copy-->
<Copy SourceFiles="#(MyItem)" DestinationFolder="%(MyDestination.FullPath)" />
<ItemGroup>
<FileWrites Include="%(MyDestination.FullPath)\*" />
</ItemGroup>
<!--The output -->
<Message Text="FileWrites: #(FileWrites)" Importance="high"/>
</Target>
Multiple projects have to be build with one ore more configurations (debug/release/...).
The output of the build needs to be copied to a folder (BuildOutputPath).
There is a default BuildOutputFolder, but for some project you can indicate that the output needs to be put in a extra child folder.
For example:
Configuration are:
- debug
- release
The projects are:
Project1 (BuildOutputFolder)
Project2 (BuildOutputFolder)
Project3 (BuildOutputFolder\Child)
The end result should look like this:
\\BuildOutput\
debug\
project1.dll
project2.dll
Child\
Project3.dll
release\
project1.dll
project2.dll
Child\
Project3.dll
I got this far atm, but can't figure out how to override the OutputPath per project.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0" DefaultTargets="Build" >
<ItemGroup>
<ConfigList Include="Debug" />
<ConfigList Include="Release" />
</ItemGroup>
<PropertyGroup>
<BuildOutputPath>$(MSBuildProjectDirectory)\BuildOutput\</BuildOutputPath>
</PropertyGroup>
<ItemGroup>
<Projects Include="project1.csproj" />
<Projects Include="project2.csproj" />
<Projects Include="project3.csproj" />
</ItemGroup>
<Target Name="Build">
<MSBuild Projects="#(Projects)"
BuildInParallel="true"
Properties="Configuration=%(ConfigList.Identity);OutputPath=$(BuildOutputPath)%(ConfigList.Identity)" />
</Target>
</Project>
How would you accomplish this in a MSBuild project file ?
Your'e attempting to call a task recursively in two different contexts. 2 configurations and 3 projects requires 6 calls to the build task. You need to layout the project in such a way that for each item in ConfigList a call is made multiplied by each item in Projects.
Also use ItemDefinitionGroup to set default shared properties:
<ItemGroup>
<ConfigList Include="Debug" />
<ConfigList Include="Release" />
</ItemGroup>
<ItemDefinitionGroup>
<Projects>
<BuildOutputPath>$(MSBuildProjectDirectory)\BuildOutput\</BuildOutputPath>
</Projects>
</ItemDefinitionGroup>
<ItemGroup>
<Projects Include="project1.csproj" />
<Projects Include="project2.csproj" />
<Projects Include="project3.csproj" >
<Subfolder>Child</Subfolder>
</Projects>
</ItemGroup>
<Target Name="Build">
<MSBuild Projects="$(MSBuildProjectFullPath)"
Targets="_BuildSingleConfiguration"
Properties="Configuration=%(ConfigList.Identity)" />
</Target>
<Target Name="_BuildSingleConfiguration">
<MSBuild Projects="#(Projects)"
BuildInParallel="true"
Properties="Configuration=$(Configuration);OutputPath=%(Projects.BuildOutputPath)$(Configuration)\%(Projects.Subfolder)" />
</Target>
</Project>
Try to do it using Project metadata
<ItemGroup>
<Projects Include="project1.csproj">
<ChildFolder/>
</Project>
<Projects Include="project2.csproj">
<ChildFolder/>
</Project>
<Projects Include="project3.csproj">
<ChildFolder>Child</ChildFolder>
</Project>
</ItemGroup>
<Target Name="Build">
<MSBuild Projects="#(Projects)"
BuildInParallel="true"
Properties="Configuration=%(ConfigList.Identity);OutputPath=$(BuildOutputPath)%(ConfigList.Identity)%(Project.ChildFolder)" />