Инструменты сборки#

Компиляция ваших проектов на языке Fortran вручную может стать довольно сложной задачей по мере роста числа файлов исходного кода и зависимостей в файлах модулей. Поддержка различных компиляторов и компоновщиков или различных платформ может стать все более сложной, если не использовать правильные инструменты для автоматического выполнения этих задач.

В зависимости от размера и цели вашего проекта могут быть использованы различные варианты автоматизации сборки.

Прежде всего, ваша интегрированная среда разработки (IDE), возможно, предоставляет способ сборки вашей программы. Популярным кроссплатформенным инструментом является Visual Studio Code от Microsoft, но существуют и другие, такие как Atom, Eclipse Photran, и Code::Blocks. Они предлагают пользовательский интерфейс, но часто очень специфичны для компилятора и платформы.

Для небольших проектов часто используется система сборки make. На основе заданных правил она может выполнять такие задачи, как (пере)компиляция объектных файлов из обновлённых файлов исходных кодов, создание библиотек, и компоновка исполняемых файлов. Чтобы использовать make, для вашего проекта вы должны создать эти правила в файле Makefile, который определяет взаимозависимости всех конечных программ, промежуточных объектных файлов или библиотек и непосредственно файлов исходных кодов. Краткое введение по этой системе сборки приводится в руководстве по make.

Инструменты сопровождения, такие как autotools и CMake, могут генерировать файлы Makefile или файлы проектов Visual Studio через высокоуровневое описание. Они абстрагируются от специфики компилятора и платформы.

Какой из этих инструментов лучше выбрать для ваших проектов, зависит от многих факторов. Выбирайте инструмент сборки, с которым вам удобно работать, он не должен мешать вам во время разработки. Если тратить больше времени на работу с инструментом сборки, чем на саму разработку, то это может быстро надоесть.

Также обратите внимание на доступность инструментов сборки. Если их использование ограничено одной IDE, смогут ли все разработчики вашего проекта получить к ним доступ? Если вы используете определённую систему сборки, работает ли она на всех платформах, для которых вы разрабатываете? Насколько велик входной порог для ваших инструментов сборки? Предварительно узнайте о кривой обучения для инструментов сборки – идеальный инструмент сборки будет бесполезен, если сначала вам сначала придётся изучить сложный язык программирования, чтобы добавить новый файл исходного кода. Наконец, посмотрите, какие другие проекты вы используете как зависимости и какие используют (или будут использовать) ваш проект как зависимость.

Использование make как инструмент сборки#

Наиболее известная и часто используемая система сборки называется make. Она выполняет действия, следуя правилам, определенным в конфигурационном файле, называемом Makefile или makefile, что обычно приводит к компиляции программы из предоставленного исходного кода.

Совет

Для ознакомления с подробным руководством по make смотрите его информационную страницу. Существует онлайн версия этой информационной страницы, доступная по ссылке.

Мы начнём с основ, находясь в пустом каталоге. Создайте и откройте файл с именем Makefile, в котором создайте простое правило под названием all:

all:
	echo "$@"

После сохранения файла Makefile запустите его, выполнив команду make в той же директории. Вы должны будете увидеть следующий вывод:

echo "all"
all

Во-первых, заметим, что make подставляем вместо $@ имя правила; во-вторых, что следует отметить, make всегда выводит выполняемую команду; наконец, мы видим результат выполнения команды echo \"all\".

Примечание

Мы всегда называем точку входа нашего Makefile all по договорённости, но вы можете выбрать для неё любое другое имя, которое вам нравится.

Примечание

Вы не должны были заметить этого, если ваш редактор работает правильно, но при описании правила отступы следует делать используя символ табуляции. Если у вас возникли проблемы с запуском приведённого выше Makefile и вы видите ошибку, похожую на такую

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

Вероятно, отступ сделан неправильно. В этом случае, используйте для отступа во второй строке символ табуляции.

Теперь мы хотим усложнить наши правила, поэтому добавим ещё одно:

PROG := my_prog

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

$(PROG):
	echo "$@"

Обратите внимание, как мы объявляем переменные в make: вы всегда должны объявлять свои локальные переменные с помощью оператора :=. Для доступа к содержимому переменной используется оператор $(...). Заметьте, что при этом необходимо заключить имя переменной в круглые скобки.

