[C++][vcpkg]vcpkgのパッケージバージョン管理、レジストリ編。

本頁の内容はvcpkgの非常に新しい機能についての説明で、正確な説明のなされた公式ドキュメントが著しく不足していることもあり、完全に手探りで書いています。他の環境やパッケージで動く保証も、また将来に渡り同じ方法が通用する保証もありません。私には見つけられなかったもっと簡単な方法もあるかも知れません。

はじめに

vcpkgにおけるレジストリとは、簡単に言えば、vcpkgが持つパッケージのバージョンや依存関係、拡張機能などを記したものである。もちろんvcpkgにはデフォルトのレジストリがあり、その実体は<vcpkg-root>/ports/<package-name>ディレクトリ内にあるCONTROLportfile.cmakeというポートファイル群として存在する1。ここにはvcpkgに含まれる1000以上のパッケージについて、リポジトリ、バージョンなどの情報が記載されている。

vcpkgには最近、このレジストリをカスタマイズする方法が実装された。簡単に言えば、デフォルトのレジストリへ追加、上書きするためのjsonファイル、CONTROLportfile.cmakeなどを作成し、その情報を用いて任意のパッケージをインストールさせるのだ。

とは言ってもこの方法は主としてオリジナルのgitリポジトリなどをvcpkgに加えるために用いるためのものである。対して、本頁はパッケージバージョン管理が目的だ。つまり、カスタムレジストリを用いてインストールするパッケージのバージョンを上書きしてやりたいのだ。マニフェストモードで一応のバージョン指定は可能になったものの、あれはプロジェクト単位で環境をゼロから構築する必要があるので、パッケージの重複インストールが必要になったりと厄介な側面がある。それを回避しつつ、クラシックモードでもマニフェストモードでも共通して使えるバージョン指定方法を見出したいのだ。

カスタムレジストリは何らかのリポジトリの試験版などを使うことなども考慮された機能ではあるため、過去のバージョンに固定することも想定内ではあるはずだが、公式ドキュメントでそのような用途の説明がまともにされておらず、それどころか公式ドキュメントの記述が2021/6/22現在古い仕様のまま更新されていなかったりもして、正確にどのような動作になっているのかが分からない。従って、本頁の解説はあくまで私の手元で何とか動作しただけの方法である。しかもパッケージによってはむやみにバージョンを変更すると依存関係に問題が生じるのかビルドに失敗することもあった2。依存パッケージをすべてバージョン指定して整理すればうまくいくのだろうとは思うが、かなりの手間になる。本当にこの方法を取るべきかどうかは十分に検討されたい。ある単一のプロジェクト専用の環境構築であれば、バージョン指定はマニフェストモードのほうが遥かに簡単である。

なおvcpkgは2021.05.12版を使用している。

カスタムレジストリ

カスタムレジストリの作成には多くのファイルが必要になる。まず先に上げたCONTROLportfile.cmake、その他、使うバージョンを指定するbaseline.json、個々のport用の<package-name>.json、そしてレジストリのオーバーライド先を指定するvcpkg-configuration.jsonだ。

カスタムレジストリでは、

  1. 自作ライブラリなどの新たなポートを作成する
  2. 既存のライブラリのポートを上書きする

といったことが可能である。このうち1.の方は本頁では解説しない。あくまで2.を用いてパッケージのバージョン指定を行うことが目的である。

今回はとりあえず、Eigen3のバージョンを古いものに引き下げることを考えてみる。

ファイルの配置

各ファイルの説明の前に、どのファイルをどのように配置するのかについて図解しておく。まずvcpkg-configuration.jsonだが、これはvcpkgがクラシックモードかマニフェストモードかで異なる。クラシックモードであれば、vcpkgのルートディレクトリ下に配置する。マニフェストモードの場合はvcpkg.jsonと同じフォルダに置くらしい(未確認)。

#クラシックモードの場合
<vcpkg-root>/
    vcpkg-configuration.json

#マニフェストモードの場合
#vcpkg.jsonと同じ場所に
<project-root>/
    vcpkg-configuration.json
    vcpkg.json

次に、上書きするポートの情報だ。

<custom-port-path(任意の場所)>/
    ports/
        <package-name>/
            CONTROL
            portfile.json
            etc...
    versions/
        baseline.json
        <first-character>-/
            eigen3.json

<custom-port-path>は任意の場所だ。これはvcpkg-configuration.json中でファイルパスを直接指定するので、どこでも構わない。 <first-character>-はそのパッケージの最初の文字を意味する。例えばeigen3の場合、ディレクトリ名はe-となる。これらは<vcpkg-root>/ports/<vcpkg-root>/versions/と同じ構造なので、あちらの中を見てみればどのようになっているのか分かるだろう。

CONTROLportfile.cmakeなどの取得

今、Eigen3のバージョンを例えば3.3.7に引き下げたいと考えているとしよう。これらはvcpkgの2020.04版など幾つかに含まれている。 過去のvcpkg中に含まれていたバージョンであれば、CONTROLportfile.cmakeなどのポートファイルはそれらの中にある/ports/eigen3というディレクトリを丸ごとコピーするという手も使える。CONTROLportfile.cmake以外にもファイルがあるかも知れないが、それらはパッチファイルなどで必要な情報であるため、ファイルを全てコピーし、これを先の図のとおりに設置する。 ただし古いvcpkg用のファイルなので、そのまま正常に動作するかどうかは何とも言えない3。場合によっては何らかの修正が必要になるかも知れない。

baseline.json<package-name>.jsonの作成

2つのjsonファイルを作成する。まずbaseline.jsonについて。

