I am trying to build a NuGet package from a project (Sub.nupkg). This works quite well.
The problems start as soon as this project itself uses a NuGet (Base.nupkg) which contains contentFiles
. These are needed "transitive", but are not copied to the target directory.
The problem can be reproduced with IronPython.StdLib
. Once this package is included, the contentFiles
are no longer copied when used in another project.
If we change the private assets in Sub.nupkg we're able to force the copy to output:
<PackageReference Include="pkg" Version="1.*">
<PrivateAssets>none</PrivateAssets>
</PackageReference>
If this is configured in Sub.nupkg it will foce a copy of the contentFiles
to the output folder.
The problem is that we're not able to create a NuGet package that configures that assets automatically.
We can specify developmentDependency
in nuspec like:
<developmentDependency>true</developmentDependency>
But this leads to this usage:
<PackageReference Include="pkg" Version="1.*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
And this does not copy all the contentFiles
to the target directory.
I tried something using *.props
but didn't work because I'm not able to reconfigure an existing PackageReference
.
Our dependencies are similar to:
contentFiles
needed to be copied to the output folder)How do we solve that problem?
I found Why are native dll's from dependent libraries not included in build output? but it does not contain any solution to that problem. And I don't understand how to use buildTransitive
to achieve our needs.
Btw: We're only using package references.
Projects for reproducing that issue using IronPython.StdLib
:
Consumer (that uses a packages that references IronPython):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
</PropertyGroup>
<ItemGroup>
<PackageReference Include="IronPythonConsumer" Version="1.0.0" />
</ItemGroup>
</Project>
Project that consumes IronPython:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net70</TargetFramework>
<Authors />
<Product>IronPythonConsumer</Product>
<AssemblyVersion>1.0.0</AssemblyVersion>
<FileVersion>1.0.0</FileVersion>
<Version>1.0.0</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>$(SolutionDir)bin\$(Configuration)</OutputPath>
</PropertyGroup>
<PropertyGroup>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="IronPython.StdLib" Version="2.7.12" />
</ItemGroup>
</Project>
You can make that work if you change the PackageReference of IronPython to:
<PackageReference Include="IronPython.StdLib" Version="2.7.12">
<PrivateAssets>none</PrivateAssets>
</PackageReference>
The Consumer does not need to be changed. But after that all the contentFiles
are copied to the output folder.
It is not an option to adjust the assets every time you use them. This leads too frequently to errors, because the compile runs without errors. The problems occur only at runtime.
I was able to solve the problem by analyzing the package Stub.System.Data.SQLite.Core.NetFramework. This package manages that the ContentFiles transitively end up in the target directory. This package manages that the contentFiles
will transitively be placed in the target directory.
This is achieved by specifying the necessary copy operations in a targets file. This file is included by MSBuild and extends the existing csproj
.
The full targets
file might look like this:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Condition="'$(MSBuildThisFileFullPath)' != '' And
Exists('$(MSBuildThisFileFullPath).user')"
Project="$(MSBuildThisFileFullPath).user" />
<ItemGroup>
<CustomInteropFiles Condition="'$(MSBuildThisFileDirectory)' != '' And
HasTrailingSlash('$(MSBuildThisFileDirectory)')"
Include="$(MSBuildThisFileDirectory)..\..\contentFiles\any\any\**" />
</ItemGroup>
<ItemGroup Condition="'$(ContentCustomInteropFiles)' != '' And
'$(ContentCustomInteropFiles)' != 'false' And
'@(CustomInteropFiles)' != ''">
<Content Include="@(CustomInteropFiles)">
<Link>%(RecursiveDir)%(FileName)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Target Name="CopyCustomInteropFiles"
Condition="'$(CopyCustomInteropFiles)' != 'false' And
'$(OutDir)' != '' And
HasTrailingSlash('$(OutDir)') And
Exists('$(OutDir)')"
Inputs="@(CustomInteropFiles)"
Outputs="@(CustomInteropFiles -> '$(OutDir)%(RecursiveDir)%(Filename)%(Extension)')">
<Copy SourceFiles="@(CustomInteropFiles)"
DestinationFiles="@(CustomInteropFiles -> '$(OutDir)%(RecursiveDir)%(Filename)%(Extension)')" />
</Target>
<Target Name="CollectCustomInteropFiles"
Condition="'$(CollectCustomInteropFiles)' != 'false'">
<ItemGroup>
<FilesForPackagingFromProject Include="@(CustomInteropFiles)">
<DestinationRelativePath>bin\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
</FilesForPackagingFromProject>
</ItemGroup>
</Target>
<PropertyGroup>
<PostBuildEventDependsOn>
$(PostBuildEventDependsOn);
CopyCustomInteropFiles;
</PostBuildEventDependsOn>
<BuildDependsOn>
$(BuildDependsOn);
CopyCustomInteropFiles;
</BuildDependsOn>
</PropertyGroup>
<PropertyGroup Condition="'$(VisualStudioVersion)' == '' Or
'$(VisualStudioVersion)' == '10.0' Or
'$(VisualStudioVersion)' == '11.0' Or
'$(VisualStudioVersion)' == '12.0' Or
'$(VisualStudioVersion)' == '14.0' Or
'$(VisualStudioVersion)' == '15.0' Or
'$(VisualStudioVersion)' == '16.0' Or
'$(VisualStudioVersion)' == '17.0'">
<PipelineCollectFilesPhaseDependsOn>
CollectCustomInteropFiles;
$(PipelineCollectFilesPhaseDependsOn);
</PipelineCollectFilesPhaseDependsOn>
</PropertyGroup>
</Project>
Explanation
<Import Condition="'$(MSBuildThisFileFullPath)' != '' And
Exists('$(MSBuildThisFileFullPath).user')"
Project="$(MSBuildThisFileFullPath).user" />
If the per-user settings file exists, import it now. The contained settings, if any, will override the default ones provided below.
<ItemGroup>
<CustomInteropFiles Condition="'$(MSBuildThisFileDirectory)' != '' And
HasTrailingSlash('$(MSBuildThisFileDirectory)')"
Include="$(MSBuildThisFileDirectory)..\..\contentFiles\any\any\**" />
</ItemGroup>
This determines the files to be copied and stores them on the variable CustomInteropFiles
. It is important to know that all NuGet packages are unpacked in the local cache. This directory structure can be accessed. The variable MSBuildThisFileDirectory
contains the directory in the local NuGet cache where this targets
file is located.
In my example this targets
file is located in the build
folder of the NuGet package and further in the target platform folder. For this reason we need to go two directories up to access the contentFiles
. In my case all files should be copied to the destination directory.
<ItemGroup Condition="'$(ContentCustomInteropFiles)' != '' And
'$(ContentCustomInteropFiles)' != 'false' And
'@(CustomInteropFiles)' != ''">
<Content Include="@(CustomInteropFiles)">
<Link>%(RecursiveDir)%(FileName)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
Here it is checked if per-user settings prevent copying and if there are files to copy. Then these files are linked and marked for copying.
<Target Name="CopyCustomInteropFiles"
Condition="'$(CopyCustomInteropFiles)' != 'false' And
'$(OutDir)' != '' And
HasTrailingSlash('$(OutDir)') And
Exists('$(OutDir)')"
Inputs="@(CustomInteropFiles)"
Outputs="@(CustomInteropFiles -> '$(OutDir)%(RecursiveDir)%(Filename)%(Extension)')">
<Copy SourceFiles="@(CustomInteropFiles)"
DestinationFiles="@(CustomInteropFiles -> '$(OutDir)%(RecursiveDir)%(Filename)%(Extension)')" />
</Target>
This is the Target
in the csproj
file, which is used to copy the files. Before that, self-explanatory tests are made whether copying is possible or not. This Target
is executed in the post-build event.
<Target Name="CollectCustomInteropFiles"
Condition="'$(CollectCustomInteropFiles)' != 'false'">
<ItemGroup>
<FilesForPackagingFromProject Include="@(CustomInteropFiles)">
<DestinationRelativePath>bin\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
</FilesForPackagingFromProject>
</ItemGroup>
</Target>
This part collects the files to be copied. This is triggered by Visual Studio.
<PropertyGroup>
<PostBuildEventDependsOn>
$(PostBuildEventDependsOn);
CopyCustomInteropFiles;
</PostBuildEventDependsOn>
<BuildDependsOn>
$(BuildDependsOn);
CopyCustomInteropFiles;
</BuildDependsOn>
</PropertyGroup>
This entry registers the post-build event and triggers the process of copying the files.
<PropertyGroup Condition="'$(VisualStudioVersion)' == '' Or
'$(VisualStudioVersion)' == '10.0' Or
'$(VisualStudioVersion)' == '11.0' Or
'$(VisualStudioVersion)' == '12.0' Or
'$(VisualStudioVersion)' == '14.0' Or
'$(VisualStudioVersion)' == '15.0' Or
'$(VisualStudioVersion)' == '16.0' Or
'$(VisualStudioVersion)' == '17.0'">
<PipelineCollectFilesPhaseDependsOn>
CollectCustomInteropFiles;
$(PipelineCollectFilesPhaseDependsOn);
</PipelineCollectFilesPhaseDependsOn>
</PropertyGroup>
When the project is opened, the files to be copied are determined.
This targets
file ensures that the files are copied to the target directory. But to trigger this, the base name of the file must match the NuGet id and it must be located in the build
folder.
In order to make the whole thing work transitively, this file has to be placed in the buildTransitive
directory.
In my case the appropriate nuspec
file looks something like this:
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata>
<id>*****</id>
<version>*****</version>
<authors*****</authors>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="file">LICENSE.txt</license>
<projectUrl>*****</projectUrl>
<description>*****</description>
<copyright>*****</copyright>
<dependencies>
<group targetFramework=".NETFramework4.7.2" />
<group targetFramework="net7.0" />
</dependencies>
<contentFiles>
<files include="**" buildAction="None" copyToOutput="false" flatten="false" />
</contentFiles>
</metadata>
<files>
<file src=".doc\**" target=".doc" />
<file src="contentFiles\**" target="contentFiles" />
<file src="build\**" target="build\net472" />
<file src="build\**" target="build\net7.0" />
<file src="build\**" target="buildTransitive\net472" />
<file src="build\**" target="buildTransitive\net7.0" />
<file src="..\_._" target="lib\net472" />
<file src="..\_._" target="lib\net7.0" />
<file src="licenses\LICENSE.txt" target="" />
</files>
</package>
With this nuspec
and targets
file the files from the contentFiles
folder are copied transitively to the target directory.
It might be possible that there is a simpler solution for this, but unfortunately I have not found it.
In the package Stub.System.Data.SQLite.Core.NetFramework a cleanup is defined. This ensures that the files are deleted from the target directory during a clean.
However, we use multi targeting (net472; net7.0), which are built in parallel by Visual Studio. This causes a sporadic error during a clean because the files are in use by another process. We have decided not to allow a clean and the files remain in the target directory.