PowerShellのモジュールの種類はどう決まる？

## 調べたいこと
PowerShellにおけるモジュールの詳細は以下の記事に極めて詳細かつ包括的に書かれている。

https://tech.guitarrapc.com/entry/2013/12/03/014013

モジュールの種類は以下の4つがある。

1. スクリプトモジュール
1. バイナリモジュール
1. マニフェストモジュール
1. ダイナミックモジュール

実はマニフェスト(`.psd1`)を使ってモジュールを作成したとしてもマニフェストモジュールになるとは**限らない**。
マニフェスト内の`RootModule`や`NestedModules`の設定に応じてスクリプトモジュールになったり、バイナリモジュールになったりする。
どういうルールでモジュールの種類が決まるのかネット上の記事をいろいろ見てみたが釈然としなかったので実際に調べてみた。

## 下準備
### フォルダ構成
以下のようなモジュール(`ManifestTest`)を作成し、Windowsの環境変数の`PSModulePath`に`ManifestTest`の**1つ上のフォルダ**を追加しておく。これで実験用のモジュールを簡単に参照できるようになる(必須ではないよ)。

```
ManifestTest             ... モジュールのフォルダ
   ManifestTest.psd1     ... マニフェスト(フォルダ名と同じファイル名)
   ManifestTest.psm1     ... メインのスクリプト(フォルダ名と同じファイル名)
   SubScript.psm1        ... サブのスクリプト
   DllCmdlet.dll         ... コマンドレットのDLL
```

### ファイル
マニフェストファイル(`ManifestTest.psd1`)は以下のように作成する。あえて余計な引数は加えずに作成する。
ファイル名はモジュールのフォルダ名と同じにする(これはお約束)。今回の検討には関係のない行は削除して見やすくしておく。
`なんちゃらToExport`についてはすべてワイルドカード指定(あるものはすべてエクスポート)にしておく。

```PowerShell
New-ModuleManifest -Path ManifestTest.psd1
```

```PowerShell1:ManifestTest.psd1
@{
    ModuleVersion     = '0.0.1'
    GUID              = 'f875b3b4-248b-4258-8bd4-8870725f80d8'
    Author            = 'TheParkSider'
    CompanyName       = 'Unknown'
    Copyright         = '(c) TheParkSider. All rights reserved.'

    # RootModule = ''
    # NestedModules = @()

    FunctionsToExport = '*'
    CmdletsToExport   = '*'
    VariablesToExport = '*'
    AliasesToExport   = '*'

    PrivateData       = @{
        PSData = @{}
    } 
}
```

メインのスクリプトファイル(`ManifestTest.psm1`)には適当に1つだけ関数を追加しておく。

```PowerShell:ManifestTest.psm1
function Get-MessageFromManifestTest {
    "Hi, Manifest test!"
}
```

サブのスクリプトファイル(`SubScript.psm1`)にも適当に1つだけ関数を追加しておく。

```PowerShell:SubScript.psm1
function Get-MessageFromSubScript {
    "Hey, Sub script!"    
}
```

自作のコマンドレットをC#のDLLとして作っておく。

```
DllCmdlet
|   DllCmdlet.sln
|
\---DllCmdlet
       DllCmdlet.csproj
       GetMessageFromDll.cs
```

