Skip to content

Make a way for Target authors to easily clean up unused/intermediate Items after Target invocation #12894

@baronfel

Description

@baronfel

Summary

Today it's common for authors of MSBuild Target logic to create Items in Targets for many reasons:

  • need to store per-item metadata/computed values
  • incrementally build up to a solution
  • track specific steps through a complex filtering/sorting workflow

An example of this is the following code from MSBuildLocator:

  <Target Name="EnsureMSBuildAssembliesNotCopied" AfterTargets="ResolvePackageAssets" Condition="'$(DisableMSBuildAssemblyCopyCheck)' != 'true'">
    <ItemGroup>
      <_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build'))" />
      <_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build.Framework'))" />
      <_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build.Utilities.Core'))" />
      <_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build.Tasks.Core'))" />
      <_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build.Engine'))" />
      <_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build.Conversion.Core'))" />
      <_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build.Runtime'))" />
      <_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build.Localization'))" />
      <_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build.Engine'))" />
      <_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.NET.StringTools'))" />
      <_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'NuGet.Frameworks'))" />
      <MSBuildAssembliesCopyLocalItems Include="@(_MSBuildAssembliesCopyLocalItems->WithMetadataValue('CopyLocal', 'true')->WithMetadataValue('AssetType', 'runtime'))" />
      <_DistinctMSBuildPackagesReferencedPoorly Include="@(MSBuildAssembliesCopyLocalItems->Metadata('NuGetPackageId')->Distinct())" />
    </ItemGroup>

    <Error
      Condition="@(_DistinctMSBuildPackagesReferencedPoorly->Count()) > 0"
      Code="MSBL001"
      Text="A PackageReference to the package '%(_DistinctMSBuildPackagesReferencedPoorly.NuGetPackageId)' at version '%(_DistinctMSBuildPackagesReferencedPoorly.NuGetPackageVersion)' is present in this project without ExcludeAssets=&quot;runtime&quot; and PrivateAssets=&quot;all&quot; set. This can cause errors at run-time due to MSBuild assembly-loading."
      HelpLink="https://aka.ms/msbuild/locator/diagnostics/MSBL001"
      />
  </Target>

Where a list of candidates is built up over time in progressive steps, then used to compute a list of PackageReferences that violate the MSBuild Locator packaging rules. In projects with many references this intermediate list can represent hundreds of Items.

These Items often represent discrete steps towards a final goal, but one that goal is accomplished they remain in the engine because there's no clear indicator in the engine if something is actually consuming those items or not.

Today, users must remember to manually clean up these lists (which we haven't done at time of writing in Locator!).

While we can add tooling that warns users when this happens, we should have a more first-class way to annotate that items can be automatically cleaned up by the engine after Target invocation.

Background and Motivation

  • Improved memory usage of the build
  • Reduced developer cognitive load

Proposed Feature

There are a few ways of approaching this, I'll highlight a few in brief:

  • The engine could track usage itself and have some logic for when to clean up Items
    • Difficult to know for certain because Inputs and Outputs of Targets aren't comprehensive today - could lead to premature deletions
  • The individual Items could support a metadata that signaled that they could be deleted after Target Invocation
    • <_MyThing Include="..." AutoRemove="true" />
    • Annoying because it's a bit manual, it makes the 'easy' thing the non-performant thing
    • Metadata name is subject to collisions - if we choose a common name we alter semantics for existing code, if we choose an ugly-but-unique name it's hard for users to remember and use
  • The Target could support declaring names of Item lists to remove after execution
    • <Target Name="DoStuff" Inputs="@(Things);@(Stuff)" Outputs="@(MyThings)" Locals="@(_MyIntermediates)">...</Target>
    • semi-declarative, shows intent, relatively easily tool-able?

Alternative Designs

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions