チュートリアル:CMake と Git を使用した C++ の簡単な依存関係管理

C++ の依存関係の管理は、多くの代替手段と多くのサードパーティ ツールが存在する、より物議を醸すトピックです。次の reddit コメントは、それをよく説明しています:

このチュートリアルでは、CMake (事実上の標準ビルド ツール) と git (事実上のソース コード バージョン管理システム) を使用した比較的単純なソリューションについて説明します。外部ツールは必要なく、すべてのプラットフォームで動作し、セットアップも比較的簡単です。これは、私が現在標準化のために使用しているシステムと同じシステムであり、私の C++ ドキュメント ジェネレーターです。

目標

いくつかの外部依存関係を使用する C++ ライブラリを開発しているとしましょう。クライアントはライブラリを操作するために外部依存関係も必要とするため、ライブラリは「通常の」プログラムとは異なります。ライブラリも同様にインストールされていることに注意してください。

さらに、外部依存関係にはヘッダーのみのものもあれば、そうでないものもあり、本当にかかるものもあります 構築するのに長い。

現在、実行できる 2 つの異なるアプローチがあります。すべてのパッケージ マネージャーは、これらのいずれかを実行します。

<オール> <リ>

ソースをダウンロードして依存関係を構築します。

<リ>

コンパイル済みのバイナリをダウンロードします。

これらのアプローチはどちらも完璧ではありません。

