Введение в систему сборки make#
Мы кратко рассказали об основах make
. В этой главе приводятся идеи и стратегии масштабирования использования make
для более крупных проектов.
Прежде чем подробнее рассмотреть систему сборки make
, остановимся на нескольких моментах:
make
– это инструмент Unix подобных систем и при переносе на отличные от них платформы, у вас могут возникнуть трудности. Тем не менее существуют различные вариантыmake
, но не все они могут поддерживать функции, которые вы захотите использовать.Хотя
make
даёт полный контроль над процессом сборки, это также означает, что вы несёте ответственность за весь процесс и должны указать плавила для каждой части вашего проекта. Вы можете обнаружить, что тратите значительное количество времени на написание и поддержкуMakefile
вместо того, чтобы разрабатывать исходный код.Вы можете работать над своим
Makefile
, но подумайте о других разработчиках проекта, которые могут быть не знакомы с системой сборкиmake
. Сколько времени, по вашему мнению, они потратят на изучение вашегоMakefile
и смогут ли они отлаживать или добавлять функции?Чистый
make
не масштабируется. Вскоре вы добавите вспомогательные программы для динамической или статической генерации вашегоMakefile
. Они вводят зависимости и возможные источники ошибок. Не следует недооценивать усилия, необходимые для тестирования и документирования этих инструментов.
Если вы считаете, что система сборки make
подходит для ваших нужд, то можете приступить к написанию своего Makefile
. В этом курсе мы будем использовать реальные примеры из перечня пакетов, которые (на момент написания статьи) используют системы сборки, отличные от make
. Это руководство представляет собой общий рекомендуемый стиль написания make
скриптов, а также служит демонстрацией полезных и интересных функций.
Совет
Даже если вы считаете систему сборки make
неподходящей для сборки вашего проекта, он является инструментом для автоматизации рабочих процессов, определяемых файлами. Возможно, вы сможете использовать её возможности в другом контексте.
Первые шаги#
В этой части статьи мы будем работать с проектом Fortran CSV module (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
Мы видим здесь семь файлов исходного кода на языке Fortran: четыре файла из каталога src
должны быть скомпилированы и добавлены в статическую библиотеку, а три в каталоге src/tests
содержат отдельные программы, которые зависят от этой статической библиотеки.
Начнём с создания простого 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
чередует элементы сборки и компиляцию файлов исходного кода, если вы не приложите дополнительные усилия для реализации каталога сборки. Кроме того, сейчас исходные файлы и зависимости указываются явно, что приводит к необходимости написания дополнительных строк даже в случае такого простого примера.
Автоматически создаваемые зависимости#
Основным недостатком системы сборки make
при сборке проектов на языке Fortran является отсутствие возможности определения зависимостей от модулей. Обычно это решается либо указанием их вручную, либо автоматическим сканированием исходного кода с помощью внешнего инструмента. Некоторые компиляторы (например, 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 (в частности, не поддерживается использование submodules), но для данного примера его будет достаточно.
Совет
Использование awk
В приведённом выше скрипте используется язык awk
, который предназначен для обработки текстового потока и использует синтаксис, похожий на синтаксис языка C. В скрипте awk
вы можете определить группы, для элементов которых вычисляются значения при определённых событиях, например, когда строка соответствует определённому шаблону, обычно выраженному в виде регулярного выражения (regular expression).
Этот сценарий awk
определяет пять групп, две из которых используют специальные шаблоны BEGIN
и END
, которые выполняются до начала работы сценария и после его завершения, соответственно. Перед началом работы скрипта мы делаем его нечувствительным к регистру, поскольку мы имеем дело с исходным кодом на языке Fortran. Мы также используем специальную переменную FILENAME
, чтобы определять какой файл мы сейчас разбираем и чтобы можно было обрабатывать несколько файлов одновременно.
С помощью трёх заданных шаблонов мы ищем в качестве первой записи, разделённой пробелами, операторы module
, use
и include
. При использовании данного шаблона не весь допустимый исходный код может быть разобран правильно. Примером кода, который не будет обработан правильно, может быть код:
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)
Обратите внимание, что при выводе информации скриптами будут использоваться рекурсивно расширенные переменные и пока не будут определены зависимости, поскольку может потребоваться объявление переменных не по порядку, а мы не хотим случайно создать какую-либо цель сборки. Вы можете убедиться, что здесь присутствует та же информация, что и в приведённом выше рукописном фрагменте. Единственным исключением является дополнительная зависимость от 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
по всем файлам объектов. Обратите внимание, что мы создали соответствие между файлами объектов и файлами зависимостей, расширяя его один раз, получаем имя объектного файла, расширяя его снова, получаем объектные файлы, от которых он зависит.
Сборка проекта с помощью 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)