LoginSignup
3
5

自作パッケージをCMakeのfind_package()に対応させる

Last updated at Posted at 2024-01-04

1. ゴール

自分で作ったパッケージを,別のCMakeLists.txtの中からfind_package()経由で利用できるようにする.

ある程度 CMakeを使っている方は,

  • 3-1. ソースコードの準備とディレクトリ構造
  • 5. 実際のCMakeLists.txtの記述
    を見るだけで十分です
    コメント豊富に書いてあります.

その前に動作環境

  • macOS 14.1.1
  • cmake 3.28.1
  • Apple clang version 15.0.0

2. 事前調査

2-1. 自作パッケージをどこにインストールするべきか

以下を踏まえて,自作パッケージをどこにインストールするべきかを考えた.
find_package()は,インストールされたパッケージの情報が格納されている設定ファイルを探索する.
設定ファイルは,CMakeのモジュールパスにあるファイルで,<パッケージ名>Config.cmakeという名前になっている.
という事は,自作パッケージの本体の置き場所の他に設定ファイルの置き場所がベターなのかを考える必要がある.

2-2. 設定ファイルをどこに置くべきか

CMakeのfind_package()がインストールされたライブラリを見つける仕組みは長くなるので多々省略するが
私の環境では,以下のディレクトリを探索している

  • CMAKE_PREFIX_PATHCMAKE_FRAMEWORK_PATH環境変数.
  • CMAKE_SYSTEM_PREFIX_PATHCMAKE_SYSTEM_FRAMEWORK_PATH変数.
  • 他にも色々 (詳しくはここを参照)
    私は,環境変数を使って設定していないのでCMAKE_PREFIX_PATHCMAKE_FRAMEWORK_PATHはブランクです.

2-2-1. CMAKE_SYSTEM_PREFIX_PATHCMAKE_SYSTEM_FRAMEWORK_PATHはどうなっているか調べてみる.

適当な動作するCMakeLists.txtを用意し,以下の記述を追加し出力を確認した.

message(STATUS "CMAKE_SYSTEM_PREFIX_PATH = ${CMAKE_SYSTEM_PREFIX_PATH}")
message(STATUS "CMAKE_SYSTEM_FRAMEWORK_PATH = ${CMAKE_SYSTEM_FRAMEWORK_PATH}")

出力は長いので必要な部分だけ抜粋すると,以下のようになる.

-- CMAKE_SYSTEM_PREFIX_PATH =/usr/local;/usr;/;/opt/homebrew/Cellar/cmake/3.28.1;/usr/local;/usr/pkg;/opt;/sw;/opt/local
-- CMAKE_SYSTEM_FRAMEWORK_PATH = Xcode関連のパスが長々と続く

2-3. パッケージの設定ファイルのインストール先を決定する

/usr/local/が検索される事が判ったので,ここを使う事にした.
実際には,/usr/local/lib/cmake以下に設定ファイルを格納されると検索対象となる.
もちろん,自分でパスを変更して独自の場所にインストールすることもできるが面倒なのでやらない.
ここなら自分で削除もできるし,他の人にも迷惑をかけないだろう.
まぁ自分のMacだけでしか使わないので,他の人に迷惑をかけることはないのだが.
実際のところ,/usr/local/bin, /usr/local/lib, /usr/local/includeには,自作パッケージのライブラリやヘッダファイルを格納するので,管理という面でも都合が良い.

3. 検証用ソースコードの準備

3-1. ソースコードの準備とディレクトリ構造

今回は,非常にシンプルなコードを用意した.
外部コマンドを実行して,その結果を返すだけのコードとした.
関数名はexecuteCommandWithResultだが,ヘッダーとソースコード名はcmdTestとした.

コードの内容には触れないが以下のようになっている.

#include "cmdTest.h"
#include <cstdio>
#include <string>
#include <iostream>

std::string executeCommandWithResult(const std::string& command,
                                            int* error_code) {
  std::string result;

  FILE* pipe = popen(command.c_str(), "r");
  if (!pipe) {
    // return "popen failed!";
    if (error_code != nullptr) *error_code = -1;

    result.clear();
    return result;
  }

  // read till end of process:
  const int LINE_BUFFER = 1024;
  char buffer[LINE_BUFFER];
  while (!feof(pipe)) {
    if (fgets(buffer, LINE_BUFFER, pipe) != NULL) result += buffer;
  }

  // close pipe
  pclose(pipe);
  if (error_code != NULL) *error_code = 0;
  return result;
}

ヘッダーは以下のようになっている.

#ifndef CMD_TEST_H
#define CMD_TEST_H

#include <cstdio>
#include <string>
#include <iostream>

extern std::string executeCommandWithResult(const std::string& command,
                                     int* error_code = nullptr);

#endif  // CMD_TEST_H

全体の配置は以下のようにした.
cmdTestConfig.cmake.inに関しては5-1で後述するので,ここではディレクトリ構成と配置を示す.

$ tree .
.
├── CMakeLists.txt
├── build
├── cmake
│   └── cmdTestConfig.cmake.in # 内容は5-1を参照
├── include
│   └── cmdTest.h
└── src
    └── cmdTest.cpp

5 directories, 4 files

4. パッケージを作る際に事前に決める項目

4-1. パッケージ名

  • <PackageName>Config.cmakeの名前を決める
    今回はcmdTestConfig.cmake.in`とした.
    とくに理由はない.

<PackageName>Config.cmakeのように,<PackageName>を使うのは,あちらこちらであるので,
下記のように先頭で変数として定義しておくと,後々の記述が楽になる.

# パッケージの名前を設定します.
set(PACKAGE_NAME cmdTest)

この名前でCMakeLists.txtからfind_package()で探索されることになる.

4-2. パッケージのインストール先

# ターゲットをインストールします.
install(TARGETS ${PROJECT_NAME} EXPORT cmdTestTargets
    LIBRARY DESTINATION lib # 共有ライブラリをインストールします.
    ARCHIVE DESTINATION lib # 静的ライブラリをインストールします.今回は記述のみで使用していません.
    RUNTIME DESTINATION bin # 実行ファイルをインストールします.今回は記述のみで使用していません.
                            # Windowsの場合は,dllファイルをインストールします.(らしい)
    INCLUDES DESTINATION include
)

4-3. パッケージのバージョン

  • バージョン番号を決める
    今回は,1.0.0とした.
    これは,CMakeのproject(cmdTest VERSION 1.34.0)で指定したバージョンと同じである必要はない.
    むしろ別の方が良い(個人の感想です)
    write_basic_package_version_file()内でバージョン指定しています.
    私は先頭の方で指定しておくことが好みなのでVERSIONを用意しwrite_basic_package_version_file()内で${VERSION}を指定しています.
    この部分の記述は以下になります.
    write_basic_package_version_fileを使うためには
    include(CMakePackageConfigHelpers)の記述がが必要になります.
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
    cmdTestConfigVersion.cmake
    VERSION ${VERSION}  # 先頭の方で set(VERSION 1.0.0)と記述しています
    COMPATIBILITY AnyNewerVersion
)
# バージョンファイルをインストールします.
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/cmdTestConfigVersion.cmake"
    DESTINATION lib/cmake/cmdTest
)

5. 実際のCMakeLists.txtの記述

5-1. その前にcmdTestConfig.cmake.in

cmdTestConfig.cmake.inは,インストール時にインストール先のディレクトリにコピーされます.
このファイルは,インストール先のディレクトリにあるcmdTestTargets.cmake(自動生成される)を読み込むためのファイルです.

cmake/cmdTestConfig.cmake.inに私の設定した内容は以下です.

# cmdTestConfig.cmake.in
include("${CMAKE_CURRENT_LIST_DIR}/cmdTestTargets.cmake")

なんか他の方法は無かったのか不思議な仕様(個人の感想です)

5-2. 通常と違うCMakeLists.txtの記述

【重要】 target_include_directories()の記述が通常と違うので注意する.

ここで,$<BUILD_INTERFACE:...>$<INSTALL_INTERFACE:...>はジェネレーター式と呼ばれ,ビルドとインストール時に異なるパスを指定するために使用されます.
ビルド時には${CMAKE_CURRENT_SOURCE_DIR}/includeが使用され,インストール時にはincludeが使用されます.
https://cmake.org/cmake/help/latest/command/target_include_directories.html
なんて面倒くさい仕様なんだろう(個人の感想)

target_include_directories(${PROJECT_NAME} PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

5-3. CMakeLists.txtの記述

# CMakeの最小バージョンを3.20として指定します.
# https://cmake.org/cmake/help/latest/command/cmake_minimum_required.html
cmake_minimum_required(VERSION 3.20)

# プロジェクトの名前とバージョンを設定します.
# https://cmake.org/cmake/help/latest/command/project.html
project(cmdTest VERSION 1.34.0)

# パッケージの名前を設定します.
set(PACKAGE_NAME cmdTest)

# パッケージのバージョンを1.0.0に設定します.
set(VERSION 1.0.0)

# 共有ライブラリを作成します.
# https://cmake.org/cmake/help/latest/command/add_library.html
# src/以下のC/C++ファイルを全て取得します.
file(GLOB SRC
    "src/*.cpp" "src/*.cc" "src/*.cxx" "src/*.c"
)
add_library(${PROJECT_NAME} SHARED ${SRC})

# ライブラリのパブリックインクルードディレクトリを設定します.
# https://cmake.org/cmake/help/latest/command/target_include_directories.html
target_include_directories(${PROJECT_NAME} PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

# ターゲットをインストールします.
# https://cmake.org/cmake/help/latest/command/install.html
install(TARGETS ${PROJECT_NAME} EXPORT ${PACKAGE_NAME}Targets
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    RUNTIME DESTINATION bin
    INCLUDES DESTINATION include
)

# includeディレクトリをインストールします.
install(DIRECTORY include/ DESTINATION include)

# エクスポートしたターゲットの設定を保存します.
# EXPORT${PACKAGE_NAME}Targets: ${PACKAGE_NAME}Targetsという名前のエクスポートセットに指定されたターゲットをインストールします.これらのターゲットは,install(TARGETS) コマンドでEXPORTオプションとして指定されます.
# FILE ${PACKAGE_NAME}Targets.cmake: インストールされるエクスポートファイルの名前を指定します.このファイルには,エクスポートされたターゲットの設定が含まれます.
# NAMESPACE ${PACKAGE_NAME}::: エクスポートされたターゲットの名前にこの名前空間が付加されます.この名前空間は,他のプロジェクトでこのライブラリを利用する際に,ターゲットを一意に識別するために使用されます.
# DESTINATION lib/cmake/${PACKAGE_NAME}: エクスポートファイルがインストールされる場所を指定します.この場所は,他のプロジェクトがこのライブラリを見つけるためのパスになります
install(EXPORT ${PACKAGE_NAME}Targets
    FILE ${PACKAGE_NAME}Targets.cmake
    NAMESPACE ${PACKAGE_NAME}::
    DESTINATION lib/cmake/${PACKAGE_NAME}
)

# ターゲット設定ファイルのインストール先を指定します.
# https://cmake.org/cmake/help/latest/command/configure_file.html
# @ONLYが指定されると,configure_fileは@VAR@形式の変数のみを置換します.これは,${VAR}形式の変数は無視され,そのままの形で出力ファイルに書き出されることを意味します.
# このオプションが有用なのは,出力ファイルがCMakeスクリプトである場合や,出力ファイルにCMake変数形式(${VAR})を含めたい場合です.@ONLYが指定されていない場合,configure_fileは@VAR@形式の変数だけでなく${VAR}形式の変数も置換しようとします.
configure_file(cmake/${PACKAGE_NAME}Config.cmake.in ${PACKAGE_NAME}Config.cmake @ONLY)

# 設定ファイルをインストールします.
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PACKAGE_NAME}Config.cmake"
    DESTINATION lib/cmake/${PACKAGE_NAME}
)

# パッケージ設定ヘルパーを含みます.
# https://cmake.org/cmake/help/latest/module/CMakePackageConfigHelpers.html
include(CMakePackageConfigHelpers)

# バージョンファイルを作成します.
# https://cmake.org/cmake/help/latest/module/CMakePackageConfigHelpers.html#command:write_basic_package_version_file
write_basic_package_version_file(
    ${PACKAGE_NAME}ConfigVersion.cmake
    VERSION ${VERSION}
    COMPATIBILITY AnyNewerVersion
)# バージョンファイルをインストールします.
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PACKAGE_NAME}ConfigVersion.cmake"
    DESTINATION lib/cmake/${PACKAGE_NAME}
)

6. パッケージのインストールと確認

  • プロジェクトのコンパイル・リンクとインストール
    これはいつもの通りです.
$ cd build
$ cmake ..
$ make
$ sudo make install
  • インストールされたファイルの確認
    /usr/local/以下に以下のファイルがインストールされている事を確認する.
~ $ ls /usr/local/include/cmdTest.h 
/usr/local/include/cmdTest.h
~ $ ls /usr/local/lib/libcmdTest.dylib 
/usr/local/lib/libcmdTest.dylib
~ $ 
  • パッケージの設定ファイルの確認
    これは自動生成されているので興味があれば中身を確認すると良いかもしれない.
    私は興味がないので中身は確認していない.
    パッケージ名がcmdTestなので,/usr/local/lib/cmake/cmdTest内に配置されている.
$ tree /usr/local/lib/cmake/cmdTest 
/usr/local/lib/cmake/cmdTest
├── cmdTestConfig.cmake
├── cmdTestConfigVersion.cmake
├── cmdTestTargets-noconfig.cmake
└── cmdTestTargets.cmake```

