パッケージ マネージャーについての雑感

2020 年 5 月に開催された Microsoft Build 2020 において、Microsoft 謹製の Windows 用パッケージ マネージャーである WinGet発表されました
ゆくゆくは Debian/Ubuntu における apt のようなツールになっていくのだろうと思います。

現時点では、まだ非常に機能が少ないプレビュー版ですが、今後一年間をかけて成熟させ、2021 年の 5 月の正式リリースを目指しているようです。
v1.0 までのロードマップも公開されています。

この記事では、この WinGet をネタに、パッケージ マネージャーに求められているものについて、考えていこうと思います。

なお、私は Linux には詳しくないので、ここで上げるような問題に apt 等のツールがどう対処しているかというようなことについてはよく知りません。ご容赦ください。

github.com

Windows 用のパッケージ マネージャー

さて、Windows 用のパッケージ マネージャーには、WinGet の他には以下のようなものがあります。

やはり、最もメジャーなのは Chocolatey であろうと思います。
個人的にあまり好きではないので、普段使いのマシンの環境構築に使おうとは思いませんが、CI 環境のように、目的特化で長期間使うことを想定しなくてよい場合にはいいかもしれないと思っています。

Scoop は「アプリのインストールに管理者権限を必要としない」というのがひとつのテーマみたいですね。
それも良し悪しかな、と思います。

AppGet は WinGet とのトラブルがあったと聞いて、初めて知りました。
ので、まったく使ったことがありません。

Microsoft Store は、コマンド ライン ツールが無いので、エンジニア向けのツールとは認識されていないと思いますが、その点に目をつぶれば、立派なパッケージ マネージャーと言えるだろうと思います。

WinGet とは

WinGet が最終的に何を目指しているのか、よくわかっていないところは多いのですが、とりあえず以降の記事で必要な点に絞って、現状を簡単に紹介しましょう。一般的な解説は他のサイトを見てください。

WinGet でインストールできるアプリケーションは、それに対応するマニフェストというものを公開します。
現状、WinGet 用のマニフェストは、ツールとは別の GitHub リポジトリで公開されています。

github.com

適当なマニフェストを開いてみればわかりますが、現在の WinGet マニフェストは、どこか別のサイトからインストーラー パッケージをダウンロードしてきて実行するというだけのものです。

また、マニフェストは、そのアプリケーションの開発元だけでなく、第三者書いて公開することが可能です。

依存関係

最近の Windows 向けのアプリケーションだと、exe や msi といったインストーラーで配布するか、exe 単体で動作するシングル バイナリで配布することが多いのではないかと思います。
Chocolatey や Scoop も、基本的には、どこか別のサイトからインストーラーや配布パッケージをダウンロードしてきて展開するという形態が多いようです。
これらのインストーラーは、単にアプリケーションのファイルを配置するのみならず、それが動作するための諸々の環境整備をも、その責務として含みます。その中には、必要な依存関係の解決も含まれます。

.NET アプリも .NET Core から自己完結型の配布がサポートされたことで、PC にあらかじめフレームワークをインストールしておく必要がなくなり、動作環境を整えるハードルがぐっと下がりました。*1

そのため、パッケージ マネージャーが、アプリケーションが動作するために必要な依存関係を解決するということに、あまり馴染みがないような気がします。

一方で、DebianUbuntu で使われる apt などでは、依存関係のインストールをサポートしています。
また、アプリケーションを配布するものとはやや毛色が違いますが、NuGetnpm などのライブラリ マネージャー(とでも言うのでしょうか)でも、依存関係の解決をサポートしています。

さて、では WinGet ではどうかというと、未だどういう形になるかはまったく未知数ながら、アプリケーションの依存関係をサポートするということが、一応は表明されています。
個人的に、最も注目している問題が、この依存関係のサポートです。
というのも、これをサポートするかしないかは、パッケージマネージャーの在り方にとてつもなく大きな影響を与えるだろうと考えるためです。

