Narzędzia do kompilacji#

Kompilowanie twojego projektu Fortran ręcznie może być dość skomplikowane w zależności od liczby plików źródłowych i współzależności w module. Obsługa różnych kompilatorów i programów łączących lub różnych platform może stawać się coraz bardziej skomplikowana dopóki nie zostaną użyte odpowiednie narzędzia do automatycznego wykonania tych zadań.

W zależności od rozmiaru twojego projektu i jego celu użyte mogą być różne narzędzia do automatyzacji kompilacji.

Na początek, twoje zintegrowane środowisko programistyczne prawdopodobnie zapewnia sposób na skompilowanie twojego programu. Popularnym międzyplatformowym narzędziem jest Microsoft Visual Studio Code, ale istnieją również inne, takie jak Atom, Eclipse Photran oraz Code::Blocks. Oferują one graficzny interfejs użytkownika, ale często są bardzo specyficzne dla kompilatora i platformy.

Dla mniejszych projektów powszechnym wyborem kompilacji programu jest system regułowy make. Bazując na zdefiniowanych zasadach może wykonywać takie działania jak (re)kompilowanie plików obiektów z zaktualizowanych plików, tworzyć biblioteki oraz łączyć pliki wykonywalne. Aby użyć make w twoim projekcie musisz zakodować te reguły w Makefile, który definiuje współzależności finałowego programu, pośrednich obiektów oraz bibliotek i plików źródłowych. Krótkie wprowadzenie do tematu znajdziesz w przewodniku po make.

Narzędzia konserwacyjne takie jak autotools i CMake mogą generować Makefiles lub pliki Visual Studio za pośrednictwem opisów wysokiego poziomu. Odbiegają one od specyfiki kompilatora i platformy.

To, na które z tych narzędzi będzie najlepsze dla twojego projektu zależy od wielu czynników. Wybierz narzędzie do kompilacji, z którym będzie ci się komfortowo pracować, nie powinno przeszkadzać ci podczas programowania. Poświęcanie większej ilości czasu na walkę z narzędziami do kompilacji niż na faktycznemu tworzeniu programu może szybko stać się frustrujące.

Zwróć również uwagę na dostępność twoich narzędzi do kompilacji. Jeśli są ograniczone do specyficznego zintegrowanego środowiska, czy wszyscy deweloperzy w twoim projekcie będą mieć dostęp do nich? Jeśli używasz konkretnego systemu kompilacji, czy działa on na wszystkich platformach, na które tworzysz program? Jak duża jest bariera wejścia twoich narzędzi do kompilacji? Rozważ również krzywą uczenia się dla narzędzi do kompilacji, idealne narzędzie do kompilacji będzie bezużyteczne jeśli najpierw musisz nauczyć się skomplikowanego języka programowania, aby dodać nowy plik źródłowy. Na koniec, zobacz czego używają inne projekty, takie, których używasz jako zależności i te które używają (lub będą używać) twojego projektu jako zależności.

Korzystanie z make jako narzędzia do kompilacji#

Najbardziej znanym i powszechnie używanym systemem kompilacji jest system make. Wykonuje działania na podstawie zasad zdefiniowanych w pliku konfiguracyjnym nazywanym Makefile lub makefile, który zazwyczaj prowadzi do skompilowania programu z dostarczonego kodu źródłowego.

Wskazówka

Aby zobaczyć szczegółowy poradnik do make odwiedź jego stronę informacyjną. Wersja online jego strony informacyjnej jest dostępna tutaj.

Zaczniemy od podstaw z początkowego folderu źródłowego. Utwórz i otwórz plik Makefile, zaczniemy od prostej zasady all:

all:
	echo "$@"

Po zapisaniu Makefile otwórz go poprzez wykonanie make w tym samym folderze. Powinieneś zobaczyć następujący wynik:

echo "all"
all

Po pierwsze zauważmy, że make zastępuje nazwę reguły $@. Po drugie, zauważmy, że make zawsze drukuje polecenie, które wykonuje. Na koniec widzimy rezultat wykonania echo "all".

Informacja

Zgodnie z konwencją punkt wejścia naszego pliku Makefile powinien nazywać się all, ale możesz wybrać dowolną nazwę.

Informacja

Jeśli twój edytor tekstu działa poprawnie to nie powinieneś tego zauważyć, ale musisz wciąć zawartość znaku reguły za pomocą tabulatora. W przypadku problemów z uruchomieniem pliku Makefile możesz zobaczyć taki błąd

Makefile:2: *** missing separator.  Stop.

