Msbuild copy and flatten with unique file names - c#

I'm trying to copy multiple files from a deep source tree that have the same file name. For example TestResults.trx. I want to copy them into a single directory (i.e. flattened). Problem is they just overwrite each other and I just end up with a single TestResults.trx in the directory.
<ItemGroup>
<SilverlightTestResults Include=".\**\*.trx" Exclude=".\TestResults\*" />
</ItemGroup>
<Copy SourceFiles="#(SilverlightTestResults)" DestinationFolder=".\TestResults">
I thought I could do a transform using some well known metadata but there doesn't seem to be anything unique in there to do it (the test results I'm trying to copy live in directories like this: .\SomeProject\bin\debug\TestResults.trx).
Copying to a directory like this like this would be ideal:
.\TestResults\TestResults1.trx
.\TestResults\TestResults2.trx
.\TestResults\TestResults3.trx
I don't care about the actual names as long as they are unique.
Any ideas, looks like a custom task is required?

I can't provide a solution that just uses msbuild - you could either use msbuildtasks
to use the <Add /> task for incrementing a counter.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
<PropertyGroup>
<FileCounter>0</FileCounter>
</PropertyGroup>
<ItemGroup>
<MySourceFiles SilverlightTestResults Include=".\**\*.trx" Exclude=".\TestResults\*"/>
</ItemGroup>
<Target Name="CopyFiles">
<Math.Add Numbers="$(FileCounter);1">
<Output TaskParameter="FileCounter" PropertyName="FileCounter" />
</Math.Add>
<Copy
SourceFiles="#(MySourceFiles)"
DestinationFiles="#(MySourceFiles->'.\TestResults\%(Filename)_$(FileCounter)%(Extension)')"
/>
</Target>
However you might do better with a custom task or probably executing a powershell script.

Yes, a custom task will be required.
You could look to see what functionality the Move task from the community task project (link here) offers, but if it doesn't do what you want then it is open source, so it will be trivial to check the source and modify it to suit your needs.

Related

Using MSBuild, how can you group .cs files with matching .g.cs files in another folder?

I have a .NET 6 project where I want some generated files - somewhere in the "Generated" folder structure - to be grouped with their non-generated equivalents. See dotnet/roslyn/issues/45645 for some related concepts.
How can you achieve this using MSBuild? Key challenges include:
Map all Generated\**\*.g.cs with their *.cs equivalents.
If necessary: Copy all .g.cs files to their siblings' locations, allowing for grouping.
Produce DependentUpon elements for the related files (.cs and .g.cs).
If necessary: Remove the copies from compilation as they are already part of compilation.
End result
The end result should look like below. Note that removing, hiding or clearing the "Generated" folder is acceptable (if not preferable).
My understanding is that DependentUpon is suitable for this, though (as far as I know) it will only work for files in the same folder.
So for our simple Console project, we would have something similar to this:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<!-- End goal (not hard-coded, obviously): -->
<ItemGroup>
<Compile Update="Program.g.cs">
<DependentUpon>Program.cs</DependentUpon>
</Compile>
</ItemGroup>
</Project>
Steps to try this out
Create a .NET 6 Console app.
Create a "Generated" folder in the root.
Add Program.g.cs in the "Generated" folder.
Put MSBuild magic in the .csproj file.
// program.cs
partial class Program
{
public static void Main()
{
Foo();
}
}
// program.g.cs
partial class Program
{
public static void Foo() => Console.WriteLine("Generated");
}
I have an example of matching the files and adding the DependentUpon metadata. However, the Visual Studio Solution Explorer window will not show the files as nested. Just adding the metadata when the project is run is not sufficient. The project file that is saved to the file system must be updated to include the DependentUpon metadata.
Regardless here is a working example of matching the files and adding the metadata:
<Target Name="MatchAndAddDependentUpon">
<!-- The file suffix for generated files -->
<PropertyGroup>
<Suffix>.g</Suffix>
</PropertyGroup>
<!-- Example data -->
<ItemGroup>
<Compile Include="aaa.cs;aaa$(Suffix).cs;bbb.cs;ccc.cs;ccc$(Suffix).cs;ddd$(Suffix).cs" />
</ItemGroup>
<ItemGroup>
<!-- Create a set of generated files and a set of candiate source files (i.e. files not in the set of generated files) -->
<Generated Include="#(Compile)" Condition="$([System.String]::Copy('%(Filename)').EndsWith('$(Suffix)'))" />
<CandidateSource Include="#(Compile)" Exclude="#(Generated)" />
<!-- Create Match as a cartesian product of Generated and CandidateSource; then prune the 'matches' that are false -->
<Match Include="#(Generated)">
<DependentUpon>%(CandidateSource.Identity)</DependentUpon>
<DependentUponFileName>%(CandidateSource.Filename)</DependentUponFileName>
</Match>
<Match Remove="#(Match)" Condition="'%(DependentUponFileName)$(Suffix)' != '%(Filename)'" />
<!-- Remove the items in Match from Compile; then include them but without the 'DependentUponFileName' -->
<Compile Remove="#(Match)" />
<Compile Include="#(Match)" RemoveMetadata="DependentUponFileName" />
</ItemGroup>
<Message Text="#(Compile->'%(Identity) (DependentUpon %(DependentUpon))', '%0d%0a')" />
</Target>
The output will look like the following:
MatchAndAddDependentUpon:
aaa.cs (DependentUpon )
bbb.cs (DependentUpon )
ccc.cs (DependentUpon )
ddd.g.cs (DependentUpon )
aaa.g.cs (DependentUpon aaa.cs)
ccc.g.cs (DependentUpon ccc.cs)
Note that the code handles the case where 'ddd.g.cs' exists in 'Compile' and 'ddd.cs', the expected parent, does not.

