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.
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.
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 meansmake
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 inmake
.Those can be introduced by comments like
While the comment will be correctly removed by
make
, the trailing two spaces are now part of the variable content. Runmake
and check that this is indeed the case:To solve this issue, you can either move the comment, or strip the whitespace with the
strip
function instead. Alternatively, you could try tojoin
the strings.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 usingmake
.