7. パッケージを使う側のCMakeLists.txtの記述

簡単なソースコードを用意して,パッケージを使う側のCMakeLists.txtの記述を確認するだけなので,とにかくシンプルに

7.1 ディレクトリ構造

$ tree .
.
├── CMakeLists.txt
├── build
├── include
└── src
    └── main.cpp

7.2 ソースコード

ただ呼び出しているだけのコード

#include <iostream>
#include <cmdTEST.h>

int main(int, char **) {
  std::cout << executeCommandWithResult("ls -l") << std::endl;
  return 0;
}
cmake_minimum_required(VERSION 3.26.3)
project(a.out VERSION 0.0.1)

file(GLOB SRC
    "src/*.cpp" "src/*.cc" "src/*.cxx" "src/*.c"
)

add_executable(${PROJECT_NAME}
    ${SRC}
)
target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/include)

target_compile_features(${PROJECT_NAME} PRIVATE c_std_17 cxx_std_17)
target_compile_options(${PROJECT_NAME} PRIVATE -O2 -Wall) # -H -v
add_custom_target(-${PROJECT_NAME}- ALL
    COMMAND "otool" "-L" "${PROJECT_NAME}"
)

find_package(cmdTest REQUIRED)
target_link_libraries(${PROJECT_NAME} cmdTest::cmdTest)