2020.04版に含まれていたEigen3のCONTROLを開くと、そのバージョンが記載されている。この中には3.3.7-4とあったので、とりあえずbaseline.jsonの"baseline"にはそのバージョンを指定しておく。"port-version"は本来、vcpkgの更新時にパッケージバージョンが変更されなかった場合、ポートの情報の重複を防ぐためにこちらを1ずつ増加させるのだが、今回のカスタムポートの場合はそもそもポートが1通りしかないので恐らく0で問題ないだろう。公式ドキュメントでも説明なく0が使われていた。

{
    "default": {
        "eigen3": {
            "baseline": "3.3.7-4",
            "port-version": 0
        }
    }
}

baseline.jsonは、今回上書きするEigen3だけでなく、上書きしたいすべてのパッケージ情報を記述する場所になる。

次に<package-name>.jsonを書く。今回はeigen3.jsonという名前になる。

{
    "versions": [
        {
            "version-string": "3.3.7-4",
            "path": "$/ports/eigen3"
        }
    ]
}

このとき"path"は<custom-port-path>をルートディレクトリとした、CONTROLなどのファイルが収められたディレクトリのパスである。"$"記号が<custom-port-path>を意味し、必ずここからの相対パスとして書く必要がある。

さて、ではこれらのファイルを先と同様、ファイル配置図のとおりに設置しておこう。

vcpkg-configuration.jsonの作成

最後にレジストリのオーバーライドについての情報をvcpkg-configuration.jsonに書き込む。今、Eigen3はカスタマイズしたポートを用いてインストールさせたいが、それ以外のパッケージはvcpkgデフォルトのレジストリから得る必要がある。それらを指定するのである。

{
    "default-registry": {
        "kind": "builtin",
        "baseline": "git commit SHA"
    },
    "registries": [
        {
            "kind": "filesystem",
            "path": "../custom_ports",
            "packages": [ "eigen3" ]
        }
    ]
}

"default-registry"の方はそのままデフォルトのレジストリ、ポートの指定のないパッケージをインストールする場合に用いられる。対して、"registries"の方がオーバーライドするレジストリだ。どのパッケージをどのレジストリから取得するかをここで指定することができる。"registries"は配列なので、ポートが複数箇所にばらけている場合は配列要素を追加していけば良い。

"kind"には

  1. "builtin" ... vcpkg同梱のレジストリ
  2. "git" ... gitリポジトリ上のports/versions/など
  3. "filesystem" ... ディスク上の場所

のいずれかを指定できる。

"builtin"の場合、"baseline"を指定する必要がある。これはvcpkgのコミットのSHA(16進数40桁の数値)を入力すればよい。何のために用いているのかはあまりドキュメント等で言及されていないが、Issuesの中で見つけた議論によると、自作パッケージのビルドや動作確認を行った依存パッケージバージョンが明確になるよう、事実上のパッケージバージョンリストとして機能するSHAを指定させるようにしたのだとか。SHAを指定しておけば、過去どのコミットにおいて動作し、動作しなかったのかを明確にでき、かつバージョン切り替えも一行の修正でできる、従って、手間は増えるがメンテナンス性全体では良くなるだろう、とのことだった。
とはいえ私はパッケージ開発者ではないので、こんなのは余計なお世話である。素直に最新版のSHAを取得して記入したし、できれば更新をもっと楽にしてほしいところ。

"git"はgit上のリポジトリをポート情報取得先として指定する場合に用いるようだが、CONTROLportfile.jsonを手作りしない限り都合よくvcpkg用のポートを持つリポジトリなんてまずないと思うので、今回の目的では使えない。自作のライブラリをvcpkgに追加する場合に限りこの方法を採用すべきだろう。

今回は"filesystem"を用いた。filesystemの場合に必要なのは"path"の情報だ。先程baseline.jsoneigen3.jsonを置いた<custom-port-path>をここに指定する。ただしどうやらドライブ文字から始まる絶対パスは受け付けないらしく、上のように<vcpkg-root>からの相対パスで書いたほうが良さそうである。

最後に"packages"を指定する。このポート情報から取得するパッケージの一覧を、この配列の中に書き連ねれば良い。eigen3以外にもカスタムしたいパッケージがcustom_portsの中にあるのなら、それも"packages"に加える。

インストール

以上のファイルの配置が適切に完了したことを確認したら、例えば下記のようにコマンドを実行して、望むバージョンが表示されるか見ておこう。

vcpkg search eigen3

特にエラーなどが出ず正しく"3.3.7-4"と表示されれば、とりあえずカスタムレジストリの作成は終了である。インストールなどは通常通り行う。

閑話

一応何とかパッケージバージョン管理の方法を見つけたのだが、ややこしいどころの騒ぎではない。マニフェストモードくらい手軽にバージョン指定を行う方法を早く用意して欲しいものである。……のだが、vcpkg自体がマニフェストモード中人にシフトしていく方針っぽいので、正直望み薄かなぁと思っている。

世の中、PCのリソースに大きな制限のある環境は少なくないというのに。プロジェクトごとに何時間もインストールを待ち何十GBもディスク容量を割くとかやってられん。


  1. このあたり本当に正しく用語の意味を理解できているのか自信がない。

  2. VTK9.0.1のバグ回避のためにバージョンを8.2に戻そうとしたときは、どうしてもVTKのビルド時にエラーが出てしまい原因も特定できず断念した。代わりにバージョンは9.0.1から変更せずカスタムレジストリでポートにバグ修正パッチを追加し対応した。VTKは如何せん大量の依存ライブラリを持つので、このような問題が生じたのだろうと思う。

  3. Eigen3を3.3.7に戻す上では動作したが、VTKを8.2に戻せなかった理由がこれら古いポートファイルにないとは言い切れていない。