Group multiple references in one markup element - msbuild

we are using some third-party library that have some dependencies that must be referenced from the projects.
So each "csproj" file will have a bunch of "Reference" elements that will always be identical.
Is there a way to group this set of elements in one standard element that could be used accross all the csproj files ?
E.g :
<ABunchOfReferences>
<Reference Include="Reference1" />
<Reference Include="Reference2" />
<Reference Include="Reference3" />
</ABunchOfReferences>
...
<ItemGroup>
<ABunchOfReferences/>
<Reference Include="System" />
<Reference Include="System.Core" />
...
</ItemGroup>
"ABunchOfReferences" would be a kind of macro globally defined and included by all the csprojs that when used would be expanded as 3 "Reference" elements.
Thanks in advance for any idea.

You can set up your references in an import file, then selectively include them in the main file,
In References.props
<ItemGroup>
<BunchOfReferences Include="Reference1" />
<BunchOfReferences Include="Reference2" />
<BunchOfReferences Include="Reference3" />
</ItemGroup>
<ItemGroup>
<MoreReferences Include="MoreReference1" />
<MoreReferences Include="MoreReference2" />
<MoreReferences Include="MoreReference3" />
</ItemGroup>
...then in the individual project file
<Import Project="PathTo\References.props" />
<ItemGroup>
<Reference Include="System" />
<References Include="#(BunchOfReferences)" />
<References Include="#(MoreReferences)" />
</ItemGroup>

You can put all your "ABunchOfReferences" in a separate file and then include that file inside your ItemGroup like this:
<ItemGroup>
<Import Project="PathToFile\yourBunchofReferenceFile" />
<Reference Include="System" />
...
</ItemGroup>

Related

How to assign the version of a specific PackageReference to a property

I am trying to get the version of a specific package that is referenced through the PackageReference, so that I can reference a tool that comes inside one of those packages. I am able to write the version using a Message task:
<Message Importance="High" Text="#(PackageReference->%(PackageReference.Version))" Condition="'%(PackageReference.Identity)' == 'Google.Protobuf.Tools'" />
How can I retrieve that value inside a property?
The relevant parts from my csproj looks like this:
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.5.0" />
<PackageReference Include="Google.Protobuf.Tools" Version="3.5.0" />
<PackageReference Include="Grpc" Version="1.8.0" />
<PackageReference Include="Grpc.Tools" Version="1.8.0" />
</ItemGroup>
<PropertyGroup>
<ProtobufCompiler>$(UserProfile)/.nuget/packages/Google.Protobuf.Tools/3.5.0/tools/windows_x64/protoc.exe</ProtobufCompiler>
<GrpcCSharpPlugin>$(UserProfile)/.nuget/packages/Grpc.Tools/1.8.0/tools/windows_x64/grpc_csharp_plugin.exe</GrpcCSharpPlugin>
</PropertyGroup>
I found that I can use the CreateProperty task for that:
<CreateProperty Value="%(PackageReference.Version)" Condition="%(PackageReference.Identity) == 'Google.Protobuf.Tools'">
<Output TaskParameter="Value" PropertyName="GoogleProtobufToolsVersion" />
</CreateProperty>
Latest Grps.Tools calls proto compiler during the build.
<ItemGroup>
<PackageReference Include="Grpc.Tools" Version="2.31.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Protobuf Include="my.proto" GrpcServices="None">
</Protobuf>

Check if Targets exists with MSBuild task

