从NuGet Packages加载程序集

25
有时在我的PowerShell脚本中,我需要访问特定的DLL,使用Add-Type -AssemblyName。但是,我需要的DLL并不总是在计算机或GAC上。例如,我可能想要一个快速的脚本,使用Dapper查询数据库。在这些情况下,我一直在字面上复制DLL以及ps1文件。我想知道这是否是常见的/好主意,是否有现有的扩展可以加载NuGet包,在全局或本地文件夹中存储它们并自动调用Add-Type -AssemblyName

这与在Node.js或Python中分别使用npmpip非常相似。

更新

我进行了一些研究,发现较旧版本的PowerShell中没有内置功能。我尝试使用nuget.exe从头开始编写一个自动化程序,取得了一些进展。

&"$(Get-Location)/nuget.exe" install $packageName -Version $version -OutputDirectory "$(Get-Location)/packages" -NoCache -NoInteractive
这将在当前文件夹的“packages”文件夹下下载给定包/版本以及其依赖项。但是,它似乎会下载每个框架版本,并没有明显的方法告诉您要在给定环境中使用哪个版本。 否则,您可以只需循环遍历结果并调用Add-Type:
Get-ChildItem .\packages\ -Recurse -Filter "*.dll" | % {
    try
    {
        Add-Type -Path $_.FullName
    }
    catch [System.Exception]
    {
    }
}

我尝试使用project.json文件和restore命令来控制框架版本,但是没有成功。这对我来说太过复杂。

我将尝试@crownedjitter建议的使用PowerShell 5。

更新

通过@crownedjitter的建议,我最终能够在NuGet中注册PackageManagement模块(请参见下面的评论)。使用以下命令,我能够重现上述Nuget.exe命令的操作:

Install-Package Dapper -Destination packages

显然,这段代码要短得多。问题在于它有相同的限制;它会使每个框架版本的包都被拉下来。如果包括 .NET Core,它会将大量的 .NET Core 框架一起带下!似乎没有办法指定目标框架(即 .NET 4.5.1 或以下)。

Install-Package grabbing dependencies for all .NET frameworks

我想知道是否有一种方法可以根据 PowerShell 的当前 $PSVersionTable.CLRVersion 字段确定要从哪些 NuGet 包文件夹中加载 DLL。


Add-Type -Path? - user4003407
1
如果NuGet包已经在机器上了,那么就可以将其指向该文件夹。我更加好奇的是一种工具,它可以下载实际的NuGet包并代表您执行“Add-Type”调用。 - Travis Parks
2个回答

37

crownedjitter的有用答案是一个很好的起点,Travis本人在评论中提供了额外的指引,但让我试着 总结一下截至Windows PowerShell v5.1 / PowerShell (Core) 7.3.2的情况

更新原始答案 在下面的部分中重新打印,其中包含一些有用的通用指针,以及链接到GitHub上关于将NuGet包与Add-Type集成的功能建议,但它显示的基于Install-Package的方法最终不足之处在于,因为它没有考虑到一个软件包的依赖关系,如BACON所指出的那样:

唯一(不太简单)的遗漏步骤是加载可能已安装的任何依赖项。因为依赖关系属性不包含足够的信息,所以似乎需要从源目录中的.nupkg文件中提取.nuspec文件,读取适当框架的<group>,并加载这些软件包的程序集。