サイドバイサイド インストール

依存関係のサポートが、なぜそんなに大きな問題を引き起こすのか。
ひとつの要因は、依存関係をサポートする場合、やり方によっては、サイドバイサイド インストールを実現する必要があるからです。

例を挙げて説明しましょう。

ユーザーが、あるアプリケーション X をインストールしようとしています。
X は、別のパッケージ Y のバージョン 1.0 に依存しています。
そのため、X をインストールすると、自動的に、Y のバージョン 1.0 もインストールされます。

一方で、Y には既に新しいバージョン 2.0 がリリースされています。
そして、悲しいことに、Y のバージョン 1.0 と 2.0 の間には互換性がありません。

このような状況で、ユーザーが、最新の Y も使いたいと思えば(あるいは、Y の 2.0 に依存する別のアプリケーションをインストールしたければ)ユーザーの PC には、Y のバージョン 1.0 と 2.0 が併存できなければなりません。でなければ、X が機能しなくなってしまうからです。

余談ですが、もしこの Y が、インストーラーによって配布されるものであった場合、1.0 と 2.0 の共存を許しているものは多くないのではないでしょうか。2.0 をインストールした瞬間、1.0 はアンインストール(アップグレード)されてしまうでしょう。
まぁ、これは WinGet の問題ではなく、アプリケーションの問題なので、ここで論じても仕方がないことですが。

ともあれ、WinGet は、(アプリケーションが対応しているならば)同じアプリケーションの複数のバージョンが同時にインストールされているという状況をサポートしなければならないわけです。

パスの解決

すると、"Y.exe" というアプリケーションを実行する要求は、Y の 1.0 と 2.0、どちらを実行すればよいでしょうか。

もし Y 2.0 をユーザーがインストールしているなら、ユーザーの意図はきっと、Y 2.0 を実行することでしょう。
一方、X から実行する場合は、Y 1.0 が実行されなければ困ってしまいます。

一般的に、パッケージ マネージャーによってインストールされたアプリケーションは、その場所をいちいち意識しなくてよいように、自動的にパスが通った場所に配置されます。
Chocolatey でも Scoop でも、Microsoft Store アプリケーションでも、これは同じです。WinGet にも、当然、期待される機能です。

ところで、X はどのように作られているでしょうか。
ひょっとしたら、Y.exe はパスの通ったところに存在することを前提にしているかもしれません。

X.exe の位置を基準に相対的なパス解決を行って Y.exe を探すことができるならば、X から実行されている場合だけ、Y 1.0 を実行することができるかもしれません。
しかし、そのような実装を X.exe に強いることは、WinGet の都合に合わせて X を実装することを要求することになってしまい、現実的ではない気がします。

X から実行するときだけ、X 専用の PATH 環境変数をセットするなどということができればいいのかもしれませんが。
npm の場合は、依存モジュールを探索するルールを設けることで対処しているようですね。

インストールされているアプリケーションの更新

WinGet は、その他のパッケージ マネージャーの例に漏れず、インストールしたアプリケーションを更新するコマンドの実装も予定されています。*2

ここでも、やはりサイドバイサイド インストールが問題になります。
winget update Y というコマンドが実行されたら、Y 1.0 と 2.0 のそれぞれを、どのように更新すべきでしょうか?

ひとつ明らかなのは、X とともにインストールされた Y 1.0 を、互換性のない 2.0 に更新することがあってはならないということです。X が動かなくなってしまうからです。

やり方はいろいろ考えられます。
X の依存関係としてインストールされた Y は決して更新しないというのが一案。この場合、X が更新されて、依存先として Y 1.1 とかを指定するようにならない限り、この Y はずっと 1.0 のままです。
あるいは、互換性のあるバージョンを宣言するというのが一案。Y 1.1 と 1.2 と 2.0 があるとして、1.1 と 1.2 はマニフェストに「1.0 と互換性がある」、2.0 は「2.0 としか互換性がない」と宣言されていれば、X と一緒にインストールされた Y 1.0 は、1.2 までは安全にアップデートできるということになります。
たとえば、Jenkins プラグインはこのような機能を持ちます

