Edited at

CMakeによるFortranビルドの省力化


cmake による Fortran90 ビルドの省力化

CMakeとは、コンパイルの自動化ツール。

FORTRANのビルドでよく見るmakeと何が違うのというと、


  • Fortran90ソースのmodule依存関係を自動解決

  • ライブラリの自動探索(MPI, OpenMP, BLAS, LAPACK, OpenCV ...)

autoconf みたいなもの?と思った方がいるかもしれませんが...

マルチプラットフォームなので、異なるコンパイラ、異なるOSでも同じ設定が使えます。

GNU/Linux(gfortran)のビルドと、Windows(ifort.exe)のビルド設定が共通化できます!


Fortran90のmodule依存関係

以下の5つのソースを正しい順番でコンパイルし、

実行プログラム abcd へとリンクしてください。


a.f90

module a

end module a


b.f90

module b

end module b


c.f90

module c

use a
use d
end module c


d.f90

module d

use b
end module d


main.f90

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が自動的に依存関係を解決してくれます。


CMakeLists.txt

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.ohogehoge.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でバイナリだけ綺麗になくなるので便利です。


~/.config/git/ignore

*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を組み合わせた簡単なプログラムをコンパイルする例です。


CMakeLists.txt

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なしでも構いません。


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を読み込むよう設定



src/CMakeLists.txt

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


CMakeLists.txt

cmake_minimum_required(VERSION 3.0)

enable_language(Fortran)

enable_testing()
# ctestコマンドによる自動テストを有効にします。

project(shared-library Fortran CXX)

include_directories(include)
add_subdirectory(src)



src/CMakeLists.txt

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.f90cxx_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を作っておくと快適です。


~/.bash_aliases

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変数にディレクトリを指定することで一か所にまとめることが可能です。


CMakeLists.txt

set(CMAKE_Fortran_MODULE_DIRECTORY ${PROJECT_BINARY_DIR}/fortran-modules)


CMAKE_Fortran_MODULE_DIRECTORY変数はグローバルに設定されるので、この方法の場合は

すべての.modファイルが一か所に集約されます。(多くのケースでこうした方が管理しやすいです)

あなたがFortran90ハッカーで、同一プロジェクトに同名のモジュールファイルを設置し、

各ソースごとに使うモジュールを切り替えないと困る場合、グローバル変数を使わずに、各ターゲットの

プロパティとして設定することで、ターゲット単位(executanle, library単位)で出力ディレクトリを変えられます。


CMakeLists.txt

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ビルドする

みたいなことをやらせることができます。


Makefile.in


NUMPROCS := @NUMPROCS@

something:
@echo 'number of process is' $(NUMPROCS) > $@



CMakeLists.txt

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ファイルを読み書きしているコードを未だに見るのは忍びないことです。この件については今後別記事を作成するかもしれません。