Here is my code:
<MSBuild Projects="outs.proj" Targets="Build;MyCustomTarget">
</MSBuild>
I need to check if MyCustomTarget exists in outs.proj before executing.
It throws an error when MyCustomTarget is not imported, so depending on the result use either Build or Build+MyCustomTarget.
Thanks in advance for your help.
Getting a list of targets is cumbersome, you can either reflect on TaskHost via BuildEngine to get current Project or re-evaluate the project with an inline task.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask TaskName="Targets" TaskFactory="CodeTaskFactory" AssemblyName="Microsoft.Build.Tasks.v12.0">
<ParameterGroup>
<Project ParameterType="System.String" Required="true" />
<All ParameterType="System.String[]" Output="true" />
<Run ParameterType="System.String[]" Output="true" />
</ParameterGroup>
<Task>
<Reference Include="Microsoft.Build" />
<Reference Include="System.Xml" />
<Code>
All = new Microsoft.Build.Evaluation.Project(Project).Targets.Select(t => t.Key).ToArray();
Run = Run.Where(All.Contains).ToArray();
</Code>
</Task>
</UsingTask>
<Target Name="Foo">
<Targets Project="$(MSBuildProjectFullPath)" Run="Foo;Baz">
<Output TaskParameter="All" ItemName="All" />
<Output TaskParameter="Run" ItemName="Run" />
</Targets>
<Message Text="All Targets: #(All)" />
<Message Text="Run Targets: #(Run)" />
</Target>
<Target Name="Bar" />
</Project>
Edit:
You don't provide much details so I can't help with your particular issue, but if new Project(Project) throws may be try ProjectCollection.GlobalProjectCollection.LoadProject(Project) instead to get to the Targets; the same collection has the LoadedProjects property as well as GetLoadedProjects and UnloadProject methods to play around with to get around your exception. If you're in control of the project file and it's flat with no Import you might want to try parsing it as a simple XML file rather than a fully fledged MSBuild project.
<XmlPeek XmlInputPath="$(MSBuildProjectFullPath)" Query="/p:Project/p:Target/#Name" Namespaces="<Namespace Prefix='p' Uri='http://schemas.microsoft.com/developer/msbuild/2003' />">
<Output TaskParameter="Result" ItemName="All" />
</XmlPeek>
<ItemGroup>
<In Include="Foo;Baz" />
<Out Include="#(In)" Exclude="#(All)" />
<Run Include="#(In)" Exclude="#(Out)" />
</ItemGroup>
<Message Text"#(Run)" />
In either case, you pass outs.proj path to whichever method you choose go with and get back #(All) with all of the targets in that project (Foo;Bar) then you filter your targets down from Foo;Baz to just Foo since Baz doesn't exit in #(All). Then you do whatever you want to do with this information, e.g. <MSBuild Projects="outs.proj" Targets="Build;#(Run)">.

MSBuild - Including References

I'm trying to learn MSBuild; and to begin I have a C# project file that looks like this:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<RootNamespace>$(MSBuildProjectName)</RootNamespace>
<AssemblyName>$(MSBuildProjectName)</AssemblyName>
<TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
<ItemGroup>
<Compile Include="**\*.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<Target Name="Build">
<Csc
AdditionalLibPaths="C:\Windows\Microsoft.NET\Framework\v4.0.30319\"
References="#(Reference)"
Sources="#(Compile)"
OutputAssembly="$(MSBuildProjectName).exe"
/>
</Target>
</Project>
However, CSC fails, saying it can't find the metadata file 'xyz', once for each reference that I've tried to include. Additionally, Visual Studio reports a similar problem:
So what am I doing wrong?
The reason you getting this error because of the Target Name = "Build"
<Target Name="Build">
<Csc AdditionalLibPaths="C:\Windows\Microsoft.NET\Framework\v4.0.30319\"
References="#(Reference)"
Sources="#(Compile)" OutputAssembly="$(MSBuildProjectName).exe" />
</Target>
In particularly the line:
<Csc AdditionalLibPaths="C:\Windows\Microsoft.NET\Framework\v4.0.30319\"
References="#(Reference)"
Sources="#(Compile)" OutputAssembly="$(MSBuildProjectName).exe" />
If you don't need you can remove this additional lib paths, if needed search on the web you should be able to fine how to set up the AddtionalLib paths. This might assist you.

get list of subdirectories in msbuild