# 以下は,find_package()のデバッグ用
# set(_searchword_ cmdTest)で検索したい文字列を指定すると表示される.
message(STATUS "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
set(_searchword_ cmdTest)
get_cmake_property(_vars_ VARIABLES)

foreach(_varname_ ${_vars_})
    if("${_varname_}" MATCHES "${_searchword_}")
        message(STATUS "${_varname_} = ${${_varname_}}")
    endif()
endforeach()

message(STATUS "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")

7.3 実行結果

cmake ..の実行結果
cmdTest_FOUND = 1となっているので,見つかっている事が確認できる.
デバッグ用に仕込んだメッセージの出力も確認できる.

$ cmake ..
  中略
-- cmdTest_CONFIG = /usr/local/lib/cmake/cmdTest/cmdTestConfig.cmake
-- cmdTest_CONSIDERED_CONFIGS = /usr/local/lib/cmake/cmdTest/cmdTestConfig.cmake
-- cmdTest_CONSIDERED_VERSIONS = 1.0.0
-- cmdTest_DIR = /usr/local/lib/cmake/cmdTest
-- cmdTest_FOUND = 1
-- cmdTest_VERSION = 1.0.0
-- cmdTest_VERSION_COUNT = 3
-- cmdTest_VERSION_MAJOR = 1
-- cmdTest_VERSION_MINOR = 0
-- cmdTest_VERSION_PATCH = 0
-- cmdTest_VERSION_TWEAK = 0
  後略

makeの実行結果
otool -L a.outを実行させているので,依存ライブラリの確認もできる.

build $ make
[100%] Built target a.out
  中略
a.out:
        @rpath/libcmdTest.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1600.157.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1336.61.1)
[100%] Built target -a.out-
build $ 

9. 最後に愚痴

CMakeはかなり使われている
もちろんGitHubやHomebrewでもかなり使われている
CMakeの公式ドキュメントは非常に読みにくい

That's all folks.

3
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5