Fortranでcmake

色々とたくさんのソースコードがあるとMakefileで依存関係を書くのが結構面倒です。また、MKLとかLAPACKとかコンパイルオプションとかをどうすればいいか困ることもあるでしょう。そんなとき、一つの選択肢として、cmakeがよいと思います。

# ターゲット
cmakeでFortanのコードをコンパイルできることを示すために、機械学習分子動力学分野で使われるオープンソースパッケージ[aenet](http://ann.atomistic.net/)をcmakeでコンパイルすることにします。aenetに関しては[PIMDとAENETを使って機械学習分子シミュレーションをやってみよう: 2021年Docker版](https://qiita.com/cometscome_phys/items/758f468481eb3c09159e)などを参考にしてください。

このソフトウェアは、
```
lib
src
```
という二つのディレクトリがありまして、libに入っている外部パッケージをコンパイルしてから、srcに入っていえるコードをコンパイルします。その際、makefileを使っている環境に応じて選ぶ形になっています。
コードとして、
1. generate.x : 記述子を作成する
2. train.x : ニューラルネットワークを訓練する
3. predict.x : エネルギーを予測する
4. libaenet : 外部から使うためのライブラリ
という四つがあります。


# cmakeファイル
cmakeについての説明は省略します。例えば[こちら](https://qiita.com/ijknabla/items/05270ae5e597705d0dae)などが参考になるかと思います。

今回やりたいことは、
- libディレクトリにある外部ライブラリを解凍してコンパイル
- aenetを自分の環境に合わせてコンパイル
というものです。

# cmakeファイル本体
色々と試行錯誤してみましたが、こんな感じになりました。
```cmake:CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(aenet Fortran C)
find_package(MPI    REQUIRED)

set (CMAKE_Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/modules)

add_library(lbfgsb STATIC )
set(LBFGSB lib/Lbfgsb.3.0)

if(NOT EXISTS ${LBFGSB})
    message(STATUS "extracting LBFGSB ...")

    execute_process(
      COMMAND ${CMAKE_COMMAND} -E tar xvf "${CMAKE_CURRENT_SOURCE_DIR}/lib/Lbfgsb.3.0.tar.gz"
      WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/lib/"
    )
    message(STATUS "extracted.")
else()

endif()




target_sources(lbfgsb
PRIVATE
${LBFGSB}/blas.f
${LBFGSB}/lbfgsb.f
${LBFGSB}/linpack.f
${LBFGSB}/timer.f
)


add_library(aenet STATIC)
add_executable(predict.x)
add_executable(train.x)
add_executable(generate.x)

option(MKLUSE "Use MKL or not" OFF)
if(MKLUSE)
set(MKL_INTERFACE lp64)
set(MKL_INTERFACE_LAYER "_lp64")
find_package(MKL REQUIRED)
set(lapacklink MKL::MKL)
else()
find_package(LAPACK REQUIRED)
set(lapacklink LAPACK::LAPACK)
endif()

set(aenet_dir src)

set(sources 
${aenet_dir}/ext/chebyshev.f90 
${aenet_dir}/ext/feedforward.f90  
${aenet_dir}/ext/io.f90  
${aenet_dir}/ext/lclist.f90  
${aenet_dir}/ext/sfbasis.f90  
${aenet_dir}/ext/sortlib.f90  
${aenet_dir}/ext/symmfunc.f90  
${aenet_dir}/ext/timing.f90
  ${aenet_dir}/ext/unittest.f90  
  ${aenet_dir}/ext/xsflib.f90
${aenet_dir}/aenet.f90 
${aenet_dir}/aeio.f90
${aenet_dir}/constants.f90
${aenet_dir}/geometry.f90
${aenet_dir}/input.f90
${aenet_dir}/optimize.f90
${aenet_dir}/parallel.F90
${aenet_dir}/potential.f90
${aenet_dir}/random.f90
${aenet_dir}/sfsetup.f90
${aenet_dir}/trainset.f90
)



target_sources(aenet
PRIVATE
${sources}
)

target_sources(predict.x
PRIVATE
${sources} 
${aenet_dir}/predict.F90
)

target_sources(train.x
PRIVATE
${sources} 
${aenet_dir}/train.F90
)

target_sources(generate.x
PRIVATE
${sources} 
${aenet_dir}/generate.F90
)

if(MPI_COMPILE_FLAGS)
  set_target_properties(aenet  PROPERTIES
    COMPILE_FLAGS "${MPI_COMPILE_FLAGS}")
    set_target_properties(predict.x  PROPERTIES
    COMPILE_FLAGS "${MPI_COMPILE_FLAGS}")
endif()

include_directories(SYSTEM ${MPI_C_INCLUDE_PATH} ${MPI_CXX_INCLUDE_PATH})

set(linklibraries ${MPI_LIBRARIES} lbfgsb MPI::MPI_Fortran ${lapacklink})

target_link_libraries(aenet PRIVATE ${linklibraries} )
target_include_directories(aenet PRIVATE ${MPI_INCLUDE_PATH})
target_compile_definitions(aenet PUBLIC PARALLEL )

target_link_libraries(predict.x PRIVATE ${linklibraries} )
target_include_directories(predict.x PRIVATE ${MPI_INCLUDE_PATH})
target_compile_definitions(predict.x PUBLIC PARALLEL )

target_link_libraries(train.x PRIVATE ${linklibraries} )
target_include_directories(train.x PRIVATE ${MPI_INCLUDE_PATH})
target_compile_definitions(train.x PUBLIC PARALLEL )

target_link_libraries(generate.x PRIVATE ${linklibraries} )
target_include_directories(generate.x PRIVATE ${MPI_INCLUDE_PATH})
target_compile_definitions(generate.x PUBLIC PARALLEL )


if (CMAKE_Fortran_COMPILER_ID STREQUAL "GNU")
  # using GCC
  target_compile_options(aenet PUBLIC -fallow-argument-mismatch)
  target_compile_options(predict.x PUBLIC -fallow-argument-mismatch -Wall -fbounds-check -O -Wuninitialized -fbacktrace -g)
  target_compile_options(train.x PUBLIC -fallow-argument-mismatch -Wall -fbounds-check -O -Wuninitialized -fbacktrace -g)
  target_compile_options(generate.x PUBLIC -fallow-argument-mismatch -Wall -fbounds-check -O -Wuninitialized -fbacktrace -g)
elseif(CMAKE_fortran_COMPILER_ID STREQUAL "Intel")

endif()

```

# 解説

解説しますと、
```
project(aenet Fortran C)
```
はcmakeでFortranとCを扱う、という宣言です。
```
set (CMAKE_Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/modules)
```
は、コンパイルによってできた.modファイルをmodulesというディレクトリに一箇所に集めるために使っています。このように集めておくと、別のソフトウェアからmoduleを利用する際にこのディレクトリを指定するだけで済むようになります。
```
if(NOT EXISTS ${LBFGSB})
    message(STATUS "extracting LBFGSB ...")

    execute_process(
      COMMAND ${CMAKE_COMMAND} -E tar xvf "${CMAKE_CURRENT_SOURCE_DIR}/lib/Lbfgsb.3.0.tar.gz"
      WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/lib/"
    )
    message(STATUS "extracted.")
else()

endif()
```
この部分は、```Lbfgsb.3.0.tar.gz```というファイルを解凍しています。cmakeを使うことで、比較的簡単にファイルを解凍するなどができます。
```
option(MKLUSE "Use MKL or not" OFF)
if(MKLUSE)
set(MKL_INTERFACE lp64)
set(MKL_INTERFACE_LAYER "_lp64")
find_package(MKL REQUIRED)
set(lapacklink MKL::MKL)
else()
find_package(LAPACK REQUIRED)
set(lapacklink LAPACK::LAPACK)
endif()
```
この部分はMKLを使うかLapackを使うかを決めるIf文でして、```cmake -DMKLUSE=on ..```などとするとMKLを使うことになります。
```
if (CMAKE_Fortran_COMPILER_ID STREQUAL "GNU")
  # using GCC
  target_compile_options(aenet PUBLIC -fallow-argument-mismatch)
  target_compile_options(predict.x PUBLIC -fallow-argument-mismatch -Wall -fbounds-check -O -Wuninitialized -fbacktrace -g)
  target_compile_options(train.x PUBLIC -fallow-argument-mismatch -Wall -fbounds-check -O -Wuninitialized -fbacktrace -g)
  target_compile_options(generate.x PUBLIC -fallow-argument-mismatch -Wall -fbounds-check -O -Wuninitialized -fbacktrace -g)
elseif(CMAKE_fortran_COMPILER_ID STREQUAL "Intel")

endif()
```
この部分は、コンパイラがgfortranかどうかを確認しています。もしgfortranであれば、```fallow-argument-mismatch```その他を付け加えています。これは、gfortran 10以降のコンパイラの場合、9まででは動いていたコードが動かなくなるのを防ぐためにつけています。なお、aenetのwebサイトから手に入るソースコードのmakefileはこの記述がありませんから、makefileを修正してmakeすることになります。

# 使い方
まず、aenetのコードをダウンロードして解凍
```
wget http://ann.atomistic.net/files/aenet-2.0.3.tar.bz2
tar xvf aenet-2.0.3.tar.bz2
cd aenet-2.0.3
```
として、CMakeLists.txtというファイルで保存したものをaenet-2.0.3ディレクトリの中におきます。そして、
```
mkdir build
cd build
```
でbuildディレクトリを作り、
```
cmake ..
```
とすることでcmakeができます。最後に、
```
make
```
とすれば完了です。cmakeがうまく動けば、ちゃんと外部パッケージも解凍してコンパイルしてリンクしてくれるはずです。