標準ライブラリのバイナリ モジュールの作成方法

私は先日、バイナリ モジュールとして実装したいモジュールのアイデアを思いつきました。 PowerShell 標準ライブラリを使用して作成したことはまだないため、これは良い機会のように思われました。 私はクロスプラットフォーム バイナリ モジュールの作成に関するガイドを使用して、問題なくこのモジュールを作成しました。 ここではその同じプロセスについて説明し、その過程で多少のコメントを追加します。

注意

この記事のオリジナル バージョンは、@KevinMarquette 氏のブログに掲載されました。 このコンテンツを共有してくださった Kevin 氏に、PowerShell チームより感謝を申し上げます。 PowerShellExplained.com のブログをご確認ください。

PowerShell 標準ライブラリとは

PowerShell 標準ライブラリを使用すると、PowerShell と Windows PowerShell 5.1 の両方で動作するクロスプラットフォーム モジュールを作成できます。

バイナリ モジュールを使用する理由

C# でモジュールを作成しているときは、PowerShell コマンドレットと関数に簡単にアクセスできなくなります。 ただし、他の多くの PowerShell コマンドに依存しないモジュールを作成する場合は、大きなパフォーマンス上の利点を得られる可能性があります。 PowerShell は、コンピューターではなく管理者向けに最適化されています。 C# に切り替えることで、PowerShell によって追加されるオーバーヘッドを軽減できます。

たとえば、JSON とハッシュテーブルを使用する多くの作業を行う重要なプロセスがあります。 PowerShell を可能な限り最適化しましたが、プロセスの完了にはまだ 12 分かかる場合があります。 モジュールには、既に多数の C# スタイルの PowerShell が含まれていました。 これにより、バイナリ モジュールへの変換がクリーンかつシンプルになります。 バイナリ モジュールに変換することで、プロセス時間を 12 分以上から 4 分以内に短縮しました。

ハイブリッド モジュール

バイナリ コマンドレットと PowerShell 拡張関数を混在させることができます。 スクリプト モジュールについての知識はすべて、同じように当てはまります。 空の psm1 ファイルが含まれているため、後で他の PowerShell 関数を追加できます。

作成したコンパイル済みのコマンドレットのほとんどが、最初は PowerShell 関数として開始されています。 すべてのバイナリ モジュールは、実際にはハイブリッド モジュールです。

ビルド スクリプト

ここでは、ビルド スクリプトをシンプルな状態にしておきました。 私は通常、CI/CD パイプラインの一部として大きな Invoke-Build スクリプトを使用します。 これにより、さらに多くの機能を実行できます。たとえば、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

これにより、サブフォルダーにライブラリ プロジェクトが作成されましたが、この追加された入れ子のレベルは不要です。 これらのファイルを 1 つ上のレベルに移動します。

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

これで、独自のコードをプロジェクトに追加する準備ができました。

バイナリ コマンドレットのビルド

次のスターター コマンドレットを含むように 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 を呼び出し、新しいコマンドレットを読み込むことができます。

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 関数とバイナリ コマンドレットの両方を同じプロジェクトに混在させることができます。

完全なモジュールのビルド

すべてを 1 つの出力フォルダーにコンパイルします。 そのためには、ビルド スクリプトを作成する必要があります。 通常は、これを 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 セッション

もう 1 つの選択肢は、適切な Pester テスト カバレッジを使用することです。 次に、新しい PowerShell セッションを開始するように build.ps1 スクリプトを調整して、ビルドを実行し、テストを実行して、セッションを閉じることができます。

インストールされているモジュールの更新

ローカルにインストールされているモジュールを更新しようとする場合、このロックが面倒になる可能性があります。 これがいずれかのセッションで読み込まれている場合は、それを見つけて閉じる必要があります。 PSGallery からインストールする場合、これはあまり問題にはなりません。モジュールのバージョン管理によって新しいものが異なるフォルダーに配置されるためです。

ローカルの PSGallery を設定し、ビルドの一部としてそれに発行することができます。 その後、その PSGallery からローカル インストールを実行します。 これは多くの作業のように聞こえますが、docker コンテナーを起動する場合と同じように簡単にできます。 PSRepository 用に NuGet サーバーを使用するに関する投稿で、これを行う方法が説明されています。

最後に

コマンドレットを作成するための C# の構文には触れませんでしたが、Windows PowerShell SDK にはこれに関する多くのドキュメントがあります。 これは、より高度な C# への足掛かりとして、ぜひ試してみる価値があります。