[C++][vcpkg]vcpkgのパッケージバージョン管理、マニフェストモード編。

はじめに

vcpkgはクラシックモードとマニフェストモードの2種類の動作があるようだ。クラシックモードはvcpkgの日本語解説でもよく説明されている、以下のようにパッケージを一つ一つ指定してインストールしていく方法である。

vcpkg install opencv3:x64-windows-static-md

もう一つのvcpkgの使い方がマニフェストモードである。これは各プロジェクトのルートディレクトリ(CMakeプロジェクトの場合、大本のCMakeLists.txtがあるディレクトリなど)にvcpkg.jsonという依存パッケージの一覧を記載したjsonファイルを配置し、ここから各パッケージをインストールさせる方法だ。これはRustのCargoなどに似て、vcpkgの実行ファイルを直接コマンドから起動する必要はなく、vcpkg.jsonに依存関係を書いておくことでCMake(またはMSBuild)の実行時に自動的にパッケージのダウンロード、インストールが行われる。

マニフェストモードは最近、各パッケージのバージョン指定インストールに対応した。クラシックモードでは難しかったパッケージバージョン管理が、マニフェストモードなら可能になる。vcpkgは将来的にこのマニフェストモードが主だった使い方となるようだ。ただその使い方はちょっとややこしいので、ここで簡単に解説したい。

マニフェストモードの基本

マニフェストモードは先述のように、vcpkg.jsonという依存パッケージの一覧を記載したファイルをプロジェクトのディレクトリ下に作成することでパッケージを管理する。通常、vcpkg.jsonのあるディレクトリの下にvcpkg_installというディレクトリが作られ、この中に依存パッケージがインストールされる。つまり依存パッケージはすべてプロジェクトのディレクトリ下にインストールされることになる。
この方法は環境をプロジェクトごとに構築でき、それ以外のプロジェクトの環境と干渉しないというメリットがある。もしプロジェクトごとに依存パッケージのバージョンやオプションが異なっているなど、独立に環境を管理したい場合は、これは非常に良い機能だ。しかし、例えばほぼ同じ依存パッケージを持つ複数のプロジェクトがある場合(あるプロジェクトと、それに依存するサブプロジェクトなど)、多くの共通するパッケージをプロジェクトそれぞれに重複してインストールしなければならないというデメリットにもなる。これではストレージ容量もインストール時間も無駄に消費してしまう。

複数プロジェクトで共有可能な環境においてパッケージのバージョンを指定したいのなら、マニフェストモードのそれではなくレジストリを使うのが現状最もマシな手段だろうと思う。レジストリの解説はこちらへ

準備

マニフェストモードとバージョン管理は2021.04.30版以前では実験的な機能で、デフォルトでは有効になっていない。これを有効にするために、環境変数VCPKG_FEATURE_FLAGSの値をmanifests,versionsに設定する必要がある。2021.05.21以降は不要だと思われるがきちんと確認していない。
manifestsは多くの場合なくてもよい(versionsのみでよい)はずだが、vcpkgコマンドを直接実行する時など、稀にこれが必要な場合があった。

vcpkg.jsonの書き方

極めてシンプルなCMakeプロジェクトを考える。次のようなファイルがMyProjectディレクトリ下にあるとしよう。

MyProject/
    CMakeLists.txt
    main.cpp
    vcpkg.json

main.cppの中身は何でも良いが、とりあえずboostとfmtに依存するものと考える。
vcpkg.jsonには、このプロジェクトがboostとfmtに依存することを明示しなければならない。次のように書く。

{
  "name": "my-project",
  "version": "1.0.0",
  "dependencies": [
    "boost",
    "fmt"
  ],
  "builtin-baseline": "git commit SHA"
}

nameはそのプロジェクトの名前、versionもそのままバージョンであり、vcpkg.jsonの先頭に必ず書いておく必要がある。 ただし、nameはvcpkgの流儀に従う表記、つまり小文字のアルファベットと数字、ハイフンのみにする必要がある。 バージョンはドット区切りの数字以外にも何通りかの記述方法があるようだが、ここでは説明しない。これが何を意味するのかはぶっちゃけよく理解していないが、ライブラリ提供者がそのライブラリのバージョンのメタデータをvcpkgに伝えるのに使うとか何とか。

builtin-baselineは依存ライブラリの最小バージョンを、vcpkgのコミットのSHAで指定する。もしインストール済みのパッケージのバージョンがbuiltin-baselineのものよりも古い場合、自動的に更新される。

え、SHAの調べ方が分からない?面倒くさい?とりあえず空欄のままCMakeを実行してエラーログを見てみれば良いんじゃないかな。

なおdependenciesにはもう一つの書き方がある。

  "dependencies": [
    {
      "name": "boost",
      "version>=": "1.65.0"
    },
    "fmt"
  ]

こちらの書き方はboostのバージョンの下限を指定している。boost以外はbuiltin-baselineのバージョンで足りるが、boostだけはもっと新しいものを使いたいなどの場合、ここに必要な最小バージョンを記述する。fmtの方の最小バージョンはbuiltin-baselineの方で判定される。

依存パッケージのバージョンを指定する。

さて、ではいよいよboostバージョンを固定する方法である。上の記述をしっかり読んだ賢明な人達は、きっと"version==": "1.64.0"とか書くんだろうなあ、と想像するだろう。残念ながらハズレである

{
  "name": "MyProject",
  "version": "1.0.0",
  "dependencies": [
    "boost",
    "fmt"
  ],
  "builtin-baseline": "git commit SHA",
  "overrides": [
    { "name": "boost", "version": "1.64.0" }
  ]
}

突如として出現したフィールド"overrides"の中に"dependencies"のバージョンを上書きする情報を記入する。何この謎仕様。ここに記入されたバージョンは"builtin-baseline"や"version>="よりも優先され、boostは1.64.0に固定される。fmtの方はoverridesには何も記されていないので、バージョンは固定されず"builtin-baseline"以上のものがインストールされる。

ただし、どうもこれがうまく機能しない時があるかも知れない。私が試した限りだと、Eigen3の一部バージョンでダウンロードが出来なかったり、そもそもvcpkgに含まれていなかったりして、事実上指定できないものがあった。明らかにバグなので、新しいバージョンでは修正されているかも知れない。

CMakeでビルドするとき

CMakeではクラシックモードと同様、CMAKE_TOOLCHAIN_FILE、VCPKG_DEFAULT_TRIPLETを指定する。それ以外は基本的に不要で、CMake実行時にもしCMakeList.txtのフォルダ内に"vcpkg.json"があれば、自動的にマニフェストモードであると判定しインストールまで実行してくれる。

閑話

マニフェストモードについては私は正直あまり深く理解しようとしていないので、これ以上のことはここでは解説しない。私の場合、開発しているライブラリやアプリケーションのどれも依存関係があり、パッケージのバージョンは基本的に揃えているので、マニフェストモードは役に立たないのだ。いや別に不便だと言うつもりはないが、クラシックモードでも簡単にバージョン管理ができるような方法があればいいのにと思ってしまう。今のところ、カスタムレジストリ以外の方法を見つけられていない。