Примечание

Объявление переменных обычно выполняется с помощью оператора :=, но make поддерживает и рекурсивно расширяемые переменные с помощью оператора =. Обычно желательно использовать первый тип объявления, так как такие переменные более предсказуемы и не имеют накладных расходов во время выполнения из-за рекурсивного расширения.

Мы добавили зависимость для правила all, а именно содержимое переменной PROG. Также мы изменили вывод, чтобы видеть все зависимости этого правила, которые хранятся в переменной $^. Теперь для нового правила, которое мы назвали по значению переменной PROG, выполняются те же операции, что были выполнены выше для правила all. Обратите внимание, как значение переменной $@ зависит от правила, в котором она используется.

Выполнив команду make снова, теперь вы должны увидеть:

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

Зависимость была правильно разрешена и вычислена перед выполнением любого действия в правиле all. Давайте запустим выполнение только второго правила: наберите команду make my_prog и в вашем терминале отобразятся только первые две строки предыдущего вывода.

Следующим шагом будет выполнение некоторых реальных действий с помощью make, мы возьмем исходный код из предыдущей главы и добавим новые правила в наш Makefile:

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

all: $(PROG)

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

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

Мы определили переменную OBJS, содержащую список объектных файлов. Наша программа зависит от списка OBJS и для каждого элемента из него мы создаём правило, чтобы скомпилировать объектный файл из файла исходного кода. Последнее правило, которое мы описали является правилом сопоставления шаблонов, где % – общий шаблон между tabulate.o и tabulate.f90, который соединяет наш объектный файл tabulate.o с файлом исходного кода tabulate.f90. Используя этот набор, мы запускаем компилятор gfortran и транслируем исходный код в объектный файл, при этом исполняемый файл пока не создаётся, так как используется ключ компилятора -c. Обратите внимание на использование переменной $< для обращения к первому элементу списка зависимостей.

После компиляции всех объектных файлов мы пытаемся скомпоновать программу, используя компоновщик не напрямую, а вызывая компилятор gfortran, чтобы создать исполняемый файл.

Теперь мы запускаем сценарий сборки командой 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

Мы помним, что у нас есть зависимости между нашими исходными файлами, поэтому мы явно добавляем эту зависимость в Makefile с помощью правила

tabulate.o: functions.o

Теперь мы можем повторить попытку запуска сценария сборки и убедиться, что процесс сборки работает правильно. Вывод должен выглядеть следующим образом

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

В каталоге должны появиться четыре новых файла. Запустите программу my_prog, чтобы убедиться, что она работает как ожидается. Попробуйте выполнить команду make ещё раз:

make: Nothing to be done for 'all'.

Используя временные метки исполняемого файла make определил, что исполняемый файл новее чем каждый из файлов tabulate.o и functions.o, которые, в свою очередь, новее чем файлы исходного кода tabulate.f90 и functions.f90. Поэтому программа уже соответствует содержимому файлов исходного кода и никаких действий по её пересборке выполнять не нужно.

В заключение мы рассмотрим готовый вариант скрипта 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)

Поскольку вы начинаете с системы сборки make мы настоятельно рекомендуем всегда добавлять указанную первую строчку, так как и в случае с оператором Fortran implicit none, мы не хотим, чтобы неявные правила испортили наш Makefile неожиданным и вредным образом.

Далее идёт раздел конфигурации, где мы определяем переменные. В случае если вы захотите указать другой компилятор, это легко сделать здесь. Мы также ввели переменную SRCS для хранения списка всех файлов исходного кода, что более интуитивно понятно, чем указание списка объектных файлов. Мы можем легко создать список объектных файлов, добавив суффикс .o при помощи функции addsuffix. .PHONY – специальное правило, которое должно использоваться для всех точек входа вашего Makefile. В нём мы определяем две точки входа: уже известное нам правило all и новое правило clean, которое удаляет все файлы, полученные в процессе сборки программы, чтобы следующая сборка выполнялась с самого начала.

Также мы немного изменили правило сборки для объектных файлов, чтобы учесть добавление суффикса .o вместо замены на него. Обратите внимание, что нам по прежнему нужно явно определять взаимозависимости в Makefile. Мы также добавили зависимость для объектных файлов от самого Makefile, на случай если вы укажите другой компилятор. Это позволит вам безопасно выполнить пересборку.

