【问题标题】:Specify build output location of PackageReference dependency DLLs指定 PackageReference 依赖 DLL 的构建输出位置
【发布时间】:2020-09-12 00:20:43
【问题描述】:

我有一个带有一些 NuGet 依赖项的项目,使用 PackageReference:

<ItemGroup>
    <PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
    <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.19" />
</ItemGroup>

我不希望将依赖项直接安装在bin\${buildConfiguration}\${framework}文件夹中(括号中的文件()):

bin
    Debug
        net472
            (MyLibrary.dll)
            (Microsoft.CSharp.dll)
            (Microsoft.Xaml.Behaviors.Wpf.dll)

相反,我希望每个构建的依赖项位于更深的子文件夹中,如下所示:

bin
    Debug
        net472
            (MyLibrary.dll)
            MyLibrary
                (Microsoft.CSharp.dll)
                (Microsoft.Xaml.Behaviors.Wpf.dll)

我知道我可以使用nuget.config 文件来控制解决方案包的下载位置,但 NuGet 依赖项的构建输出保持不变—— bin/release/framework 文件夹。

请注意,我想要的不仅仅是移动文件,这可以通过构建后步骤来完成,而且不是很有用。我需要在构建时更改对依赖 DLL 的引用以引用子文件夹而不是根文件夹;所以我可以将根文件夹的全部内容作为一个整体复制到不同的位置,并且仍然可以工作。

MyLibrary.dll 是使用 SDK 格式的项目构建的,该项目使用 PackageReference;它可以是 .NET Framework、.NET Core 或 .NET Standard。

我该怎么做?


一些背景

我编写了一个 Visual Studio debugging visualizer for expressions。调试可视化工具是手动复制的单个 DLL - 连同它们的依赖项 - 到 Documents 下的特定子文件夹 - 例如Visual Studio 2019\Visualizers -- 或 VS 安装文件夹的 Visualizers 子文件夹。

如果有两个可视化工具依赖于同一个库的不同版本,则其中一个可能会损坏。删除可视化工具是一件容易发生的事情,因为它会删除不需要的依赖项。

在为 .NET Core 或 .NET Standard 编写可视化工具时,需要创建多个 DLL;这些 DLL 中的每一个都可能有自己的依赖项。

如果可以将依赖项输出到同名的子文件夹,那将是朝着正确方向迈出的一步。

Developer community feature request 和(现已关闭)request to document better solutions to this problem