Given a list of directories:
<ItemGroup>
<Dirs Include="Foo\Dir1" />
<Dirs Include="Foo\Dir2" />
</ItemGroup>
How can I get a list of all subdirectories.
Transforming this list with "$(Identity)\**" does not match anything and transforming with "$(Identity)\**\*" and then with RelativeDir yields only directories that contain files.
Currently I have to resort to C#:
<UsingTask TaskName="GetSubdirectories" TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<Directories ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
<SubDirectories ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true" />
</ParameterGroup>
<Task>
<Code Type="Fragment" Language="cs"><![CDATA[
var result = new List<ITaskItem>();
foreach (var dirItem in Directories) {
foreach (var dir in Directory.GetDirectories(dirItem.ItemSpec, "*", SearchOption.AllDirectories)) {
if (dir.Contains(#"\.svn\") || dir.EndsWith(#"\.svn")) continue;
result.Add(new TaskItem(dir));
}
}
SubDirectories = result.ToArray();
]]></Code>
</Task>
</UsingTask>
<GetSubdirectories Directories="#(Dirs)">
<Output TaskParameter="SubDirectories" ItemName="SubDirs" />
</GetSubdirectories>
But I would like to know if there is an easier way.
Excerpted from the book "MSBuild Trickery":
<Import Project="EnableAllPropertyFunctions.tasks" />
<Target Name="GetSubdirectories">
<ItemGroup>
<Dirs Include="$([System.IO.Directory]::
EnumerateDirectories(
`.\Foo`,
`*`,
System.IO.SearchOption.AllDirectories))"
/>
</ItemGroup>
<Message Text="%(Dirs.Identity)" />
</Target>
You'll need to first enable the extended property function set by ensuring that the environment variable MSBuildEnableAllPropertyFunctions is set to the value 1 (that is what the imported .tasks file accomplishes, with an inline task).
Once #(Dirs) is set up, you can filter it with the Remove attribute to get rid of the Subversion folders.
<CreateItem Include="$(OutputFolder)\*\*.*">
<Output TaskParameter="Include" ItemName="FilesInSubFolders" />
</CreateItem>
<RemoveDuplicates Inputs="#(FilesInSubFolders->'%(RelativeDir)')">
<Output TaskParameter="Filtered" ItemName="SubDirs"/>
</RemoveDuplicates>
<Message Text="#(SubDirs)"/>
This will put all the immediate subfolder paths into #(SubDirs). If you change Include="$(OutputFolder)\*\*.*" to Include="$(OutputFolder)\**\*.*", it'll include all subfolders recursively.
To expand on Brian's answer with a fully self-contained example:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="GetSubdirectories">
<UsingTask TaskName="SetEnvironmentVariable"
TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v$(MSBuildToolsVersion).dll">
<ParameterGroup>
<Name ParameterType="System.String" Required="true" />
<Value ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Using Namespace="System" />
<Code Type="Fragment" Language="cs"><![CDATA[
Environment.SetEnvironmentVariable(Name, Value);
]]></Code>
</Task>
</UsingTask>
<Target Name="GetSubdirectories">
<SetEnvironmentVariable Name="MSBuildEnableAllPropertyFunctions" Value="1" />
<ItemGroup>
<Dirs Include="$([System.IO.Directory]::EnumerateFiles('.\Stuff', '*', System.IO.SearchOption.AllDirectories))"/>
</ItemGroup>
<Message Text="%(Dirs.Identity)" />
</Target>
</Project>
I got the UsingTask example from this answer.

Why doesn't Content Remove work for MSBuild ItemGroup?

I have an AfterCompile target defined in my csproj which involves minifying and combining JS and CSS. I then add them to ItemGroup Content and remove the unnecessary files, however the Remove paramter does not seem to work.
<Target Name="AfterCompile">
<ItemGroup>
<JS_Combine Include="js\c??.*.min.js" />
<CSS_Combine Include="css\c??.*.min.css" />
</ItemGroup>
<!-- Combine JS -->
<ReadLinesFromFile File="%(JS_Combine.Identity)">
<Output TaskParameter="Lines" ItemName="JSLines" />
</ReadLinesFromFile>
<WriteLinesToFile File="js\combined.min.js" Lines="#(JSLines)" Overwrite="true" />
<!-- Combine CSS -->
<ReadLinesFromFile File="%(CSS_Combine.Identity)">
<Output TaskParameter="Lines" ItemName="CSSLines" />
</ReadLinesFromFile>
<WriteLinesToFile File="css\combined.min.css" Lines="#(CSSLines)" Overwrite="true" />
<!-- Tidy up -->
<ItemGroup>
<Content Include="js\combined.min.js" />
<Content Include="css\combined.min.css" />
<Content Remove="#(JS_Combine)" />
<Content Remove="#(CSS_Combine)" />
</ItemGroup>
<!-- DEBUG message -->
<Message Text="DEBUG: #(Content)" Importance="high" />
</Target>
The debug message still shows #(Content) as having the unnecessary js files. Can anyone tell me what's happening?
In order to recreate you situation I created this sample script
<Project DefaultTargets="Demo" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Demo">
<ItemGroup>
<JS_Combine Include="js\c01.min.js;js\c02.min.js;js\c03.min.js;" />
<CSS_Combine Include="css\c01.min.css;css\c02.min.css;css\c03.min.css;" />
</ItemGroup>
<ItemGroup>
<Content Include="#(JS_Combine);#(CSS_Combine)"/>
</ItemGroup>
<Message Text="Content Before: #(Content)" Importance="high" />
<!-- Tidy up -->
<ItemGroup>
<Content Include="js\combined.min.js" />
<Content Include="css\combined.min.css" />
<Content Remove="#(JS_Combine)" />
<Content Remove="#(CSS_Combine)" />
</ItemGroup>
<Message Text="-------------------------"/>
<Message Text="Content After: #(Content)" Importance="high" />
</Target>
</Project>
It works for me here is the results:
Project "C:\Data\Development\My Code\Community\MSBuild\RemoveTest\Remove01.proj" on node
1 (default targets).
Demo:
Content Before: js\c01.min.js;js\c02.min.js;js\c03.min.js;css\c01.min.css;css\c02.min.c
ss;css\c03.min.css
-------------------------
Content After: js\combined.min.js;css\combined.min.css
Done Building Project "C:\Data\Development\My Code\Community\MSBuild\RemoveTest\Remove01.
proj" (default targets).
Are you still having issues with this?