LINUX.ORG.RU

Использование системы сборки SCons для сборки проекта на языке Fortran

 , ,


0

1

Система сборки 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.

★★★★★

Проверено: maxcom ()

Ответ на: комментарий от LINUX-ORG-RU

Добавил про разделение «релизной» сборки и сборки с отладочной информацией.

Разработка SCons в последние пару лет как-то возобновилась и стала более активной.

grem ★★★★★ ()
Ответ на: комментарий от grem

Разработка SCons в последние пару лет как-то возобновилась и стала более активной.

Я так понял meson это тот же scons только яйца вид с боку =)

К слову спецом пошёл сюда https://mesonbuild.com/Comparisons.html

Slow. Requires you to pass your configuration settings on every invocation. That is, if you do scons OPT1 OPT2 and then just scons, it will reconfigure everything without settings OPT1 and OPT2. Every other build system remembers build options from the previous invocation.

Про медленность, фиг с ним. Но дальше что за бред, типа надо придумать минус, придумали :D

Ну да ладно. Сам использовать конечно же не буду, но вот если попадётся софт собираемый «скунсом» будет куда поглядеть для понимания нюансов, спасиба ::)

LINUX-ORG-RU ★★★★★ ()
Ответ на: комментарий от LINUX-ORG-RU

На самом деле не сказать что настолько заметно медленнее. Я попытался собрать wesnoth с помощью cmake и scons на основе гентушного ebuild, но надо тщательнее проверять, что цели сборки совпадают и флаги. Так то в обоих случаях ~13-14 минут вышло на моём компе от начала конфигурирования до конца компиляции. Но конфигурация cmake быстрее, за счёт этого есть общий выигрыш по времени.

У scons не разделяются вызовы конфигурации и сборки - они сразу друг за другом следуют.

Насчёт «reconfigure everything» - есть возможность сохранять параметры в конфиг и загружать в следующий раз оттуда, если новый повторный вызов производится без указания параметров. Но мудрить самому придётся, но недолго.

grem ★★★★★ ()
Ответ на: комментарий от LINUX-ORG-RU

надо придумать минус, придумали :D

Вспомнил, что мне это напоминает - это же как в «бойцовском клубе»:

Если кандидат молод, мы говорим ему, что он слишком молод. Если он толстый, мы говорим ему, что он слишком толст. Если стар – что слишком стар. Если тощий – слишком тощ. Если белый – слишком бел. Если черный – слишком черен.

grem ★★★★★ ()
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.