Wcięcie jest prawdopodobnie niepoprawne. W tym przypadku zamień wcięcie w drugiej linii znakiem tabulacji.

Teraz chcemy stworzyć bardziej skomplikowane zasady, więc dodajemy kolejną regułę:

PROG := my_prog

all: $(PROG)
	echo "$@ depends on $^"

$(PROG):
	echo "$@"

Zauważmy jak deklarujemy zmienne w make. Zmienne lokalne powinny być zawsze zadeklarowane poprzez :=. Aby zdobyć dostęp do zawartości zmiennej używamy $(...), nazwa zmienniej musi być zawarta w nawiasach.

Informacja

Deklaracja zmiennych zazwyczaj odbywa się za pomocą :=, jednak make pozwala zarówno na zmienne =, jak i na zmienne rekursywnie rozbudowane. Zwykle, pierwszy typ deklaracji jest bardziej pożądany, ponieważ jest bardziej przewidywalny i nie mają narzutu czasu wykonania wynikającego z rekurencyjnej rozbudowy.

Wprowadziliśmy zależność dla reguły all, a mianowicie zawartość zmiennej PROG, zmodyfikowaliśmy również wydruk, a teraz chcemy zobaczyć wszystkie zależności tej reguły, które są przechowywane w zmiennej $^. Teraz dla nowej reguły, której nadajemy nazwę po wartości zmiennej PROG, która robi to samo, co zrobiliśmy wcześniej dla reguły all. Zwróćmy uwagę, jak wartość $@ jest zależna od reguły, w której jest używana.

Ponownie sprawdzamy poprzez wykonanie make i powinniśmy zobaczyć:

echo "my_prog"
my_prog
echo "all depends on my_prog"
all depends on my_prog

Zależność została poprawnie rozwiązana i określona zanim jakakolwiek akcja na reguły all została wykonana. Teraz wykonajmy tylko drugą regułę: wpiszmy make my_prog, a zobaczymy tylko dwa pierwsze wersy w terminalu.

Następnym krokiem jest wykonanie prawdziwych akcji z make, weźmy kod źródłowy z poprzedniego rozdziału i dodajmy nowe reguły do naszego Makefile:

OBJS := tabulate.o functions.o
PROG := my_prog

all: $(PROG)

$(PROG): $(OBJS)
	gfortran -o $@ $^

$(OBJS): %.o: %.f90
	gfortran -c -o $@ $<

Definiujemy OBJS, które jest skrótem od object files (pliki obiektów). Nasz program zależy od OBJS i dla każdego pliku obiektu tworzymy regułę, aby stworzyć je z pliku źródłowego. Ostatnia reguła, którą wprowadziliśmy to reguła dopasowania wzorca, % jest wspólnym wzorcem dla tabulate.o i tabulate.f90, który łączy nasz plik obiektu tabulate.o z plikiem źródłowym tabulate.f90. Mając to ustawione, uruchamiamy nasz kompilator ( gfortran ) i tłumaczymy plik źródłowy na plik obiektu, nie tworzymy jeszcze pliku wykonywalnego z powodu flagi -c. Zwróćmy uwagę na użycie $< dla pierwszego elementu zależności tutaj.

Po skompilowaniu wszystkich plików obiektowych próbujemy połączyć program. Nie używamy bezpośredniego linkera, ale gfortran do wygenerowania pliku wykonywalnego.

Teraz uruchamiamy skrypt kompilacji poleceniem make:

gfortran -c -o tabulate.o tabulate.f90
tabulate.f90:2:7:

    2 |    use user_functions
      |       1
Fatal Error: Cannot open module file ‘user_functions.mod’ for reading at (1): No such file or directory
compilation terminated.
make: *** [Makefile:10: tabulate.f90.o] Error 1

Pamiętamy, że mamy zależności pomiędzy naszymi plikami źródłowymi, dlatego dodajemy zależność jawnie do pliku Makefile za pomocą

tabulate.o: functions.o

Teraz możemy ponowić próbę i zobaczyć, że kompilacja przebiegła pomyślnie. Rezultat powinien być następujący

gfortran -c -o functions.o functions.f90
gfortran -c -o tabulate.o tabulate.f90
gfortran -o my_prog tabulate.o functions.o

Powinieneś teraz znaleźć cztery nowe pliki w twoim folderze. Uruchom my_prog, aby upewnić się, że wszystko działa zgodnie z oczekiwaniami. Teraz wykonajmy ponownie make :

make: Nothing to be done for 'all'.

