Wstęp do make#
Omówiliśmy już podstawy make
. W tym rozdziale zaprezentujemy pomysły i strategie skalowania make
w większych projektach.
Zanim przejdziemy do szczegółów make
, rozważmy kilka punktów:
make
jest narzędziem Unix i może sprawiać ci problemy podczas przenoszenia na platformy inne niż Unix. Oprócz tego, są również dostępne różne odmianymake
, nie wszystkie mogą obsługiwać funkcje, których chcesz użyć.make
daje ci pełną kontrolę nad procesem kompilacji, oznacza to również, że to ty jesteś za niego odpowiedzialny oraz musisz sprecyzować zasady dla każdego szczegółu twojego projektu. Może okazać się, że przeznaczysz znaczną część swojego czasu na pisanie i utrzymywanie twojegoMakefile
zamiast na pisanie kodu źródłowego.Możesz pracować ze swoim
Makefile
, jednak pomysł również o innych deweloperach w twoim projekcie, którzy mogą być niezaznajomieni zmake
. Jak dużo czasu będą musieli poświęcić na nauczenie się twojegoMakefile
i czy będą w stanie go debugować i dodawać własne funkcje?Czysty
make
nie będzie się skalował. Wkrótce dodasz programy posiłkowe, aby dynamicznie lub statycznie generować twójMakefile
. Te z kolei wprowadzają zależności i prawdopodobne źródła błędów. Wysiłek włożony w testowanie i dokumentowanie tych narzędzi powinien być wzięty pod uwagę.
Jeśli myślisz, że make
jest odpowiedni dla twoich potrzeb, możesz zacząć pisać swój plik Makefile
. W tym kursie użyjemy rzeczywistych przykładów z indeksu pakietów, który (w czasie pisania tego kursu) używa innych systemów kompilacji niż make
. Ten poradnik przedstawia ogólny rekomendowany styl pisania make
oraz powinien służyć jako demonstracja użytecznych i interesujących funkcji.
Wskazówka
Nawet jeśli make
nie wydaje ci się odpowiedni do kompilacji twojego projektu, jest to narzędzie do automatyzacji przepływu pracy zdefiniowanej przez pliki. Może uda ci się wykorzystać jego moc w innym kontekście.
Pierwsze kroki#
W tej części będziemy pracowali z modułem Fortran CSV (v1.2.0). Naszym celem jest napisanie Makefile
, aby skompilować ten projekt do biblioteki statycznej. Zacznij od sklonowania repozytorium
git clone https://github.com/jacobwilliams/fortran-csv-module -b 1.2.0
cd fortran-csv-module
Wskazówka
W tej części będziemy pracowali z kodem o tagu 1.2.0
, aby mógł być z łatwością odtworzony. Jednak możesz użyć też najnowszej wersji lub całkowicie innego projektu.
Ten projekt używa FoBiS jako swojego systemu kompilacji, możesz więc sprawdzić build.sh
aby zobaczyć opcje użyte dla FoBiS. My jednak napiszemy Makefile
dla tego projektu. Na początek sprawdzamy strukturę folderów oraz pliki źródłowe
.
├── 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
Znaleźliśmy siedem różnych plików źródłowych Fortran; cztery w src
powinny być skompilowane i dodane do biblioteki statycznej, a trzy pliki w src/tests
zawierają osobne indywidualne programy, które zależą od tej statycznej biblioteki.
Zacznij od stworzenia prostego 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)
Wywołanie make
powinno zbudować bibliotekę statyczną oraz sprawdzić pliki wykonywalne:
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
Należy zwrócić tutaj uwagę na kilka rzeczy. Kompilacja za pomocą make
zazwyczaj przeplata artefakty kompilacji z kodem źródłowym, chyba że włożysz dodatkowy wysiłek w zaimplementowanie folderu kompilacji. Dodatkowo, teraz pliki źródłowe i zależności są określane jawnie, co skutkuje kilkoma dodatkowymi linijkami nawet dla tak małego projektu.
Automatyczne wygenerowane zależności#
Największa wadą make
przy kompilacji Fortrana jest brak możliwości określenia zależności modułu. Jest to zwykle rozwiązywane przez dodanie ich ręcznie lub automatycznie skanowanie kodu źródłowego za pomocą narzędzi zewnętrznych. Niektóre kompilatory (tak jak Intel Fortran) oferują również generowanie zależności w formacie make
.
Zanim jednak zanurzymy się w generowanie zależności, zarysujmy solidną koncepcję podejścia do problemu zależności. Po pierwsze, chcemy sposobu, który pozwoli na przetworzenie wszystkich plików źródłowych oddzielnie, kiedy każdy plik źródłowy zapewnia (module
) lub wymaga (use
) modułów. Podczas generowania zależności tylko nazwy plików źródłowych oraz pliki modułów są znane i żadne informacje o nazwach plików obiektowych nie powinny być wymagane.
Jeśli sprawdzisz powyższą sekcję zależności zauważysz, że wszystkie zależności są zdefiniowane pomiędzy plikami obiektowymi a nie plikami źródłowymi. Aby to zmienić, możemy wygenerować mapę z plików źródłowych do odpowiednich plików obiektowych:
# Define a map from each file name to its object file
obj = $(src).o
$(foreach src, $(SRCS) $(TEST_SRCS), $(eval $(src) := $(obj)))
Zauważ deklarację obj
jako rekursywnie rozszerzaną zmienną. Używany tego mechanizmu do efektywnego definiowania funkcji w make
. Funkcja foreach
pozwala nam na wykonanie pętli po wszystkich plikach źródłowych, a funkcja eval
na wygenerowanie instrukcji make
oraz określenie ich dla Makefile
.
Odpowiednio dostosowujemy zależności, ponieważ możemy teraz zdefiniować nazwy plików obiektowych poprzez nazwy plików źródłowych:
# 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)
Ta sama strategia tworzenia mapy jest już używana dla plików modułowych, teraz jest ona poszerzona też do plików obiektowych.
Aby automatycznie wygenerować mapę poszczególnych zależności użyjemy skryptu 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]
}
Skrypt ten zakłada kilka rzeczy na temat kodu źródłowego, który analizuje, dlatego nie będzie działał poprawnie z każdym kodem Fortran (submoduły nie są wspierane), ale dla tego przykładu jest to wystarczające.
Wskazówka
Używanie awk
Powyższy skrypt używa języka awk
, który został stworzony w celu przetwarzania strumieniowego tekstu (ang. text stream processing) i używa składni podobnej do C. W awk
możesz definować grupy, które są ewaluowane na podstawie pewnych zdarzeń _np. _ kiedy dany wiersz pasuje do określonego wzorca, zazwyczaj wyrażonego wyrażeniem regularnym.
Ten skrypt awk
definiuje pięć grup, dwie z nich używają specjalnego wzorca BEGIN
oraz END
, które są uruchamiane odpowiednio zanim skrypt się rozpocznie oraz po skończeniu skryptu. Zanim skrypt się rozpocznie sprawiamy, aby nieuwzględniał wielkości liter, ponieważ mamy tutaj do czynienia z kodem źródłowym Fortran. Używamy również specjalnej zmiennej FILENAME
, aby określić, który plik jest aktualnie poddawany analizie oraz aby umożliwić przetwarzanie kilku plików na raz.
Mając określone trzy wzorce, szukamy nimi poleceńmodule
, use
oraz include
jako pierwszego wpisu rozdzielonego spacją. Przy tym zastosowanym wzorze nie każdy kod Fortran zostanie poprawnie przeanalizowany. Przykładem błędu może być:
use::my_module,only:proc
Aby ten kod mógł zostać przeanalizowany przez awk
możemy dodać jeszcze jedną grupę zaraz po grupie BEGIN
modyfikując tym samym strumień podczas jego przetwarzania za pomocą
{
gsub(/,|:/, " ")
}
Teoretycznie potrzebny byłby pełny parser Fortran, aby poradzić sobie z kontynuacjami wierszy oraz innymi trudnościami. Mogłoby być to możliwe do zaimplementowania w awk
, ale ostatecznie wymagałoby ogromnego skryptu.
Also, keep in mind that generating the dependencies should be fast, an expensive parser can produce a significant overhead when generating dependencies for a large code base. Making reasonable assumptions can simplify and speed up this step, but also introduces an error source in your build tools.
Make the script executable (chmod +x gen-deps.awk
) and test it with
./gen-deps.awk $(find src -name '*.[fF]90')
. You should see output like this:
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)
Note that the scripts output will use recursively expanded variables and not
define any dependencies yet, because out-of-order declaration of variables
might be necessary and we do not want to create any target by accident.
You can verify that the same information as in the above handwritten snippet is
present. The only exception is the additional dependency on the
iso_fortran_env.mod
, since it is an undefined variable it will just expand
to an empty string and not introduce any further dependencies.
Now, you can finally include this piece in your Makefile
to automate the
dependency generation:
# 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)
Here additional dependency files are generated for each source file individually
and than included into the main Makefile
.
Also, the dependency files are added as dependency to the object files to ensure
they are generated before the object is compiled. The pipe character in
the dependencies defines an order of the rules without a timestamp dependency,
because it is not necessary to recompile an object file in case dependencies are
regenerated and potentially unchanged.
Again, we make use of the eval
function to generate the dependencies in a
foreach
loop over all object files. Note that we created a map between
the object files in the dependency files, expanding dep
once yields the
object file name, expanding it again yields the object files it depends on.
Building your project with make
should give an output similar to
./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
Once the dependency files are generated, make
will only update them if the
source changes and not require to rebuild them again for every invocation.
Wskazówka
With correct dependencies you can leverage parallel execution of your Makefile
, just use the -j
flag to create multiple make
processes.
Since dependencies can now be generated automatically, there is no need to specify
the source files explicitly, the wildcard
function can be used to determine
them dynamically:
# List of all source files
SRCS := $(wildcard src/*.f90) \
$(wildcard src/*.F90)
TEST_SRCS := $(wildcard src/tests/*.f90)