§ 8. Системы сборки кода

Введение

01 Системы сборки кода — это специальные программы, которые собирают и пересобирают код проекта в автоматическом режиме по заранее заданным правилам. Эти системы определяют зависимости между файлами с исходным кодом и выходными файлами (программами, библиотеками и конфигурационными файлами) и в нескольких параллельных процессах выполняют команды компиляции для всех изменившихся со времени последней сборки файлов, соблюдая зависимости. Вторая задача систем сборок — это поиск в операционной системе и подключение к проекту библиотек и программ, которая реализуется наиболее удобными способами в зависимости от операционной системы.

02 В новых языках программирования (Rust, Go) параллельная сборка и поиск зависимостей уже встроены, но для существующих языков (C, C++, Fortran) это невозможно сделать, поэтому для них используют отдельные системы сборки. Про них и пойдет речь в этой лекции.

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

04 Как правило, системы сборки поддерживают опции для включения или отключения различных компонент программы. Результатом работы системы сборки является директория, в которой находятся сгенерированные файлы, а также файл с дальнейшими командами для подчиненной (более низкоуровневой) системы сборки. К высокоуровневым системам относятся Autoconf, Cmake, Meson, к низкоуровневым — Make, Ninja. Мы будем изучать Make, Meson и Ninja, а также программу для работы с зависимостями Pkg-config.

Make

05 Классической, а также самой простой системой сборки является GNU Make. Правила сборки для этой системы описываются в файле Makefile, и состоят из списка файлов, которые генерирует (выходные файлы) команда, двоеточия, списка файлов, которые команда использует (входные файлы) для генерации и команд генерации. Если время последнего обновления хотя бы одного входного файла позже времени последнего обновления хотя бы одного выходного файла, то команда выполняется. В противом случае выходные файлы считаются актуальными. Все команды в файле начинаются с символа табуляции.

06 Часто в списке выходных файлов указывают несуществующий файл, тогда команда запускается каждый раз, когда вводится команда make имя-файла. По умолчанию команда make собирает первый файл.

all:
	echo Hello
main.copy: main
	cp main main.copy
Пример Makefile.
make all       # сборка файла all
make           # сборка файла all
make main.copy # сборка файла main.copy
Примеры сборки с помощью Make.

07 Поскольку Make был разработан для сборки программ, то в него встроены удобные правила для компиляторов, которые работают даже без наличия в директории Makefile. Например, команда make main.o запустит команду компиляции прогаммы $CXX $CXXFLAGS -c -o main.o main.cc. Здесь компилятор и флаги компиляции задаются переменными среды CXX и CXXFLAGS, а входной файл определяется автоматически по названию выходного. Если переменные не определены, то используется компилятор по умолчанию (GCC) и флаги по умолчанию (по умолчанию их нет). Команда make main запустит коману линковки программы $CC $LDFLAGS -o main main.o. Встроенного правила для сборки библиотек в Make нет, но его легко написать самостоятельно.

make main.o                          # компиляция main.o из main.cc 
env CXXFLAGS='-O3' make main.o       # компиляция main.o из main.cc с максимальным уровнем оптимизации
env CXX='clang++' make main.o        # компиляция main.o из main.cc с помощью компилятора clang++
make main                            # линковка main из main.o
env CC=g++ LDFLAGS='-flto' make main # линковка main из main.o с флагом оптимизации
Примеры сборки кода с помощью Make.

08 Переменные среды, используемые Make для сборки программ, стали стандартом и для других систем сборки. Практически все системы сборки их поддерживают. Эти переменные чаще всего используются мейнтенерами пакетов для различных дистрибутивов Linux: в этих переменных указываются флаги, которые используются для сборки всех пакетов дистрибутива. Например, в некоторых системах используется флаг -fstack-protector-strong, который позволяет получить защиту от взлома программ даже при наличии в них ошибок работы с памятью.

ПеременнаяОписание
CCкомпилятор языка C и линковщик для всех языков
CFLAGSфлаги компилятора языка C
CXXкомпилятор языка C++
CXXFLAGSфлаги компилятора языка C++
FCкомпилятор языка Fortran
FFLAGSфлаги компилятора языка Fortran
LDFLAGSфлаги линковщика
Список переменных среды, используемых в Make и других системах сборки.

09 Основным недостатком Make является чрезмерная простота команд: в правилах сборки не учитываются заголовочные файлы, которые включаются в основные файлы с помощью макросов. Это означает, что при изменении заголовочного файла пересборка основного не произойдет. Это послужило причиной создания более совершенных и высокоуровневых систем сборок. Тем не менее, Make до сих пор используется в небольших проектах, в том числе, в проектах, где нужно собирать не код, а что-то другое. Причина этому — простота использования и наличие Make во всех дистрибутивах Linux.

Meson и Ninja

10 Любой проект Meson начинается с дерева директорий, в каждой из которых находится файл для сборки. В meson это файл meson.build. В проектах на C++ с небольшими вариациями используется следующее дерево директорий.

doc                 # документация                
src
├── test            # код модульных тестов
│   ├── component1  # структура папок такая же,
│   ├── component2  # как и в основном коде
│   └── component3
└── myproject       # основной код
    ├── component1 
    ├── component2 
    └── component3
