make入門#
簡単にmake
の基本について説明しました。この章では、より大規模なプロジェクトでmake
を拡張するためのアイデアと戦略を紹介します。
make
の詳細に入る前に、いくつかの点を考慮してください。
make
はUnixツールであり、非Unixプラットフォームへの移植時に問題が発生する可能性があります。ただし、さまざまなバージョンのmake
があり、すべてが使用したい機能をサポートしているとは限りません。make
を使用するとビルドプロセスを完全に制御できますが、ビルドプロセスの全責任を負うことになり、プロジェクトのあらゆる詳細についてルールを指定する必要があります。ソースコードの開発ではなく、Makefile
の記述と保守に多くの時間を費やすことになるかもしれません。Makefile
を使用できますが、make
に慣れていないプロジェクトの他の開発者を考えてみましょう。彼らがMakefile
を学習するのにどれだけの時間をかけることを期待し、デバッグや機能の追加を行うことができるでしょうか?純粋な
make
はスケールしません。すぐに、Makefile
を動的または静的に生成するための補助プログラムを追加することになります。これにより、依存関係と潜在的なエラーの原因が導入されます。これらのツールのテストとドキュメント作成に必要な労力は過小評価すべきではありません。
make
がニーズに適していると考える場合は、Makefile
の記述を開始できます。このコースでは、パッケージインデックスからの現実世界の例を使用します(執筆時点では、make
以外のビルドシステムを使用しています)。このガイドは、make
を書くための一般的な推奨スタイルを示すとともに、有用で興味深い機能のデモンストレーションとしても機能します。
ヒント
プロジェクトのビルドにmake
が適していない場合でも、ファイルによって定義されたワークフローを自動化するためのツールです。別のコンテキストでその力を活用できるかもしれません。
はじめに#
このパートでは、Fortran CSVモジュール(v1.2.0)を使用します。目標は、このプロジェクトを静的ライブラリにコンパイルするためのMakefile
を作成することです。リポジトリのクローンを作成することから始めます。
git clone https://github.com/jacobwilliams/fortran-csv-module -b 1.2.0
cd fortran-csv-module
ヒント
このパートでは、できるだけ再現性を高めるために、タグ1.2.0
のコードを使用します。最新バージョンまたは別のプロジェクトを使用しても構いません。
このプロジェクトはFoBiSをビルドシステムとして使用しており、build.sh
でFoBiSで使用されるオプションを確認できます。このプロジェクトのMakefile
を作成しようとしています。まず、ディレクトリ構造とソースファイルを確認します。
.
├── build.sh
├── files
│ ├── test_2_columns.csv
│ └── test.csv
├── fortran-csv-module.md
├── LICENSE
├── README.md
└── src
├── csv_kinds.f90
├── csv_module.F90
├── csv_parameters.f90
├── csv_utilities.f90
└── tests
├── csv_read_test.f90
├── csv_test.f90
└── csv_write_test.f90
7つの異なるFortranソースファイルがあります。src
内の4つはコンパイルして静的ライブラリに追加する必要がありますが、src/tests
内の3つは、この静的ライブラリに依存する個々のプログラムを含んでいます。
簡単なMakefile
を作成することから始めます。
# Disable the default rules
MAKEFLAGS += --no-builtin-rules --no-builtin-variables
# Project name
NAME := csv
# Configuration settings
FC := gfortran
AR := ar rcs
LD := $(FC)
RM := rm -f
# List of all source files
SRCS := src/csv_kinds.f90 \
src/csv_module.F90 \
src/csv_parameters.f90 \
src/csv_utilities.f90
TEST_SRCS := src/tests/csv_read_test.f90 \
src/tests/csv_test.f90 \
src/tests/csv_write_test.f90
# Create lists of the build artefacts in this project
OBJS := $(addsuffix .o, $(SRCS))
TEST_OBJS := $(addsuffix .o, $(TEST_SRCS))
LIB := $(patsubst %, lib%.a, $(NAME))
TEST_EXE := $(patsubst %.f90, %.exe, $(TEST_SRCS))
# Declare all public targets
.PHONY: all clean
all: $(LIB) $(TEST_EXE)
# Create the static library from the object files
$(LIB): $(OBJS)
$(AR) $@ $^
# Link the test executables
$(TEST_EXE): %.exe: %.f90.o $(LIB)
$(LD) -o $@ $^
# Create object files from Fortran source
$(OBJS) $(TEST_OBJS): %.o: %
$(FC) -c -o $@ $<
# Define all module interdependencies
csv_kinds.mod := src/csv_kinds.f90.o
csv_module.mod := src/csv_module.F90.o
csv_parameters.mod := src/csv_parameters.f90.o
csv_utilities.mod := src/csv_utilities.f90.o
src/csv_module.F90.o: $(csv_utilities.mod)
src/csv_module.F90.o: $(csv_kinds.mod)
src/csv_module.F90.o: $(csv_parameters.mod)
src/csv_parameters.f90.o: $(csv_kinds.mod)
src/csv_utilities.f90.o: $(csv_kinds.mod)
src/csv_utilities.f90.o: $(csv_parameters.mod)
src/tests/csv_read_test.f90.o: $(csv_module.mod)
src/tests/csv_test.f90.o: $(csv_module.mod)
src/tests/csv_write_test.f90.o: $(csv_module.mod)
# Cleanup, filter to avoid removing source code by accident
clean:
$(RM) $(filter %.o, $(OBJS) $(TEST_OBJS)) $(filter %.exe, $(TEST_EXE)) $(LIB) $(wildcard *.mod)
make
を呼び出すと、期待どおりに静的ライブラリとテスト実行ファイルがビルドされます。
gfortran -c -o src/csv_kinds.f90.o src/csv_kinds.f90
gfortran -c -o src/csv_parameters.f90.o src/csv_parameters.f90
gfortran -c -o src/csv_utilities.f90.o src/csv_utilities.f90
gfortran -c -o src/csv_module.F90.o src/csv_module.F90
ar rcs libcsv.a src/csv_kinds.f90.o src/csv_module.F90.o src/csv_parameters.f90.o src/csv_utilities.f90.o
gfortran -c -o src/tests/csv_read_test.f90.o src/tests/csv_read_test.f90
gfortran -o src/tests/csv_read_test.exe src/tests/csv_read_test.f90.o libcsv.a
gfortran -c -o src/tests/csv_test.f90.o src/tests/csv_test.f90
gfortran -o src/tests/csv_test.exe src/tests/csv_test.f90.o libcsv.a
gfortran -c -o src/tests/csv_write_test.f90.o src/tests/csv_write_test.f90
gfortran -o src/tests/csv_write_test.exe src/tests/csv_write_test.f90.o libcsv.a
いくつか注意すべき点があります。make
ビルドは通常、ビルドアーティファクトとソースコードを混在させます。ビルドディレクトリを実装することに余分な労力を費やさない限りです。また、現時点ではソースファイルと依存関係が明示的に指定されているため、このような単純なプロジェクトでもさらにいくつかの行が追加されます。
自動生成された依存関係#
Fortranのmake
の主な欠点は、モジュールの依存関係を決定する機能がないことです。これは通常、手動で追加するか、外部ツールを使用してソースコードを自動的にスキャンすることによって解決されます。一部のコンパイラ(Intel Fortranコンパイラなど)は、make
形式で依存関係を生成することもできます。
依存関係の生成について詳しく説明する前に、依存関係の問題に対する堅牢なアプローチの概念の概要を説明します。まず、各ソースファイルが(module
)を提供するか、(use
)モジュールを必要とする場合、すべてのソースファイルを独立して処理できるアプローチが必要です。依存関係を生成する際には、ソースファイルとそのモジュールファイルの名前のみがわかっており、オブジェクトファイルの名前に関する情報は必要ありません。
上記の依存関係のセクションを確認すると、すべての依存関係がソースファイルではなくオブジェクトファイル間に定義されていることに気付くでしょう。これを変更するために、ソースファイルとその対応するオブジェクトファイルのマッピングを生成できます。
# Define a map from each file name to its object file
obj = $(src).o
$(foreach src, $(SRCS) $(TEST_SRCS), $(eval $(src) := $(obj)))
obj
が再帰的に展開される変数として宣言されていることに注意してください。事実上、このメカニズムを使用してmake
で関数を定義しています。foreach
関数は、すべてのソースファイルをループ処理することを可能にし、eval
関数はmake
ステートメントを生成し、このMakefile
に対して評価することを可能にします。
ソースファイル名を使用してオブジェクトファイル名を定義できるようになったため、依存関係をそれに応じて調整します。
# Define all module interdependencies
csv_kinds.mod := $(src/csv_kinds.f90)
csv_module.mod := $(src/csv_module.F90)
csv_parameters.mod := $(src/csv_parameters.f90)
csv_utilities.mod := $(src/csv_utilities.f90)
$(src/csv_module.F90): $(csv_utilities.mod)
$(src/csv_module.F90): $(csv_kinds.mod)
$(src/csv_module.F90): $(csv_parameters.mod)
$(src/csv_parameters.f90): $(csv_kinds.mod)
$(src/csv_utilities.f90): $(csv_kinds.mod)
$(src/csv_utilities.f90): $(csv_parameters.mod)
$(src/tests/csv_read_test.f90): $(csv_module.mod)
$(src/tests/csv_test.f90): $(csv_module.mod)
$(src/tests/csv_write_test.f90): $(csv_module.mod)
マッピングの作成という同じ戦略は、モジュールファイルにもすでに使用されています。それがオブジェクトファイルにも拡張されただけです。
それぞれの依存関係マップを自動的に生成するために、ここではawk
スクリプトを使用します。
#!/usr/bin/awk -f
BEGIN {
# Fortran is case insensitive, disable case sensitivity for matching
IGNORECASE = 1
}
# Match a module statement
# - the first argument ($1) should be the whole word module
# - the second argument ($2) should be a valid module name
$1 ~ /^module$/ &&
$2 ~ /^[a-zA-Z][a-zA-Z0-9_]*$/ {
# count module names per file to avoid having modules twice in our list
if (modc[FILENAME,$2]++ == 0) {
# add to the module list, the generated module name is expected
# to be lowercase, the FILENAME is the current source file
mod[++im] = sprintf("%s.mod = $(%s)", tolower($2), FILENAME)
}
}
# Match a use statement
# - the first argument ($1) should be the whole word use
# - the second argument ($2) should be a valid module name
$1 ~ /^use$/ &&
$2 ~ /^[a-zA-Z][a-zA-Z0-9_]*,?$/ {
# Remove a trailing comma from an optional only statement
gsub(/,/, "", $2)
# count used module names per file to avoid using modules twice in our list
if (usec[FILENAME,$2]++ == 0) {
# add to the used modules, the generated module name is expected
# to be lowercase, the FILENAME is the current source file
use[++iu] = sprintf("$(%s) += $(%s.mod)", FILENAME, tolower($2))
}
}
# Match an include statement
# - the first argument ($1) should be the whole word include
# - the second argument ($2) can be everything, as long as delimited by quotes
$1 ~ /^(#:?)?include$/ &&
$2 ~ /^["'].+["']$/ {
# Remove quotes from the included file name
gsub(/'|"/, "", $2)
# count included files per file to avoid having duplicates in our list
if (incc[FILENAME,$2]++ == 0) {
# Add the included file to our list, this might be case-sensitive
inc[++ii] = sprintf("$(%s) += %s", FILENAME, $2)
}
}
# Finally, produce the output for make, loop over all modules, use statements
# and include statements, empty lists are ignored in awk
END {
for (i in mod) print mod[i]
for (i in use) print use[i]
for (i in inc) print inc[i]
}
このスクリプトは、解析するソースコードについていくつかの仮定をしているため、すべてのFortranコードで動作するとは限りません(特に、サブモジュールはサポートされていません)。しかし、この例では十分です。
ヒント
awkの使用
awk
スクリプトは、テキストストリーム処理のために設計されており、Cに似た構文を使用するawk
言語を使用しています。awk
では、特定のイベント(たとえば、通常は正規表現で表現される特定のパターンに一致する行)で評価されるグループを定義できます。
このawk
スクリプトは5つのグループを定義しており、そのうち2つは、スクリプトの開始前と終了後にそれぞれ実行される特別なパターンBEGIN
とEND
を使用しています。スクリプトの開始前に、Fortranソースコードを処理しているため、スクリプトを大文字と小文字を区別しないようにします。また、現在解析しているファイルを確認し、複数のファイルを一度に処理できるように、特別な変数FILENAME
を使用します。
定義された3つのパターンを使用して、最初のスペース区切りのエントリとしてmodule
、use
、include
ステートメントを検索しています。使用されているパターンでは、すべての有効なFortranコードが正しく解析されるとは限りません。失敗する例としては、次のようなものがあります。
use::my_module,only:proc
awk
スクリプトで解析できるようにするには、BEGIN
グループの直後に別のグループを追加し、処理中にストリームを変更します。
{
gsub(/,|:/, " ")
}
理論的には、継続行やその他の困難に対処するには、完全なFortranパーサーが必要です。これはawk
で実装できますが、最終的には巨大なスクリプトが必要になります。
また、依存関係の生成は高速である必要があることに注意してください。高価なパーサーは、大規模なコードベースの依存関係を生成する際に大きなオーバーヘッドを発生させる可能性があります。妥当な仮定を行うことで、この手順を簡素化および高速化できますが、ビルドツールにエラーソースが導入されることになります。
スクリプトを実行可能にしてください(chmod +x gen-deps.awk
)。そして、./gen-deps.awk $(find src -name '*.[fF]90')
でテストしてください。以下の様な出力が表示されるはずです。
csv_utilities.mod = $(src/csv_utilities.f90)
csv_kinds.mod = $(src/csv_kinds.f90)
csv_parameters.mod = $(src/csv_parameters.f90)
csv_module.mod = $(src/csv_module.F90)
$(src/csv_utilities.f90) += $(csv_kinds.mod)
$(src/csv_utilities.f90) += $(csv_parameters.mod)
$(src/csv_kinds.f90) += $(iso_fortran_env.mod)
$(src/tests/csv_read_test.f90) += $(csv_module.mod)
$(src/tests/csv_read_test.f90) += $(iso_fortran_env.mod)
$(src/tests/csv_write_test.f90) += $(csv_module.mod)
$(src/tests/csv_write_test.f90) += $(iso_fortran_env.mod)
$(src/tests/csv_test.f90) += $(csv_module.mod)
$(src/tests/csv_test.f90) += $(iso_fortran_env.mod)
$(src/csv_parameters.f90) += $(csv_kinds.mod)
$(src/csv_module.F90) += $(csv_utilities.mod)
$(src/csv_module.F90) += $(csv_kinds.mod)
$(src/csv_module.F90) += $(csv_parameters.mod)
$(src/csv_module.F90) += $(iso_fortran_env.mod)
スクリプトの出力は、再帰的に展開された変数を使用し、依存関係はまだ定義されていません。変数の順序が異なる宣言が必要となる可能性があり、誤ってターゲットを作成したくないためです。上記の handwritten スニペットと同じ情報が含まれていることを確認できます。iso_fortran_env.mod
への追加の依存関係が例外です。これは未定義の変数であるため、空文字列に展開され、それ以上の依存関係は導入されません。
これで、依存関係の生成を自動化するために、この部分をMakefile
に含めることができます。
# Disable the default rules
MAKEFLAGS += --no-builtin-rules --no-builtin-variables
# Project name
NAME := csv
# Configuration settings
FC := gfortran
AR := ar rcs
LD := $(FC)
RM := rm -f
GD := ./gen-deps.awk
# List of all source files
SRCS := src/csv_kinds.f90 \
src/csv_module.F90 \
src/csv_parameters.f90 \
src/csv_utilities.f90
TEST_SRCS := src/tests/csv_read_test.f90 \
src/tests/csv_test.f90 \
src/tests/csv_write_test.f90
# Add source and tests directories to search paths
vpath % .: src
vpath % .: src/tests
# Define a map from each file name to its object file
obj = $(src).o
$(foreach src, $(SRCS) $(TEST_SRCS), $(eval $(src) := $(obj)))
# Create lists of the build artefacts in this project
OBJS := $(addsuffix .o, $(SRCS))
DEPS := $(addsuffix .d, $(SRCS))
TEST_OBJS := $(addsuffix .o, $(TEST_SRCS))
TEST_DEPS := $(addsuffix .d, $(TEST_SRCS))
LIB := $(patsubst %, lib%.a, $(NAME))
TEST_EXE := $(patsubst %.f90, %.exe, $(TEST_SRCS))
# Declare all public targets
.PHONY: all clean
all: $(LIB) $(TEST_EXE)
# Create the static library from the object files
$(LIB): $(OBJS)
$(AR) $@ $^
# Link the test executables
$(TEST_EXE): %.exe: %.f90.o $(LIB)
$(LD) -o $@ $^
# Create object files from Fortran source
$(OBJS) $(TEST_OBJS): %.o: % | %.d
$(FC) -c -o $@ $<
# Process the Fortran source for module dependencies
$(DEPS) $(TEST_DEPS): %.d: %
$(GD) $< > $@
# Define all module interdependencies
include $(DEPS) $(TEST_DEPS)
$(foreach dep, $(OBJS) $(TEST_OBJS), $(eval $(dep): $($(dep))))
# Cleanup, filter to avoid removing source code by accident
clean:
$(RM) $(filter %.o, $(OBJS) $(TEST_OBJS)) $(filter %.d, $(DEPS) $(TEST_DEPS)) $(filter %.exe, $(TEST_EXE)) $(LIB) $(wildcard *.mod)
ここでは、各ソースファイルごとに追加の依存ファイルが生成され、メインのMakefile
に含まれます。また、依存ファイルはオブジェクトファイルへの依存関係として追加され、オブジェクトがコンパイルされる前に依存ファイルが生成されるようにします。依存関係内のパイプ文字は、タイムスタンプの依存関係なしにルールの順序を定義します。これは、依存関係が再生成され、変更されていない場合にオブジェクトファイルを再コンパイルする必要がないためです。
再び、eval
関数を用いて、全てのオブジェクトファイルに対するforeach
ループ内で依存関係を生成します。dep
を一度展開するとオブジェクトファイル名になり、もう一度展開すると、そのオブジェクトファイルが依存するオブジェクトファイルになるという、オブジェクトファイルと依存ファイル間のマッピングを作成していることに注意してください。
make
でプロジェクトをビルドすると、以下のような出力が表示されます。
./gen-deps.awk src/csv_utilities.f90 > src/csv_utilities.f90.d
./gen-deps.awk src/csv_parameters.f90 > src/csv_parameters.f90.d
./gen-deps.awk src/csv_module.F90 > src/csv_module.F90.d
./gen-deps.awk src/csv_kinds.f90 > src/csv_kinds.f90.d
gfortran -c -o src/csv_kinds.f90.o src/csv_kinds.f90
gfortran -c -o src/csv_parameters.f90.o src/csv_parameters.f90
gfortran -c -o src/csv_utilities.f90.o src/csv_utilities.f90
gfortran -c -o src/csv_module.F90.o src/csv_module.F90
ar rcs libcsv.a src/csv_kinds.f90.o src/csv_module.F90.o src/csv_parameters.f90.o src/csv_utilities.f90.o
./gen-deps.awk src/tests/csv_read_test.f90 > src/tests/csv_read_test.f90.d
gfortran -c -o src/tests/csv_read_test.f90.o src/tests/csv_read_test.f90
gfortran -o src/tests/csv_read_test.exe src/tests/csv_read_test.f90.o libcsv.a
./gen-deps.awk src/tests/csv_test.f90 > src/tests/csv_test.f90.d
gfortran -c -o src/tests/csv_test.f90.o src/tests/csv_test.f90
gfortran -o src/tests/csv_test.exe src/tests/csv_test.f90.o libcsv.a
./gen-deps.awk src/tests/csv_write_test.f90 > src/tests/csv_write_test.f90.d
gfortran -c -o src/tests/csv_write_test.f90.o src/tests/csv_write_test.f90
gfortran -o src/tests/csv_write_test.exe src/tests/csv_write_test.f90.o libcsv.a
依存ファイルが生成されると、make
はソースが変更された場合にのみそれらを更新し、毎回呼び出すたびに再構築する必要はありません。
ヒント
正しい依存関係があれば、Makefile
の並列実行を活用できます。-j
フラグを使用して、複数のmake
プロセスを作成するだけです。
依存関係は自動的に生成できるようになったため、ソースファイルを明示的に指定する必要はありません。wildcard
関数を用いて動的に決定できます。
# List of all source files
SRCS := $(wildcard src/*.f90) \
$(wildcard src/*.F90)
TEST_SRCS := $(wildcard src/tests/*.f90)