cmake による Fortran90 ビルドの省力化
CMakeとは、コンパイルの自動化ツール。
FORTRANのビルドでよく見るmake
と何が違うのというと、
- Fortran90ソースのmodule依存関係を自動解決
- ライブラリの自動探索(MPI, OpenMP, BLAS, LAPACK, OpenCV ...)
autoconf
みたいなもの?と思った方がいるかもしれませんが...
マルチプラットフォームなので、異なるコンパイラ、異なるOSでも同じ設定が使えます。
GNU/Linux(gfortran)のビルドと、Windows(ifort.exe)のビルド設定が共通化できます!
Fortran90のmodule依存関係
以下の5つのソースを正しい順番でコンパイルし、
実行プログラム abcd
へとリンクしてください。
module a
end module a
module b
end module b
module c
use a
use d
end module c
module d
use b
end module d
program main
use c
print *, "hello, world!"
end program main
↓
↓
↓
↓
↓
- c.f90は a.f90とd.f90の後にコンパイルする必要があります。
- d.f90は b.f90の後にコンパイルする必要があります。
- main.f90は c.f90の後にコンパイルする必要があります。
この条件を満たすのは、以下の三つの順序です。
a.f90 -> b.f90 -> d.f90 -> c.f90 -> main.f90
b.f90 -> a.f90 -> d.f90 -> c.f90 -> main.f90
b.f90 -> d.f90 -> a.f90 -> c.f90 -> main.f90
横断的関心事(例外コード、ログメッセージ、IO/装置番号)を一か所にまとめる設計をすると、
様々な場所にuse error_code
だとかuse logger
だとかuse unitnum_manager
が現れます。
少しずつ依存関係が複雑になっていき、人力で管理できなくなっていきます。
Fortran2003以降のコードのことも考えると、上記に加えてクラスの継承による依存関係も考慮する必要があり、依存関係を記述するコストがどんどん大きくなります。
CMakeの出番です。
CMakeによるmodule依存関係の解決
先ほどの例であれば、以下のような設定ファイルCMakeLists.txt
を
準備すれば、CMakeが自動的に依存関係を解決してくれます。
cmake_minimum_required(VERSION 3.0)
# cmake 自体のバージョンを明示します。
enable_language(Fortran)
# cmake の名の通り、デフォルトでは C/C++ しか扱わないので、
# Fortran向けの設定を生成するように指定します。
project(simple-dependency Fortran)
set(EXECUTABLE abcd)
# CMakeの変数宣言です。実行体の名前を後から変更しやすいよう
# 最初から変数で定義したほうがいいと思います。
add_executable(${EXECUTABLE} # "abcd" のことです
a.f90
b.f90
c.f90
d.f90
main.f90
)
# 実行体ファイルをビルドするための設定です。
# コンパイルに必要なソースを列挙するだけでOKです。
CMakeを使えば、実行体のコンパイルに必要なソースを列挙するだけで、コンパイル順序は自動計算されます。
コードを書くときは機能の分割に集中することができるので、Makefileの書き方がわからないというだけの理由で、巨大なmoduleを作ってしまう というような事態を回避することができます。
CMake のインストール
Windowsなら、CMakeダウンロードページから、どうぞ。
Unix系なら、パッケージマネージャでもインストールできます。apt
の例を示します。
$ sudo apt install cmake cmake-curses-gui
CMakeの動かし方
CMakeはGUIで使うのが一般的です。 もちろん機能はcmake
と全く同じ。
設定されたCMake変数を視覚的に確認できるので、よく使われています。
- Windowsなら
cmake-gui.exe
- Unix系なら、
ccmake
<- Curses の C です
CMakeには、Source DirectoryとBinary Directoryというものがあります。
その名の通りですが、コンパイルした後のバイナリを別の場所にできるということです。
Binary DirectoryとSource Directoryを別にしたビルドのことをout-of-treeビルドと呼びます。
out-of-treeに対してin-treeビルドとはhogehoge.o
がhogehoge.f90
と同じディレクトリに生成されるようなビルドを指し、標準的なMakefile
はこちらになります。
CMakeにおけるSource Directoryとは、先ほどの例で示したCMakeLists.txt
が存在するディレクトリのことです。
サブディレクトリにCMakeLists.txt
を配置して、階層化されたビルドを行うこともできます。
Binary Directoryはデフォルトではcmake
コマンドを実行したカレントディレクトリとなります。Source Directoryの直下のbuild/
とするのが一般的です。
以上のことから、以下のようなコマンドでビルドをするのが一般的です。
$ cd /path/to/source-directory
$ mkdir build && cd build
$ cmake ..
$ make
Binary Directoryの直上にSource Directoryがあるので、
cmake ..
となるわけです。
私はよく、build-Linux64-intel/
やbuild-MSVC15/
やdebug-build/
やparallel-build/
などなど
目的別に複数のビルドディレクトリを用意してビルドしています。
以下のような.gitignore
を書いてやれば、git clean
でバイナリだけ綺麗になくなるので便利です。
*build*/
CMakeはビルドを複数の段階に分けることで、環境への依存が少なくなるようにしてあります。
- Configureでは、
CMakeLists.txt
の設定を読み込んで、必要なコンパイラやライブラリを探します。 - GenerateではConfigureの結果をもとに、ビルドツールの入力ファイル(
Makefile
,.sln
)を自動生成します。 - 生成された入力ファイルでビルドします。
cmake
(CUI) では、Configure+Generateまで一括で行います。
ccmake
ではそれぞれ[c]
[g]
キーに対応しております。
cmake-gui.exe
ではボタンをクリックして行います。
Generatorとは、ビルドツールの入力ファイルをGenerateするもので、ビルドツールごとに存在します。
cmake --help
でGeneratorの一覧を見ることができます。
以下はUbuntu1804(wsl)の場合の例です。
Generators
The following generators are available on this platform:
Unix Makefiles = Generates standard UNIX makefiles.
Ninja = Generates build.ninja files.
Watcom WMake = Generates Watcom WMake makefiles.
CodeBlocks - Ninja = Generates CodeBlocks project files.
CodeBlocks - Unix Makefiles = Generates CodeBlocks project files.
CodeLite - Ninja = Generates CodeLite project files.
CodeLite - Unix Makefiles = Generates CodeLite project files.
Sublime Text 2 - Ninja = Generates Sublime Text 2 project files.
Sublime Text 2 - Unix Makefiles
= Generates Sublime Text 2 project files.
Kate - Ninja = Generates Kate project files.
Kate - Unix Makefiles = Generates Kate project files.
Eclipse CDT4 - Ninja = Generates Eclipse CDT 4.0 project files.
Eclipse CDT4 - Unix Makefiles= Generates Eclipse CDT 4.0 project files.
KDevelop3 = Generates KDevelop 3 project files.
KDevelop3 - Unix Makefiles = Generates KDevelop 3 project files.
CMakeの様々な例
以下の例はgithubに上げておきました。
git clone https://github.com/ijknabla/cmake_fortran_example.git
simple-dependency
最初にmodule依存関係の例として示したものです。
まずはcmake
ccmake
コマンドの動作に慣れてください。
more-dependency
simple-dependencyの規模を大きくして、module [a-z] を依存関係を考慮してビルドします。
依存関係を正しく計算できているので、cmake
が生成したMakefile
は、並列ビルドに対応しています。一直線でない込み入った依存関係には、同時にコンパイルしてもいい組が意外にあるものなので、make -j4
と並列ビルドをさせると、スピードアップしていることがわかると思います。
external-library
MPI, OpenMPを組み合わせた簡単なプログラムをコンパイルする例です。
cmake_minimum_required(VERSION 3.0)
project(external-library Fortran)
enable_language(Fortran)
find_package(MPI REQUIRED)
find_package(OpenMP REQUIRED)
# MPI, OpenMPのConfigureを行う
set(EXECUTABLE MPI-OpenMP-hybrid)
add_executable(${EXECUTABLE}
main.f90
communications.f90
)
target_link_libraries(${EXECUTABLE}
PRIVATE
MPI::MPI_Fortran
OpenMP::OpenMP_Fortran
)
# executable targetの要求するライブラリとして指定
mpiexec
で実行してください。
$ cd external-library
$ mkdir build && cd build
$ ccmake .. # [c] [g]
$ make
$ mpiexec -np 4 ./MPI-OpenMP-hybrid
標準のインストールに設定が含まれていないライブラリを使いたい場合、find${package-name}.cmake
というスクリプトを書くことで、対応させられます。
googleで検索するとオープンソースライセンスで配布されているケースがあるので、それを利用するのも手だと思います。
(有名なライブラリのパッケージはだいたい揃っていますが。)
static-libary
静的ライブラリをターゲットとして追加する場合、
add_library(${TARGET_NANE} STATIC ...)
とします。
多機能なライブラリを作り、各機能ごとにコマンドラインツールを作る、というようなケースが当てはまるでしょう。
cmake_minimum_required(VERSION 3.0)
enable_language(Fortran)
project(static-library Fortran)
set(STATIC_LIB some )
set(TOOL1 tool1)
set(TOOL2 tool2)
add_library(${STATIC_LIB}
STATIC
some_library.f90
)
# external-library の時と違って、
# ライブラリ自体のコンパイルを行うための設定を書きます
add_executable(${TOOL1}
tool1.f90
)
target_link_libraries(${TOOL1}
PRIVATE
${STATIC_LIB}
)
add_executable(${TOOL2}
tool2.f90
)
target_link_libraries(${TOOL2}
PRIVATE
${STATIC_LIB}
)
# 実行体が二つあるので、add_executable を二度書きます
multilingual
... 次は共有ライブラリ(.so
, .dll
)の話題に進みたいのですが、
"共有" させるにあたって、C/C++言語との相互利用について先に説明しておきます。
FortranとC++を混ぜたプログラムの例です。
C++のincludeディレクトリに対応するために、階層的なビルドとしてあります。
multilingual/
├── CMakeLists.txt <--
├── include
│ └── greetings.h
└── src
├── CMakeLists.txt <--
├── greetings.cxx
├── greetings.f90
└── main.f90
includeディレクトリのファイルはビルド対象ではないので、CMakeLists.txt
なしでも構いません。
cmake_minimum_required(VERSION 3.0)
enable_language(Fortran)
project(multilingual CXX Fortran)
# CXX(C++)とFortranを使うことを明示
include_directories(include) # サブディレクトリをインクルードディレクトリに追加
add_subdirectory(src) # src/CMakeListsを読み込むよう設定
set(EXECUTABLE sayHello)
add_executable(${EXECUTABLE}
greetings.cxx
greetings.f90
main.f90
)
# 拡張子から判断して、適切なライブラリ/リンカを選んでくれるので、
# Fortranのみの場合と使い勝手は同じ
set_target_properties(${EXECUTABLE}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}
)
# サブディレクトリでexecutableターゲットを作ると保存先がそのサブディレクトリになるので、
# Makefileと同じ場所に出力するように変更
cmake_minimum_required
, project
, enable_language
などは、プロジェクト全体の設定となるので、
サブディレクトリには書かないでよいです。
C++のiostream
へFortranの文字列を渡して、hello, world! します
shared-library
add_library(${TARGET_NANE} SHARED ...)
で、共有ライブラリのターゲットを追加できます。
SHARED
の名の通り、他のプログラムから利用されることを考えて、ライブラリ本体に加えてヘッダとモジュールファイルを指定のディレクトリにインストールするための設定についてもここで紹介します。
Fortranのシンボル名(リンカから見たfunction/subroutine名)のルールは、規格で統一されているわけではないので、gfortranで作った.so
をifortで読み込ませると、シンボル名が異なってうまく動かないケースがあります。これは、外部手続き(FORTRAN77的方法)で行った場合は問題ないケースがありますが、モジュール副プログラムを使った場合はほぼ確実にリンクできないと考えてください。
一方でC言語のシンボル名はコードに書かれた関数名と常に一致するので、異なるコンパイラでモジュール副プログラムを共有させたい場合は、モジュール副プログラムをC言語関数としてエクスポート/インポートさせることで、だいぶ動かしやすくすることができます。
Fortranの共有ライブラリをC言語のシンボル名でエクスポートすることは、上記の利点のほかに、C/C++や「C呼び出し機能はあるがFortranには対応していない言語」から利用可能にできるというメリットもあります。
そうすると、実装はすべてFortranだけれども、C関数として公開するためのヘッダファイルも用意し、インストールするような設定を作る必要が出てきます。
前置きが長くなりましたが、CMakeの話に移ります
基本的なディレクトリ階層は、multilingual
と同じです。
shared-library/
├── CMakeLists.txt
├── include
│ └── reynolds.h
└── src
├── CMakeLists.txt
├── cxx_test.cxx
├── fortran_test.f90
├── reynolds_c_interface.f90
├── reynolds_cxx_interface.cxx
└── reynolds_lib.f90
cmake_minimum_required(VERSION 3.0)
enable_language(Fortran)
enable_testing()
# ctestコマンドによる自動テストを有効にします。
project(shared-library Fortran CXX)
include_directories(include)
add_subdirectory(src)
set(SHARED_LIB reynolds )
set(FORTRAN_TEST fortran_test)
set(CXX_TEST cxx_test )
add_library(${SHARED_LIB}
SHARED
reynolds_lib.f90
reynolds_c_interface.f90
reynolds_cxx_interface.cxx
)
set_target_properties(${SHARED_LIB}
PROPERTIES
PUBLIC_HEADER ../include/reynolds.h
)
install(TARGETS ${SHARED_LIB}
LIBRARY DESTINATION lib
PUBLIC_HEADER DESTINATION include
)
# C/C++言語の共有ライブラリなら、
# 共有ライブラリと公開ヘッダのインストールで足りますが...
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/reynolds_lib.mod
DESTINATION lib
)
# Fortranのモジュールファイルもインストールする必要があります。
# 以下は、作成された共有ライブラリのテストコードであり、
# インストールされません。
add_executable(${FORTRAN_TEST}
fortran_test.f90
)
target_link_libraries(${FORTRAN_TEST}
PRIVATE
${SHARED_LIB}
)
add_executable(${CXX_TEST}
cxx_test.cxx
)
target_link_libraries(${CXX_TEST}
PRIVATE
${SHARED_LIB}
)
add_test(
NAME Fortran_Compertibility
COMMAND $<TARGET_FILE:${FORTRAN_TEST}>
)
add_test(
NAME C++_Compertibility
COMMAND $<TARGET_FILE:${CXX_TEST}>
)
install(...)
でターゲットと保存先を指定することで、make install
ができるようになります。
インストール先はCMAKE_INSTALL_PREFIX
変数で変更可能で、ccmake
で入力すると簡単です。デフォルトでは/usr/local
にインストールしてしまうのですが、~/.local
など、ホームディレクトリ下を指定したほうが都合がよいでしょう。
インストール先を確認できたら、make install
してみてください。
次にFortran特有の問題なのですが、モジュール副プログラムを共有ライブラリから利用する場合、.mod
ファイルも必要になるので、installに.mod
ファイルを含める必要があります。.mod
ファイルをどこに設置するかについては議論の余地があります。
- -I オプションで使うんだから
include/
派 - 中身がバイナリだが実行はできないので
lib/
派 <- 今回の例はこれです - includeでもlibでもないので別のディレクトリにする派 (fortran-modulesとか)
インストール対象(配布物)がライブラリとヘッダとモジュールだけなので、開発者向けのテストコードを別途用意します。
今回の例ではfortran_test.f90
とcxx_test.cxx
がそれぞれの言語へのバインディングに対するテストとなっております。
これらは、ctest
コマンドでまとめてテストされるようになっています。テストコードは、失敗したときに0以外のリターンコードを返すようなexecutableならなんでもよいので、c++とFortranのテストをまとめて管理させています。 テストにかかった時間などはCMakeが整形して出力してくれるので、とにかく失敗時にstop 1
するコードを設置するだけでテストができます。
テスト結果の例です。
Test project /mnt/d/work/cmake_fortran_example/shared-library/build
Start 1: Fortran_Compertibility
1/2 Test #1: Fortran_Compertibility ........... Passed 0.03 sec
Start 2: C++_Compertibility
2/2 Test #2: C++_Compertibility ............... Passed 0.03 sec
100% tests passed, 0 tests failed out of 2
Total Test time (real) = 0.08 sec
テストコードはinstall(...)
を設定していないので、インストールされません。
コンパイル環境について
先ほども述べたように、CMakeはビルドツールの入力ファイルを生成するステップを挟むことで柔軟なビルドを可能にしています。
どのビルドツールの入力ファイルを生成するかは、使用するジェネレータによって決まります。
WindowsならGUIで指定し、Unix系ならcmake -G ${Generator-Name}
で指定します。
ここまでの例では、特にジェネレータを指定していなかったので、
WindowsではVisual Studio
Unix-likeではMakefile
になっているはずです。
Makefile
と一緒に、eclipse
などIDEのプロジェクトファイルを書き出すこともできるようですね。Photoran
と組み合わせたりできるのでしょうか?(未検証)
C/C++のビルドではNinjaというモダンなツールを使うことでビルドを高速化できるのですが...
CMake側の都合で、Fortranを含む場合はNinjaジェネレータが使えないという問題があるので、
結局のところVisual StudioかMakefileの二択となります。
また、Generatorと独立して、コンパイラを変えることが可能です。
Windowsの場合、GUIで設定したのち、.exeのパスを指定することで、
CMakeが有効なコンパイラか自動で確認してくれます。
Unix系の場合は、以下の二通りの方法で設定できます。
intelの例を示します。
$ env CC=icc FC=ifort cmake .. #1
$ cmake -DCMAKE_C_COMPILER=icc -DCMAKE_Fortran_COMPILER=ifort #2
一つ目は、環境変数で指定する昔ながらの方法
二つ目は、CMake変数で指定する方法だけれどもいちいち長いので...
以下のようなaliasを作っておくと快適です。
alias icmake='cmake -DCMAKE_C_COMPILER=icc -DCMAKE_Fortran_COMPILER=ifort -DMPI_C_COMPILER=mpiicc -DMPI_Fortran_COMPILER=mpiifort $@'
alias iccmake='ccmake -DCMAKE_C_COMPILER=icc -DCMAKE_Fortran_COMPILER=ifort -DMPI_C_COMPILER=mpiicc -DMPI_Fortran_COMPILER=mpiifort $@'
以下は、私が実務でCMakeを使ったことのある環境の一覧です。
OS(distro) | コンパイラ | ジェネレータ | 備考 |
---|---|---|---|
Ubuntu1904 | gnu | 'Unix Makefiles' | +OpenMPI |
CentOS 7.2 | gnu | 'Unix Makefiles' | +IntelMPI |
CentOS 7.2 | intel | 'Unix Makefiles' | +IntelMPI |
Ubuntu1804(wsl) | gnu | 'Unix Makefiles' | +OpenMPI |
Ubuntu1804(wsl) | intel | 'Unix Makefiles' | +OpenMPI |
Windows10 | intel | 'Visual Studio 14 2015' | C/C++はMSVC |
Windows10 (MSYS2 MinGW64) | gnu | 'MSYS2 Makefiles' | OpenModelica DevEnv |
MSYS2 MinGWの場合、MSYS2 Makefile
とする必要があります。Unix Makefiles
との細かい差を受け止めてくれて、
Unixの時とほぼ同じ使い勝手になり、とてもありがたいですね。
書き下すと痛感しますが、gnu intel 以外のコンパイラをあまり使ったことがないのと、Mac OSを触ったことがないので、片手落ち感がありますね。今後PGIコンパイラについても確認しておきたいと思っています。
Ubuntu1804(wsl)が普段使いで、ここでコーディングとデバッグを行い、問題ない場合に本番環境(計算サーバ)で実行する、というのが私の日常です。
Windows版ビルドを用意しようとしたとき、引き継いだVisual Studioのソリューションファイルが古すぎたり、絶対パスが仕込まれていて面倒なときは、結局cmake
に統一したほうが早かったことが多かったです。
その他Tips
See also ...
CMakeについてはQiitaにタグがあり、さまざまな情報が得られます。
今回は、Fortranユーザーを狙って、簡単な導入からMPIやモジュールの話題に進みましたが、
もっと詳しくCMakeの条件分岐や繰り返しや関数定義やマクロについて勉強したい場合は公式ドキュメントを読みましょう。
モジュールの出力ディレクトリを変更する方法
一般的なFortranコンパイラは、.mod
の検索パスとインクルードファイルの検索パスの指定オプションを
同じ-I
にしてあるので、別ディレクトリの.mod
を使いたい場合はinclude directories(...)
に指定すればよいです。
問題は、.f90
から.mod
を生成する際に出力されるディレクトリのコントロールで、
デフォルトの動作では、.mod
ファイルは、ターゲットが定義されたSource Directoryに対応するBinary Directoryになります。
.mod
の出力ディレクトリを変更するオプションは、gfortranの-J
ifortの-module
など、細かい差が多いですが、
CMakeではCMAKE_Fortran_MODULE_DIRECTORY
変数にディレクトリを指定することで一か所にまとめることが可能です。
set(CMAKE_Fortran_MODULE_DIRECTORY ${PROJECT_BINARY_DIR}/fortran-modules)
CMAKE_Fortran_MODULE_DIRECTORY
変数はグローバルに設定されるので、この方法の場合は
すべての.mod
ファイルが一か所に集約されます。(多くのケースでこうした方が管理しやすいです)
あなたがFortran90ハッカーで、同一プロジェクトに同名のモジュールファイルを設置し、
各ソースごとに使うモジュールを切り替えないと困る場合、グローバル変数を使わずに、各ターゲットの
プロパティとして設定することで、ターゲット単位(executanle, library単位)で出力ディレクトリを変えられます。
set_target_properties(${EXECUTABLE_OR_LIBRARY}
PROPERTIES
Fortran_MODULE_DIRECTORY ${PROJECT_BINARY_DIR}/fortran_modules/hacked
)
CMakeによるCMakeのビルド
CMakeはバージョン間の差がまぁまぁあります。
特に2.8と3.xでは機能に大きな隔たりがあります。
計算サーバやスパコンの場合、安定したdistroを使いたいために古めのバージョンで固定されていることがあります。
(まずsudoerに確認すべきなのですが)
どうしても最新のCMakeを使いたい場合、CMakeでCMakeをビルドすることができます。
いわゆるブートストラッピングというもので、スマートなビルドを提供するCMakeらしいやり方ですね。
それよりも古いdistroを使っている場合、まずCMakeをビルドしなければいけませんが、
その場合autoconf && ./configure && make
で行います。
卵が先か鶏が先かでいえば卵が先だったようです。
Makefile自体を埋め込む
FORTRAN界ではときどき非常に息の長いプロジェクトを見かけることがあります。
大量のMakefileがcshやperlやsedやawkを呼びながらビルドをするようなものです。
設定を書き換えるために様々な場所のMakefile触らないといけません。
ここで、configure_file(...)
を使ってCMake変数をMakefileに書き出させることで、
変数の設定はccmake
で一括にできるようになります。
さらに、CMakeのadd_custom_target(...)
またはadd_custom_command(...)
を使って、Makefileを呼び出すようにすると
- Makefileをout-of-treeビルドし
- ビルドされたMakefileでin-treeビルドする
みたいなことをやらせることができます。
NUMPROCS := @NUMPROCS@
something:
@echo 'number of process is' $(NUMPROCS) > $@
cmake_minimum_required(VERSION 3.0)
set(NUMPROCS 8 CACHE STRING "number of process")
set(SOMETHING something)
set(MAKEFILE ${CMAKE_CURRENT_BINARY_DIR}/Makefile.generated)
configure_file(
Makefile.in
${MAKEFILE}
@ONLY
)
add_custom_target(${SOMETHING} ALL
${CMAKE_MAKE_PROGRAM} -f ${MAKEFILE} ${SOMETING}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS ${MAKEFILE}
)
Visualization Tool Kit
注目すべき点として、CMakeがそもそもVTKのビルドツール開発から派生して生まれたものであるために、VTKのサポートが手厚いということがあります。
しかし、VTK自体にFortranサポートがないために、FortranのI/Oだけでテキスト形式の.vtk
ファイルを読み書きしているコードを未だに見るのは忍びないことです。この件については今後別記事を作成するかもしれません。