如何创建标准库二进制模块

我最近对模块有一个想法,我想作为二进制模块实现。 我还没有使用 PowerShell 标准库创建过模块,这看起来是个不错的机会。 我使用了 创建跨平台二进制模块 指南来创建此模块,没有任何障碍。 我们会重复相同的过程,我将在过程中加入一些额外的评论。

注意

本文的 原始版本 出现在 @KevinMarquette撰写的博客上。 PowerShell 团队感谢 Kevin 与我们共享此内容。 请在 PowerShellExplained.com查看他的博客。

什么是 PowerShell 标准库?

借助 PowerShell 标准库,我们可以创建可在 PowerShell 和 Windows PowerShell 5.1 中运行的跨平台模块。

为什么是二进制模块?

在 C# 中编写模块时,你将失去对 PowerShell cmdlet 和函数的轻松访问。 但是,如果要创建不依赖于许多其他 PowerShell 命令的模块,则性能优势可能很大。 PowerShell 已针对管理员而不是计算机进行优化。 通过切换到 C#,可以摆脱 PowerShell 添加的开销。

例如,我们有一个关键流程要通过 JSON 和哈希表完成大量工作。 我们尽可能优化 PowerShell,但该过程仍需要 12 分钟才能完成。 该模块已包含大量 C# 样式 PowerShell。 这使得转换为二进制模块干净且简单。 通过转换为二进制模块,我们将处理时间从 12 分钟缩短到 4 分钟以下。

混合模块

可以将二进制 cmdlet 与 PowerShell 高级函数混合使用。 有关脚本模块的一切内容都同样适用。 包含空 psm1 文件,以便稍后可以添加其他 PowerShell 函数。

我已创建的几乎所有已编译 cmdlet 一开始都是 PowerShell 函数。 我们的所有二进制模块都是真正的混合模块。

生成脚本

我在这里保持了构建脚本的简洁。 我通常使用一个大型 Invoke-Build 脚本作为 CI/CD 流水线的一部分。 它执行更神奇的操作,例如运行 Pester 测试、运行 PSScriptAnalyzer、管理版本以及发布到 PSGallery。 一旦开始为模块使用生成脚本,我就能找到要添加到其中的许多内容。

规划模块

本模块的计划是为 C# 代码创建一个 src 文件夹,并像为脚本模块一样构建其余部分。 这包括使用生成脚本将所有内容编译到 Output 文件夹中。 文件夹结构如下所示:

MyModule
├───src
├───Output
│   └───MyModule
├───MyModule
│   ├───Data
│   ├───Private
│   └───Public
└───Tests

入门

首先,我需要创建文件夹并创建 git 存储库。 我使用 $module 作为模块名称的占位符。 这应该使你能够更轻松地根据需要重复使用这些示例。

$module = 'MyModule'
New-Item -Path $module -Type Directory
Set-Location $module
git init

然后创建根级别文件夹。

New-Item -Path 'src' -Type Directory
New-Item -Path 'Output' -Type Directory
New-Item -Path 'Tests' -Type Directory
New-Item -Path $module -Type Directory

二进制模块设置

本文重点介绍二进制模块,以便从中开始。 本部分从 创建跨平台二进制模块 指南中提取示例。 如果需要更多详细信息或存在任何问题,请查看该指南。

首先,我们需要检查已安装 dotnet core SDK 的版本。 我使用的是 2.1.4,但在继续之前,你应该拥有 2.0.0 或更高版本。

PS> dotnet --version
2.1.4

我正在为这一部分做出 src 文件夹。

Set-Location 'src'

使用 dotnet 命令创建新的类库。

dotnet new classlib --name $module

这在子文件夹中创建了库项目,但我不想要这种多余的嵌套层级。 我要将这些文件提升一个级别。

Move-Item -Path .\$module\* -Destination .\
Remove-Item $module -Recurse

设置项目的 .NET Core SDK 版本。 我有 2.1 SDK,因此我要指定 2.1.0。 如果使用 2.0 SDK,请使用 2.0.0

dotnet new globaljson --sdk-version 2.1.0

PowerShell 标准库NuGet 包 添加到项目中。 请确保使用最新版本,以达到所需的兼容性级别。 我默认为最新版本,但我认为此模块不会利用 PowerShell 3.0 以外的任何功能。

dotnet add package PowerShellStandard.Library --version 7.0.0-preview.1

我们应有如下所示的 src 文件夹:

PS> Get-ChildItem
    Directory: \MyModule\src

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----        7/14/2018   9:51 PM                obj
-a----        7/14/2018   9:51 PM             86 Class1.cs
-a----        7/14/2018  10:03 PM            259 MyModule.csproj
-a----        7/14/2018  10:05 PM             45 global.json

现在,我们已准备好将自己的代码添加到项目中。

构建二进制 cmdlet

我们需要更新 src\Class1.cs 以包含此入门命令行小程序:

using System;
using System.Management.Automation;

namespace MyModule
{
    [Cmdlet( VerbsDiagnostic.Resolve , "MyCmdlet")]
    public class ResolveMyCmdletCommand : PSCmdlet
    {
        [Parameter(Position=0)]
        public Object InputObject { get; set; }

        protected override void EndProcessing()
        {
            this.WriteObject(this.InputObject);
            base.EndProcessing();
        }
    }
}

重命名文件以匹配类名。

Rename-Item .\Class1.cs .\ResolveMyCmdletCommand.cs