Теперь вы знаете о make достаточно, чтобы использовать его для создания небольших проектов. Если вы планируете использовать make более широко, мы подготовили для вас несколько советов.

Совет

В этом руководстве мы избегали использования и отключили многие часто используемые возможности make, которые могут быть особенно проблемными при неправильном применении. Мы настоятельно рекомендуем воздержаться от использования встроенных правил и переменных, если вы не чувствуете себя уверенно при работе с make, и использовать явное объявление переменных и правил.

Вы обнаружите, что make является удобным инструментом для автоматизации коротких взаимозависимых рабочих процессов и создания небольших проектов. Но для больших проектов вы, вероятно, вскоре столкнётесь с некоторыми его ограничениями. Поэтому обычно make используется не сам по себе, а в сочетании с другими инструментами для создания файла Makefile полностью или создания его фрагментов.

Рекурсивно расширяемые переменные#

Во многих проектах часто встречаются рекурсивно расширяемые переменные (объявление с использованием оператора = вместо :=). Рекурсивное расширение ваших переменных позволяет объявлять их не по порядку и делать другие хитрые трюки с использованием make, так как они определяются как правила, которые расширяются во время выполнения, а не во время анализа.

Например, объявление и использование ваших флагов компиляции Fortran с помощью приведённого ниже фрагмента будет работать совершенно нормально:

all:
	echo $(FFLAGS)

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

После выполнения команды make вы должны получить ожидаемый (или, возможно, неожиданный) вывод

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

Совет

добавление к списку с помощью оператора += для неинициализированной переменной приведёт к её рекурсивному расширению, причём это состояние будет наследоваться при всех дальнейших добавлениях.

Хотя эта возможность кажется интересной, она имеет тенденцию приводить к удивительным и неожиданным результатам. Обычно при определении переменных для вашего компилятора нет особых причин использовать рекурсивное расширение.

Нужного эффекта при объявлении можно добиться используя оператор :=:

all:
	echo $(FFLAGS)

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

Важно

всегда думайте о содержимом файла Makefile как о целом наборе правил, так как он должен быть полностью проанализирован, прежде чем любое правило сможет быть выполнено.

Вы можете использовать любой вид переменных, который вам больше нравится, но смешивать их использование следует с осторожностью. Важно знать о различиях между этими двумя видами и о соответствующих последствиях.

Комментарии и символ пробела#

Есть некоторые предостережения по поводу символа пробела и комментариев, которые могут время от времени проявляться при использовании make. Во-первых, утилита make не знает ни одного типа данных, кроме строк и разделителем по умолчанию является пробел. Это означает, что make будет испытывать трудности при попытке собрать проект, в именах файлов которого используются символы пробела. Если вы столкнулись с такой проблемой, то переименование файла, возможно, будет самым простым её решением.

Другая распространённая проблема – символы пробела в начале и конце строки. Если их ввести, то make воспримет их как часть строки и это повлияет на содержимое этих строк внутри файла make.

Такое может произойти при добавлении комментария

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

Хотя комментарий будет корректно удалён при анализе утилитой make, но два завершающих пробельных символа являются частью содержимого переменной. Запустите команду make и убедитесь, что это действительно так:

echo "/usr  /lib"
/usr  /lib

Чтобы разрешить эту проблему, вы можете либо перенести комментарий на следующую строку, либо удалить пробелы в содержимом переменной с помощью функции strip. В качестве альтернативного решения можно попробовать соединить строки с помощью функции join.

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

В целом, ни одно из этих решений не сделает ваш файл Makefile более удобным для чтения, поэтому при написании и использовании правил make следует уделять повышенное внимание символам пробела и комментариям.

Система сборки meson#

После того, как вы изучили основы make,которую мы называем низкоуровневой системой сборки, мы ознакомим вас с meson, высокоуровневой системой сборки. В то время как в низкоуровневой системе сборки вы указываете, как собрать вашу программу, в высокоуровневой системе сборки вы можете указать, что собирать. Высокоуровневая система сборки определит как создать файлы сборки для низкоуровневой системы сборки и сгенерирует их за вас.