いずれの場合であっても、新しいバージョンがあるのに、敢えてアップデートしないということは、セキュリティ上のリスク等を抱え込む可能性があります。
そのような場合には、ユーザーに何らかの注意を促す仕組みがあってもよいかもしれませんね。これは依存関係に関係なく有用であろうと思います。

Semantic Versioning

新しいバージョンが以前のバージョンと互換性があるかどうかを判断するために、Semantic Versioning という仕様があります。
端的に言うと、メジャー バージョン番号が変わっていれば、以前のバージョンと互換性がなくなっている(部分がある)ということを意味するというものです。

しかし、すべてのアプリケーションが、この仕様に従ってバージョニングされているとは限りません。1.0 から 1.01 になっただけで、互換性がなくなるアプリケーションがあるかもしれません。

WinGet のマニフェストも Semantic Versioning に従ってバージョニングされるべきという提案もあります。
アプリケーションのバージョンが Semantic Versioning に従っていないのなら、それとは別個にマニフェストだけでも Semantic Versioning に従ってバージョニングすればよいという意見もありますが、二系統のまったく関係ないバージョン番号が存在することになるので、ユーザーを混乱させることになるだけだと思います。私は反対です。

自動更新機能を持つアプリケーション

アプリケーション自体が自動更新機能を持っている場合があります。WinGet は Microsoft Store アプリケーションのインストールもサポートする予定なので、それが代表例ということになるでしょう。
アプリケーションの機能によって、新しいバージョンに更新されたことを、WinGet が検知できなければ、問題が起きる可能性があります。

別のパッケージから依存されているアプリケーションが、それ自体の機能によってアップデートされてしまうことによって、互換性が破壊されるという懸念も考えられるかもしれません。
とはいえ、そこまで WinGet が介入してアップデートを阻止するということは可能なのでしょうか……?

パッケージ保持ポリシー

現状の WinGet は、冒頭で紹介したように、リポジトリにあるのはマニフェストだけで、インストーラーは別のサイトからダウンロードする構造を取っています。
このような仕組みは、パッケージの依存関係を実現する上で、大きな障害になり得ます。

先の例で言えば、Y 2.0 が公開されているのに、1.0 のインストーラーがいつまでもサイトに残っているだろうか、ということです。
古いバージョンを残すも消すも、Y の作者次第なのです。先の節で述べたように、古いバージョンにセキュリティ リスクでもあればなおのことです。

しかし、それでは X が困ってしまいます。
WinGet 経由で X をインストールしようにも、X が必要としている Y 1.0 がもう公開されていない、ということがあり得るからです。

たとえば NuGet.org では、このような事態を防ぐために、一度公開したパッケージの削除は原則的にサポートしていません。
特定のバージョンを非表示にするということは可能ですが、これは新規にインストールする場合の検索に出てこなくなるというだけであり、過去に公開されていたバージョン番号を知っていれば、ダウンロードすることは可能です。
これは、そのバージョンに依存している別のパッケージが壊れないようにするためです。
また、過去に公開されたパッケージの完全な削除を許容してしまうと、他の悪意あるパッケージが、その名前を乗っ取る可能性があるという問題もあります。

パッケージの依存関係をサポートするのであれば、WinGet にも、このような保持ポリシーが必要であろうと思います。
しかし、このような保持ポリシーを実現するためには、インストーラー パッケージ自体をリポジトリでホストしなければなりません。現在のように、リポジトリ運営者の監督下にないサーバーからダウンロードしてくることしかできない仕様では、このような保持ポリシーは実現できません。
そのため、依存関係をサポートする上では、少なくともそのような強制力のあるリポジトリMicrosoft が公開すべきであると考えます。