然后,我们可以生成模块。

PS> dotnet build

Microsoft (R) Build Engine version 15.5.180.51428 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

Restore completed in 18.19 ms for C:\workspace\MyModule\src\MyModule.csproj.
MyModule -> C:\workspace\MyModule\src\bin\Debug\netstandard2.0\MyModule.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:02.19

我们可以在新 dll 上调用 Import-Module 来加载新的 cmdlet。

PS> Import-Module .\bin\Debug\netstandard2.0\$module.dll
PS> Get-Command -Module $module

CommandType Name                    Version Source
----------- ----                    ------- ------
Cmdlet      Resolve-MyCmdlet        1.0.0.0 MyModule

如果系统上的导入失败,请尝试将 .NET 更新到 4.7.1 或更高版本。 创建跨平台二进制模块 指南详细介绍了旧版 .NET 的 .NET 支持和兼容性。

模块清单

我们可以导入 dll 并具有工作模块,这很酷。 我想继续使用它并创建一个模块清单。 如果稍后要发布到 PSGallery,我们需要该清单。

可以从项目的根目录运行此命令来创建所需的模块清单。

$manifestSplat = @{
    Path              = ".\$module\$module.psd1"
    Author            = 'Kevin Marquette'
    NestedModules     = @('bin\MyModule.dll')
    RootModule        = "$module.psm1"
    FunctionsToExport = @('Resolve-MyCmdlet')
}
New-ModuleManifest @manifestSplat

我还将为未来的 PowerShell 函数创建一个空根模块。

Set-Content -Value '' -Path ".\$module\$module.psm1"

这样,我就可以在同一项目中混合普通 PowerShell 函数和二进制 cmdlet。

生成完整模块

我将所有内容一起编译为输出文件夹。 我们需要创建一个生成脚本来执行此作。 我通常会将此内容添加到 Invoke-Build 脚本,但我们可以为此示例保持简单。 将它添加到项目根目录中的 build.ps1

$module = 'MyModule'
Push-Location $PSScriptRoot

dotnet build $PSScriptRoot\src -o $PSScriptRoot\output\$module\bin
Copy-Item "$PSScriptRoot\$module\*" "$PSScriptRoot\output\$module" -Recurse -Force

Import-Module "$PSScriptRoot\Output\$module\$module.psd1"
Invoke-Pester "$PSScriptRoot\Tests"

这些命令生成 DLL 并将其放入 output\$module\bin 文件夹中。 然后,它将其他模块文件复制到原地。

Output
└───MyModule
    ├───MyModule.psd1
    ├───MyModule.psm1
    └───bin
        ├───MyModule.deps.json
        ├───MyModule.dll
        └───MyModule.pdb

此时,可以使用 psd1 文件导入模块。

Import-Module ".\Output\$module\$module.psd1"

在这里,我们可以将 .\Output\$module 文件夹拖放到 $Env:PSModulePath 目录中,每当我们需要它时,它会自动加载命令。

更新:dotnet new PSModule

我了解到,dotnet 工具具有 PSModule 模板。

我上面概述的所有步骤仍然有效,但此模板省去了其中的许多步骤。它仍然是一个比较新的模板,目前正在进行一些改进。 期待它从现在起不断变得更好。

安装和使用 PSModule 模板的方法如下。

dotnet new -i Microsoft.PowerShell.Standard.Module.Template
dotnet new psmodule
dotnet build
Import-Module "bin\Debug\netstandard2.0\$module.dll"
Get-Module $module

此最小可行模板负责添加 .NET SDK、PowerShell 标准库,并在项目中创建一个示例类。 可以立即生成并运行它。

重要详细信息

在结束本文之前,下面是值得一提的一些其他详细信息。

卸载DLL文件

加载二进制模块后,无法真正卸载它。 DLL 文件被锁定,直到卸载它。 这在开发时可能会很烦人,因为每次进行更改并想要生成它时,文件通常会被锁定。 解决此问题的唯一可靠方法是关闭加载 DLL 的 PowerShell 会话。

VS Code 重新加载窗口操作

我的大部分 PowerShell 开发工作是在 VS Code中完成的。 当我使用二进制模块(或包含类的模块)时,我已习惯每次生成时重新加载 VS Code。 按 Ctrl+Shift+P 会弹出命令窗口,Reload Window 始终在列表顶部。

嵌套的 PowerShell 会话

另一种选择是具有良好的 Pester 测试覆盖率。 然后,可以调整 build.ps1 脚本以启动新的 PowerShell 会话、执行生成、运行测试并关闭会话。

更新已安装的模块

尝试更新本地安装的模块时,此锁定可能会令人恼火。 如果有任何会话加载了它,则必须找到并关闭它。 从 PSGallery 进行安装时,这不太存在问题,因为模块版本控制会将新版本放在不同的文件夹中。

你可以设置一个本地的 PSGallery,并在构建过程中向其发布内容。 然后从该 PSGallery 进行本地安装。 这听起来像是很多工作,但这可以像启动 docker 容器一样简单。 我在 的帖子“在 PSRepository中使用 NuGet 服务器”中介绍了一种实现该方法的方法。

最后的想法

我没有涉及用于创建 cmdlet 的 C# 语法,不过在 Windows PowerShell SDK中有大量相关文档。 作为深入了解 C# 的跳板,这绝对是值得尝试了解的内容。