Fortran 2018 + fpm + VS Codeの開発環境を整える ― Docker導入済みのWSL2で始める
はじめに
以前、「Fortranの次を考える ― Juliaで始める数値シミュレーション基盤」という記事を書きました。既存のFortranソルバーを残しながら、Juliaでも小さな数値計算を作り、計算結果や処理時間を比較していこうという内容です。
そのとき、もう一つ整えておきたいと思ったのが、比較の基準となるFortran側の開発環境です。
河川の1D計算や2D氾濫計算のような数値シミュレーションでは、長く使われてきたFortranのコードが今でも大切な役割を担っています。新しい言語を試すにしても、すでにある計算コードをすぐに置き換える必要はありません。むしろ、Fortran側を整理し、テストを実行できる状態にしておけば、新しい実装と落ち着いて比較できます。
一方で、Fortranの開発環境は、ソースファイルが増えるほどコンパイル手順が複雑になりがちです。コンパイラのオプションがシェルスクリプトごとに違っていたり、開発PCごとにツールの版が異なっていたりすると、計算結果を比較する前の準備にも時間がかかります。
そこで今回は、Fortran 2018、Fortran Package Manager(fpm)、Visual Studio Code(VS Code)をDockerコンテナにまとめることにしました。ビルドとテストはfpmで管理し、編集・コード補完・デバッグはVS Codeから行います。
今回の前提は、Windows 11 + WSL2 Ubuntu 24.04で、Docker EngineとDocker Composeがすでに使える状態です。Docker Desktopは使用しません。Docker自体のインストール方法も扱わず、既存のDocker EngineにFortran用の開発コンテナを追加します。
まずは小さな矩形水路の計算を使って、次の作業を一通り確認できる構成にします。
- Fortran 2018の構文を使った計算モジュールを作ります。
- fpmでビルド、実行、単体テストを行います。
- VS Codeとfortlsでコードを編集し、定義ジャンプなどを利用します。
- GDBでブレークポイントを設定して処理を追います。
- 将来、既存FortranソルバーとJulia版を比較する際に、実行条件を記録しやすい構成にします。
なお、最後の比較は今後の話です。今回の矩形水路サンプルだけで、河川ソルバーの妥当性や水収支を検証できたという意味ではありません。 まずは開発環境と小さな単体テストを整えるところから始めます。
1. 使用する環境
| 項目 | 構成 |
|---|---|
| PC | Windows 11 |
| Linux環境 | WSL2 / Ubuntu 24.04 |
| Docker | WSL2上のDocker Engine(導入済み) |
| Compose |
docker compose(利用可能な状態) |
| コンテナOS | Ubuntu 24.04 |
| Fortranコンパイラ | gfortran(Ubuntu 24.04のパッケージ) |
| Fortranの標準 | Fortran 2018(-std=f2018を指定) |
| パッケージ管理 | fpm 0.13.0 |
| Language Server | fortls |
| デバッガ | GDB |
| エディタ | Windows側のVS Code + WSL + Dev Containers |
なお、-std=f2018を指定したからといって、gfortranがFortran 2018のすべての機能を実装しているわけではありません。使いたい機能が現在のコンパイラで利用できるかは、個別に確認する必要があります。この記事では、gfortranで扱えるSELECT RANKなどを使います。
また、今回使用するfpmの配布バイナリはLinux x86_64向けです。ARM64環境ではそのまま利用できません。
2. 開発環境の構成
Windows 11のVS Code → WSL2 Ubuntu 24.04 → Docker Engine / Compose → Fortran開発コンテナ(gfortran、fpm、fortls、GDB)の関係を示します。ソースコードはWSL2側に置き、コンテナにマウントする構成です。
ソースコードはWSL2のホームディレクトリに保存します。VS CodeはWindowsで起動しますが、ファイル操作とコンパイルはWSL2とコンテナ内で行います。
例えば、作業ディレクトリは次のようにします。
mkdir -p ~/work
cd ~/work
/mnt/c以下にもプロジェクトを置けますが、Linux向けの開発ではWSL2側に置いた方が、ファイルの権限や大量のファイルを扱うビルドで問題を避けやすくなります。
Dockerコンテナにgfortranとfpmを入れるため、WindowsやWSL2のホスト側にこれらを別途インストールする必要はありません。
Fortranを比較の基準として残す理由
前の記事では、Juliaを試す際に、最初から大きな2D氾濫モデルへ進まず、単純な1D河道から始める方針にしました。Fortran版とJulia版で同じ初期条件・境界条件を使い、結果を一つずつ照合するためです。
そのためには、Fortran側についても「過去に動いたソースがある」だけでなく、どのコンパイラ、どのオプション、どの入力データで計算した結果なのかを確認できる方が便利です。これから組むDocker環境は、その土台にすることを意識しています。
もちろん、Fortranの結果だから無条件に正しい、ということではありません。テスト済みの計算ケースや観測値との比較など、妥当性を確認できる材料と合わせて基準を作る必要があります。
Fortran 2018とfpmを使う理由
Fortran 2018では、既存の配列演算やモジュールを使ったプログラミングに加えて、SELECT RANKやassumed-rank引数といった機能を利用できます。配列のランクに応じた処理をまとめたい場合などに使える機能です。
一方、fpmはビルド、依存関係、実行プログラム、テストを一つのプロジェクトとして管理するためのツールです。Fortranの標準規格そのものではなく、開発作業を整理するための仕組みと考えると分かりやすいと思います。
今回、Fortran 2018のすべての機能を使うわけではありません。言語の新しい機能を試すことと、既存ソルバーの数値計算部分を安定して管理することを分けて進めます。
3. プロジェクトのディレクトリ構成
今回のサンプルは、次のように分けます。
fortran2018-fpm-vscode/
├── Dockerfile
├── compose.yaml
├── fpm.toml
├── .dockerignore
├── .gitignore
├── .devcontainer/
│ └── devcontainer.json
├── .vscode/
│ ├── extensions.json
│ ├── tasks.json
│ └── launch.json
├── src/
│ └── hydraulics.f90
├── app/
│ └── main.f90
├── test/
│ └── test_hydraulics.f90
└── scripts/
├── run.sh
├── test.sh
├── build-debug.sh
└── verify.sh
fpmでは、計算処理をsrc/、実行プログラムをapp/、テストをtest/に分けて管理できます。今回のような小さなサンプルでも最初から分けておくと、後でソルバーを追加するときに整理しやすくなります。
将来、Julia版との比較に使う場合は、この構成にcases/を追加し、同じ入力データとFortranの基準結果をまとめて保存する予定です。ただし、今回配布するサンプルにはcases/はまだ含まれていません。
設定ファイルや長いソースコードは、必要な部分だけ開けるように折りたたんで掲載しています。ファイル名の行をクリックすると内容が表示されます。
ソースコードのダウンロード
この記事で使うサンプルプログラムと開発環境の設定ファイルは、ダウンロードから fortran2018_fpm_vscode_sources.zip の名前で入手できます。
ZIPにはFortranのソースコード、テスト、Dockerfile、compose.yaml、fpm.toml、VS Codeの設定、実行用スクリプトをまとめています。この記事のMarkdown本文は含めていません。
ダウンロードしたZIPをWSL2側に置き、ZIPがあるディレクトリで次を実行します。
mkdir -p ~/work
unzip ./fortran2018_fpm_vscode_sources.zip -d ~/work
cd ~/work/fortran2018-fpm-vscode
展開先はfortran2018-fpm-vscode/です。この後のビルドや実行は、展開したディレクトリで行います。
以降のコマンドは、特に断りのない限りWSL2のターミナルで実行します。docker composeのコマンドは、compose.yamlのあるプロジェクト直下で実行してください。
4. Dockerfileを作成する
開発コンテナには、gfortran、fpm、fortls、GDBを導入します。OSはUbuntu 24.04を使用します。
以下が今回のDockerfileです。
Dockerfile(コード全体を表示)
# Ubuntu 24.04 / GNU Fortran / fpm / fortls / GDB 開発イメージ
# 対応アーキテクチャ: linux/amd64(fpm の公開バイナリに合わせる)
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
FC=gfortran \
FPM_FC=gfortran \
PATH=/opt/fortls/bin:${PATH}
# gfortran は Ubuntu 24.04 標準の GNU Fortran(通常は GCC 13 系)を使用。
# python3-venv は fortls 用の仮想環境を分離して管理するために必要。
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash ca-certificates curl g++ gcc gdb git gfortran make \
python3 python3-pip python3-venv libgomp1 \
&& rm -rf /var/lib/apt/lists/*
# fpm の公式 GitHub Release バイナリ。バージョンと SHA256 を固定する。
# ARM64 用ではないため、別アーキテクチャの誤用時はビルドを停止する。
ARG FPM_VERSION=0.13.0
ARG FPM_SHA256=2e50a8649ac4136b9cf6b63c5d1b01adf26c02eedc18f32e16261c0ce655d46d
RUN test "$(uname -m)" = 'x86_64' \
&& curl --fail --location --silent --show-error --retry 3 \
"https://github.com/fortran-lang/fpm/releases/download/v${FPM_VERSION}/fpm-${FPM_VERSION}-linux-x86_64-gcc-12" \
--output /tmp/fpm \
&& echo "${FPM_SHA256} /tmp/fpm" | sha256sum --check - \
&& install -m 0755 /tmp/fpm /usr/local/bin/fpm \
&& rm /tmp/fpm \
&& fpm --version
# Pythonのシステム環境は変更せず、fortlsだけ仮想環境に導入する。
RUN python3 -m venv /opt/fortls \
&& /opt/fortls/bin/python -m pip install --no-cache-dir fortls \
&& /opt/fortls/bin/fortls --version
# Ubuntu 24.04 には UID 1000 の ubuntu ユーザーが用意されている。
# WSL2 側の一般的な UID 1000 と一致させて、生成物の root 所有を防ぐ。
USER ubuntu
WORKDIR /workspace
CMD ["sleep", "infinity"]
まず、apt-getでgfortranとGDBなどをインストールしています。fortlsはPython製のLanguage Serverなので、システムのPython環境に直接入れず、/opt/fortlsの仮想環境に分けています。
fpmはバージョンを0.13.0に固定しました。公式の配布ファイルをダウンロードし、SHA256ハッシュを検証してから配置します。ビルドするたびに異なる版が入ることを避けるためです。
最後にubuntuユーザーで動作させています。通常のUID 1000のWSL2ユーザーであれば、マウントした作業ディレクトリにroot所有の生成物ができる問題を避けやすくなります。WSL2側のUIDが1000ではない場合は、必要に応じてユーザーIDの調整が必要です。
5. Docker Composeを設定する
続いてcompose.yamlを作ります。
compose.yaml(コード全体を表示)
# WSL2 Ubuntu 24.04 の Docker Engine 向け構成。
services:
fortran:
build:
context: .
dockerfile: Dockerfile
image: fortran2018-fpm-vscode:local
platform: linux/amd64
init: true
# GDB が開発コンテナ内プロセスを ptrace できるようにする。
# 開発専用設定: 本番コンテナでの利用は推奨しない。
cap_add:
- SYS_PTRACE
security_opt:
- seccomp:unconfined
working_dir: /workspace
volumes:
- .:/workspace
command: ["sleep", "infinity"]
tty: true
stdin_open: true
ソースコードのディレクトリを/workspaceにマウントします。これにより、VS Codeで編集したファイルをコンテナ内のgfortranやfpmからそのまま利用できます。
SYS_PTRACEとseccomp:unconfinedはGDBでデバッグするための開発用設定です。セキュリティの制限が緩くなるので、本番運用向けコンテナにそのまま流用しないでください。
platform: linux/amd64は、今回固定したfpmのx86_64バイナリに合わせた指定です。
6. fpmのプロジェクトを定義する
Fortran Package Manager(fpm)は、Fortranのビルドや依存パッケージ、テストをまとめて扱うためのツールです。複数のソースファイルを個別のコンパイルコマンドで管理する代わりに、fpm.tomlを使います。
fpm build # ビルド
fpm run # プログラムを実行
fpm test # テストを実行
今回のfpm.tomlは次のとおりです。
fpm.toml(コード全体を表示)
name = "fortran2018_lab"
version = "0.1.0"
license = "MIT"
[build]
auto-executables = false
auto-tests = false
auto-examples = false
[fortran]
implicit-typing = false
implicit-external = false
source-form = "free"
[[executable]]
name = "demo"
source-dir = "app"
main = "main.f90"
[[test]]
name = "test_hydraulics"
source-dir = "test"
main = "test_hydraulics.f90"
実行プログラムはdemo、テストはtest_hydraulicsという名前で登録しています。auto-executablesとauto-testsをfalseにして、実行対象を明示しています。
[fortran]では暗黙の型宣言などを無効にしています。ただし、Fortran 2018としてのコンパイルチェックはfpm.tomlではなく、ビルド時の-std=f2018で指定します。
7. Fortran 2018のサンプルを作る
今回は、水路の幅と水深から流水断面積を求める簡単なモジュールを使います。計算自体は単純ですが、Fortran 2018のassumed-rank引数とSELECT RANKを試せます。
7.1 計算モジュール
src/hydraulics.f90を作成します。
src/hydraulics.f90(コード全体を表示)
!> @brief 矩形水路の断面積と平均水深を扱う計算モジュール。
!!
!! fpm のライブラリソースに配置することを想定している。
!! mean_water_depth の assumed-rank 引数と SELECT RANK は
!! Fortran 2018 で標準化された構文である。
module hydraulics
use, intrinsic :: iso_fortran_env, only: real64
implicit none (type, external)
private
public :: rectangular_area, mean_water_depth
contains
!> @brief 矩形水路の流水断面積 [m^2] を求める。
!! @param[in] width 水路幅 [m]
!! @param[in] depth 水深 [m]
!! @return 流水断面積 [m^2]
!! @note サンプルなので負の入力は 0 に切り詰める。
pure elemental function rectangular_area(width, depth) result(area)
real(real64), intent(in) :: width, depth
real(real64) :: area
area = max(width, 0.0_real64) * max(depth, 0.0_real64)
end function rectangular_area
!> @brief スカラー、1次元配列、2次元配列の平均水深を求める。
!! @param[in] depths 水深 [m]。想定ランクは0、1、2。
!! @return 平均水深 [m]。空配列の場合は 0。
!! @note 3次元以上は仕様対象外として ERROR STOP する。
pure function mean_water_depth(depths) result(mean_depth)
real(real64), intent(in) :: depths(..)
real(real64) :: mean_depth
! SELECT RANK は Fortran 2018 の機能。
select rank (depths)
rank (0)
mean_depth = depths
rank (1)
if (size(depths) == 0) then
mean_depth = 0.0_real64
else
mean_depth = sum(depths) / real(size(depths), real64)
end if
rank (2)
if (size(depths) == 0) then
mean_depth = 0.0_real64
else
mean_depth = sum(depths) / real(size(depths), real64)
end if
rank default
error stop "mean_water_depth: unsupported array rank"
end select
end function mean_water_depth
end module hydraulics
rectangular_areaにはELEMENTALを指定しています。このため、スカラーだけでなく配列にも同じ関数を適用できます。
mean_water_depthの引数にあるdepths(..)は、配列の次元数(ランク)を呼び出し側から受け取る書き方です。SELECT RANKによって、スカラー、1次元配列、2次元配列で処理を切り替えています。
ここでは説明を単純にするため、負の幅や水深は0に切り詰め、空配列の平均は0としています。実際の水理計算では、入力値の妥当性確認やエラーの扱いを別途設計する必要があります。
7.2 実行プログラム
続いてapp/main.f90を作成します。
app/main.f90(コード全体を表示)
!> @brief 開発環境を試すためのサンプル実行プログラム。
program demo
use, intrinsic :: iso_fortran_env, only: real64
use hydraulics, only: rectangular_area, mean_water_depth
implicit none (type, external)
real(real64), parameter :: width(3) = [2.0_real64, 3.0_real64, 4.0_real64]
real(real64), parameter :: depth(3) = [0.5_real64, 1.0_real64, 1.5_real64]
real(real64) :: area(3), avg_depth
! ELEMENTAL 関数なので配列に対しても要素単位で処理できる。
area = rectangular_area(width, depth)
avg_depth = mean_water_depth(depth)
print '(a)', "Fortran 2018 / fpm demo"
print '(a,3f8.3)', "Areas [m^2] :", area
print '(a,f8.3)', "Mean depth [m] :", avg_depth
end program demo
水路幅を2、3、4 m、水深を0.5、1.0、1.5 mとし、3断面の流水断面積と平均水深を計算します。
想定する計算結果は次のとおりです。
Fortran 2018 / fpm demo
Areas [m^2] : 1.000 3.000 6.000
Mean depth [m] : 1.000
7.3 単体テスト
数値計算プログラムは、コードがコンパイルできることと、計算結果が正しいことを別々に確認しておく必要があります。
test/test_hydraulics.f90では、流水断面積に加えて、SELECT RANKの各分岐と空配列の場合をテストします。
test/test_hydraulics.f90(コード全体を表示)
!> @brief 流水断面積と SELECT RANK に対する単体テスト。
!! 期待値が一致しない場合は ERROR STOP でプロセスを失敗させる。
program test_hydraulics
use, intrinsic :: iso_fortran_env, only: real64
use hydraulics, only: rectangular_area, mean_water_depth
implicit none (type, external)
real(real64), parameter :: tol = 1.0e-12_real64
real(real64), parameter :: depth1(3) = [0.5_real64, 1.0_real64, 1.5_real64]
real(real64), parameter :: depth2(2,2) = reshape( &
[1.0_real64, 2.0_real64, 3.0_real64, 4.0_real64], [2,2])
real(real64) :: empty(0)
call assert_near(rectangular_area(2.0_real64, 1.5_real64), &
3.0_real64, "rectangular area")
call assert_near(rectangular_area(2.0_real64, -1.0_real64), &
0.0_real64, "negative depth")
call assert_near(mean_water_depth(2.0_real64), &
2.0_real64, "rank 0")
call assert_near(mean_water_depth(depth1), &
1.0_real64, "rank 1")
call assert_near(mean_water_depth(depth2), &
2.5_real64, "rank 2")
call assert_near(mean_water_depth(empty), &
0.0_real64, "empty array")
print '(a)', "PASS: all hydraulics tests"
contains
!> @brief 浮動小数点数値を許容誤差つきで比較する。
subroutine assert_near(actual, expected, name)
real(real64), intent(in) :: actual, expected
character(len=*), intent(in) :: name
if (abs(actual - expected) > tol) then
print '(3a,2es18.8)', "FAIL: ", name, ": ", actual, expected
error stop 1
end if
end subroutine assert_near
end program test_hydraulics
期待値との差が許容誤差を超えた場合は、ERROR STOPでテストを失敗させています。fpm testは、その終了状態によってテストの失敗を検知できます。
8. 既存のDocker Engineでコンテナを起動する
ここまでのファイルを用意したら、WSL2のターミナルでプロジェクトディレクトリに移動します。
cd ~/work/fortran2018-fpm-vscode
# Docker EngineとComposeを利用できるか確認
docker version
docker compose version
Docker Engineは導入済みなので、新たなインストール作業は不要です。次のコマンドで開発イメージを作成して起動します。
# イメージをビルド
docker compose build
# コンテナを起動
docker compose up -d
# 起動状況を確認
docker compose ps
以降はコンテナの中でFortranをビルドします。まず導入したツールのバージョンを確認します。
docker compose exec fortran gfortran --version
docker compose exec fortran fpm --version
docker compose exec fortran fortls --version
docker compose exec fortran gdb --version
直接コンテナに入って作業することもできます。
docker compose exec fortran bash
# ここからコンテナ内
fpm build
fpm run
fpm test
exit
docker compose execの後に書いたfpmなどのコマンドはコンテナ内で実行されます。WSL2ホスト側にfpmを導入する必要はありません。
9. Fortran 2018としてビルド・実行・テストする
通常のfpm runに加えて、gfortranの標準指定と警告オプションを明示しておきます。
実行
docker compose exec fortran \
fpm run --flag '-std=f2018 -Wall -Wextra -O2'
単体テスト
docker compose exec fortran \
fpm test --profile debug \
--flag '-std=f2018 -Wall -Wextra -Wimplicit-interface -fcheck=all -fbacktrace -g -O0'
テストでは、デバッグ情報-gに加えて-fcheck=allで実行時チェックを有効にしています。特に配列の範囲外参照などの不具合を早めに見つけるためです。
テストが正常に完了すると、サンプルには次のメッセージが表示されます。
PASS: all hydraulics tests
毎回長いオプションを入力する代わりに、scripts/run.shとscripts/test.shにまとめてあります。スクリプトの内容は記事末尾に掲載します。
docker compose exec fortran bash ./scripts/run.sh
docker compose exec fortran bash ./scripts/test.sh
図2:ターミナルでの動作確認(後で挿入)
docker compose psでコンテナを確認し、fpm runの計算結果とfpm testのPASS表示を並べて示します。
10. VS Codeから開発コンテナへ接続する
次に、WindowsのVS Codeからコンテナ内で開発できるようにします。
VS Codeには、次の拡張機能をインストールします。
| 拡張機能 | 用途 |
|---|---|
WSL(ms-vscode-remote.remote-wsl) |
WSL2上のフォルダを開く |
Dev Containers(ms-vscode-remote.remote-containers) |
Dockerコンテナ内に接続する |
Fortran向けのModern Fortran、C/C++、TOML関連の拡張機能は、次のdevcontainer.jsonでコンテナ側へ導入するように設定します。
10.1 devcontainer.json
.devcontainer/devcontainer.json(コード全体を表示)
{
"name": "Fortran 2018 + fpm + fortls",
"dockerComposeFile": "../compose.yaml",
"service": "fortran",
"workspaceFolder": "/workspace",
"remoteUser": "ubuntu",
"shutdownAction": "stopCompose",
"customizations": {
"vscode": {
"extensions": [
"fortran-lang.linter-gfortran",
"ms-vscode.cpptools",
"tamasfe.even-better-toml"
],
"settings": {
"fortran.fortls.path": "/opt/fortls/bin/fortls",
"fortran.linter.compiler": "gfortran",
"fortran.linter.extraArgs": ["-std=f2018", "-Wall", "-Wextra"],
"files.associations": {
"*.f90": "FortranFreeForm",
"*.F90": "FortranFreeForm"
}
}
}
}
}
この設定では、compose.yamlのfortranサービスを開発環境に指定しています。VS Codeの作業フォルダは/workspaceです。
Modern Fortranは、補完や定義ジャンプなどの機能にfortlsを使います。fortran.fortls.pathでコンテナ内の実行ファイルを指定しているため、Windows側にfortlsを入れる必要はありません。
10.2 VS Codeから開く
WSL2のターミナルで次のコマンドを実行します。
cd ~/work/fortran2018-fpm-vscode
code .
VS CodeがWSL接続で開いたら、F1を押してコマンドパレットから次を選びます。
Dev Containers: Reopen in Container
初回はコンテナの準備が行われます。接続後、VS Code左下の接続先表示や統合ターミナルで、コンテナ内の/workspaceを操作していることを確認します。
図3:VS Codeの接続画面(後で挿入)
Dev Containers: Reopen in Containerの選択画面と、接続後にsrc/hydraulics.f90を開いた状態を示します。
11. VS Codeのタスクでビルドとテストを実行する
VS Codeからは、ターミナルで毎回コマンドを入力する代わりにタスクを使えます。.vscode/tasks.jsonにビルド、実行、テスト、バージョン確認を登録します。
.vscode/tasks.json(コード全体を表示)
{
"version": "2.0.0",
"tasks": [
{
"label": "fpm: build (debug)",
"type": "shell",
"command": "bash ./scripts/build-debug.sh",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$gcc"
]
},
{
"label": "fpm: run",
"type": "shell",
"command": "bash ./scripts/run.sh",
"problemMatcher": [
"$gcc"
]
},
{
"label": "fpm: test",
"type": "shell",
"command": "bash ./scripts/test.sh",
"group": {
"kind": "test",
"isDefault": true
},
"problemMatcher": [
"$gcc"
]
},
{
"label": "fpm: versions",
"type": "shell",
"command": "gfortran --version | head -n 1 && fpm --version && fortls --version && gdb --version | head -n 1",
"problemMatcher": []
}
]
}
タスクの操作は次のとおりです。
| 操作 | VS Code |
|---|---|
| デバッグビルド | Ctrl + Shift + B |
| プログラム実行 |
Terminal → Run Task → fpm: run
|
| 単体テスト |
Terminal → Run Task → fpm: test
|
| ツールのバージョン確認 |
Terminal → Run Task → fpm: versions
|
Ctrl + Shift + Bで実行するbuild-debug.shは、デバッグ用のコンパイルオプションを指定し、実行ファイルを.debug/bin/demoに配置します。fpmのビルドディレクトリは設定によって変わるため、GDBが参照する実行ファイルの位置を固定する目的です。
12. VS CodeからGDBでデバッグする
.vscode/launch.jsonでは、デバッグ対象とGDBの設定を指定します。
.vscode/launch.json(コード全体を表示)
{
"version": "0.2.0",
"configurations": [
{
"name": "Fortran: debug demo (GDB)",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/.debug/bin/demo",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb",
"preLaunchTask": "fpm: build (debug)",
"setupCommands": [
{
"description": "GDB pretty printing",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
preLaunchTaskにfpm: build (debug)を指定しているので、F5でデバッグを開始する前にデバッグビルドを実行します。
実際の操作は次の流れです。
-
app/main.f90を開きます。 -
area = rectangular_area(width, depth)の行にブレークポイントを設定します。 -
F5でデバッグを開始します。 - 停止したら
width、depth、areaの内容を確認します。 - ステップ実行し、
mean_water_depthの処理も確認します。
図4:GDBデバッグ画面(後で挿入)
app/main.f90のブレークポイント、変数ウォッチ、ステップ実行中の画面を示します。
このプロジェクトではSYS_PTRACEを使えるようにしていますが、ホスト側の設定やセキュリティポリシーによってはデバッグが制限される場合もあります。
13. よく確認するポイント
コンテナが起動しない場合
WSL2のターミナルでDocker Engineが応答するか確認します。
docker version
docker compose ps
docker composeが見つからない場合は、Composeプラグインが利用できる状態かを確認してください。この記事ではDockerのインストール手順は扱いません。
fpmが見つからない場合
コンテナ内に接続しているかを確認します。fpmはWSL2ホスト側ではなくコンテナ内に導入しています。
docker compose exec fortran which fpm
WSL2側のファイルに書き込めない場合
ホストとコンテナ内のユーザーIDが一致していない可能性があります。WSL2でid -u、コンテナ内でid -uを確認してください。
id -u
docker compose exec fortran id -u
Windowsで編集したシェルスクリプトが動かない場合
改行コードがCRLFになっていないか確認します。LinuxのシェルスクリプトはLFで保存してください。ZIPから展開したファイルに実行権限が付いていない場合でも、今回の手順のようにbash ./scripts/run.shとして起動できます。
ARM64のPCでビルドする場合
今回のDockerfileは、fpmの公式linux-x86_64バイナリを使っています。ARM64ではfpmの入手方法とplatform指定を変更する必要があります。この構成をそのままARM64向けとして扱わないようにしてください。
14. 開発コンテナの終了と再開
作業を終えるときは次のコマンドを使用します。
# コンテナを停止・削除(ソースコードやイメージは残る)
docker compose down
# 次回の起動
docker compose up -d
# Dockerfileなどを変更した場合
docker compose up -d --build
15. 今回確認できたこと
記事中のサンプルコードは、GNU Fortran 14.2.0で-std=f2018を指定して実コンパイルし、実行結果と単体テストを確認しました。また、JSON、TOML、YAMLの構文検証も実施しています。
ただし、この確認環境ではDockerとfpm自体を実行できないため、Dockerイメージのビルド、コンテナ内でのfpm実行、VS CodeからのGDB操作は未確認です。これらは構築先のWSL2環境で、記事中の手順に沿って確認してください。
16. Fortranの計算結果を比較の基準にするには
今回の環境が動いたら、次に考えたいのは実際の数値ソルバーをどう管理するかです。
Juliaの記事では、同じ条件の1D河川計算をFortranとJuliaで実行し、流量・水位・水収支・計算時間を比較する方針を立てました。そのためには、Fortran側の基準結果を作る手順も決めておく必要があります。
16.1 同じ入力条件を使う
最初は単純な河道で十分です。例えば、一本の河道、少ない計算点、一定の時間刻み、明確な上下流端境界を持つケースから始めます。最初から合流や分岐を増やすと、結果が違ったときに原因を切り分けにくくなります。
Fortran版とJulia版では、少なくとも次の条件をそろえます。
| 項目 | そろえる内容 |
|---|---|
| 計算格子 | 断面数、区間長、標高など |
| 初期条件 | 初期水位、初期流量など |
| 境界条件 | 上流流量、下流水位・流出条件など |
| 時間条件 | 計算時間、時間刻み、出力間隔 |
| 物理パラメータ | 粗度係数など |
| 数値条件 | 計算方法、精度、収束・安定化条件 |
例えば単一河道の試験ケースなら、後で次のようなディレクトリを追加できます。
cases/
└── simple_channel/
├── input/ # 共通の入力条件
├── expected/ # Fortran基準結果
└── comparison/ # Julia版との比較結果
これは今後の構成案です。この記事のDocker環境に、Juliaや比較ツールを一緒にインストールするわけではありません。Juliaは別の開発環境として管理し、入出力データで比較する形を想定しています。
16.2 計算結果の比較項目を決める
一致を判断するときは、プログラムの標準出力が同じであることだけでは不十分です。特に河川計算では、時系列と保存則を分けて確認したいところです。
| 確認項目 | 比較の内容 |
|---|---|
| 流量 | 時系列、ピーク流量、ピーク時刻 |
| 水位 | 時系列、最大水位 |
| 水収支 | 流入量・流出量・貯留量変化の整合 |
| 境界条件 | 指定した条件が計算中に反映されているか |
| 数値安定性 |
NaN、Inf、異常振動の有無 |
| 再現性 | 同じ入力に対して同じ条件で再計算できるか |
| 計算時間 | 計測区間とビルド条件をそろえた処理時間 |
水収支を確認する場合、一般的な形は次のようになります。
水収支残差 = 総流入量 − 総流出量 − 貯留量の変化
ただし、実際には計算領域の定義、雨量・横流入・越流などの取り扱いによって、残差の評価に含める項目が変わります。ソルバーの収支式に合わせて定義し、許容誤差を決めてから判定します。
また、FortranとJuliaでは、浮動小数点演算の順序や最適化の違いによって小さな差が出る場合があります。ファイルの文字列が完全一致するかどうかではなく、絶対誤差と相対誤差、そして物理的に意味のある指標で比較する必要があります。
16.3 計算時の条件を残す
Dockerで環境をまとめていても、コンパイラのバージョンやオプションまで何も考えなくてよいわけではありません。比較結果を残す際は、次のような情報も一緒に記録します。
# Fortranコンテナ内でコンパイラ・ビルドツールの版を確認
# (ホスト側で実行する場合は docker compose exec を付ける)
gfortran --version
fpm --version
さらに、デバッグ用の-O0 -g -fcheck=allと、実行速度を確認するための-O2などのビルド条件は分けておきます。性能比較では、コンパイラの版、最適化オプション、実数精度、OpenMPの有無、CPUスレッド数、測定方法を記録する方が後で見直しやすくなります。
JuliaはJITコンパイルを使うため、Julia版の性能を測る際は初回のコンパイル時間と、ウォームアップ後の計算時間も分けます。ここは前の記事でも意識した点です。
図5:Fortran版を基準にした比較手順(後で挿入)
共通の入力条件からFortran版とJulia版を別々に実行し、流量・水位・水収支・計算時間を比較する構成を示します。Fortran側ではDocker、fpm、gfortran、単体テストによって実行条件を管理することも図に入れます。
17. 次に進めたいこと
今回の矩形水路サンプルでは、SELECT RANKを使ったモジュールと単体テストを試すところまでです。ここから実際のソルバーへ広げるなら、まずは単一河道の1D計算をfpmプロジェクトに入れて、境界条件と水収支を確認するところから始めたいと思います。
その後、Node–Reach構造、横流入、降雨流出へ少しずつ進める形がよさそうです。機能を追加するたびにFortran版のテストと結果を残しておけば、Juliaなど別の言語で実装するときにも比較しやすくなります。
Fortran 2018の開発環境
↓
fpmでビルド・単体テスト
↓
単一河道1Dの基準ケース
↓
境界条件・水収支を確認
↓
Julia版を同じ入力で実行
↓
計算結果と計算時間を比較
ここでは、FortranからJuliaへ置き換えること自体を目標にはしません。どちらの実装についても、計算結果を説明できる状態にすることを先に考えます。
おわりに
今回は、Dockerを導入済みのWSL2環境を使い、Fortran 2018、fpm、VS Codeを組み合わせた開発環境を整理しました。ソースコードをsrc/、実行プログラムをapp/、テストをtest/に分け、コンテナ内でビルドとテストを行う構成です。
小さなプログラムではfpmの便利さを実感しにくいかもしれません。ただ、計算モジュールが増えてきたときに、コンパイル手順やテストを一つにまとめられるのは助かります。コンテナを作り直しても、同じ設定から作業を再開しやすくなります。
前の記事でJuliaを試してみようと考えたのも、Fortranをやめたいからではありません。既存のFortranの計算結果を大切にしながら、新しい実装の良いところを確かめたいというのが出発点です。そのためにも、まずFortran側の開発環境と検証手順を整えておきます。
付録:実行用シェルスクリプト
本文で使用したスクリプトをまとめて掲載します。scripts/ディレクトリにそれぞれ保存してください。
scripts/run.sh
#!/usr/bin/env bash
# Fortran 2018 に沿ったコンパイラオプションを指定して実行する。
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."
fpm run --flag '-std=f2018 -Wall -Wextra -O2'
scripts/test.sh
#!/usr/bin/env bash
# 実行時チェックを有効にして、fpm の全テストを実行する。
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."
fpm test --profile debug --flag '-std=f2018 -Wall -Wextra -Wimplicit-interface -fcheck=all -fbacktrace -g -O0'
scripts/build-debug.sh(コード全体を表示)
#!/usr/bin/env bash
# fpm のデバッグビルド結果を、GDB が参照する固定パスにコピーする。
# fpm はビルド設定で出力先ディレクトリ名が変わるため、直接参照しない。
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."
FLAGS='-std=f2018 -Wall -Wextra -Wimplicit-interface -fcheck=all -fbacktrace -g -O0'
fpm build --profile debug --flag "$FLAGS"
# fpm の出力階層から最新の実行可能な demo を見つける。
# shellcheck disable=SC2016
source_bin="$(find build -type f -path '*/app/demo' -executable -printf '%T@ %p\n' \
| sort -nr | sed -n '1s/^[^ ]* //p')"
if [[ -z "$source_bin" ]]; then
echo 'ERROR: fpm build output (app/demo) was not found.' >&2
exit 1
fi
mkdir -p .debug/bin
cp -f -- "$source_bin" .debug/bin/demo
chmod u+x .debug/bin/demo
printf 'Debugger target: %s/.debug/bin/demo\n' "$PWD"
scripts/verify.sh(コード全体を表示)
#!/usr/bin/env bash
# Docker 内のコンパイラ・fpm・Language Server とユニットテストを確認する。
# 実行失敗を隠さず、最初のエラーで停止する。
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."
command -v gfortran
command -v fpm
command -v fortls
command -v gdb
gfortran --version | head -n 1
fpm --version
fortls --version
gdb --version | head -n 1
bash ./scripts/test.sh
参考資料
- Fortranの次を考える ― Juliaで始める数値シミュレーション基盤(Qiita)
- Fortran Package Manager(fpm)
- fpm公式リポジトリ
- fpm 0.13.0 配布ページ
- Visual Studio Code:Developing in WSL
- Visual Studio Code:Dev Containers
- GNU Fortranのコンパイラオプション