Korzystając z sygnatur czasowych pliku wykonywalnego make udało się nam ustalić, że jest on nowszy niż tabulate.o i functions.o, które z kolei są nowsze niż tabulate.f90 i functions.f90. Dlatego program jest już aktualny, zawiera najnowszy kod i nie trzeba wykonywać żadnych działań.

Na koniec, spójrzmy na kompletny plik Makefile.

# Disable all of make's built-in rules (similar to Fortran's implicit none)
MAKEFLAGS += --no-builtin-rules --no-builtin-variables
# configuration
FC := gfortran
LD := $(FC)
RM := rm -f
# list of all source files
SRCS := tabulate.f90 functions.f90
PROG := my_prog

OBJS := $(addsuffix .o, $(SRCS))

.PHONY: all clean
all: $(PROG)

$(PROG): $(OBJS)
	$(LD) -o $@ $^

$(OBJS): %.o: %
	$(FC) -c -o $@ $<

# define dependencies between object files
tabulate.f90.o: functions.f90.o user_functions.mod

# rebuild all object files in case this Makefile changes
$(OBJS): $(MAKEFILE_LIST)

clean:
	$(RM) $(filter %.o, $(OBJS)) $(wildcard *.mod) $(PROG)

Since you are starting with make we highly recommend to always include the first line, like with Fortran’s implicit none we do not want to have implicit rules messing up our Makefile in surprising and harmful ways.

Next, we have a configuration section where we define variables, in case you want to switch out your compiler, it can be easily done here. We also introduced the SRCS variable to hold all source files, which is more intuitive than specifying object files. We can easily create the object files by appending a .o suffix using the functions addsuffix. The .PHONY is a special rule, which should be used for all entry points of your Makefile, here we define two entry point, we already know all, the new clean rule deletes all the build artifacts again such that we indeed start with a clean directory.

Also, we slightly changed the build rule for the object files to account for appending the .o suffix instead of substituting it. Notice that we still need to explicitly define the interdependencies in the Makefile. We also added a dependency for the object files on the Makefile itself, in case you change the compiler, this will allow you to safely rebuild.

Now you know enough about make to use it for building small projects. If you plan to use make more extensively, we have compiled a few tips for you as well.

Wskazówka

In this guide, we avoided and disabled a lot of the commonly used make features that can be particularly troublesome if not used correctly, we highly recommend staying away from the builtin rules and variables if you do not feel confident working with make, but explicitly declare all variables and rules.

You will find that make is capable tool to automate short interdependent workflows and to build small projects. But for larger projects, you will probably soon run against some of it limitations. Usually, make is therefore not used alone but combined with other tools to generate the Makefile completely or in parts.

Recursively expanded variables#

Commonly seen in many projects are recursively expanded variables (declared with = instead of :=). Recursive expansion of your variables allows out-of-order declaration and other neat tricks with make, since they are defined as rules, which are expanded at runtime, rather than being defined while parsing.

For example, declaring and using your Fortran flags with this snippet will work completely fine:

all:
	echo $(FFLAGS)

FFLAGS = $(include_dirs) -O
include_dirs += -I./include
include_dirs += -I/opt/some_dep/include

You should find the expected (or maybe unexpected) printout after running make

echo -I./include -I/opt/some_dep/include -O
-I./include -I/opt/some_dep/include -O

Wskazówka

appending with += to an undefined variable will produce a recursively expanded variable with this state being inherited for all further appending.

While, it seems like an interesting feature to use, it tends to lead to surprising and unexpected outcomes. Usually, when defining variables like your compiler, there is little reason to actually use the recursive expansion at all.

The same can easily be achieved using the := declaration:

all:
	echo $(FFLAGS)

include_dirs := -I./include
include_dirs += -I/opt/some_dep/include
FFLAGS := $(include_dirs) -O

Ważne

always think of a Makefile as a whole set of rules, it must be parsed completely before any rule can be evaluated.

You can use whatever kind of variables you like most, mixing them should be done carefully, of course. It is important to be aware of the differences between the two kinds and the respective implications.

Comments and whitespace#

There are some caveats with whitespace and comments, which might pop up from time to time when using make. First, make does not know of any data type except for strings and the default separator is just a space. This means make will give a hard time trying to build a project which has spaces in file names. If you encounter such case, renaming the file is possibly the easiest solution at hand.

Another common problem is leading and trailing whitespace, once introduced, make will happily carry it along and it does in fact make a difference when comparing strings in make.

Those can be introduced by comments like

prefix := /usr  # path to install location
install:
	echo "$(prefix)/lib"

While the comment will be correctly removed by make, the trailing two spaces are now part of the variable content. Run make and check that this is indeed the case:

echo "/usr  /lib"
/usr  /lib

To solve this issue, you can either move the comment, or strip the whitespace with the strip function instead. Alternatively, you could try to join the strings.

prefix := /usr  # path to install location
install:
	echo "$(strip $(prefix))/lib"
	echo "$(join $(join $(prefix), /), lib)"

All in all, none of this solutions will make your Makefile more readable, therefore, it is prudent to pay extra attention to whitespace and comments when writing and using make.

The meson build system#

After you have learned the basics of make, which we call a low-level build system, we will introduce meson, a high-level build system. While you specify in a low-level build system how to build your program, you can use a high-level build system to specify what to build. A high-level build system will deal for you with how and generate build files for a low-level build system.

There are plenty of high-level build systems available, but we will focus on meson because it is constructed to be particularly user friendly. The default low-level build-system of meson is called ninja.

Let’s have a look at a complete meson.build file:

project('my_proj', 'fortran', meson_version: '>=0.49')
executable('my_prog', files('tabulate.f90', 'functions.f90'))

And we are already done, the next step is to configure our low-level build system with meson setup build, you should see output somewhat similar to this

The Meson build system
Version: 0.53.2
Source dir: /home/awvwgk/Examples
Build dir: /home/awvwgk/Examples/build
Build type: native build
Project name: my_proj
Project version: undefined
Fortran compiler for the host machine: gfortran (gcc 9.2.1 "GNU Fortran (Arch Linux 9.2.1+20200130-2) 9.2.1 20200130")
Fortran linker for the host machine: gfortran ld.bfd 2.34
Host machine cpu family: x86_64
Host machine cpu: x86_64
Build targets in project: 1

Found ninja-1.10.0 at /usr/bin/ninja

The provided information at this point is already more detailed than anything we could have provided in a Makefile, let’s run the build with ninja -C build, which should show something like

[1/4] Compiling Fortran object 'my_prog@exe/functions.f90.o'.
[2/4] Dep hack
[3/4] Compiling Fortran object 'my_prog@exe/tabulate.f90.o'.
[4/4] Linking target my_prog.

Find and test your program at build/my_prog to ensure it works correctly. We note the steps ninja performed are the same we would have coded up in a Makefile (including the dependency), yet we did not have to specify them, have a look at your meson.build file again:

project('my_proj', 'fortran', meson_version: '>=0.49')
executable('my_prog', files('tabulate.f90', 'functions.f90'))

We only specified that we have a Fortran project (which happens to require a certain version of meson for the Fortran support) and told meson to build an executable my_prog from the files tabulate.f90 and functions.f90. We had not to tell meson how to build the project, it figured this out by itself.

Informacja

meson is a cross-platform build system, the project you just specified for your program can be used to compile binaries for your native operating system or to cross-compile your project for other platforms. Similarly, the meson.build file is portable and will work on different platforms as well.

The documentation of meson can be found at the meson-build webpage.

Creating a CMake project#

Similar to meson CMake is a high-level build system as well and commonly used to build Fortran projects.

Informacja

CMake follows a slightly different strategy and provides you with a complete programming language to create your build files. This is has the advantage that you can do almost everything with CMake, but your CMake build files can also become as complex as the program you are building.

Start by creating the file CMakeLists.txt with the content

cmake_minimum_required(VERSION 3.7)
project("my_proj" LANGUAGES "Fortran")
add_executable("my_prog" "tabulate.f90" "functions.f90")

Similar to meson we are already done with our CMake build file. We configure our low-level build files with cmake -B build -G Ninja, you should see output similar to this

-- The Fortran compiler identification is GNU 10.2.0
-- Detecting Fortran compiler ABI info
-- Detecting Fortran compiler ABI info - done
-- Check for working Fortran compiler: /usr/bin/f95 - skipped
-- Checking whether /usr/bin/f95 supports Fortran 90
-- Checking whether /usr/bin/f95 supports Fortran 90 - yes
-- Configuring done
-- Generating done
-- Build files have been written to: /home/awvwgk/Examples/build

You might be surprised that CMake tries to use the compiler f95, fortunately this is just a symbolic link to gfortran on most systems and not the actual f95 compiler. To give CMake a better hint you can export the environment variable FC=gfortran rerunning should show the correct compiler name now

-- The Fortran compiler identification is GNU 10.2.0
-- Detecting Fortran compiler ABI info
-- Detecting Fortran compiler ABI info - done
-- Check for working Fortran compiler: /usr/bin/gfortran - skipped
-- Checking whether /usr/bin/gfortran supports Fortran 90
-- Checking whether /usr/bin/gfortran supports Fortran 90 - yes
-- Configuring done
-- Generating done
-- Build files have been written to: /home/awvwgk/Example/build

