鷲ノ巣

C# とか PowerShell とか。当ブログの記事は(特に公開直後は)頻繁に改定される場合があることをご了承ください。

Update-Module の罠

いや、別に PowerShell の罠シリーズをやろうとしているわけではないのですが。
あと、今回の罠は、はまる人はあまりいないと思います…。

私は時々、PowerShellGet でインストールしたモジュールを最新版にするために、

Get-InstalledModule | Update-Module

というのをやります。

ところが今回、新しいバージョンが既にリリースされているのに、これをやっても、モジュールが更新されないという現象が起きて、ちょっと調べていました。

インストールはされていた

そもそも Update-Module は、その名前に反して、既にインストールされているモジュールを更新することはありません。
PowerShell のモジュールは、同じモジュールのバージョン違いのものがサイド バイ サイドでインストールできるようになっています。
そのため、Update-Module は、新しいバージョンをインストールするだけで、既にインストールされている古いバージョンを削除したり、上書き更新したりはしません。*1
Import-Module でモジュールを読み込む際は、インストールされている複数のバージョンの中から、最新のものが自動的に読み込まれます。

で、今回のモジュールも、新しいバージョンはインストールはされていたのです。
が、それが読み込まれず、古いバージョンが引き続き読み込まれていたのです。何故か。

2 つのモジュール ディレクト

PowerShell が認識するモジュール ディレクトリは、環境変数 PSModulePath に(セミコン区切りで複数のパスが)セットされています。
このパスはいくつでも指定できますが、PowerShellGet が認識するディレクトリは、以下の 2 つだけです。

ユーザー スコープのモジュール ディレクトリは、ユーザーのドキュメント ディレクトリ以下にあります。
例えば私の場合だと、C:\Users\aetos\Documents\WindowsPowerShell\Modules です。
もう一つ、マシン スコープのモジュール ディレクトリもあります。
こちらは C:\Program Files\WindowsPowerShell\Modules です。

Import-Module でモジュールを読み込む際は、PSModulePath に登録されているパスを、前から順番に見ていきます。
そのパス下で指定されたモジュールが見つかれば、それ以上の探索は行いません。
先程、複数のバージョンがインストールされている場合は最新のものが読み込まれると言いました。が、PSModulePath の順はそれよりも優先されます。
かつ、ユーザー スコープのモジュール パスは、PowerShell が実行時に PSModulePath の先頭に自動で付加します。
つまり、モジュールの探索順において、ユーザー スコープでインストールされているモジュールは、マシン スコープでインストールされているモジュールよりも優先されます。
たとえ、マシンスコープの方に新しいバージョンがインストールされていようとも、です。

つまり、今回の問題は、ユーザー スコープのモジュール ディレクトリに古いバージョンがあるのに、Update-Module がマシン スコープの方に新しいモジュールをインストールしてしまった結果、新しいバージョンが読み込まれなかった、という問題だったわけです。

本来は起きないはず…

Install-Module コマンドで新しいモジュールをインストールする時は、-Scope パラメーターで、ユーザー スコープでインストールするか、マシン スコープでインストールするかを選ぶことができます。
CurrentUser を指定すればユーザー スコープに、AllUsers を指定すればマシン スコープにインストールされます。
ですから、Install-Module コマンドを使って、わざと古いバージョンをユーザー スコープに、新しいバージョンをマシン スコープにインストールすれば、こういう問題を起こすことはできます。*2

しかし、Update-Module コマンドに -Scope パラメーターはありません。
Update-Module は既にインストールされているモジュールのスコープに合わせて、適切な方に新しいバージョンをインストールしてくれます。
ですから、Update-Module では、起こるはずのない問題なんです。

ところが、今回はそうはならなかった。何故か。

謎のメタデータファイル

Update-Module は、内部で新しいバージョンのモジュールのインストールをしていると言いましたね。つまり、内部で Install-Module を呼んでいるんです。
その際、-Scope パラメーターに CurrentUser を指定するのか、AllUsers を指定するのかを、どうにかして判断しているわけです。
その判断基準が、モジュール ディレクトリの中にある、PSGetModuleInfo.xml という、謎のメタデータ ファイルだったのです。

このファイルは、C:\Users\aetos\Documents\WindowsPowerShell\Modules\HogeModule\1.0.0.0\PSGetModuleInfo.xml のように、個々のモジュールのバージョン ディレクトリの下にあります。
が、隠しファイル属性がついているので、外してやらないと見えません。

で、このファイルの中を見ると、このような行があります。

<S N="InstalledLocation">C:\Users\aetos\Documents\WindowsPowerShell\Modules\HogeModule\1.0.0.0</S>

これはつまり、このファイルがあるディレクトリと同じです。本来は。
Update-Module は、このパスを見て、内部で -Scope の値を決定していたのです。
具体的に言うと、このパスが、ユーザー スコープのモジュール パス、つまり、私の場合で言えば C:\Users\aetos\Documents\WindowsPowerShell\Modules で始まっていればユーザー スコープで、そうでなければマシン スコープでインストールするのです。

パスが変わっていた

さて、種明かしです。

実はこのマシンは、過去に OS の再インストールを行っています。
その際、Windows のアカウント名が、予期せず変わってしまったのです。が、大した問題だと思わず、そのまま使っていました。
OS の再インストール後、バックアップから、ユーザー スコープのモジュール ディレクトリを含む、ドキュメント ディレクトリを復元しました。
その時、一緒に、この謎のメタデータ ファイルも復元されました。
その中に記録されている InstalledLocation の値が、古いユーザー名を含むパスだったというわけです。

Update-Module は、このメタデータ ファイルを読み込み、InstalledLocation の値が、現在のユーザー スコープのモジュール パスではないために、マシン スコープの方にインストールしてしまった、というのが、今回の問題の顛末となります。

まとめ

今回は、各モジュールについて、PSGetModuleInfo.xmlInstalledLocation の値を手で書き換えることで解消しました。
しかし、根本的な対策としては…ユーザー スコープのモジュール ディレクトリを、バックアップから復元したり、別のマシンに持って行ったりはしない方がいいよ、ということになりますかね?

*1:使われることのない古いバージョンがどんどん溜まってディスクを圧迫するという問題があります。古いバージョンをクリーンアップするコマンドかオプションも提供して欲しいと思います。

*2:警告してくれてもよさそうなものです。