下面的方法可以解决这个问题,但请注意,它首先需要下载和安装带有其dotnet CLI的.NET SDK

  • 创建一个辅助项目文件夹,将包添加到其中并切换到该文件夹;例如:

    • Set-Location (New-Item -Type Directory assemblies)
  • 在该文件夹中创建一个虚拟库项目:

    • 对于 PowerShell (Core),默认针对最新安装的 .NET (Core) SDK:
      • dotnet new classlib
    • 对于 Windows PowerShell 或针对 .NET 5+ 特定操作系统 框架:
      • 针对仍与 .NET Framework 兼容的最高 .NET 标准
        • dotnet new classlib -f netstandard2.0
      • 仅针对特定 .NET Framework 版本或 .NET 5+ 操作系统特定框架
        • 您需要手动编辑生成的.csproj并更新<TargetFramework>元素;例如,要针对最新和最后版本 4.8,请使用<TargetFramework>net48</TargetFramework>;要针对具有 Windows 特定 API 的 .NET 6.0,请使用
          <TargetFramework>net6.0-windows</TargetFramework>
      • 针对 .NET Framework 可能需要先下载开发人员包
  • 添加一个对所需包的引用;例如:

    • dotnet add package Dapper

    • 要引用特定版本,请添加-v <version>

  • 发布虚拟项目,这将复制所有必需的 DLL(包括依赖项)到发布文件夹中:

    • dotnet publish -c Release
    • 重要提示:参数-c的确切大小写决定相应输出文件夹的确切大小写;为了确保您的代码也可以在区分大小写的文件系统上正常工作,特别是在 Linux 上,请确保在引用输出二进制文件的文件路径中使用完全相同的大小写
  • 测试包的主要程序集是否可以加载;例如:

    • Add-Type -Path bin/Release/*/publish/Dapper.dll

    • 验证包的类型是否可用;例如:[Dapper.DbString]::new()

现在你可以直接从辅助项目引用主DLL,或者将所有bin/Release/*/publish/*.dll文件复制到您选择的文件夹中,并从那里引用它。以下示例脚本展示了一个脚本,该脚本按需下载Terminal.Gui包,并在与脚本位置相对应的assemblies子文件夹中创建辅助项目。
$packageName = 'Terminal.Gui'
$assembly = "$packageName.dll"
# Set to @() to get the latest stable version.
$packageVersionArgs = '-v', '1.0.0-pre.4'

$projectFolder = 'assemblies' # Subfolder for the aux. project
$assemblyPath = "$PSScriptRoot/$projectFolder/bin/Release/*/publish/$assembly"
$literalAssemblyPath = Convert-Path -ErrorAction Ignore $assemblyPath

if ($literalAssemblyPath) {
  Write-Verbose -vb "Package '$packageName' already installed. Loading main assembly: $literalAssemblyPath"
  Add-Type -ErrorAction Stop -LiteralPath $literalAssemblyPath
}
else {

  Write-Verbose -vb "Installing package '$packageName'..."

  $null = Get-Command -ErrorAction Stop -CommandType Application dotnet

  Push-Location (New-Item -ErrorAction Stop -Type Directory "$PSScriptRoot/$projectFolder")

  $null = dotnet new classlib
  $null = dotnet add package $packageName @packageVersionArgs
  $null = dotnet publish -c Release
  
  Pop-Location

  Write-Verbose -vb "Loading main assembly: $assemblyPath"  
  Add-Type -ErrorAction Stop -Path $assemblyPath
}

# Instantiate a type from the package to verify that it was loaded.
"Listing property names of a [Terminal.Gui.Button] instance:"
[Terminal.Gui.Button]::new().psobject.Properties.Name

注意:

  • 有些软件包依赖于本地库,dotnet publish会将这些本地库放在发布文件夹的runtimes子文件夹树中,平台特定的子文件夹如runtimes\win-x64\native

  • 在Windows PowerShell中,Add-Type -LiteralPath(及其底层的.NET API方法[System.Reflection.Assembly]::LoadFrom())可以找到适合平台的本地库,但奇怪的是,在PowerShell(Core) 7.2.0-preview.9中不起作用-至少在观察版本5.0.9的Microsoft.Data.Sqlite NuGet包时如此。

  • 解决方法是在runtimes子文件夹树中找到适合平台的本地库,并将其直接复制到发布文件夹中。讨论了这个过程的“Add-NuGetType”帮助函数,见这个答案


原始答案

  • 如上所述,PowerShell v5+(包括PowerShell Core)附带了PackageManagement模块,它是一个元包管理器,通过提供程序提供对多个存储库的访问;可以在v3和v4中按需安装此模块(此下载标有“2016年3月预览版”,这是我能找到的最新版本)。

    • Find-PackageProvider列出所有可用的提供程序。
    • Get-PackageProvider列出已安装的提供程序。
  • 正是nuget提供程序使得通过Install-Package安装Nuget包成为可能,但存在两个潜在障碍

    • 可能未安装nuget提供程序。

    • 它可能使用了不正确的API URL进行安装,从而使Find-Package无法返回结果。

测试是否安装了nuget提供程序:

# If this fails, the provider isn't installed
Get-PackageProvider nuget