left-pad 問題

npmjs.com は過去に left-pad 問題を引き起こしたことがあります。
left-pad というモジュールが、作者の意向によって公開停止になったことにより、それに(直接的/間接的に)依存する数千ものライブラリが壊れてしまったというものです。
left-pad に依存していたものの中には、著名な babel も含まれていたことなどもあって、非常に大きな混乱をもたらしました。

現在では、npmjs.com は GitHub の傘下に入りGitHub は Microsoft によって買収されています。npmjs.com は、間接的にせよ Microsoft によって運営されているわけです。
また、先のようなポリシーを持つ NuGet.org も、Microsoft が運営しているものです。
こうしたリポジトリ運営の経験を持つ Microsoft なのですから、WinGet リポジトリの運営にあたっても、賢明な判断を下してくれるであろうと期待したいところです。

著作権問題と普及のジレンマ

しかし、このような保持ポリシーを持つリポジトリの実現の障害として、以下のような問題があります。

この 2 点から導かれる結論は、インストーラー パッケージを WinGet リポジトリでホストさせることは、インストーラーを第三者が外部サイトに勝手に転載させることになり、著作権侵害になり得るということです。

かといって、どちらかを諦めるという選択肢はありません。
マニフェストはアプリケーション作者のみが作成できるという制約を課しても、あるいは、転載フリーなインストーラーのみ使用可能という制約を課しても、WinGet の普及にとって大きな障害となるからです。
既に存在する大量のプロプライエタリなアプリケーションを扱えるということは、Windows 向けのパッケージ マネージャーとしては、是非とも達成しなければならない要件です。

言われてみれば、な解法

どうせ、転載 OK なパッケージにしか安心して依存できないのであれば、パッケージ マネージャーが提供する依存性解決機能になど頼らず、X のパッケージ内に Y をごっそり抱え込んでしまえばよいですね。
そうすれば、この章で長々と言ってきたような問題はすべて解消されます。
少々ディスク容量は食いますが。

パッケージの信頼性

あるパッケージをインストールすべきか否か、そのパッケージは信頼できるのか、どのように判断したらよいでしょうか。
もちろん、最終的には利用者の自己責任に帰します。それは、Web ブラウザ経由でアプリケーションの公式サイトからダウンロードした場合と同じです。
とはいえ、パッケージ マネージャーという間接層が入ることによって、無用な心配事を増やすべきではありません。

既に述べたように、現状の WinGet では、アプリケーション作者以外がマニフェストを作ることが一般的です。そのようなマニフェストを、どうすれば信頼できるでしょうか。
最初はひとまず、インストーラーのダウンロード元が信頼できるサイトかどうかで判断できるかもしれません。
しかし、もしマニフェスト作者に悪意があれば、新しいバージョンであると称して、全く関係のないインストーラーにすり替えることができるかもしれません。
アップデートの都度、URL を確認するような煩雑な運用では、使い物になりません。

Microsoft が公開している GitHub 上のマニフェスト リポジトリマニフェストを発行するためには、GitHub 上でプルリクエストを出す必要があります。
このプルリクエストは、自動的に処理され、一定の審査を経て公開されるようですが、審査の詳細は公表されていません。

あるマニフェストのあるバージョンと、その後継と目されるバージョンが、本当に同一の作者によるものかという問題もあります。
ひょっとすると、GitHub アカウントごとリネームされてしまえば、他者がパッケージ名を乗っ取ることも可能かもしれません。

マニフェスト自体に何らかのデジタル署名を施すことによって、作者の身元や、バージョンの継続性を担保できるべきなのではないかとも思います。
しかし、いわゆるコード署名証明書は非常に高価であり、個人開発者が取得するのは困難です。何かもっと安価な方法がないでしょうか。

