Rabadash8820
Rabadash8820

Reputation: 2524

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!

Upvotes: 5

Views: 2971

Answers (2)

Nikita Nemkin
Nikita Nemkin

Reputation: 2820

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>

Upvotes: 1

LoLance
LoLance

Reputation: 28126

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:

enter image description here

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

Upvotes: 5

Related Questions