GenerateResource task is not creating resources file

I have written a task for generating resource as below inside .csproj file. But the .resources and .resurces.dll files are not generated. What am I doing wrong here?
<ItemGroup>
<Compile Include="$(GeneratedFilesOutputFolder)\MessageInfo.cs" Condition="Exists('$(GeneratedFilesOutputFolder)\MessageInfo.cs')" />
<EmbeddedResource Include="$(GeneratedFilesOutputFolder)\MessageInfo.resx" LogicalName="MessageInfo.resources"/>
</ItemGroup>
<Target Name = "GenerateResources">
<GenerateResource
Sources="$(GeneratedFilesOutputFolder)\MessageInfo.resx"
OutputResources="$(GeneratedFilesOutputFolder)\MessageInfo.resources">
<Output TaskParameter="OutputResources"
ItemName="MessageInfoResource"/>
<Output TaskParameter = "FilesWritten" ItemName = "FileWrites"/>
</GenerateResource>
</Target>
<Target Name="GenerateSatelliteAssemblies"
Inputs="#(MessageInfoResource)"
Outputs="$(BuildPath)\MessageInfo.resources.dll" >
<AL
EmbedResources = "#(MessageInfoResource)"
ToolExe="$(AlToolExe)"
ToolPath="$(AlToolPath)"
OutputAssembly = "$(BuildPath)\MessageInfo.resources.dll" >
<Output TaskParameter="OutputAssembly" ItemName="FileWrites"/>
</AL>
<CreateItem
Include = "$(BuildPath)\MessageInfo.resources.dll" >
<Output TaskParameter = "Include" ItemName = "SatelliteAssemblies" />
</CreateItem>
</Target>
At a glance, potential culprits include Exists('$(GeneratedFilesOutputFolder)\MessageInfo.cs') not evaluating to True, the target hooks you are using not being triggered as you expect or your targets being replaced by empty targets due to the relevant .targets file(s) importing after your targets are evaluated. The best way to check these things is to add -bl to your dotnet build or msbuild command, install MSBuild Structured/Binary Log Viewer and use it to open the msbuild.binlog file in the directory containing the project or solution you are building. This will allow you to inspect the targets that result after your .csproj and all imports are evaluated, the result of your item conditional, whether the target call hierarchy includes your targets the way you expect and whether any relevant targets are skipped due to Inputs and Outputs attributes.

Can an MSBuild Item use a Property set by a Target?