Структура проекта.

11 Здесь myproject — название проекта, componentN — логическая единица проекта. Как правило, каждая компонента собирается в отдельную библиотеку (или исполняемый файл), которая затем присоединяется к основному исполняемому файлу. В больших проектах из одной компоненты могут получиться несколько библиотек или исполняемых файлов. При такой схеме название проекта и название компоненты являются частью пути до заголовочного файла, а имя файла совпадает с именем класса, который в нем объявлен. В больших проектах кроме класса в файле могут быть объявлены вспомогательные функции или классы. Чтобы использовать класс Ship из компоненты component1 в каком-либо файле проекта, его следует подключить, используя путь относительно директории src.

#include <myproject/component1/ship.hh>

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

13 Корневой файл meson.build для вышеописанной упрощенной структуры содержит параметры проекта: название, версию, язык, опции сборки по умолчанию. Команда project должна быть первой командой в корневом файле. Команда subdir исполняет команды из файла meson.build в указанной в качестве аргумента директории.

project(
    'myproject',                       # название проекта
    'cpp',                             # язык
    version: '0.1.0',                  # версия кода
    meson_version: '>=0.50',           # минимально поддерживаемая версия Meson
    default_options: ['cpp_std=c++20'] # используемый стандарт C++
)
subdir('src')
Корневой файл meson.build.
# сохранение пути до текущей директории
# для подключения заголовочных файлов
# в виде <myproject/...>
src = include_directories('.')
subdir('myproject')
Файл src/meson.build.
myproject_src = files([
    'main.cc' # список исходных файлов
])
executable(
    'myproject',              # название исполняемого файла
    include_directories: src, # где ищутся заголовочные файлы
    sources: myproject_src,   # список исходный файлов
    dependencies: [],         # зависимости проекта (если имеются)
    install: true             # устанавливать ли файл
)
Файл src/myproject/meson.build.

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

meson . build
cd build
ninja

После первичной инициализации после любого изменения кода достаточно набрать ninja для пересборки проекта. При этом пересоберется только измененная и зависимые от нее части кода.

Pkg-config

14 Для того чтобы подключить библиотеку к вашей программе, достаточно указать директорию с заголовочными файлами, директорию с библиотекой, а также название библиотеки с помощью опции -l. Если библиотека установлена в систему стандартным способом, то, как правило, достаточно только названия. Процесс подключения библиотек к проекту автоматизируется с помощью программы pkg-config, которая по названию пакета выдает флаги компилятора и флаги линковщика, которые необходимы для корректной сборки проекта с этой библиотекой.

КомандаОписание
pkg-config --cflags OpenCLвывести флаги компилятора для подключения библиотеки OpenCL
pkg-config --libs OpenCLвывести флаги линковщика для подключения библиотеки OpenCL
pkg-config --cflags 'OpenCL >= 1.2'вывести флаги компилятора для подключения библиотеки OpenCL версии не ниже 1.2
pkg-config --list-allвывести список библиотек, установленных в системе и имеющих файл pkg-config
Список команд pkg-config для подключения библиотек.

15 Файл pkg-config для своего проекта обычно делают из шаблона, в котором прописываются директории, в которые устанавливается проект. В Meson это можно сделать автоматически с помощью модуля pkgconfig. Файл установится также автоматически при установке всего проекта.

Name: OpenCL
Description: Open Computing Language Client Driver Loader
Version: 2.2
Libs: -L/usr/lib64 -lOpenCL
Cflags: -I/usr/include
Содержиое файла OpenCL.pc для библиотеки OpenCL.
pkgconfig = import('pkgconfig')
pkgconfig.generate(
    unistdx_lib,
    version: meson.project_version(),
    name: 'unistdx',
    description: 'C++ wrappers for libc',
)
Генерация файла unistdx.pc в Meson для проекта Unistdx.

16 Библиотеки также можно автоматически искать с помощью pkg-config. В Meson это команда dependency. В случае с Make команды поиска прописываются в CXXFLAGS и LDFLAGS.

env CXXFLAGS="$(pkg-config --cflags OpenCL)" LDFLAGS="$(pkg-config --libs OpenCL)" make
Подключение библиотеки в Make.
OpenCL = dependency('OpenCL')
executable(
    ...
    dependencies: [OpenCL],
    ...
)
Подключение библиотеки в Meson.

Задания

Make1 балл

17 Напишите Makefile с правилами для сборки программы, состоящей из файлов main.cc и main.hh: первый файл включает второй с помощью #include. Программа должна пересобираться при изменении main.hh. Команды сборки писать не надо, просто укажите зависимости между файлами.

#include "main.hh"
int main() { return 0; }
Файл main.cc.

Make + pkg-config1 балл

18 Соберите код из первого задания с библиотекой zlib. Библиотеку подключите с помощью pkg-config.

Meson1 балл

19 Соберите код из первого задания с библиотекой zlib с помощью Meson.

Git + Meson2 балла

20 Скачайте проект unistdx с помощью команды git. Соберите его с помощью Meson с флагами компилятора -march=native -O3 и флагом линковщика -flto.

Видео

Запись лекции 23.10.2021.