如果已安装:请验证软件包源URI是否正确:
  • 打开一个elevated的 PowerShell会话。
  • 运行Get-PackageSource
    • 如果找到名称为Nugettest的源,则将其删除:
      • Unregister-PackageSource Nugettest
    • 如果源nuget.org的列Location中显示https://api.nuget.org/v3/index.json(或其他内容),请更新它:
    • Set-PackageSource nuget.org -NewLocation https://www.nuget.org/api/v2 -Trusted
    • 警告:这可能会破坏在Visual Studio中浏览NuGet packages的功能:请参见https://github.com/PowerShell/PowerShellGet/issues/107
如果未安装:请从头安装提供程序。
  • Open an elevated PowerShell session.

  • Run the following commands:

    Install-PackageProvider nuget
    Register-PackageSource -ProviderName nuget -name nuget.org -Location https://www.nuget.org/api/v2 -Trusted
    
完成以上步骤后,NuGet包的发现(例如Find-Package Dapper)和安装(例如Install-Package Dapper)应该成功。
默认情况下,Install-PackageAllUsers范围内安装,需要提升权限,但您可以选择使用-Scope CurrentUser选项仅在当前用户的上下文中安装。 使用下载的NuGet包: 注意:请参见GitHub问题#6724,该问题建议通过扩展Add-Type来简化在PowerShell中使用NuGet包的过程,这将省去所有后续步骤,在PowerShell Core 7.3.2中仍然需要这些步骤。
  • As demonstrated in the question, you need to manually load the package's assemblies into your PowerShell session with Add-Type -Path <assembly-file-path>; however, in the era of .NET Core, packages may have DLLs for different .NET environments, so you cannot always blindly load all *.dll files in the package folder:

  • In order to discover the file-system location of a downloaded package, query the .Source property of the relevant object returned by Get-Package:

    (Get-Package Dapper).Source
    
  • To see the full paths of all DLLs inside the package, run the following:

    (Get-ChildItem -Filter *.dll -Recurse (Split-Path (Get-Package Dapper).Source)).FullName
    
  • Looking at the full DLL paths should tell you which DLL(s) are the right ones to load for your environment; using the example of the Dapper package:

    C:\Program Files\PackageManagement\NuGet\Packages\Dapper.1.50.4\lib\net451\Dapper.dll
    C:\Program Files\PackageManagement\NuGet\Packages\Dapper.1.50.4\lib\netstandard1.3\Dapper.dll
    C:\Program Files\PackageManagement\NuGet\Packages\Dapper.1.50.4\lib\netstandard2.0\Dapper.dll
    
  • Given that .NET Standard DLLs run on all .NET platforms, however, you can programmatically look for the (latest) such DLLs and load them:

    (Get-Item (Join-Path (Split-Path (Get-Package Dapper).Source) lib/netstandard*) | 
      Sort-Object { [version] ($_.Name -replace '^netstandard') })[-1] |
        Get-ChildItem -Filter *.dll -Recurse |
          ForEach-Object { Add-Type -LiteralPath $_.FullName }
    
    • The above looks for the highest available .NET Standard version DLLs; if you want to target a specific version, the command becomes easier; e.g., for .NET Standard 2.0:

        Get-ChildItem -Recurse -Filter *.dll -LiteralPath (Join-Path (Split-Path (Get-Package Dapper).Source) lib/netstandard2.0) |
          ForEach-Object { Add-Type -LiteralPath $_.FullName }
      

6
你是否正在使用 Powershell 5?如果是的话,它有一个包管理模块: Powershell 5 中的 PackageManagement 模块 看起来它是开源的:https://github.com/OneGet

这个能工作的诀窍是什么?我收到了以下消息:Find-Package: 没有找到指定搜索条件和软件包名称'Dapper'的匹配项。尝试使用 Get-PackageSource 查看所有可用的注册包源。 - Travis Parks
3
有点儿莫名其妙,但是 NuGet 的位置错了。我必须注销提供程序 Unregister-PackageSource nuget.org 然后重新注册它 Register-PackageSource -Name nuget.org -Location http://www.nuget.org/api/v2 -Force -Trusted - Travis Parks
使用-Location标志的Set-PackageSource完全没有任何作用...太棒了。 - Travis Parks
1
@TravisParks:确实很痛苦:我不知道它来自哪里,但是PSv5.1安装可能会出现一个新的、但不受支持的API URL(https://api.nuget.org/v3/index.json),这会阻止Find-Package/Install-Package找到NuGet包。至于Set-PackageSource:你需要使用-NewLocation - mklement0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接