Система сборки SCons в настоящее время не пользуется популярностью, а зря – это не самый плохой выбор для вашего проекта (даже если его части на разных языках), особенно, если учесть, что его скрипт сборки может выполнять вызовы языка Python напрямую, что значительно расширяет возможности управления процессом сборки, списками файлов и т.п.
SCons не использует внешние низкоуровневые системы сборки, как это делают CMake или Meson, полагаясь на свою собственную встроенную. Есть экспериментальная поддержка внешней низкоуровневой системы сборки Ninja, но её поддержка очень экспериментальная.
Если скорость сборки вашего проекта слишком критична (это должен быть очень большой проект), то, возможно, SCons вам скорее не подойдёт.
Оценка разницы в скорости здесь всё равно не приводится, но желающие могут протестировать её на примере
проекта The Battle for Wesnoth, где помимо файла проекта SCons (файл SConstruct
)
поддерживается система сборки CMake.
Я не использую какую-либо систему сборки на регулярной основе (да я вообще не программист!), поэтому не знаю даже базовых тонкостей той или иной системы, в том числе и рассматриваемой. По этой причине сравнения между ними здесь приводиться не будет. Возможно, что даже описанные ниже вещи можно сделать в рамках SCons проще и иначе.
SCons, по умолчанию, не проверяет изменился ли файл на основе временных меток. Вместо этого он проверяет контрольные суммы файлов. Но данное поведение настраивается: взамен можно выбрать проверку временных меток, либо смешанную – одновременно на основе проверки контрольной суммы и временных меток.
Разумеется, что возможности SCons далеко не исчерпываются тем, что рассматривается в данной статье. С более подробной справкой можно ознакомиться:
Для демонстрации выбран проект на языке Fortran,
так как сборка проекта на языке C или C++ будет немного проще и поэтому не так интересна.
К тому же примеров для этих языков в сети гораздо больше.
В качестве проекта рассмотрим небольшую программу табулирования значений функции (взятой с одной из страниц https://fortran-lang.org), состоящей из двух файлов:
основной программы «tabulate.f90»
program tabulate
use user_functions
implicit none
real :: x, xbegin, xend
integer :: i, steps
write(*,*) 'Please enter the range (begin, end) and the number of steps:'
read(*,*) xbegin, xend, steps
do i = 0, steps
x = xbegin + i * (xend - xbegin) / steps
write(*,'(2f10.4)') x, f(x)
end do
end program tabulate
и файла «functions.f90», содержащего модуль, описывающий функцию, табличные значения которой будут выведены в результате выполнения программы
module user_functions
implicit none
contains
real function f( x )
real, intent(in) :: x
f = x - x**2 + sin(x)
end function f
end module user_functions
Её несложно собрать вручную с помощью команды (добавим флаг оптимизации -O2
для наглядности)
gfortran -O2 functions.f90 tabulate.f90
Здесь придётся явно указать порядок сборки файлов, иначе при компиляции файла tabulate.f90
не будет найден модуль user_functions.mod
,
который появляется как результат компиляции файла functions.f90
.
Попробуем собрать этот проект, используя систему сборки SCons. Создадим файл SConstruct
со следующим содержимым:
Program('tabfunc', ['tabulate.f90', 'functions.f90'])
В вызываемом здесь методе Program
первым параметром указывается имя выходного исполняемого файла (здесь tabfunc
).
Если он не указан, то в качестве имени выходного файла будет взято имя первого файла в списке файлов
исходного кода. Указание языка проекта не требуется, он определяется автоматически на основе расширений.
Порядок сборки, как и в других высокоуровневых системах, определяется также автоматически.
Помимо метода Program
существует и метод Object
, который возвращает список объектных файлов (и модулей),
но его рассматривать не будем.
Можно использовать переменную, которой в качестве значения присвоен список файлов:
f90_files = ['tabulate.f90', 'functions.f90']
Program('tabfunc', f90_files)
Вместо явного указания списка файлов исходного кода, в файле проекта можно использовать функцию SCons Glob
с указанием шаблона имён файлов:
Program('tabfunc', Glob('*.f90'))
Встроенная функция Split
позволяет задать список файлов следующим образом:
Program('tabfunc', Split('tabulate.f90 functions.f90'))
Или даже с использованием переменной и многострочного синтаксиса Python:
f90_files = Split("""tabulate.f90
functions.f90""")
Program('tabfunc', f90_files)
Остановимся на первоначальном варианте содержимого файла SConsctuct
и запустим сборку проекта командой 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 tabfunc tabulate.o functions.o
scons: done building targets.
Очистка результатов сборки выполняется командой scons -c
:
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Cleaning targets ...
Removed functions.o
Removed user_functions.mod
Removed tabulate.o
Removed tabfunc
scons: done cleaning targets.
Если добавить флаг -Q
, то вывод будет менее детализированным – scons -Q
:
gfortran -o functions.o -c functions.f90
gfortran -o tabulate.o -c tabulate.f90
gfortran -o tabfunc tabulate.o functions.o
Будем использовать этот тип вывода в дальнейшем для краткости изложения.
Как мы видим, SCons сначала явно собирает объектные файлы из файлов исходного кода,
указанных в методе Program
(и модули, если исходный файл - файл модуля),
а затем компонует объектные файлы в исполняемый файл.
Но ведь во время «ручной» сборки мы добавили флаг оптимизации -O2
, который здесь пока не применяется.
Как его добавить?
Переменные окружения в системе сборки SCons
Здесь начинается самое интересное: SCons не импортирует по умолчанию
внешние переменные окружения и особенно переменые окружения пользователя.
Особенно это касается переменной PATH
, содержащей нестандартные пути
к внешним утилитам.
Такой подход позволяет изолировать процесс сборки и делает его независимым
от внешних фактров, гарантируя, что сборку можно будет воспроизвести
в другом окружении.
По этой причине дополнительные переменные окружения задаются или передаются во внутренние окружения сборки, каждое из которых может иметь своё собственное имя. В них же можно в качестве параметров задавать и флаги компилятора.
В случае языка Fortran, в связи с разнообразием стандартов, в SCons предусмотрены переменные окружения для каждого типа файлов, соответствующему определённому стандарту. Это с одной стороны обеспечивает гибкость управления процессом сборки, с другой запутывает (меня).
Итак, мы хотим задать флаг оптимизации при компиляции в процессе сборки
и явно указать какой компилятор или команду его вызова использовать.
Для наглядности будем использовать команду вызова компилятора x86_64-pc-linux-gnu-gfortran
.
Инициализируем окружение сборки с именем env
и укажем явно команду вызова компилятора F90
и команду компилятора, которая будет вызывать компоновщик FORTRAN
(не спрашивайте почему именно так, я пока не разобрался).
В этом случае файл нашего проекта SConstruct
примет вид:
env = Environment(F90='x86_64-pc-linux-gnu-gfortran', FORTRAN='x86_64-pc-linux-gnu-gfortran')
env.Program('tabfunc', ['tabulate.f90', 'functions.f90'])
Метод Program
теперь вызывается для объявленного окружения с именем env
.
Дополнительные флаги компиляции и компоновщика можно объявить при инициализации
окружения, либо добавить их позже, с помощью метода Append
. Флаг -O2
для сборки
объектных файлов можно добавить либо добавлением его в переменную F90FLAGS
, либо в FORTRANCOMMONFLAGS
(но точно не в FORTRANFLAGS
).
В этот раз инициализируем переменные окружения в разных строках.
Флаги компоновщика передаются в переменной LINKFLAGS
– здесь мы добавим, для примера,
флаг -g
, но в дальнейшем его использовать не будем:
env = Environment(F90='x86_64-pc-linux-gnu-gfortran',
FORTRAN='x86_64-pc-linux-gnu-gfortran')
env.Append(FORTRANCOMMONFLAGS='-O2')
env.Append(LINKFLAGS='-g')
env.Program('tabfunc', ['tabulate.f90', 'functions.f90'])
Разумеется, что если нам действительно нужна отладочная информация, то флаг -g
нужно добавить и на этапе компиляции файлов исходного когда в объектные файлы, а не только на этапе компоновки.
Запуск scons -Q
приведёт к сборке проекта:
x86_64-pc-linux-gnu-gfortran -o functions.o -c -O2 functions.f90
x86_64-pc-linux-gnu-gfortran -o tabulate.o -c -O2 tabulate.f90
x86_64-pc-linux-gnu-gfortran -o tabfunc -g tabulate.o functions.o
Первые две строки используют для сборки объектных файлов команду из переменной F90
,
а третья, в данном случае, вызывает компоновщик через вызов компилятора командой из переменной FORTRAN
.
Перед тем как перейти к следующему подразделу, выполним для удаления файлов,
полученных в процессе сборки, команду scons -c
.
Создание дерева проекта
Пример достаточно простой, без сложной иерархии файлов и каталогов,
поэтому вложенные подпроекты с использованием дополнительных файлов
SConscript
здесь не рассматриваются.
В качестве примера создания дерева проекта с определённой иерархией
здесь будет использоваться метод VariantDir
.
Переместим наши файлы проекта *.f90
в подкаталог src
и добавим в файл проекта SConstruct
вызов метода VariantDir
, в котором будет укажем, что сборка объектных файлов
будет выполняться в подкаталоге build/obj
, а в методе Program
укажем, что исполняемый файл должен быть размещён в подкаталоге build
.
В переменной окружения FORTRANMODDIR
укажем, что файлы модулей в процессе
компиляции будут помещаться в подкаталог build/include
.
Получим следующий файл проекта SConstruct
(LINKFLAGS
, как было обещено, теперь убран):
env = Environment(F90='x86_64-pc-linux-gnu-gfortran',
FORTRAN='x86_64-pc-linux-gnu-gfortran')
env.Append(FORTRANCOMMONFLAGS='-O2')
env.Append(FORTRANMODDIR='build/include')
env.VariantDir('build/obj', 'src', duplicate=False)
env.Program('build/tabfunc', ['build/obj/tabulate.f90', 'build/obj/functions.f90'])
Обратите внимание на две вещи:
- опцию
duplicate=False
– Scons создаёт «копии» (на самом деле линки) файлов исходных кодов в каталогах компиляции в объектные файлы, данная опция удаляет эти копии в конце процесса сборки, иначе они остаются; - пути к файлам исходных кодов в методе
Program
теперь указывают на пути к создаваемым «копиям»build/obj/*.f90
вместоsrc/*.f90
– да, должно быть именно так.
Использование текущего каталога .
как источника файлов исходного кода в методе VariantDir
крайне не рекомендуется, поэтому мы поместили файлы *.f90
в подкаталог src
.
При запуске scons -Q
:
x86_64-pc-linux-gnu-gfortran -o build/obj/functions.o -c -O2 -Jbuild/include src/functions.f90
x86_64-pc-linux-gnu-gfortran -o build/obj/tabulate.o -c -O2 -Jbuild/include src/tabulate.f90
x86_64-pc-linux-gnu-gfortran -o build/tabfunc build/obj/tabulate.o build/obj/functions.o
получим следующую структуру каталогов проекта:
.
├── SConstruct
├── build
│ ├── include
│ │ └── user_functions.mod
│ ├── obj
│ │ ├── functions.o
│ │ └── tabulate.o
│ └── tabfunc
└── src
├── functions.f90
└── tabulate.f90
Подключение внешних библиотек
Допустим, мы хотим использовать в нашем проекте внешнюю библиотеку, например,
fortran-stdlib. В моём случае в системе
она установлена так, что путь к самой библиотеке – /usr/lib64/libfortran_stdlib.so
,
а файлы её модулей находятся в директории /usr/include/fortran_stdlib
.
Добавим в файл проекта tabulate.f90
, в дополнение к существующей реализации, использование функции arange
из модуля
stdlib_math
для создания массива точек (назовём его xrange
):
program tabulate
use user_functions
use stdlib_math, only : arange
implicit none
real :: x, xbegin, xend
integer :: i, steps
real, allocatable :: xrange(:)
write(*,*) 'Please enter the range (begin, end) and the number of steps:'
read(*,*) xbegin, xend, steps
do i = 0, steps
x = xbegin + i * (xend - xbegin) / steps
write(*,'(2f10.4)') x, f(x)
end do
write(*,*) repeat('-', 20)
xrange = arange(xbegin, xend, (xend-xbegin)/steps)
do i = 1, steps+1
write(*,'(2f10.4)') xrange(i), f(xrange(i))
end do
end program tabulate
Для сборки проекта теперь в наш файл SConstruct
нужно в переменные окружения
F90PATH
и LIBS
добавить путь, по которому следует искать подключаемые модули
и имя подключаемой библиотеки, соответственно:
env = Environment(F90='x86_64-pc-linux-gnu-gfortran',
FORTRAN='x86_64-pc-linux-gnu-gfortran')
env.Append(FORTRANCOMMONFLAGS='-O2')
env.Append(F90PATH='/usr/include/fortran_stdlib')
env.Append(LIBS='fortran_stdlib')
env.Append(FORTRANMODDIR='build/include')
env.VariantDir('build/obj', 'src', duplicate=False)
env.Program('build/tabfunc', ['build/obj/tabulate.f90', 'build/obj/functions.f90'])
В результате запуска сборки (командой scons -Q
) увидим результат:
x86_64-pc-linux-gnu-gfortran -o build/obj/functions.o -c -O2 -I/usr/include/fortran_stdlib -Jbuild/include src/functions.f90
x86_64-pc-linux-gnu-gfortran -o build/obj/tabulate.o -c -O2 -I/usr/include/fortran_stdlib -Jbuild/include src/tabulate.f90
x86_64-pc-linux-gnu-gfortran -o build/tabfunc build/obj/tabulate.o build/obj/functions.o -lfortran_stdlib
Используемая библиотека поставляется с файлом /usr/lib64/pkgconfig/fortran_stdlib.pc
:
prefix=/usr
libdir=${prefix}/lib64
includedir=${prefix}/include
moduledir=${prefix}/include/fortran_stdlib
Name: fortran_stdlib
Description: Community driven and agreed upon de facto standard library for Fortran
Version: 0.2.1
Libs: -L${libdir} -lfortran_stdlib
Cflags: -I${includedir} -I${moduledir}
Используя метод ParseConfig
, из этого файла можно извлечь флаги для передачи их компилятору.
Напрямую извлечь переменную окружения F90PATH
в данном случае не получится,
поэтому её расширим через её присвоение извлечённому значению переменной CPPPATH
.
В итоге получим следующий файл проекта SConstruct
:
env = Environment(F90='x86_64-pc-linux-gnu-gfortran',
FORTRAN='x86_64-pc-linux-gnu-gfortran')
env.Append(FORTRANCOMMONFLAGS='-O2')
env.ParseConfig("pkg-config fortran_stdlib --cflags --libs")
env.Append(F90PATH=env['CPPPATH'])
env.Append(FORTRANMODDIR='build/include')
env.VariantDir('build/obj', 'src', duplicate=False)
env.Program('build/tabfunc', ['build/obj/tabulate.f90', 'build/obj/functions.f90'])
и в результате запуска сборки (scons -Q
) получим вывод аналогичный тому, что был в предыдущем случае:
x86_64-pc-linux-gnu-gfortran -o build/obj/functions.o -c -O2 -I/usr/include/fortran_stdlib -Jbuild/include src/functions.f90
x86_64-pc-linux-gnu-gfortran -o build/obj/tabulate.o -c -O2 -I/usr/include/fortran_stdlib -Jbuild/include src/tabulate.f90
x86_64-pc-linux-gnu-gfortran -o build/tabfunc build/obj/tabulate.o build/obj/functions.o -lfortran_stdlib
При ручной сборке, без создания иерархии каталогов, можно было бы собрать проект с помощью команды:
gfortran -O2 -I/usr/include/fortran_stdlib/ functions.f90 tabulate.f90 -lfortran_stdlib
Но, как упоминалось, рассматривается простой проект и когда количество файлов становится больше, то приходится самому следить за порядком следования файлов и взаимными зависимостями между ними для указания правильного порядка сборки.
На этом, пожалуй, пока хватит.
Update #1: Создание дерева проекта с целью ‘debug’
Допустим, что в рамках создания дерева проекта мы хотим иметь возможность собирать наш проект так, чтобы исполняемый файл хранил отладочную информацию. Вернёмся к варианту нашей программы tabulate.f90
до «подключения внешних библиотек».
Для получения нужной реализации в файле SConstruct
определим переменную debug
, которой присвоим значения аргумента командной строки debug
. Добавим условие для присвоения значения переменной имени подкаталога build_dir
(build/
или debug/
) и значений флагов компилятора и компоновщика, в зависимости значения переменной debug
. После остаётся подставить переменную build_dir
в вызовы методов Append
, VariantDir
и Program
:
env = Environment(F90='x86_64-pc-linux-gnu-gfortran',
FORTRAN='x86_64-pc-linux-gnu-gfortran')
debug = ARGUMENTS.get('debug', 0)
if int(debug):
build_dir='debug/'
env.Append(FORTRANCOMMONFLAGS='-g')
env.Append(LINKFLAGS='-g')
else:
build_dir='build/'
env.Append(FORTRANCOMMONFLAGS='-O2')
env.Append(FORTRANMODDIR=build_dir+'include')
env.VariantDir(build_dir+'obj', 'src', duplicate=False)
env.Program(build_dir+'tabfunc', [build_dir+'obj/tabulate.f90', build_dir+'obj/functions.f90'])
Проверяем, как работает наш новый файл проекта, вызвав сборку несколько раз:
$ scons -Q
x86_64-pc-linux-gnu-gfortran -o build/obj/functions.o -c -O2 -Jbuild/include src/functions.f90
x86_64-pc-linux-gnu-gfortran -o build/obj/tabulate.o -c -O2 -Jbuild/include src/tabulate.f90
x86_64-pc-linux-gnu-gfortran -o build/tabfunc build/obj/tabulate.o build/obj/functions.o
$ scons -Q debug=0
scons: `.' is up to date.
$ scons -Q debug=1
x86_64-pc-linux-gnu-gfortran -o debug/obj/functions.o -c -g -Jdebug/include src/functions.f90
x86_64-pc-linux-gnu-gfortran -o debug/obj/tabulate.o -c -g -Jdebug/include src/tabulate.f90
x86_64-pc-linux-gnu-gfortran -o debug/tabfunc -g debug/obj/tabulate.o debug/obj/functions.o
$ scons -Q debug=1
scons: `.' is up to date.
В резульате получаем следующую структуру каталогов и файлов:
.
├── SConstruct
├── build
│ ├── include
│ │ └── user_functions.mod
│ ├── obj
│ │ ├── functions.o
│ │ └── tabulate.o
│ └── tabfunc
├── debug
│ ├── include
│ │ └── user_functions.mod
│ ├── obj
│ │ ├── functions.o
│ │ └── tabulate.o
│ └── tabfunc
└── src
├── functions.f90
└── tabulate.f90
Теперь попробуем всё же проделать то же самое с использованием дополнительного SConscript
файла.
В корневой директории проекта наш SConstruct
файл для этой цели станет таким:
debug = ARGUMENTS.get('debug', 0)
if int(debug):
build_dir='#debug/'
else:
build_dir='#build/'
SConscript('src/SConscript', exports=['debug', 'build_dir'], variant_dir=build_dir+'obj', duplicate=False)
Здесь в значении переменной build_dir
символ #
перед именем каталога указывает, что используется путь относительно основного SConstruct
файла (корневой директории проекта). Это важно, так как переменная будет экспортирована во внутренний подкаталог. Функция SConscript()
подключает файл src/SConscript
, с экспортом в него значений переменных debug
и build_dir
, а также в параметре variant_dir
сообщаем в каком каталоге будет происходить сборка объектных файлов. Использование параметра duplicate=False
аналогично его использованию в методе VariantDir
, описанному выше - копии (линки) исходных файлов и src/SConscript
будут удалены после окончания процесса сборки.
В подкаталоге src
создадим SConscript
файл следующего содержания:
Import('debug', 'build_dir')
env = Environment(F90='x86_64-pc-linux-gnu-gfortran',
FORTRAN='x86_64-pc-linux-gnu-gfortran')
if int(debug):
env.Append(FORTRANCOMMONFLAGS='-g')
env.Append(LINKFLAGS='-g')
else:
env.Append(FORTRANCOMMONFLAGS='-O2')
env.Append(FORTRANMODDIR=build_dir+'include')
env.Program(build_dir+'tabfunc', [build_dir+'obj/tabulate.f90', build_dir+'obj/functions.f90'])
Здесь мы импортируем переменные debug
, build_dir
и используем их значения, чтобы в зависимости от цели сборки применялись те или иные флаги компиляции и компоновки и сборка происходила в определённых подкаталогах.
Выполнение команд scons -Q
и scons -Q debug=1
приведёт к сборке проекта и созданию структуры каталогов и файлов, аналогичной той, что получилась в предыдущем способе, за исключением того, что теперь у нас есть дополнительный файл src/SConscript
.
Немного удобнее будет использовать переменную SCons COMMAND_LINE_TARGETS
для получения списка целей сборки, указанных в командной строке. Тогда в файле SConstruct
(в варианте без использования SConscript
) нужно заменить строки
debug = ARGUMENTS.get('debug', 0)
if int(debug):
на строку
if 'debug' in COMMAND_LINE_TARGETS:
тогда для сборки версии программы, содержащей отладочную информацию, достаточно будет вызвать команду scons -Q debug
.