[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に戻せなかった理由がこれら古いポートファイルにないとは言い切れていない。

[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"があれば、自動的にマニフェストモードであると判定しインストールまで実行してくれる。

閑話

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

[C++][CMake]複数のコンポーネントから成るパッケージを作りたい時。

動機

多数のライブラリを含む自作のパッケージを、find_packageで他のプロジェクトからコンポーネント単位で利用できるようにしたかった。
つまり例えば次のように、ある自作のパッケージ(mylib)が複数のライブラリ(lib1、lib2、lib3)を持っており、その中からlib1とlib3だけ利用したい、というような状況だ。

find_package(mylib COMPONENTS lib1 lib3 REQUIRED)
target_link_library(foo mylib::lib1 mylib::lib3)

mylibConfig.cmakeのようなファイルを自動生成する方法は、単一のライブラリの場合は随所に日本語の解説があるので何とかなったが、しかし上のようにコンポーネント毎にfind_packageを機能させる方法がわからなかった。数日間あちこち調べ回って、先程ようやく動くようになったところだ。プロフェッショナルな方々からすれば常識なのかも知れないが、初心者にとっては非常に難解だった。日本語解説が見当たらなかったので、折角だし纏めておく。

CMakeLists.txtの書き方

まずmylibが次のようなディレクトリ構造になっているものとする。

mylib/
    lib1/
        lib1.h
        lib1.cpp
        CMakeLists.txt
    lib2/
        lib2.h
        lib2.cpp
        CMakeLists.txt
    lib3/
        lib3.h
        lib3.cpp
        CMakeLists.txt
    CMakeLists.txt
    Config.cmake.in #このファイルの役目は後ほど解説する。

これをインストールしたとき、次のようなディレクトリ構造になっていてほしい。

include/
    mylib/
        lib1/
            lib1.h
        lib2/
            lib2.h
        lib3/
            lib3.h
lib/
    {BUILD_TYPE}/ #BUILD_TYPEはDebug、Releaseなど。
        lib1.lib
        lib2.lib
        lib3.lib
cmake/
    mylibConfig.cmake 他

以上を前提として、まずはそれぞれのディレクトリにあるCMakeLists.txtについて詳しく見ていく。

mylib/lib1/CMakeLists.txt
#mylib/lib1/CMakeLists.txt lib2やlib3でもほぼ同様。

project(lib1)

add_library(lib1 lib1.cpp)
add_library(mylib::lib1 ALIAS lib1)

set_target_properties(
    lib1
    PROPERTIES
        PUBLIC_HEADER "lib1.h")

install(
    TARGETS lib1
    EXPORT mylibTargets
    INCLUDES DESTINATION include
    PUBLIC_HEADER DESTINATION include/mylib/lib1
    ARCHIVE DESTINATION lib/${CMAKE_BUILD_TYPE})

installコマンド中でEXPORT mylibTargetsと指定している。lib1~lib3までの各インクルードディレクトリ、ライブラリディレクトリなどの情報は、このmylibTargetsという対象にエクスポートされるらしい。このmylibTargetsは最終的にmylib/CMakeLists.txt中のコマンドによりmylibTargets.cmakeというファイルへと出力される。
なおset_target_propertiesでは、installによってコピーされるべきヘッダファイルを指定している。これがないとinstall(TARGETS)コマンドでPUBLIC_HEADER DESTINATIONを指定しても何もコピーされない。
install(FILE)というコマンドでヘッダファイルを個別にコピーさせても良いのだが、上の書き方のほうが私は好みだ。

mylib/CMakeLists.txt
#mylib/CMakeLists.txt
cmake_minimum_required(VERSION 3.8) #本当に3.8が必要なのかは知らない。

project(mylib)

add_subdirectory(lib1)
add_subdirectory(lib2)
add_subdirectory(lib3)

install(
    EXPORT mylibTargets
    DESTINATION cmake
    NAMESPACE mylib::
    FILE mylibTargets.cmake)

include(CMakePackageConfigHelpers)
configure_package_config_file( 
    "Config.cmake.in" 
    "mylibConfig.cmake"
    INSTALL_DESTINATION cmake)

install(
    FILES "${CMAKE_CURRENT_BINARY_DIR}/mylibConfig.cmake"
    DESTINATION "cmake")

10行目からのinstall(EXPORT mylibTargets...)の部分をまず見てみる。先のmylib/lib1/CMakeLists.txtでエクスポート先をmylibTargetsに指定していたが、これを実際にmylibTargets.cmakeというファイルに出力しているのがこのコマンドのようだ。

16~20行がmylibConfig.cmakeを生成する部分である。まずConfig.cmake.inというファイルを事前に用意しておき、これに基づいてmylibConfig.cmakeを作っているのが、configure_package_config_fileというコマンドだ。Config.cmake.iniの中身は以下の通り。

@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/mylibTargets.cmake")

22行目のinstall(FILES...)により、mylibConfig.cmakeのコピーを指示している。これで必要な全てのファイルはCMAKE_INSTALL_PREFIXに指定した場所へコピーされる。

最後に

installとfind_package意味わからん。
最近ちょっとRustで遊んでいたのだが、あちらのCargoはすこぶる使いやすかった。C++の場合はどのパッケージを使ってどのソースファイルからビルドして、と一々指定しなければならないのだが、Cargoはそれらの指定が何ら必要ないのだ。単に「このパッケージを使いたい」と一行書くだけで、そのパッケージのダウンロード、インストール、リンクなど全てを自動で行ってくれるのだ。感動的である。
その代わり、クレートやモジュールなどの切り分け方、それらのフォルダ構造などが厳密に指定されており、プログラマの自由を許さない形になっていた。最初はRustの縛りの強さに戸惑ったが、あれはあれでビルド、配布しやすさに全振りしたのだと思えば納得できる仕様だ。

C++の場合、言語自体の仕様が自由すぎるのだ。Rustと違ってプログラマ側が自由に決定できる部分があまりに多いので、CMake側でも指定しなければならないパラメータがとんでもなく多くなる。決してCMakeが悪いわけではない。よく頑張っている。
だが正直、CMakeを他人に勧められないなぁとは思った。ある程度使えるようになったら研究室内でCMakeの布教をしようかと考えていたけれども、やめておくことにした。環境がWindowsだけならVisual Studioのプロジェクトの方が初心者には遥かにとっつきやすい。vcpkgと組み合わせればややこしい設定も殆どいらない。幸い研究室内ではWindowsがデフォルトで、Macは個人が使っているだけ、Linuxはほぼ必要ないので、クロスプラットフォームを視野にいれた大規模開発でもしない限り、敢えてこんな意味不明なビルドツールを理解するコストは支払わなくていいだろう。

Rustが可変長引数ジェネリクス、可変長引数関数、placement newなどを実装してくれたら私も移行できるのだが、まだ当面は実現しそうにない雰囲気だったので、今後数年以上はC++に頼ることになりそうである。Rustが覇権を握る日は来るのだろうか。結局、何でも自由にできるC/C++が生き残ったりするんじゃなかろうか。