<オール>
  • 一部のプロジェクトは巨大であるという欠点があります そのため、パッケージ マネージャーはビルド後にバイナリをキャッシュすることがよくありますが、これはこの範囲では実行できません。
  • はるかに優れているように見えますが、ABI という 3 文字が原因で問題が発生します。アプリケーション バイナリ インターフェース (コンパイル時のインターフェースの方法) は標準化されていません。異なるプラットフォーム、コンパイラ、標準ライブラリの実装に対して同じバイナリを使用することはできません。 、ビルド タイプ (デバッグとリリース)、ムーン フェーズ、およびその他の無数の要因。事前にコンパイルされたバイナリが必要な場合は、正確な システムと同じ構成
  • ある コンパイル済みのバイナリをダウンロードするだけで十分な 1 つの状況:システムのパッケージ マネージャーを使用する場合。すべてのライブラリは、1 つのシステムの下で 1 つのコンパイラと 1 つの標準ライブラリで構築されているため、すべてが連携して動作します。パッケージ管理を OS に送信し、ライブラリ Y のバージョン X をインストールする必要があると簡単に述べますが、すべての人が ArchLinux や、パッケージとしてすべての最新バージョンを含む同様の Linux ディストリビューションを使用しているわけではありません。

    したがって、1)/2) を組み合わせて使用​​することにしました。まず、システム上でコンパイル済みのバイナリを探し、何も見つからない場合にのみ、ソースを取得してビルドします。ライブラリが既にインストールされているユーザーは、それを持っていない人だけがコンパイルにペナルティを課します.そして誰かがそれを持っておらず、それがコンパイルされることを知っている場合は、それを取得する別の方法を探すことができます.

    それでは、各ステップの詳細と CMake での実装方法を見ていきましょう。

    ステップ 0:コンパイル済みのバイナリを探す

    簡単な方法

    CMake は find_package() を提供します パッケージを探す関数 お使いのコンピュータにインストールされています。パッケージ 基本的には、CMakeLists.txt で定義されているかのように使用できるターゲットをセットアップする CMake ファイルです。 適切にセットアップされたターゲットの場合、必要なのは次のようなものです:

    find_package(dependency [VERSION 1.42])
    target_link_libraries(my_target PUBLIC dependency_target)
    # for a proper library this also setups any required include directories or other compilation options
    

    困難な道

    しかし、すべての CMake プロジェクトが find_package() をサポートしているわけではありません .

    それらのために、CMake はより手動の関数セットを提供します:find_file()find_library()find_path()find_program .これらの関数は、ファイル、ライブラリ、パス、またはプログラムを見つけようとします (duh)。次のように使用できます:

    find_XXX(VARIABLE_FOR_RESULT "stuff-your-looking-for" locations-where-it-might-be)
    

    たとえば、foo というライブラリを探すには Unix システムの場合:

    find_library(FOO_LIBRARY "foo" "/usr/lib" "/usr/local/lib")
    

    探しているものが見つからない場合、変数は「VAR-NOTFOUND」に設定され、if(NOT VARIABLE) を通じて検出できます。 .ユーザーはキャッシュ内の値を上書きして、CMake が必要なものを見つけるのを「助ける」ことができることに注意してください。

    使いやすいように、ライブラリが適切にセットアップされているかのように使用できる「偽の」ターゲットを作成することもできます。

    find_path(FOO_INCLUDE_DIR ...)
    find_library(FOO_LIBRARY ...)
    
    if(FOO_INCLUDE_DIR AND FOO_LIBRARY)
     add_library(foo INTERFACE)
     target_include_directories(foo INTERFACE ${FOO_INCLUDE_DIR})
     target_link_libraries(foo INTERFACE ${FOO_LIBRARY})
    else()
     ... # read on
    endif()
    

    INTERFACE library は実際には存在しないライブラリですが、 INTERFACE を設定できます 誰かがライブラリにリンクした場合に渡されるプロパティ

    ここで、コンパイル済みのバイナリを見つけて、それが正しいバージョンであることを確認するために何かを行った場合は、完了です。そのまま使用できます。

    そうでなければ、物事は面白くなってきています.

    ケース 1:ヘッダーのみのライブラリ

    システムにインストールされていないヘッダーのみのライブラリがある場合は、ヘッダー ファイルをダウンロードして使用可能にするだけです。

    ステップ 1:ソースを入手する

    できる ライブラリを独自のソースにバンドルするだけですが、私はそうしません。おそらく、Git またはその他のバージョン管理システムを使用しているでしょう。 新しいリリースをコピーして貼り付けたばかりの外部ライブラリの更新から来るノイズで差分を汚染することは、間違っていると感じます.

    ただし、Git にはよ​​り良い解決策があります:git submodules.A submodule 別のリポジトリのコミットへのポインターと比較できます。ソースは履歴に保存されず、それへのリンクのみです。必要に応じてリンクが逆参照され、作業ツリーで外部ライブラリが利用可能になります。

    新しいサブモジュールを作成するには、git submodule add <repository-url> を実行します .これにより、リポジトリのデフォルト ブランチの先頭への「ポインタ」が初期化されます。また、作業ディレクトリに複製されるため、external という名前のサブディレクトリで実行することをお勧めします。 リポジトリ foo のソース external/foo で利用可能になります 普通に複製されたかのように。

    ただし、ユーザーがクローンを作成すると、サブモジュールはしません 複製されます (デフォルト)。ユーザーが git submodule update --init -- external/foo を発行すると、複製されます。 (上記の例)。そして、これは CMake 内で活用できます:

    # step 0
    find_path(FOO_INCLUDE_DIR ...)
    
    if((NOT FOO_INCLUDE_DIR) OR (NOT EXISTS ${FOO_INCLUDE_DIR})
     # we couldn't find the header files for FOO or they don't exist
     message("Unable to find foo")
    
     # we have a submodule setup for foo, assume it is under external/foo
     # now we need to clone this submodule
     execute_process(COMMAND git submodule update --init -- external/foo
     WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
    
     # set FOO_INCLUDE_DIR properly
     set(FOO_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external/foo/path/to/include
     CACHE PATH "foo include directory")
    
     # also install it
     install(DIRECTORY ${FOO_INCLUDE_DIR}/foo DESTINATION ${some_dest})
    
     # for convenience setup a target
     add_library(foo INTERFACE)
     target_include_directories(foo INTERFACE
     $<BUILD_INTERFACE:${FOO_INCLUDE_DIR}>
     $<INSTALL_INTERFACE:${some_dest}>)
    
     # need to export target as well
     install(TARGETS foo EXPORT my_export_set DESTINATION ${some_dest})
    else()
     # see above, setup target as well
    endif()
    

    依存関係が見つからない場合は、サブモジュールを複製する必要があります。これは execute_process() によって行われます メッセージを出力した後。それが完了すると、ソースが得られ、インクルード ディレクトリ変数を再度設定できます。

    また、ヘッダーもインストールする必要があることに注意してください。これは、インストールされたターゲットでヘッダーを使用できる必要があるためです。そのためには、install(DIRECTORY) を呼び出す必要があります。 .最後のフォルダー名、つまり install(DIRECTORY /some/path) が保持されることに注意してください。 、フォルダー path を配置します そのために、架空の foo を追加しました パスへのディレクトリ (したがって、foo のヘッダーは path/to/include/foo の下にあります) ).

    最後に、ステップ 0 で説明したように、便利なターゲットが作成されます。ジェネレーター式 が必要であることに注意してください。 インクルードディレクトリを設定するとき:ライブラリを構築するとき、ヘッダーは ${FOO_INCLUDE_DIR} にあります 、しかし、一度インストールされると、ヘッダーはインストール先にあります.

    ステップ 2:…完了です!

    ヘッダーが見つかった別のケースで同じターゲットを作成すると仮定すると、次のように使用できます。

    target_link_libraries(my_target PUBLIC foo)
    

    ケース 2:CMake でビルドする必要があるライブラリ

    ライブラリがヘッダーのみではなく、「適切な」CMake セットアップを備えている場合、実際には作業は少なくなります。

    ステップ 1:ソースを入手する

    ヘッダーのみの場合とまったく同じです。プリコンパイル済みバイナリが見つからない場合は、サブモジュールをクローンします。

    ステップ 2:ライブラリをビルドする

    ライブラリは CMake を使用するため、add_subdirectory() を使用できます。 すべてのターゲットを利用可能にするコマンド:

    if((NOT FOO_LIBRARY) OR ...)
     ...
    
     # build it
     add_subdirectory(external/foo)
    else()
     ...
    endif()
    

    add_subdirectory() のおかげで コマンドを実行するとライブラリが CMake によって自動的にビルドされ、すべてのターゲットが利用可能になります。ターゲットが適切にセットアップされている場合は、target_link_libraries() を呼び出すだけで済みます。 それ以外の場合は、add_subdirectory() の後のターゲット プロパティを「修正」することをお勧めします。

    ケース 3:別のビルドシステムでビルドする必要があるライブラリ

    これは最も手間のかかる作業ですが、シームレスな方法で実行できます。他の場合と同様にソースをフェッチした後、コマンドを発行してビルドする必要もあります。

    しかし、ライブラリを構築するためにユーザーが入力するコマンドを単純に「偽装」することはできます。 git サブモジュールの場合と同様です。execute_process() 構成時にコマンドを実行します (つまり、cmake -D... -G.. path/to/source )、add_custom_command()add_custom_target() ビルド時にコマンドを実行します (例:cmake --build path/to/build ).

    次に、偽のターゲットを作成して統合を非常に簡単にし、いつか CMake に切り替えることを期待することもできます。

    ケース 4:ビルドに非常に時間がかかるライブラリ

    これは問題のあるケースです。ケース 2 と 3 のソリューションでも依存関係が構築されます。ただし、依存関係が巨大である場合 ビルド時間が長いプロジェクトでは、これは実行できない可能性があります。

    ただし、幸運にも、依存関係に C API が含まれている場合もあります。その場合、ABI の問題はほとんどなく、OS とコンパイラ用にコンパイル済みのバイナリを簡単に取得できます。

    しかし、運が悪い場合もあります。この場合、弾丸をかむ必要があり、ユーザーが自分で依存関係をインストールする必要があります。

    結論

    ここで紹介したシステムは、セットアップが非常に簡単で (依存関係が適切にセットアップされている場合)、ユーザーに対して完全に透過的です:

    通常の 3 つのコマンド git clone ... を発行するだけです。 、 cmake ... および cmake --build . .他のすべてはビルド システムによって行われます.これにより、特に CI が非常に簡単になります.

    この種のシステムを標準語で使用しました。ソースはここにあります。まだ読んでいない場合は、インストール チュートリアルもお勧めします。