2022年6月30日

C/C++ の簡易パッケージマネージャを作った話

概要
    ある程度大きなソフトウェアを開発するには, ソフトウェアを機能ごとにモジュール化して, 機能ごとに開発すると,効率的に分業できる. また,インターフェースに互換性があれば, 複数ライブラリをベンチマークして, 機能ごとに最もユースケースに合うライブラリを利用することもできる.
  パッケージ管理において Python は非常に参考になる言語で, 例えば,requirements.txt にインストールしたいライブラリとバージョンを記載して pip を実行すれば,ものの数分で環境が構築できる. しかも,venv などを利用すれば仮想環境として構築できるため, 同じ PC の中に複数プロジェクト向けの環境を用意できる. こうした仕組みは是非とも C++ にも欲しい.
    既存の C++ 向けパッケージマネージャーもあるが,なぜか pip でのインストールが必要だったり,デファクトスタンダードというレベルのものはないため, C/C++ 向けの簡易パッケージマネージャーを試しに開発してみることにした.

※ C++ で既存のパッケージマネージャーとしては ConanVcpkg, Hunter, Buckaroo, poac などがある.ここで紹介するのは,趣味で作ったおもちゃの(プロダクトレベルでない)パッケージマネージャーなので,本格的に利用する場合は,よくよく各パッケージマネージャーを比較検討することをお勧めする.
※ 2013 年以降,docker の登場により,C++ の環境としても構築しやすくなってはいるが, docker の主眼は(パッケージ管理ではなく)環境そのものの仮想化なので,別の問題として考える.
目標
パッケージマネージャーの開発には終わりがないので,ここでは下記の 2 点を目標とする.
- 最新の gcc が扱えること
- Ubuntu 環境で動作すること
要求仕様
細かい要求としては下記となる.
- ベースとなるコンパイラを指定できること
  - gcc の `7.5.0`, `8.4.0`, `9.4.0`, `10.3.0`, `11.2.0`, `12.1.0` を選択できるようにする.
  - 開発環境を整える上で,ベースとなるコンパイラを指定して固定できることは重要
- ビルド済みのアーカイブされたパッケージを利用できること
  - C/C++ 系の大きなライブラリは,build に数時間かかることもある
   (例えば gcc のビルドには)ビルド済みのアーカイブを利用できることは重要
- ビルド環境を提供すること
  - shell script でユーザが,ソースコードのビルドやインストールと,
   アーカイブされたパッケージのインストールができるようにサポートする
    - C/C++ 系では,それぞれの開発者が思い思いの方法で自分たちのライブラリをビルドしている.
     統一されたビルド環境を提供することは,不可能なので,全て shell script から呼び出すことにする
  - docker をユーザがフックしてビルドできるようにする (gcc など,複なコードをビルドする場合は,
   既にビルドできると分かっている環境を docker で用意すると楽にビルドできる)
- パージョンの解決ができること
  - 最適なバージョンの組み合わせの探査までは行わないが,
   指定されたバージョンの範囲から逸脱すれば,エラーを返すようにする
- 誰でもパッケージを公開できること
  - `IMPORT` でパッケージの URL を指定することで,思い思いにパッケージをダウンロードできるようにする
    - 通常はリポジトリがあって,パッケージを集中管理するのが常だと思うが,
     ここでは,ユーザがそれぞれ自分のサーバに,ソースコードやアーカイブされたパッケージ,
     ビルド用,インストール用の shell script を用意することにする.
- オフラインインストールできること
  - 別にオンラインのマシンを用意しておき,パッケージをダウンロードし,
   オフラインのマシンにコピーすることで,オフライン環境でもインストールできるようにする
- 非 root 環境でインストールできること
  - インストール先はユーザディレクトリとするため,非 root でインストールできる
  - 共用計算機などで root 権限が与えられないことは常であるため,非 root でインストールできることは重要
gcc のビルド
C++での開発にあたり,まずはベースとなるコンパイラを固定することはよくあることであるから, パッケージマネージャーとしても,ベースとなるコンパイラが指定できることは重要となる.

幸い gcc のビルドに関しては,多数の blog 記事などがあり参考になったが, 現在のシステム gcc のバージョンと blog 記事の gcc のバージョンが違い, 同じ手順ではビルドできなかった.

無理にビルドすることも考えられたが, ここでは docker で gcc のビルド環境を揃えてしまうことにした.

実際,繊細なビルド環境が必要なパッケージは多いので, パッケージマネージャーとしてもビルド工程で docker をフックできるようにした.

パッケージマネージャーとしては, インストールパッケージを指定する段階で,ビルド工程(ローカルにインストールした gcc 環境か,docker 環境か,システム環境か)を選択できるようにした.

例えば,こんな具合である.

BUILD_ENV, SYSTEM_ENV; // select `SYSTEM_ENV`, `CPM_ENV` or `DOCKER_ENV`.


`DOCKER_ENV` にした場合は,dockerfile (とその他必要ファイル) のあるディレクトリを指定する.