プロジェクトの`TargetFramework`は.NET Standard 2.0にした。これでWindows PowerShell、PowerShell(オープンソース版)のどちらからでも使用できるDLLになるらしい。
NuGetで[PowerShellStandard.Library](https://www.nuget.org/packages/PowerShellStandard.Library/5.1.0)を取得しておく。これでコマンドレットをC#で作成できるようになる。

```XML:DllCmdlet.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <ApplicationIcon />
    <OutputType>Library</OutputType>
    <StartupObject />
  </PropertyGroup>

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
    <DocumentationFile></DocumentationFile>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="PowerShellStandard.Library" Version="5.1.0" />
  </ItemGroup>

</Project>
```

コマンドレットは以下のような感じに作る。引数なしで文字列だけを返すコマンドレットである。

```C#:GetMessageFromDll.cs
using System;
using System.Management.Automation;

namespace DllCmdlet
{
    [Cmdlet(VerbsCommon.Get, "MessageFromDll")]
    [OutputType(typeof(string))]

    public class GetMessageFromDll : PSCmdlet
    {
        protected override void ProcessRecord()
        {
            WriteObject("Hi, DLL!");
        }
    }
}
```

ソースコードは[こちら](https://github.com/TheParkSider/ManifestTest)。

## そもそもマニフェストがなかったらどうなるのか？
マニフェスト`ManifestTest.psd1`をいったん削除してからこのモジュールを読み込んだらどのように認識されるのか？
PowerShellのコンソールを起動して以下のコマンドを入力する。

```PowerShell
Get-Module -ListAvailable | where { $_.Name -eq "ManifestTest" }

ModuleType Version    Name           ExportedCommands
---------- -------    ----           ----------------
Script     0.0        ManifestTest   Get-MessageFromManifestTest
```

`ModuleType`を見れば明らかなようにスクリプトモジュールとして認識されている。フォルダ名`ManifestTest`と名称が同じスクリプトファイル`ManifestTest.pmd1`が参照されるようだ。
このモジュールのプロパティを見てみるとメタ情報はスッカラカン(たとえばバージョンやGUIDは全部ゼロ)だが、これで十分という人もいるのではないか？
つまり、**超手っ取り早くモジュールを作りたかったら`PSModulePath`の中に任意の名称のモジュールのフォルダを作成し、そのフォルダと同名のスクリプトファイル(`.psm1`)をその中に入れればOK**だ。

## スクリプトファイルにメタ情報などを加えてみる
さすがにバージョンや著作権情報もないのは困るのでマニフェスト`ManifestTest.psd1`をモジュールのフォルダ上に戻してみよう。
マニフェストの中身は前述したとおりまったくのデフォルト状態である。
PowerShellのコンソールを再起動してモジュールの情報を再読み込みしてみる(こうしないと`PSModulePath`が再読み込みされないはず)。

```PowerShell
Get-Module -ListAvailable | where { $_.Name -eq "ManifestTest" }

ModuleType Version    Name            ExportedCommands
---------- -------    ----            ----------------
Manifest   0.0.1      ManifestTest
```

マニフェストモジュールとして読み込まれ、バージョンやGUID、著作権情報にマニフェストで指定した情報が書き込まれている。
ところが`ExportedCommands`が空になっている。マニフェストを使用する場合は**どのスクリプトファイルの関数をエクスポートするかを指定しなければいけない**ようだ。
マニフェストがないときはモジュールと同名のスクリプトファイル(`.psm1`)を暗黙的に参照してくれるのになあ。

ではマニフェストを以下のように変更し、`RootModule`にメインのスクリプトファイル`ManifestTest.psm1`を指定してみる。

```PowerShell1:ManifestTest.psd1
@{
    (略)
    Copyright         = '(c) TheParkSider. All rights reserved.'

    RootModule = 'ManifestTest.psm1'            # ここが変わったよ
    # NestedModules = @()

    (略)
}
```

モジュールの情報を再読み込みしてみる。

```PowerShell
Get-Module -ListAvailable | where { $_.Name -eq "ManifestTest" }

ModuleType Version    Name             ExportedCommands
---------- -------    ----             ----------------
Script     0.0.1      ManifestTest     Get-MessageFromManifestTest
```

`RootModule`に指定したスクリプトファイルにあった関数がきちんとエクスポートされるようになった。
ところが`ModuleType`が`Script`になっている！`RootModule`を指定する前はマニフェストモジュールだったのに。
バージョン情報などはマニフェストで指定したものだ。マニフェスト使っているけどマニフェストモジュールじゃないものが作られている。

冒頭で紹介した記事にも以下の記載があるので公知の事実のようだ。そういうものだと考えるしかないのだろうか。

> **RootModule**
> このパラメータにバイナリモジュール(.dll)やモジュールファイル(.psm1)の名前(.psd1の同一パスに.ps1mが存在することが同一パスとなる)を指定することで、対象のモジュールをモジュールファイルの記述(あるいバイナリモジュールの記述)通りに読み込んでからマニフェスト(.psd1)の記述で制御します。

>このパラメータで モジュールファイル(.psm1) を指定したとき、Import-Module で読み込まれるモジュールタイプは スクリプトモジュールとなります。

[PowerShell のモジュール詳解とモジュールへのコマンドレット配置手法を考える](https://tech.guitarrapc.com/entry/2013/12/03/014013)より引用



## マニフェストを使ったマニフェストモジュールはどうやって作る？
マニフェストを使ったスクリプトモジュールではなく、マニフェストを使ったマニフェストモジュールを作りたい。
以下のようにやってみよう。

```PowerShell1:ManifestTest.psd1
@{
    (略)
    Copyright         = '(c) TheParkSider. All rights reserved.'

    RootModule = ''                         # ここが変わったよ
    NestedModules = @('ManifestTest.psm1')  # ここが変わったよ

    (略)
}
```

```PowerShell
Get-Module -ListAvailable | where { $_.Name -eq "ManifestTest" }

ModuleType Version    Name             ExportedCommands
---------- -------    ----             ----------------
Manifest   0.0.1      ManifestTest     Get-MessageFromManifestTest
```

マニフェストモジュールになった。

ではこうしたらどうだろう。`RootModule`と`NestedModules`にそれぞれ異なるスクリプトファイルを指定する。

```PowerShell1:ManifestTest.psd1
@{
    (略)
    Copyright         = '(c) TheParkSider. All rights reserved.'

    RootModule = 'ManifestTest.psm1'     # ここが変わったよ
    NestedModules = @('SubScript.psm1')  # ここが変わったよ

    (略)
}
```

```PowerShell
Get-Module -ListAvailable | where { $_.Name -eq "ManifestTest" }

ModuleType Version    Name              ExportedCommands
---------- -------    ----              ----------------
Script     0.0.1      ManifestTest      {Get-MessageFromManifestTest, Get-MessageFromSubScript}
```

今度はスクリプトモジュールになった。つまり、**`RootModule`にスクリプトファイルを指定したらマニフェストを使っていてもスクリプトモジュールになってしまう**ことが分かった。

ではさらにこうしたらどうだろう。`NestedModules`は配列なので複数要素を追加できるから、メインとサブのスクリプトファイルを指定してみる。

```PowerShell1:ManifestTest.psd1
@{
    (略)
    Copyright         = '(c) TheParkSider. All rights reserved.'

    RootModule = ''                                           # ここが変わったよ
    NestedModules = @('ManifestTest.psm1', 'SubScript.psm1')  # ここが変わったよ

    (略)
}
```

```PowerShell
Get-Module -ListAvailable | where { $_.Name -eq "ManifestTest" }

ModuleType Version    Name             ExportedCommands
---------- -------    ----             ----------------
Manifest   0.0.1      ManifestTest     {Get-MessageFromManifestTest, Get-MessageFromSubScript}
```

なるほど。`NestedModules`だけにスクリプトファイルを指定した場合はマニフェストモジュールになることがわかった。

直近の2つの例はモジュールの種類は違うものの、エクスポートされたコマンドやモジュールのメタ情報は基本的に同じである。
どうやって使い分けるんだろうなあ。

## バイナリモジュールはどう作る？
バイナリモジュールを作る場合は`RootModule`にバイナリファイルを指定するらしい。

```PowerShell1:ManifestTest.psd1
@{
    (略)
    Copyright         = '(c) TheParkSider. All rights reserved.'

    RootModule = 'DllCmdlet.dll'     # ここが変わったよ
    NestedModules = @()              # ここが変わったよ

    (略)
}
```

```PowerShell
Get-Module -ListAvailable | where { $_.Name -eq "ManifestTest" }

ModuleType Version    Name            ExportedCommands
---------- -------    ----            ----------------
Binary     0.0.1      ManifestTest    Get-MessageFromDll
```

たしかにバイナリモジュールになっている。`RootModule`にスクリプトファイルを指定したときのようにスクリプトモジュールになることはなかった。
さらに`NestedModules`にスクリプトファイルを追加してバイナリとスクリプトの合わせ技のモジュールを作ってみる。

```PowerShell1:ManifestTest.psd1
@{
    (略)
    Copyright         = '(c) TheParkSider. All rights reserved.'

    RootModule = 'DllCmdlet.dll'
    NestedModules = @('ManifestTest.psm1', 'SubScript.psm1') # ここが変わったよ

    (略)
}
```

```PowerShell
Get-Module -ListAvailable | where { $_.Name -eq "ManifestTest" }

ModuleType Version    Name           ExportedCommands
---------- -------    ----           ----------------
Binary     0.0.1      ManifestTest   {Get-MessageFromDll, Get-MessageFromManifestTest, Get-Mess...
```

バイナリモジュールのままである。

ではこれはどうだろう。`RootModule`はスクリプト、`NestedModules`にバイナリを入れてみる。

```PowerShell1:ManifestTest.psd1
@{
    (略)
    Copyright         = '(c) TheParkSider. All rights reserved.'

    RootModule = 'ManifestTest.psm1'                      # ここが変わったよ
    NestedModules = @('DllCmdlet.dll', 'SubScript.psm1')  # ここが変わったよ

    (略)
}
```

```PowerShell
Get-Module -ListAvailable | where { $_.Name -eq "ManifestTest" }

ModuleType Version    Name              ExportedCommands
---------- -------    ----              ----------------
Script     0.0.1      ManifestTest      {Get-MessageFromDll, Get-MessageFromManifestTest, Get-Mess...
```

スクリプトモジュールになっている。つまりバイナリを含むモジュールであっても`RootModule`にスクリプトファイルが指定されていたらスクリプトモジュールになってしまうのだ。なんかもうよくわかんねえな。

最後にこれはどうだ。`RootModule`は空、`NestedModules`にバイナリとスクリプトを入れてみる。

```PowerShell1:ManifestTest.psd1
@{
    (略)
    Copyright         = '(c) TheParkSider. All rights reserved.'

    RootModule = ''                                          # ここが変わったよ
    NestedModules = @('DllCmdlet.dll', 'ManifestTest.psm1')  # ここが変わったよ

    (略)
}
```

```PowerShell
Get-Module -ListAvailable | where { $_.Name -eq "ManifestTest" }

ModuleType Version    Name              ExportedCommands
---------- -------    ----              ----------------
Manifest   0.0.1      ManifestTest      {Get-MessageFromDll, Get-MessageFromManifestTest}
```

マニフェストモジュールになっている。`RootModule`が空ならば`NestedModules`がバイナリだろうとスクリプトだろうとマニフェストモジュールになるようだ。

## まとめ
まとめると以下のようなフローチャートでモジュールの種類が決定される。
`NestedModule`にもいろいろなものを入れてみたが、結局`RootModule`の指定が重要であることがわかった。
![PowerShellModule.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57609/cb1a33d8-40c9-aca7-5df1-9e1963ada7ae.png)

"スクリプトモジュールは〇〇ができない"というようなモジュールの種類による機能制限はないように思うので、そもそもモジュールの種類はあまり意識する必要はないかも(違っていたら教えてください)。