Существует множество высокоуровневых систем сборки, но мы остановимся на meson , потому что она построена так, чтобы быть особенно удобной для пользователя. Низкоуровневая система сборки, используемая в meson по умолчанию, называется ninja.

Давайте посмотрим на готовый файл meson.build:

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

И этого достаточно, следующим шагом будет настройка нашей низкоуровневой системы сборки с помощью команды meson setup build, в результате выполнения которой вы должны увидеть результат, похожий на этот

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

Представленная информация на данный момент уже более подробна, чем все, что мы могли бы описать в файле Makefile. Давайте запустим сборку с помощью команды ninja -C build, в результате выполнения которой отобразится что-то похожее на вывод

[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.

Найдите и протестируйте свою программу в каталоге build/my_prog, чтобы убедиться, что она работает правильно. Заметим, что последовательность шагов, которую выполнила система сборки ninja, аналогична той, которую бы мы записали в Makefile (включая указание зависимости), но нам не пришлось её явно описывать. Посмотрите ещё раз на свой файл meson.build:

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

Мы только указали, что у нас есть проект на языке Fortran (для поддержки проектов на языке Fortran требуется определенная версия meson) и сказали системе сборки meson собрать исполняемый файл my_prog из файлов исходного кода tabulate.f90 и functions.f90. Нам не нужно было указывать meson, как собирать проект, система сборки разобралась с этим сама.

Примечание

meson является кроссплатформенной системой сборки, поэтому файл её проекта, который вы только что создали для своей программы, может быть использован для компиляции исполняемых файлов для вашей родной операционной системы или для кросс-компиляции вашего проекта для других платформ. Аналогично, файл meson.build является переносимым и будет работать на разных платформах.

Документация системы сборки meson доступна на веб-странице meson-build.

Создание проекта в системе сборки CMake#

Подобно meson, система сборки CMake является высокоуровневой и часто используется для сборки проектов на языке Fortran.

Примечание

CMake придерживается несколько иной стратегии и предоставляет вам полный язык программирования для создания файлов сборки. Преимуществом является то, что с помощью CMake, можно делать практически все, но ваши файлы сборки CMake могут стать такими же сложными, как и программа, которую вы собираете.

Начните с создания файла CMakeLists.txt со следующим содержимым

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

Как и в случае с meson, этого достаточно для сборки нашей программы с помощью файла сборки CMake. Мы настроим наши низкоуровневые файлы сборки с помощью команды cmake -B build -G Ninja, в результате выполнения которой вы увидите вывод, похожий на следующий

-- 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

Вас может удивить, что CMake пытается использовать компилятор f95. К счастью, в большинстве систем это имя символической ссылки на компилятор gfortran, а не непосредственно компилятор f95. Чтобы подсказать CMake какой компилятор использовать, вы можете экспортировать переменную окружения FC=gfortran. Повторный вызов приведённый выше команды настройки должен отобразить правильное имя компилятора

-- 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

Аналогичным образом вы можете использовать ваш компилятор Intel Fortran compiler для сборки вашего проекта (установив переменную окружения FC=ifort).

CMake обеспечивает поддержку нескольких низкоуровневых файлов сборки, так как по умолчанию они зависят от используемой платформы. Мы будем применять только низкоуровневую систему сборки ninja, так как мы уже использовали её вместе с meson. Как и прежде, собрать проект при этом можно с помощью команды 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

Найдите и протестируйте свою программу в каталоге build/my_prog, чтобы убедиться, что она работает правильно. Последовательность шагов, выполняемая системой сборки ninja в этот раз несколько отличается, потому что существует более одного способа записи низкоуровневых файлов сборки для выполнения задачи сборки проекта. К счастью, нам не нужно беспокоиться об этом, а поручить нашей системе сборки учесть эти детали за нас.

Наконец, мы кратко напомним содержимое готового файла CMakeLists.txt, используемого для описания нашего проекта:

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

Мы указали, что у нас есть проект на языке Fortran и сказали системе сборки CMake создать исполняемый файл my_prog на основе файлов исходного кода tabulate.f90 и functions.f90. CMake знает, как собрать исполняемый файл из указанных исходных файлов, поэтому нам не нужно беспокоиться о фактически выполняемых шагах в процессе сборки.

Совет

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.

Примечание

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)

Совет

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.