BUILD_ENV, DOCKER_ENV, cpm/build_env/docker/ubuntu18.04_for_build_gcc;

アーカイブパッケージの利用
パッケージマネージャーにビルド工程を入れると, 例えば,上記の gcc のビルドだけで1時間とか2時間とか,待たされることになる.

開発上の都合にしても,耐えられる時間では無かったため,早急にアーカイブしたビルド済みのパッケージを利用できるようにする必要があった.

パッケージマネージャーとしては,デフォルトでアーカイブがあればアーカイブを使うようにした.またデバッグなどの用途のために,ソースコードからビルドするのか,アーカイブを利用するのかを,明示的に指定することもできるようにした.

例えば,こんな具合である.

INSTALL_MODE, auto; // select `src`, `archive` or `auto`.


また,下記のように,ビルドが成功した場合に,アーカイブを作るオプションも用意した(ビルド対象は別途 `packages_cpm.txt` に指定しておく).

cpm/exe -a true


ところで,gcc などのライブラリは `*.la` ファイルや `*.pc` ファイルを吐き出すことがある. これらのファイルには,ライブラリのリンクに必要なパス情報が含まれているので,インストール先が変わるごとに,適切に置換する必要がある.

ここまですると,指定したパッケージのインストールまではなんとかなる.
パッケージのバージョン解決
ここは,力技で実装した.

本当は,依存関係のパターンを調べ上げて,最も新しいパッケージがインストールされるようにするとよいが,外部のパッケージを IMPORT できるようにと考えると,関連するライブラリの依存情報を全てダウンロードする必要がある.

根こそぎ依存関係をダウンロードするのは(程度により)現実的でないことや,計算量も増えることから,最適なパターンの算出は諦めて,「少なくとも要求した範囲にバージョンが収まっているかどうか」を検出することにした. このため,最適な組み合わせでなくとも,要求した範囲にバージョンが収まっていれば,インストールは続行されるし,範囲外となれば,インストールはエラーで止まることになる.

実装としては下記になる
- cpm/src/version_processor.cpp - この記事公開時点のコミット -
- cpm/test/version_processor.hpp - この記事公開時点のコミット -
- cpm/src/dependency_graph_generator.cpp - この記事公開時点のコミット -
- cpm/test/dependency_graph_generator.hpp - この記事公開時点のコミット -
パッケージの公開
パッケージの公開にあたり,パッケージリポジトリの管理者にお伺いを立てるのは,面倒である(し,パッケージリポジトリを管理するのも面倒である).

ここでは単純に,URL を指定してインストールに必要なファイルをダウンロードできるようにした.

例えば,`packages_cpm.txt` に下記のように記載する.

IMPORT, CPM_libExample_to_IMPORT, 0.1.0, "https://github.com/admiswalker/CPM_libExample_to_IMPORT/raw/main/cpm_import/script/0.1.0/download_installation_scripts.sh";
CPM_libExample_to_IMPORT, ==0.1.0;


- CPM_libExample_to_IMPORT - この記事公開時点のコミット -
オフラインインストール
オフラインインストールでは,必要データをオンラインマシンで根こそぎダウンロードしておき,オフラインマシンにコピーすることで,インストールできるようにした.

例えば,以下の要領で,必要データをダウンロードできる(インストール対象は別途 `packages_cpm.txt` に指定しておく).

cpm/exe -c true


パッケージマネージャーの開発にあたり,同じライブラリを何度もインストールして試行錯誤する必要があった. こうした試行錯誤は,ライブラリ提供先のサーバに負荷を掛けるため,ダウンロードしたデータのキャッシュ機能は早い内に実装していた. オフラインインストール用にキャッシュ機能を拡張した.
非 root 対応
パッケージは全てユーザ領域にインストールするように設計した.

パッケージマネージャー本体のビルドには,build-essential が必要となるが,make だけでビルドできるため,大抵の状況に対応できるはずである.(無理であれば,別のマシンでビルドしておく必要がある)

唯一 docker を利用するオプションを使用するには,システム管理者により,システム側に docker engine がインストールされており,ユーザが docker グループに追加されている必要がある.
おもちゃでなくなるには
先に述べたように,ここで紹介したのはおもちゃ(プロダクトレベルでない)のパッケージマネージャーである. プロダクトレベルにするには,少なくとも十分な量のシステムテストの実装(今は一部のテストしか実装されていない)と,より多くのライブラリへの対応,ドキュメントの整備が求められる.

また,雑なパスの解決(置換)をしていたりするので,パスの状況により正常に動作しないことは十分に考えられる.こうした細やかな点を正しく実装する必要がある.

加えて,現在は Ubuntu OS しか想定していないが,それ以外の OS への対応も必要だろう.

少なくとも今のように遊びでやるにはやることが多いので,仕事にしないと不可能だろう. 本当に困った個人なり企業が投資するしかないと思う. という訳で,この辺りで終わろうと思う.
付録

0 件のコメント:

コメントを投稿