In a similar manner you could use your Intel Fortran compiler instead to build your project (set FC=ifort).

CMake provides support for several low-level build files, since the default is platform specific, we will just use ninja since we already used it together with meson. As before, build your project with ninja -C build:

[1/6] Building Fortran preprocessed CMakeFiles/my_prog.dir/functions.f90-pp.f90
[2/6] Building Fortran preprocessed CMakeFiles/my_prog.dir/tabulate.f90-pp.f90
[3/6] Generating Fortran dyndep file CMakeFiles/my_prog.dir/Fortran.dd
[4/6] Building Fortran object CMakeFiles/my_prog.dir/functions.f90.o
[5/6] Building Fortran object CMakeFiles/my_prog.dir/tabulate.f90.o
[6/6] Linking Fortran executable my_prog

Find and test your program at build/my_prog to ensure it works correctly. The steps ninja performed are somewhat different, because there is usually more than one way to write the low-level build files to accomplish the task of building a project. Fortunately, we do not have to concern ourselves but have our build system handle those details for us.

Finally, we will shortly recap on our complete CMakeLists.txt to specify our project:

cmake_minimum_required(VERSION 3.7)
project("my_proj" LANGUAGES "Fortran")
add_executable("my_prog" "tabulate.f90" "functions.f90")

We specified that we have a Fortran project and told CMake to create an executable my_prog from the files tabulate.f90 and functions.f90. CMake knows the details how to build the executable from the specified sources, so we do not have to worry about the actual steps in the build process.

Wskazówka

CMake’s official reference can be found at the CMake webpage. It is organised in manpages, which are also available with your local CMake installation as well using man cmake. While it covers all functionality of CMake, it sometimes covers them only very briefly.

The SCons build system#

SCons is one another high-level cross-platform build system with automatic dependency analysis that supports Fortran projects. SCons configuration files are executed as Python scripts but it could be successfully used without knowledge about Python programming. The using of Python scripting if needed allows to handle build process and file naming in more sophisticated manner.

Informacja

SCons doesn’t automatically passes the external environment variables such as PATH therefore it will not find programs and tools that installed into non-standard locations unless are specified or passed via appropriate variables. That guaranties that build isn’t affected by external (especially user’s) environment variables and build is repeatable. The most of such variables and compiler options and flags are required to be configured within special „isolated” Environments (please refer to User Guide for additional information).

The SCons doesn’t use external low-level build systems and relies on own build system. The ninja support as external tool to generate ninja.build file is highly experimental (available since scons 4.2) and required to be enabled explicitly by additional configuration.

The simple SCons project is SConstruct file that contains:

Program('my_proj', ['tabulate.f90', 'functions.f90'])

The next step is to build our project with command scons:

scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
gfortran -o functions.o -c functions.f90
gfortran -o tabulate.o -c tabulate.f90
gfortran -o my_proj tabulate.o functions.o
scons: done building targets.

or with scons -Q to disable extended output:

gfortran -o functions.o -c functions.f90
gfortran -o tabulate.o -c tabulate.f90
gfortran -o my_proj tabulate.o functions.o

Find and test your program my_prog at the same directory as source files (by default) to ensure it works correctly.

To cleanup the build artifacts run scons -c (or scons -Qc):

scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Cleaning targets ...
Removed functions.o
Removed user_functions.mod
Removed tabulate.o
Removed my_proj
scons: done cleaning targets.

In our SCons SConstruct file

Program('my_proj', ['tabulate.f90', 'functions.f90'])

we specified executable target name my_proj (optional, if omitted the first source file name is used) and list of source files ['tabulate.f90', 'functions.f90']. There is no need to specify project source files language – it’s detected automatically by SCons for supported languages.

The list of source files could be specified with SCons Glob function:

Program('my_proj', Glob('*.f90'))

or using SCons Split function:

Program('my_proj', Split('tabulate.f90 functions.f90'))

or in more readable form by assigning variable:

src_files = Split('tabulate.f90 functions.f90')
Program('my_proj', src_files)

In case of Split function the multiple lines could be used with Python „triple-quote” syntax:

src_files = Split("""tabulate.f90
                     functions.f90""")
Program('my_proj', src_files)

Wskazówka

SCons official webpage provides: User Guide with extended description of various aspects how to handle the build process, Frequently Asked Questions page and man page.