make入門#

簡単にmakeの基本について説明しました。この章では、より大規模なプロジェクトでmakeを拡張するためのアイデアと戦略を紹介します。

makeの詳細に入る前に、いくつかの点を考慮してください。

  1. makeはUnixツールであり、非Unixプラットフォームへの移植時に問題が発生する可能性があります。ただし、さまざまなバージョンのmakeがあり、すべてが使用したい機能をサポートしているとは限りません。

  2. makeを使用するとビルドプロセスを完全に制御できますが、ビルドプロセスの全責任を負うことになり、プロジェクトのあらゆる詳細についてルールを指定する必要があります。ソースコードの開発ではなく、Makefileの記述と保守に多くの時間を費やすことになるかもしれません。

  3. Makefileを使用できますが、makeに慣れていないプロジェクトの他の開発者を考えてみましょう。彼らがMakefileを学習するのにどれだけの時間をかけることを期待し、デバッグや機能の追加を行うことができるでしょうか?

  4. 純粋な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つは、スクリプトの開始前と終了後にそれぞれ実行される特別なパターンBEGINENDを使用しています。スクリプトの開始前に、Fortranソースコードを処理しているため、スクリプトを大文字と小文字を区別しないようにします。また、現在解析しているファイルを確認し、複数のファイルを一度に処理できるように、特別な変数FILENAMEを使用します。

定義された3つのパターンを使用して、最初のスペース区切りのエントリとしてmoduleuseincludeステートメントを検索しています。使用されているパターンでは、すべての有効な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)