I'm trying to achieve the following with MSBuild: my main project (MyProject.csproj) should include a couple Reference items, but the path to one of those References is the value of the SomeProperty property, which is set by a Target. Specifically, the value for SomeProperty is parsed from a file using ReadLinesFromFileTask.
Here is the high-level structure of MyProject.csproj:
<Project>
<Target Name="CreateSomeProperty">
<!-- Tasks that ultimately set $(SomeProperty) by parsing a value with ReadLinesFromFileTask -->
</Target>
<ItemGroup>
<Reference Include="$(SomeProperty)" />
<!-- Other Reference items -->
</ItemGroup>
</Project>
Unfortunately, this setup is not working. I see those little yellow triangles under the Dependencies node of MyProject in the VS Solution Explorer, since the project is looking for a DLL at a path with missing characters. Similarly, when I build the project, I get a bunch of The type or namespace name could not be found errors, even though I still see the output from a Message Task inside my Target. Presumably, the CreatePathProperty Target is running during the execution phase, after the Reference items have already failed to load during the evaluation phase.
Is there a way to make a setup like this work? I've tried setting BeforeTargets="Build" in the Target element, and setting InitialTargets="CreateSomeProperty" in the Project element, but nothing seems to work. Any help would be much appreciated!
Can an MSBuild Item use a Property set by a Target?
Yes, I'm sure it's possible if you're in .net framework project with old csproj format and what you want is a supported scenario in VS2017(Only did the test in VS2017).
Tips:
Normally msbuild reads the Properties and Items before it executes your custom target. So we should use something like BeforeTargets="BeforeResolveReferences" to make sure the correct order in this scenario is custom target runs=>create the property=>msbuild reads the info about references and the property.
Otherwise the order(wrong order when BeforeTargets="Build" or what) should be: Msbuild reads the info about references(now the property is not defined yet)=>the target runs and creates the property.
Solution:
Add this script to the bottom of your xx.csproj.
<!-- Make sure it executes before msbuild reads the ItemGroup above and loads the references -->
<Target Name="MyTest" BeforeTargets="BeforeResolveReferences">
<ItemGroup>
<!-- Define a TestFile to represent the file I read -->
<TestFile Include="D:\test.txt" />
</ItemGroup>
<!-- Pass the file to read to the ReadLinesFromFile task -->
<ReadLinesFromFile File="#(TestFile)">
<!--Create the Property that represents the normal hintpath(SomePath\DLLName.dll)-->
<Output TaskParameter="Lines" PropertyName="HintPathFromFile" />
</ReadLinesFromFile>
<!-- Output the HintPath in output log to check if the path is right -->
<Message Text="$(HintPathFromFile)" Importance="high" />
<ItemGroup>
<Reference Include="TestReference">
<!--It actually equals:<HintPath>D:\AssemblyFolder\TestReference.dll</HintPath>-->
<HintPath>$(HintPathFromFile)</HintPath>
</Reference>
</ItemGroup>
</Target>
In addition:
I did the test with test.txt file whose content is:
I'm not sure about the actual content(and format) of your file, but if you only have path like D:\AssemblyFolder\ in that file, you should pass the D:\AssemblyFolder\+YourAssemblyName.dll to <HintPath> metadata. Cause the default reference format with hintpath looks like this:
<Reference Include="ClassLibrary1">
<HintPath>path\ClassLibrary1.dll</HintPath>
</Reference>
Pay attention to the path format! Hope my answer helps :)
In msbuild, static items can't depend on dynamic properties by design.
Accepted answer provides a work-around: use dynamic items, which, naturally, can depend on dynamic properties. But dynamic items don't show up in the IDE.
When dealing with files, more IDE-friendly approach is to create the item statically and update it when the target runs:
<!-- Static item definition -->
<ItemGroup>
<SomeItem Include="item_file" />
</ItemGroup>
<Target Name="...">
<!-- When the target runs, find the static item using Condition and change its properties -->
<ItemGroup>
<SomeItem Condition="'%(SomeItem.Identity)' == 'item_file'">
<SomeProperty>New Value</SomeProperty>
</SomeItem>
</ItemGroup>
</Target>
There's also an open msbuild issue (#889) to support improved syntax for this:
<Target Name="...">
<ItemGroup>
<!-- Update has the same syntax as Include -->
<SomeItem Update="item_file">
<SomeProperty>New Value</SomeProperty>
</SomeItem>
</ItemGroup>
</Target>

Change the application target framework when changing build configurations in Visual Studio

