Can an MSBuild Item use a Property set by a Target? - c#

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>

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.

Msbuild step to generate c# file and then compile it

I have a .csproj named P with a custom task in it.
<UsingTask TaskName="[FullTaskName]" AssemblyFile="$(TargetPath)" />
<Target Name="[CustomTaskName]" BeforeTargets="Build">
<[CustomTask] />
</Target>
This custom task replace the content of an included file F in P. This replacement can lead to errors in F.
However when I build P (and then run the custom task), the build pass whatever the content of F.
How to include the new content of F when building P?
Changing BeforeTargets="Build" to BeforeTargets="BeforeBuild" seems to be working.

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.)

Msbuild copy and flatten with unique file names

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.

Categories

Resources