【问题讨论】:

    标签: .net .net-core nuget


    【解决方案1】:

    我只在最基本的场景中测试了这个,所以也许一个多目标项目需要对此进行修改,也许非 sdk 风格的项目与 sdk 风格的项目工作方式不同,但是:

    调查

    关于调查任何 MSBuild,您需要了解的最重要的一件事情是 binary log output,可以使用 MSBuild Structed Log Viewer 查看。

    所以,我运行了dotnet new consoledotnet add package NuGet.Versioning,因为我确实需要在控制台应用程序中进行 SemVer2 比较。现在,我运行dotnet build -blstart msbuild.binlog

    在 MSBuild 日志查看器中,搜索包中的程序集名称和单词 copy。就我而言,我搜索了“copy nuget.versioning.dll”,它找到了一个结果。单击它,我看到该消息是由名为“Copy”的任务输出的,该任务在名为“_CopyFilesMarkedCopyLocal”的目标中运行。单击树中的任务复制,它会在运行复制任务的行上打开 Microsoft.Common.CurrentVersion.targets 的文本视图,我看到了:

        <Copy
            SourceFiles="@(ReferenceCopyLocalPaths)"
            DestinationFiles="@(ReferenceCopyLocalPaths->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')"
            SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
            OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
            Retries="$(CopyRetryCount)"
            RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
            UseHardlinksIfPossible="$(CreateHardLinksForCopyLocalIfPossible)"
            UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyLocalIfPossible)"
            Condition="'$(UseCommonOutputDirectory)' != 'true'"
                >
    

    注意目的地DestinationFiles="@(ReferenceCopyLocalPaths-&gt;'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')"。好的,DestinationSubDirectory 听起来很有希望。检查复制任务的参数,DestinationFiles 项没有设置任何DestinationSubDirectory,所以看来我可以将其设置为我想要的任何相对路径。

    让我们搜索ReferenceCopyLocalPaths 项的定义位置。我看到“AddItem ReferenceCopyLocalPaths”的两个搜索结果,但检查它们的“调用堆栈”,我发现它们都在名为“ResolveReferences”的目标下。

    最后一件事,因为这个问题是关于来自 PackageReference 的程序集,所以我要格外小心,所以我查看了 ReferenceCopyLocalPaths 项,并注意到它有一个名为 NuGetPackageId 的元数据项。

    那么现在:

    • 我想在 ResolveReferences 目标之后运行我自己的目标
    • 我希望它更新 ReferenceCopyLocalPaths 项
    • NuGetPackageId 元数据的定义位置
    • 将 DestinationSubDirectory 设置为某个路径

    解决方案

    将此目标添加到您的 csproj 中:

      <Target Name="CopyPackageAssembliesToSubFolder" AfterTargets="ResolveReferences">
        <ItemGroup>
          <ReferenceCopyLocalPaths Condition=" '%(ReferenceCopyLocalPaths.NuGetPackageId)' != '' "
            Update="%(ReferenceCopyLocalPaths)"
            DestinationSubDirectory="libs\" />
        </ItemGroup>
      </Target>
    

    现在,当我运行 dotnet clean ; dotnet build 时,我看到 bin 目录中有一个 libs/ 文件夹,其中包含 NuGet.Versioning.dll。

    【讨论】:

    • 可执行文件是否运行? IOW,程序集解析器会找到依赖程序集吗?即使我将bin 文件夹的内容移动到其他位置,是否会找到依赖项? (我无法测试 ATM,我会尽快尝试。)
    • 该问题仅询问如何将二进制文件复制到子目录,而不是如何配置运行时如何从子目录加载依赖项。所以我认为它超出了范围。它也有许多不同的答案,具体取决于许多事情,例如应用程序是 .net 框架,还是 .net 核心,无论是否是独立应用程序。这些场景中的每一个都足够复杂,可以有自己的问题和答案,但是通过一些搜索已经可以找到其中几个场景的解决方案。
    • 这个问题只询问如何将二进制文件复制到子目录。 技术上是正确的,但如果是这种情况,我可以简单地使用构建后步骤。我将进行编辑以澄清。如问题中所述,特定用例适用于由 Visual Studio 加载的可视化 DLL,而不是独立应用程序;我对 Visual Studio 如何搜索依赖项几乎没有控制权。此处的特定用例(再次如问题中所述)适用于可视化器端的 .NET Framework,以及调试器端的 .NET Core 和 .NET Standard。所有的项目都是...
    • ... 使用PackageReference;我希望有一些足够简单和广泛的东西来涵盖所有这些情况。
    • 有没有办法从目标中排除项目引用?如果我有一个项目引用,它本身有项目引用,这些项目将被放置在“libs”文件夹中,但我只想将引用放置在此文件夹中的解决方案之外。
    【解决方案2】:

    这样做有两个部分:

    1. 在构建期间,将依赖项放在子文件夹中; @zivkan 在this answer 中描述的技术效果很好。
    2. 主程序集需要在编译时或运行时查找依赖程序集。我不确定是否可以在编译时执行此操作,但我选择在运行时执行此操作,代码如下(部分取自 this answer:
    //using System;
    //using System.Linq;
    //using System.Reflection;
    //using static System.IO.Path;
    //using static System.StringComparison;
    //using static System.Reflection.Assembly;
    //using System.IO;
    
    public static class SubfolderAssemblyResolver {
        public static void Hook(string subfolderKey) {
            if (string.IsNullOrWhiteSpace(subfolderKey)) { return; }
            if (!string.IsNullOrWhiteSpace(subfolderPath)) { return; }
    
            subfolderPath = Combine(
                GetDirectoryName(GetCallingAssembly().Location),
                subfolderKey
            );
            basePath = GetDirectoryName(subfolderPath);
    
            AppDomain.CurrentDomain.AssemblyResolve += resolver;
        }
    
        private static bool EndsWithAny(this string s, StringComparison comparisonType, params string[] testStrings) => testStrings.Any(x => s.EndsWith(x, comparisonType));
    
        private static readonly string[] exclusions = new[] { ".xmlserializers", ".resources" };
        private static readonly string[] patterns = new[] { "*.dll", "*.exe" };
    
        private static string? basePath;
        private static string? subfolderPath;
    
        private static Assembly? resolver(object sender, ResolveEventArgs e) {
            var loadedAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName == e.Name);
            if (loadedAssembly is { }) { return loadedAssembly; }
    
            var n = new AssemblyName(e.Name);
            if (n.Name.EndsWithAny(OrdinalIgnoreCase, exclusions)) { return null; }
    
            // search in basePath first, as it's probably the better dependency
            var assemblyPath =
                resolveFromFolder(basePath!) ??
                resolveFromFolder(subfolderPath!) ??
                null;
    
            if (assemblyPath is null) { return null; }
    
            return LoadFrom(assemblyPath);
    
            string resolveFromFolder(string folder) =>
                patterns
                    .SelectMany(pattern => Directory.EnumerateFiles(folder, pattern))
                    .FirstOrDefault(filePath => {
                        try {
                            return n.Name.Equals(AssemblyName.GetAssemblyName(filePath).Name, OrdinalIgnoreCase);
                        } catch {
                            return false;
                        }
                    });
        }
    }
    

    它的名字是这样的:

    SubfolderAssemblyResolver.Hook("SubfolderName");
    

    此代码在 .NET Framework、.NET Core(在 3.1 上测试)和 .NET Standard 2.0 上运行。

    【讨论】:

      【解决方案3】:

      我有一个类似的问题(这个问题是最接近的搜索结果),但我想将 nuget dll 文件复制到自定义目录中,以便稍后将它们嵌入到我的应用程序中(用于动态程序集加载)。采用@zivkan 在his answer 中的修改方法:

        <!-- After nuget resolves the dependencies, get the list of all dlls and copy them to a custom location -->
        <!-- https://stackoverflow.com/questions/62001105/specify-build-output-location-of-packagereference-dependency-dlls -->
        <!-- https://docs.microsoft.com/en-us/visualstudio/msbuild/how-to-extend-the-visual-studio-build-process?view=vs-2019 -->
        <Target Name="CopyPackageAssembliesToResourcesForEmbedding" AfterTargets="ResolveReferences">
          <Message Text="Copying dlls to Resources directory" />
          <!-- Create a property of the dll output directory -->
          <PropertyGroup>
            <DllOutputDir>Resources\DLL</DllOutputDir>
          </PropertyGroup>
          <!-- Create an item group that will only include dll locations of nuget packages -->
          <ItemGroup>
            <!-- This will force the nuget dlls into a subdir called 'libs'. 
                 Usefull for if you want to test dynamic assembly loading but don't want to 
                 change each nuget package reference node to explicitly not copy to the output directory -->
            <NugetPkgs Include="@(ReferenceCopyLocalPaths)" Condition=" '%(ReferenceCopyLocalPaths.NuGetPackageId)' != '' " />
            <NugetDlls Include="@(NugetPkgs)" Condition=" '%(NugetPkgs.Extension)' == '.dll' " />
          </ItemGroup>
          <!-- For debugging -->
          <!-- <Message Text="ReferenceCopyLocalPaths: @(ReferenceCopyLocalPaths)" /> -->
          <!-- <Message Text="NugetPkgs: @(NugetPkgs)" /> -->
          <!-- <Message Text="NugetDlls: @(NugetDlls)" /> -->
          <!-- https://docs.microsoft.com/en-us/visualstudio/msbuild/makedir-task?view=vs-2019 -->
          <MakeDir Directories="$(DllOutputDir)" />
          <!-- https://docs.microsoft.com/en-us/visualstudio/msbuild/copy-task?view=vs-2019 -->
          <Copy SourceFiles="@(NugetDlls)" DestinationFolder="$(DllOutputDir)" />
        </Target>
      

      我希望这可以帮助处于类似情况的其他人

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-11-11
        • 1970-01-01
        • 2014-01-08
        • 2023-04-01
        • 1970-01-01
        相关资源
        最近更新 更多