I have a solution with a few class libraries and a console project. Each one of the libraries have an Inputs
dir next to the .csproj
file with a bunch of files.
I'm currently doing this in each one of the library projects, so that the different files end up being copied to the console project output dir. It works as expected:
<ItemGroup>
<None Update="Inputs\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
Now I'm trying to extract that functionality to Directory.Build.props
by doing something like this:
<ItemGroup Condition="$(MSBuildProjectName.StartsWith('AoC')) AND !$(MSBuildProjectName.EndsWith('Test'))" >
<None Update="$(MSBuildProjectDirectory)\Inputs\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
and I end up with no files being copied to the output directory (neither the library ones or the runner).
Interestingly enough:
Inputs
dir in \bin\Debug\net8.0
with files created from the original ItemGroup
from a project, those files get deleted if I only keep the 'generic' ItemGroup
from Directory.Build.props
file.Inputs
dir, it's not even created.Additionally, this target in Directory.Build.props
shows the expected project paths where the Input
folders live:
<Target Name="PrintDirs" BeforeTargets="Build" Condition="$(MSBuildProjectName.StartsWith('AoC')) AND !$(MSBuildProjectName.EndsWith('Test'))">
<Message Importance="High" Text="||---> $(MSBuildProjectDirectory)" />
</Target>
<..>\AoCMultiYear> dotnet build
Versión de MSBuild 17.8.3+195e7f5a3 para .NET
Determinando los proyectos que se van a restaurar...
Todos los proyectos están actualizados para la restauración.
Common -> <..>\AoCMultiYear\src\Common\bin\Debug\net8.0\Common.dll
AoC2020 -> <..>\AoCMultiYear\src\AoC2020\bin\Debug\net8.0\AoC2020.dll
AoC2021 -> <..>\AoCMultiYear\src\AoC2021\bin\Debug\net8.0\AoC2021.dll
||---> <..>\AoCMultiYear\src\AoC2020
||---> <..>\AoCMultiYear\src\AoC2021
AoC2020.Test -> <..>\AoCMultiYear\tests\AoC2020.Test\bin\Debug\net8.0\AoC2020.Test.dll
AoC2022 -> <..>\AoCMultiYear\src\AoC2022\bin\Debug\net8.0\AoC2022.dll
||---> <..>\AoCMultiYear\src\AoC2022
Runner -> <..>\AoCMultiYear\src\Runner\bin\Debug\net8.0\Runner.dll
Which piece of the msbuild
puzzle am I missing here? :)
There are two types of MSBuild project files: legacy style and SDK style. Legacy projects are used for the older Windows only .NET Framework. SDK projects were developed for .NET (aka .NET Core) and can support both .NET and .NET Framework.
MSBuild project files are XML. If the Project
element has an SDK
attribute, it is an SDK style project.
e.g.
<Project Sdk="Microsoft.NET.Sdk">
...
</Project>
SDK style projects move a lot of content out of the actual project file and implement some new capabilities. But with this there are some behavior changes.
Legacy projects generally explicitly specify files that are part of the project. SDK projects has a set of default includes for files. Pertinent to the question, files in the project folder and sub-folders that are not Compile
or resource items or of a few other special types are included in the None
item collection.
The difference between a Directory.Build.props
file and a Directory.Build.targets
file is when the file is imported. Both files can contain any valid MSBuild content. (A .props
file is not limited to properties and a .targets
file is not limited to targets.)
The Directory.Build.props
file is imported very early, before the content of the project file itself and before many standard properties have been defined.
The Directory.Build.targets
file is imported after the content of the project file.
As a rule of thumb, put content into the Directory.Build.targets
file. When there is a need to alter the behavior of the definition of standard properties and items, then use the Directory.Build.props
file. As an example, there is a set of properties for altering the default item inclusion. Setting these properties should be done in the Directory.Build.props
file. The project file itself (and the Directory.Build.targets
file) are too late.
When factoring common functionality from across a set of project files where the functionality is currently in the project file and is working correctly, move the common code to the Directory.Build.targets
file. Essentially that is like moving the code to the end of the project file. You won't break the code because of the special early import of the Directory.Build.props
file.
Inputs
folderAssuming an SDK style project, create a Directory.Build.targets
file with the following content:
<Project>
<PropertyGroup>
<InputsDirectoryName Condition="'$(InputsDirectoryName)' == ''">Inputs</InputsDirectoryName>
<!-- EnableCopyInputs: default if not set; normalize to always be either 'true' or 'false' -->
<EnableCopyInputs Condition="'$(EnableCopyInputs)' == ''">true</EnableCopyInputs>
<EnableCopyInputs Condition="'$(EnableCopyInputs)' != 'true' and '$(EnableCopyInputs)' != 'false'">false</EnableCopyInputs>
</PropertyGroup>
<ItemGroup Condition="$(EnableCopyInputs) and Exists('$(InputsDirectoryName)')">
<None Update="$(InputsDirectoryName)\*" >
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="DisplayCopyInputsSettings">
<Message Text="EnableCopyInputs: $(EnableCopyInputs)" />
<Message Text="InputsDirectoryName: $(InputsDirectoryName)" />
<PropertyGroup>
<InputsDirectoryNameExists>false</InputsDirectoryNameExists>
<InputsDirectoryNameExists Condition="Exists('$(InputsDirectoryName)')">false</InputsDirectoryNameExists>
</PropertyGroup>
<Message Text="Directory '$(InputsDirectoryName)' Exists: $(InputsDirectoryNameExists)" />
<Message Text="@(None->'%(Identity), %(CopyToOutputDirectory)', '%0d%0a')" />
</Target>
</Project>
Notes
Directory.Build.targets
file. The common logic runs in the context of each individual project.InputsDirectoryName
) and can be overridden. (This could be done on a per project basis.)EnableCopyInputs
).
false
within a project file and/or it could be set based on the project name within the Directory.Build.targets
file.ItemGroup
condition requires that the copy feature is enabled and that the directory exists. If a project doesn't have an appropriate existing directory, no changes are made.Upgrade
is used to modify the metadata of the existing items. They are existing items because the SDK logic will have auto-included them.
Upgrade
can't be used in the Directory.Build.props
file because the SDK's auto inclusion hasn't been performed yet.DisplayCopyInputsSettings
target reports relevant properties and items. It can be used against a project, e.g. dotnet msbuild -t:DisplayCopyInputsSettings myproject.csproj
.