Chocolatey には trusted package という概念があります。
パッケージが、元のソフトウェア作者か、または、信頼できるコミュニティによって保守されていることを、人力で確認する仕組みのようです。
どうやって検証しているのかよくわかりませんが。

個人的には、同じアプリケーションに対して、その作者が公開しているマニフェストと、第三者が公開しているマニフェストがあったら、作者が公開している方を使いたいという気になります。
場合によっては、マニフェストが、作者がサポートしていないような方法でアプリケーションをインストールするように作られているかもしれません。
そのような場合も含め、作者が何らかの異議申し立てをできる仕組みが必要かもしれません。

Scoop と UAC

Scoop は個人的に Chocolatey より好ましいと感じているパッケージ マネージャーです。それは、Scoop がアプリケーションのインストール時に UAC 昇格を必要とせず、ユーザーのホーム ディレクトリ下にインストールするというポリシーを持っているからです。
Chocolatey でもそのような運用は不可能ではありません(ポータブル アプリケーションであれば可能です)が、原則的には昇格して利用するものであるという印象を受けます。

ただ、Scoop にもいただけない点があります。
それは、管理者権限なしでのインストールを可能にすることに重きを置くあまり、exe や msi で提供されているインストーラーからも、それを実行せずに、なんとかして中身のファイルを取り出して配置しようとする点です。
msi の場合、msiexec /a というオプションを利用しているようですが、これは本来、管理者用インストール ポイントを作成するための機能です。平たく言えば、展開したファイル群の中に含まれるインストーラーをさらに実行してインストールするためのものであって、中身をそのまま実行するためのものではないのです。
それでうまく実行できるアプリケーションも少なからずあるとは思いますが、個人的にこれは、アプリケーション作者がサポートしていない利用法であると思います。
そのため、私は、このようなことをする Scoop パッケージは信頼しません。

Scoop ならともかくとして、Microsoft 公式のパッケージ マネージャーである WinGet が、ただ UAC 昇格を回避したいという理由だけのために、Windows Installer の機能の目的外利用を支持しないことを望みます。

コンテナ技術への期待……?

UAC は煩わしいものですが、もちろんメリットもあります。*3
例えば、Windows デスクトップ アプリケーションの場合、アプリケーションが利用する DLL を読み込む方法の仕様上、悪意のある DLL を配置されると読み込んでしまうリスクがあります。
アプリケーションが Program Files ディレクトリ下に配置されていれば、この危険性は、若干ながら軽減されます。

とはいえ、Microsoft が考える、この問題への対策の本命は、アプリケーションを UWP 化することです。
先のページには、UWP アプリケーションの場合、DLL の検索方法が異なることが書かれています。
UWP アプリケーションとは、ただ見た目やインストール方法が異なるのみならず、このようにセキュリティ上のメリットもあるものなのです。

Windows 10X では、従来のデスクトップ アプリケーションはコンテナ環境で隔離されて実行されると言われています。
ひょっとすると、先の依存パッケージ問題も含め、コンテナ技術で何とかなるのでしょうか……?

まとめ

パッケージ マネージャー、ムツカシイネ。

開発者は Linux 等でのパッケージ マネージメント経験を、そのまま Windows 上でも実現したいと思うかもしれませんが、Windows には Windows の事情もあります。
拙速に事を運んで、全部がダメになるようなことがないことを祈ります。

WinGet の行く末を見守りたいと思います。現時点では不安でいっぱいですが。

*1:.NET Core 3.0 からは、依存関係込みで 単一ファイルにパッケージングすることを標準でサポートし、さらに利便性が向上しました。

*2:apt のように update コマンドと upgrade コマンドを分けるべきなのかどうか紛糾しているようですが。

*3:マルウェアの主流が、システムの中核を破損するような性質のものから、ひっそりと個人情報を抜き取るようなものに変わったことで、メリットは薄れているようにも思います。UAC は、昇格しなくても抜ける情報を抜かれることに関しては全く無力です。