I have these build configurations:
These platform configurations:
And these compiler conditionals:
NET40
NET45
My solution is a huge API that consists in 20 solutions, some of those solutions consumes Async keywords and other beneffits that are available only from .NetFx 4.5.
That part of the code I have it in a conditional in this way:
#If NET45 then
Sub Async
...
End Sub
#Else
Sub
...
End Sub
#End If
Then, what I'm trying to do is clear, the .NetFx 4.5 build configurations should compile the block of the NET45 conditional, and the .NetFx 4.0 build configurations should compile the block of the #Else part.
The problem I found is that if I change the application target framework in the project settings, the change persist in all the other build configurations, and I would like to avoid that persistance.
So how I can do this?.
Note:
I marked this question with the C# tag because is a general Visual Studio environment question, but I will clarify that my solutions are written in Vb.Net, because I know there are some big differences between the C# project settings and also their compiler params so maybe a C# advanced answer could not help me.
My suggestion is to get rid of conditional statements in code by moving platform/target/etc sencitive code in partial files. Then I would go to project file and would make the icluded files sensitive on particular condition using all the fuctionality ms-build provides
Example:
Create brand new VB Console App in Visual Studio
add three class files ClassDotNetFeatures40.vb, ClassDotNetFeatures45.vb, GenericClass.vb
Add the following code
in GenericClass.vb
Partial Public Class GenericClass
Public Sub Hello()
Console.Write("Hello ")
End Sub
End Class
in ClassDotNetFeatures40.vb
Partial Public Class GenericClass
Public Sub Word()
Console.Write("4.0 Word!")
End Sub
End Class
in
ClassDotNetFeatures45.vb
Public Class GenericClass
Public Sub Word()
Console.Write("4.5 Word!")
End Sub
End Class
Put the following code in Module1.vb
Sub Main()
Dim o = New GenericClass()
o.Hello()
o.Word()
End Sub
Save all
Right click on your solution and press Unload Project
Right click on the project file and press Edit Project
Find the following lines:
<Compile Include="ClassDotNetFeatures40.vb" />
<Compile Include="ClassDotNetFeatures45.vb" />
and replace them with
<Compile Condition="'$(Configuration)' == 'Debug'" Include="ClassDotNetFeatures40.vb" />
<Compile Condition="'$(Configuration)' == 'Release'" Include="ClassDotNetFeatures45.vb" />
press save
right click on project file and press Reload
now when you run the project undo debug you will get:
Hello 4.0 Word!
undo release you willl get:
Hello 4.5 Word!
You will need to change project files manually (I've played with csproj - hopefully vbproj works in the same way).
All project configurations properties described in the sections like this one:
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
...
</PropertyGroup>
Please notice Condition statement - that describes that this particular property set specified for Debug, AnyCPU configuration.
What you need to do is to move TargetFrameworkVersion property from general top level to configuration-specific levels, something like this:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<!-- general properties here - removing framework related... -->
<!--<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>-->
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<!-- Use 4.0 for Debug -->
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<!-- other properties here... -->
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<!-- Use 4.5 for Release -->
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<!-- other properties here... -->
</PropertyGroup>
Please notice that VS.Net GUI does NOT support this, and will not display correct values in the project Properties window; though it will use these values for build.
Depending on complexity of your solution, you might found some other artifacts, as VS.Net will not reload project properly, but at least that should work with build from console.
Additionally, you might need to use similar conditional "hacks" to reference correct libraries.

Using ILMerge with log4net is causing "inaccessible due to protection level" error

I created a wrapper class for the initialization of my log4net logging objects in order to make it easier to establish custom properties in the ThreadContext. This occurs within a class library that I have established along with many other useful functions. To join all of the various libraries I have also added an AfterBuild target to ILMerge using the '/internalize' switch.
All references to this initializer method within the library targeted by ILMerge seem to work just fine. However, when I reference this merged library in other places. My implementation throws protection level errors. I have tried adding various things to the optional exclude (/internalize:excludes.txt) file but this doesn't seem to work.
Example excludes.txt:
log4net.Config
log4net.ThreadContext
log4net.LogManager
Has anyone else had this issue?
[EDIT]:
Here is the code:
[assembly: log4net.Config.XmlConfigurator(Watch = true)]
namespace Logging
{
public static class Log4NetThreadContext
{
public static ILog Initialize(Type declaringType)
{
// Read from configuration
XmlConfigurator.Configure();
// Set Properties
ThreadContext.Properties["ID"] = ...
...
...
...
if(System.Diagnostics.Debugger.IsAttached)
{
// Special debugging logger
return LogManager.GetLogger("DEBUG_MODE");
}
else
{
// Root logger
return LogManager.GetLogger(declaringType);
}
}
}
}
I'm utilizing this code like so..
private static readonly Type declaringType =
MethodBase.GetCurrentMethod().DeclaringType;
private static readonly ILog log =
Log4NetThreadContext.Initialize(declaringType);
...
log.Info("Something useful");
[EDIT]:
This is my AfterBuild target
<Target Name="AfterBuild">
<CreateItem Include="#(ReferenceCopyLocalPaths)" Condition="'%(Extension)'=='.dll'">
<Output ItemName="AssembliesToMerge" TaskParameter="Include" />
</CreateItem>
<Message Text="MERGING: #(AssembliesToMerge->'%(Filename)')" Importance="High" />
<Exec Command=""$(ProgramFiles)\Microsoft\Ilmerge\Ilmerge.exe" /targetplatform:v2 /log /internalize:"ilmerge.excludes.txt" /keyfile:$(AssemblyOriginatorKeyFile) /out:#(MainAssembly) "#(IntermediateAssembly)" #(AssembliesToMerge->'"%(FullPath)"', ' ')" />
<Delete Files="#(ReferenceCopyLocalPaths->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')" />
Is there just a better way in general to debug protection level issues?
Log4NetThreadContext.Initialize(System.Type)' is inaccessible due to its protection level
Ultimately the easiest thing to do is to exclude log4net completely from the ilmerge process and maintain it as a dependent assembly.
So after much torture here's the "not-so-obvious" solution..
The excludes were not required after all, the real answer is to use the /lib:[path] switch in ilmerge.
I updated the AfterBuild target to remove the excludes from the /internalize switch. Next I added the /lib switch to pass in the location of the log4net dll as a dependent reference. It looks like this:
<Target Name="AfterBuild">
<CreateItem Include="#(ReferenceCopyLocalPaths)" Condition="'%(Extension)'=='.dll'">
<Output ItemName="AssembliesToMerge" TaskParameter="Include" />
</CreateItem>
<Message Text="MERGING: #(AssembliesToMerge->'%(Filename)')" Importance="High" />
<Exec Command=""$(ProgramFiles)\Microsoft\Ilmerge\Ilmerge.exe" /lib:..\packages\log4net.2.0.0\lib\net35-full\ /targetplatform:v2 /log /internalize /keyfile:$(AssemblyOriginatorKeyFile) /out:#(MainAssembly) "#(IntermediateAssembly)" #(AssembliesToMerge->'"%(FullPath)"', ' ')" />
<Delete Files="#(ReferenceCopyLocalPaths->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')" />
</Target>
In addition, I've added another target to restrict the list of assemblies included in the merge by adding a unique <ILMerge /> element to the references located in my .csproj file
<Target Name="AfterResolveReferences">
<Message Text="Filtering out ILMerge assemblies from ReferenceCopyLocalPaths..." Importance="High" />
<ItemGroup>
<ReferenceCopyLocalPaths Remove="#(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.ILMerge)'=='false'" />
</ItemGroup>
</Target>
Thus the reference elements were listed like so to accommodate:
...
<Reference Include="Ionic.Zip">
<HintPath>..\packages\DotNetZip.1.9.1.8\lib\net20\Ionic.Zip.dll</HintPath>
<ILMerge>True</ILMerge>
</Reference>
<Reference Include="log4net">
<HintPath>..\packages\log4net.2.0.0\lib\net35-full\log4net.dll</HintPath>
<ILMerge>False</ILMerge>
...
There's probably a better (programmatic) alternative for explicitly adding ILMerge=False values to the /lib switch but, in my case it is sufficient due to there being only one excluded item. Otherwise you may need to add additional paths manually.
Credit for the 'AfterResolveReferences' technique I've listed goes to http://www.hanselman.com/blog/MixingLanguagesInASingleAssemblyInVisualStudioSeamlesslyWithILMergeAndMSBuild.aspx
Hopefully this helps someone!
This usually happens to me when I am running the code from a remote location. .NET has permission issues when accessing the config file from a remote location. For example, a code repository setup on a network share.
I've only every been able to overcome it by moving my code to a local harddrive (as I don't have admin rights to elevate the security of the network share to "Trusted", etc).
(Note that this is primarily an issue with versions of Visual Studio prior to 2005. I think this was fixed in 2008 onwards, so it may not affect you. You don't say what version you are using, though.)

Categories

Resources