Каждый раз, в течение многих лет, собирая пилотную версию мизерного проекта или простой утилиты, мне кажется, что уж в этот раз точно обойдусь обычным скриптом для сборки, и никакие сборщики проекта мне не понадобятся. Но суровая реальность приводит меня в чувство уже в течение первых нескольких минут работы. Сначала оказывается, что до невозможности простая программка нуждается в JSON-парсере, HTTP-запросах CURL и прочих библиотеках. А по мере возбуждения хотелок эти все зависимости нарастают как снежный ком. И все мечты быстро скомпилировать страничку кода встречают на каждом шаге всё новые и новые проблемы.
Вот сегодня и расскажу о том, какие бывают способы борьбы с зависимостями и сборки проекта из множества файлов на Си++. Заодно те, кто не любят Си++, смогут порадоваться «прелестям» этого процесса. И хоть тема очень важная для программистов, но я обратил внимание, что даже многолетний опыт не гарантирует понимания этих процессов. Но сразу предупреждаю — история длинная даже с учетом всех попыток не убегать на смежные темы.
Немного лирики
Натолкнула меня написать этот пост следующая история. Я хотел собрать минимального Telegram-бота на Си++, и всё, что мне нужно было, — это подключить библиотеку tgbot-cpp:
git clone https://github.com/reo7sp/tgbot-cpp
cd tgbot-cpp
mkdir build
cd build
cmake ..
make -j 96
Но не тут-то было. Tgbot-cpp зависит от библиотек curl (для работы с HTTPS) и boost (для асинхронной работы с сетью), и они есть уже в моей системе Gentoo самой последней версии на сегодняшний день: dev-libs/boost-1.87.0 и net-misc/curl-8.11.1-r2. И это проблема! Потому что с версии boost 1.69.0 поменяли и вырезали на корню множество устаревших функций (io_service заменили на io_context). Конечно же, о том, что они устаревшие, всех предупреждали ещё в Ветхом Завете три тысячелетия назад, но пока всё компилилось и работало, ни у кого не было причин обращать внимание на предупреждения при компиляции и переделывать код. Однако наступает момент, когда приходится пересматривать подход в соответствии с новыми требованиями библиотеки, иначе старый код перестанет работать.
Tgbot-cpp отказался работать с новым boost. Править библиотеку tgbot-cpp у меня ради небольшого эксперимента желания не было, поэтому меня посетила простая идея использовать старый boost. Но установить эту либу в систему из пакетов возможности нет, а через make install исключено, потому что это ключевая библиотека, от которой зависят другие пакеты. Все установленные программы завязаны на последний boost. И пакетов с программами и библиотеками, зависящих от boost, около полусотни. Значит нужно собрать старый boost отдельно от системы, подключив к проекту через cmake. Опа! Но cmake тоже новый, и на старую версию boost он ругается ошибками.
Ну и откажусь от установки в систему. Всё равно плохая идея с учётом того, что разработка проектов с установкой зависимостей в систему захламляет её. Я умный. Притащу с GitHub исходники boost в проект и скомпилирую через Cmake. Но не тут-то было. Оказалось, что в новых версиях такого мощного и любимого всеми сборщика проектов Cmake изменились политики поиска библиотек через функцию FindPackage, что он категорически отказался находить старые версии, как я ни плясал над ним. В общем решил я свою задачу трёх тел только настроив сборку в Docker-контейнере, выбрав специально не самую свежую Ubuntu 20.04, соответственно, с не самым свежим софтом на борту.
Подобная проблема при сборке бывает не часто, но иногда складываются звезды в подобные ситуации, поэтому программисты на Си++ обязаны, как и девопсы, прекрасно понимать процесс сборки проекта, а также понимать максимальное количество решений этой проблемы. Поэтому самое время рассказать, как мы за много лет к такому пришли, как компилировали свои проекты древние люди и что с этим делать сейчас.
Си/Си++ — статические и динамические библиотеки
Почти всё в посте касается как языка Си, так и Си++, но о важных различиях я постараюсь рассказать. Компиляция проекта на Си++ состоит из нескольких этапов: препроцессинг текстовых файлов, компиляция cpp-файлов в бинарные объекты и компоновка (линковка, связывание). Если вы не углублялись в этапы компиляции на Си++, то для дальнейшего понимания сути сегодняшнего поста настойчиво рекомендую ознакомиться. Эти этапы уже обсуждались на Хабре.
Конечным результатом сборки проекта на Си++ будет либо запускаемый файл (например, ELF или EXE), либо библиотека, которую будут использовать другие программисты. Этим другим программистом можете быть и вы в будущем, поэтому есть резон собрать её качественно с пониманием всех деталей дальнейшей поддержки. Но даже если вы не собираетесь создавать свои библиотеки, то вам никак не избежать использования чужих библиотек в проектах чуть сложнее «Hello world». Поэтому для сборки проекта обязательно понимание, что такое библиотека и как её готовить.
Библиотека — это сборник откомпилированного кода в виде классов или функций. Библиотека может быть динамической либо статической.
▍ Динамические библиотеки
Динамические библиотеки — это .so или .dll файлы в зависимости от операционной системы.
so — shared object на Linux,
dll — dynamic-link library на Windows.
Давайте для примера исследуем команду ls. Напишите в командной строке своего Линукса:
$ ldd /bin/ls
# ldd /bin/ls
linux-vdso.so.1 (0x00007ffef39e3000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007efe5aa77000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007efe5a885000)
libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007efe5a7f4000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007efe5a7ee000)
/lib64/ld-linux-x86-64.so.2 (0x00007efe5aad0000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007efe5a7cb000)
Вы увидите все .so файлы, которые должны присутствовать в системе для запуска этой команды. Если в выводе ldd напротив файла .so не написан полный путь, значит эта зависимость не найдена. ОС ищет динамические библиотеки для запуска в директориях, которые перечислены в следующих файлах:
/etc/ld.so.conf
/etc/ld.so.conf.d/*
По умолчанию при компиляции программ компиляторы подключают библиотеки динамически. Это означает, что когда мы скопируем нашу программу на другую систему, она будет требовать, чтобы в той системе были доступны необходимые .so файлы. Иногда требуются .so именно той версии, которая использовалась при линковке. Версия динамической библиотеки — это цифры после расширения .so. Например, libc.so.6.
В двух словах динамическая библиотека — это скомпилированный бинарный код библиотеки в отдельном файле so/dll, который загружается в память при запуске программы. При динамической линковке в программе грубо говоря остается лишь ссылка на этот код в файле .so без копирования самого кода. Такие ссылки на функции называются символами. Увидеть символы запускаемой программы, например /bin/ls, можно так:
$ nm -D /bin/ls
00000000000231e8 D Version
0000000000018000 R _IO_stdin_used
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
U __assert_fail
U __ctype_b_loc
U __ctype_get_mb_cur_max
U __ctype_tolower_loc
U __ctype_toupper_loc
U __cxa_atexit
w __cxa_finalize
U __errno_location
U __fpending
U __fprintf_chk
U __freading
U __fxstat
U __fxstatat
w __gmon_start__
U __libc_start_main
U __lxstat
U __memcpy_chk
U __overflow
U __printf_chk
0000000000023280 B __progname
00000000000232a0 B __progname_full
U __snprintf_chk
U __sprintf_chk
U __stack_chk_fail
U __strtoul_internal
U __xstat
U _exit
0000000000016bb0 T _obstack_allocated_p
...
Адрес и буква 'B' возле символа означают, что эта функция находится в разделе BSS (block started by symbol), а вот буква 'U' означает Undefined и говорит нам о том, что символ используется в данном объектном файле, но его определение (код) находится в другом объектном файле.
T (text): Символ находится в секции .text (секция кода). Обычно это означает функцию.
D (data): Символ находится в секции .data (инициализированные данные). Обычно это означает глобальную или статическую переменную, которая инициализирована.
B (bss): Символ находится в секции .bss (неинициализированные данные). Обычно это означает глобальную или статическую переменную, которая не инициализирована.
U (undefined): Символ не определён в данном объектном файле. Это означает, что символ используется, но его определение находится в другом объектном файле или в другой библиотеке. Такие символы должны быть разрешены линковщиком во время сборки.
Другие типы (встречаются реже):
t (text, local): Аналогично T, но символ является локальным для данного объектного файла (static function).
d (data, local): Аналогично D, но символ является локальным для данного объектного файла (static variable).
b (bss, local): Аналогично B, но символ является локальным для данного объектного файла.
R (read-only data): Символ находится в секции .rodata (данные только для чтения). Обычно это константы или строковые литералы.
r (read-only data, local): Аналогично R, но символ является локальным.
N (debugging symbol): Символ используется для отладки (например, информация о строках кода, переменных).
C (common): Символ является «common» символом. Это специальный тип символа, который используется для неинициализированных глобальных переменных. Он похож на B, но обрабатывается линковщиком немного иначе.
— (no symbol): В некоторых случаях nm может выводить строки без типа символа. Это может означать, что строка представляет собой не символ, а другую информацию (например, адрес секции).
Дополнительные модификаторы:
Перед типом символа могут стоять другие буквы, указывающие на дополнительные свойства символа.
l (lowercase): Символ является локальным (static).
g (global): Символ является глобальным (доступным из других объектных файлов).
w (weak): Символ является «слабым». Это означает, что если линковщик не найдет сильного определения для этого символа, он может использовать слабое определение или вовсе проигнорировать его отсутствие.
▍ Статические библиотеки
Но почему не собрать весь код в сам EXE-файл чтобы не заморачиваться с наличием тысяч so/dll файлов и еще следить, чтобы каждый был нужной версии? Так действительно делают, когда хотят добиться максимальной переносимости, чтобы быть уверенными, что при переносе на другую систему наша программа не будет зависеть от наличия и версий установленных библиотек.
Но почему все программы не собирают статически всегда, если это так удобно и надёжно? Потому что при компиляции весь код из .a файла копируется в бинарный файл программы, и программа распухает как на дрожжах при подключении каждой дополнительной библиотеки. И самое страшное случится, если все программы, которые собраны статически, попытаются загрузиться в память одновременно. Каждая из программ будет содержать копии кода используемых библиотек. Таким образом, динамические библиотеки экономят нам память, загружаясь лишь в единственном экземпляре. Например, это позволит для одновременно запущенных libreoffice, qt, gimp, mplayer, chrome загрузить в память лишь одну общую копию libc, libjpeg, libpng и многих других общих библиотек.
Возьмём для примера основу всех программ на Си — библиотеку стандартных функций языка — GNU Linux C. В Linux Gentoo я через команду 'equery f sys-libs/glibc' могу увидеть все файлы, которые установлены с этой библиотекой. В других дистрибутивах на основе **apt**-менеджера пакетов, чтобы увидеть файлы из пакета, всё немного сложнее: сначала ищем, какие пакеты содержат в имени **libc**, устанавливаем **apt-file**, обновляем его базу данных и, в конце концов, исследуем найденный пакет:
# dpkg -l | grep libc
# apt install apt-file
# apt-file update
# apt-file list libc6
Вот несколько файлов из пакета libc, которые мы обсудим:
/usr/lib64/libc.a
/usr/lib64/libc.so
Мы видим, что libc есть в файлах .a и в файлах .so.
Внутрь файла с расширением '.a' можно заглянуть командой ar:
ar -t /usr/lib64/libc.a
fprintf.o
fwprintf.o
swprintf.o
vwprintf.o
wprintf.o
vswprintf.o
vasprintf.o
iovdprintf.o
vsnprintf.o
obprintf.o
asprintf_chk.o
dprintf_chk.o
fprintf_chk.o
fwprintf_chk.o
obprintf_chk.o
printf_chk.o
snprintf_chk.o
sprintf_chk.o
swprintf_chk.o
vasprintf_chk.o
vdprintf_chk.o
vfprintf_chk.o
vfwprintf_chk.o
vobprintf_chk.o
vprintf_chk.o
vsnprintf_chk.o
vsprintf_chk.o
vswprintf_chk.o
vwprintf_chk.o
wprintf_chk.o
dl-printf.o
...
Этот .a архив и есть статическая библиотека. Я показал часть из огромного списка файлов из этого архива, чтобы читатель сам мог догадаться, что представляет собой этот архив. Мы видим в именах .o файлов стандартные функции из библиотеки Си. Изначально каждая функция была отдельным исходным .c файлом. Именно такие же объектные файлы получаются, когда мы компилируем поэтапно файлы проекта на Си или Си++, чтобы в конце концов слинковать их потом в одну программу mainprogram.exe:
gcc myfunctions1.c -o myfunctions1.o
gcc myfunctions2.c -o myfunctions2.o
gcc mainprogram.c myfunctions1.o myfunctions2.o -o mainprogram.exe
Для примера я привел Linux Gentoo, потому что в этой системе практически все программы устанавливаются компилированием из исходников, в отличие от большинства других дистрибутивов. Установку статических библиотек в Gentoo можно контролировать, установив USE флаг 'static-libs' для пакетного менеджера emerge. Это очень удобно для разработчиков. В других дистрибутивах по умолчанию устанавливаются только динамические версии библиотек. Но если вам для разработки понадобятся статические библиотеки в других дистрибутивах Linux, то они иногда доступны в пакете dev либо в отдельном пакете, который содержит в конце названия '-static':
# apt install glibc-static
Если таких пакетов нет, то для сборки необходимых вам статических библиотек придётся прибегнуть к самостоятельной сборке из исходников. Но стоит ещё учитывать, что не каждая библиотека может быть скомпилирована статически.
▍ Создание своей библиотеки
Создать свою динамическую библиотеку проще простого.
Создаём файл с функциями нашей библиотеки mylib.c:
#include <stdio.h>
void my_function(const char *message) {
printf("Message from library: %s\n", message);
}
int my_add_function(int a, int b){
return a + b;
}
Компилируем в объектный файл:
gcc -c -fPIC mylib.c -o mylib.o
Опция -c инструктирует gcc не линковать и остановиться сразу после компиляции.
Опция -fPIC (Position Independent Code) крайне важна для создания динамических библиотек. Она генерирует код, который может быть загружен в любую область памяти без необходимости изменения адресов.
-o предписывает сохранить работу в указанный файл (output) mylib.o
Заглянуть внутрь полученного объектного файла можно утилитой objdump из пакета binututils.
$ objdump -t mylib.o
mylib.o: file format elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 mylib.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .rodata 0000000000000000 .rodata
0000000000000000 g F .text 000000000000002e my_function
0000000000000000 *UND* 0000000000000000 printf
000000000000002e g F .text 0000000000000018 my_add_function
Как пользоваться подобными утилитами, почитайте в отдельных постах, а мы поскачем галопом по Европам дальше. В выводе мы видим отсылки как на наши функции my_add_function(), my_function(), так и на используемую из нашего кода функцию Си printf(). Так и должно быть.
Объектный файл — это ещё не библиотека. Сделаем из него динамическую библиотеку:
gcc -shared -o mylib.so mylib.o
Вот теперь мы получили mylib.so. Если мы проверим зависимости этой динамической библиотеки через 'ldd mylib.so', то увидим, что она зависит от libc.so.6. Именно об этом и говорит надпись UND (undefined) возле строки printf в выводе objdump: эта функция не определена в нашем объектом файле.
Для сборки статической библиотеки нужно не использовать опцию -fPIC при компиляции объектного файла:
gcc -c mylib.c -o mylib.o
ar rcs mylib.a mylib.o
ar: Утилита архивации из пакета binutils.
r: (insert or replace) Вставить или заменить файлы в архиве.
c: (create) Создать архив, если он не существует.
s: (create an index) Создать индекс архива. Это необходимо для ускорения линковки.
На выходе получим статическую библиотеку mylib.a, содержимое которой можно проверить командой 'ar -t mylib.a', которая покажет, что в нашем архиве имеется один объектный файл mylib.o.
Искажение имён (name mangling)
Ещё одна очень важная вещь, на которую нужно обратить внимание. Для создания mylib.o мы использовали Си компилятор gcc. Давайте посмотрим, какой объект сгенерирует Си++ компилятор g++, запущенный точно с теми же аргументами на том же самом файле:
$ g++ -c mylib.c -o mylib.o
$ objdump -t mylib.o
mylib.o: file format elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 mylib.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .rodata 0000000000000000 .rodata
0000000000000000 g F .text 000000000000002e _Z11my_functionPKc
0000000000000000 *UND* 0000000000000000 printf
000000000000002e g F .text 0000000000000018 _Z15my_add_functionii
О боже! Что произошло с нашими функциями? my_function стала _Z11my_functionPKc, а my_add_function превратилась в _Z15my_add_functionii. Это называется искажение имён, или в оригинале name mangling. На русском языке по этой теме материалов не густо, поэтому даю на английском.
В двух словах искажение имен предоставляет средства для кодирования дополнительной информации в имени функции, структуры, класса или другого типа данных, чтобы передать больше семантической информации от компилятора к компоновщику. Каждый символ перед именем функции и после несёт информацию о типах принимаемых и возвращаемых значений. Цифра означает длину имени функции. Если приноровиться, то можно деманглить на пальцах без компьютера. Искажение имён есть не только в Си++, но в Java, Python, FreePascal, RUST, Fortran, Objective-C, SWIFT.
Установите утилиту c++filt и декодируйте имена Си++ на здоровье:
$ c++filt _Z11my_functionPKc
my_function(char const*)
$ c++filt _Z15my_add_functionii
my_add_function(int, int)
Разбиение проекта на файлы
При написании программы принято разбивать код на файлы c, cpp с функциями или классами, группируя их по назначению или по какой-либо абстракции, чтоб было легко находить и работать с ними, не держа единовременно в памяти все мелочи о проекте. Файлы группируют по директориям. А чтобы эти взаимосвязанные друг с другом файлы можно было скомпилировать, по отдельности выделяют прототипы функций, классов и переменные в отдельные заголовочные файлы с расширением .h или .hpp (header). Так, компилятор при сборке определенного cpp-файла знает о существовании объявленных функций, но не держит в памяти их код.
Лайфхак
Для понимания процесса компиляции и ловли багов, связанных с директивами препроцессора, такими как циклические #include, можно остановить компилятор после фазы препроцессора. Для gcc и g++ это делается опцией -E:
g++ -E main.cpp
Ручная компиляция
Типичная строка компиляции компилятором gcc выглядит так:
g++ main.cpp myfunctions1.cpp myfunctions2.cpp
Именно так учат студентов компилировать несколько файлов. Эта процедура совместит препроцессинг, компиляцию и линковку. Однако для компиляции таким способом реального проекта вроде Chrome browser или GIMP памяти никакого компьютера не хватит. Кроме этого, на большом количество файлов с такой единовременной компиляцией и линковкой во время разработки мы столкнемся с тем, что для каждого выходного бинарного файла все исходные cpp нужно перекомпилировать заново. Поэтому правильно будет разбить компиляцию на этапы:
g++ -c main.cpp -o main.o
g++ -c myfunctions1.cpp -o myfunctions1.o
g++ -c myfunctions2.cpp -o myfunctions2.o
g++ main.o myfunctions1.o myfunctions2.o -o program
Опция -c говорит компилятору остановиться после компиляции, не делая линковку, а опция -o предписывает сохранить полученный код в объектный файл main.o.
Так, изменив только один файл для пересборки проекта, нам достаточно перекомпилировать только его и заново слинковать выходной бинарник. Можно сохранить эти действия в bash-скрипт, но тогда понадобится дополнительный код в скрипте, который будет отслеживать изменённые файлы и перекомпилировать только их. Этот код будет одинаковым велосипедом для всех подобных скриптов. Такой велосипед уже изобретён и является утилитой make, которая компилирует проект по рецепту из Makefile.
Про инструмент make для обработки Makefile и про другие сборщики проектов расскажу в следующем посте.
© 2025 ООО «МТ ФИНАНС»
Telegram-канал со скидками, розыгрышами призов и новостями IT ?
Комментарии (24)
unreal_undead2
28.01.2025 13:12Таким образом, динамические библиотеки экономят нам память, загружаясь лишь в единственном экземпляре.
Не упомянули один важный момент - при динамической линковке мы автоматически подхватим оптимизации и фиксы в новых версиях тех же libc или libssl при апдейте системы. И ещё нюанс с использованием LGPL библиотек в проприетарных продуктах.
SergeyNovak Автор
28.01.2025 13:12Верно подмечено. Не просто так динамическая линковка везде является дефолтом если на смену подхода нет очень веских причин.
А вот по поводу LGPL меня эта информация обходила стороной и стала новостью:
LGPL позволяет использовать библиотеку в проприетарном продукте, если библиотека подключается динамически (например, через динамические библиотеки.dll
,.so
).unreal_undead2
28.01.2025 13:12Видимо разработкой больших коммерческих приложений мало занимались ) Обычно в компаниях тренинги на эту тему проводят.
m-n-o
28.01.2025 13:12Приятная статья, спасибо большое!
Лично для меня ldd было спасением, когда я пеовый раз компилировал на серваке с кучей версий компиляторов и библиотек.
unreal_undead2
28.01.2025 13:12Ещё про переменную окружения LD_DEBUG стоит знать для расследования случаев посложнее (ldd показывает, что всё есть, но программа всё равно не грузится).
qweururu
28.01.2025 13:12Какое-то неосиляторство(а скорее попытка похайпить на модной среди низкоквалифицированной публики теме).
зависит от библиотек curl (для работы с HTTPS) и boost (для асинхронной работы с сетью), и они есть уже в моей системе Gentoo самой последней версии на сегодняшний день: ... И это проблема! Потому что с версии boost 1.69.0 поменяли и вырезали на корню множество устаревших функций
Во первых, по слухам в генте есть возможность ставить сколько угодно версий библиотек/приложений рядом(кажется там это называется слоты).
Во вторых
меня посетила простая идея использовать старый boost. Но установить эту либу в систему из пакетов возможности нет, а через make install исключено, потому что это ключевая библиотека, от которой зависят другие пакеты
никто не рассказал, что через "make install" можно поставить в отдельный префикс и не трогать всё остальное.
В третьих
boost (для асинхронной работы с сетью)
это судя по всему асио, которое ho и которое вообще не нужно собирать/ставить куда-то. Кстати, как и любая адекватная библиотека для цпп.
Значит нужно собрать старый boost отдельно от системы, подключив к проекту через cmake. Опа! Но cmake тоже новый, и на старую версию boost он ругается ошибками.
Туда же.
Но даже если вы не собираетесь создавать свои библиотеки, то вам никак не избежать использования чужих библиотек в проектах чуть сложнее «Hello world».
Это откуда-то из скриптухи, где без библиотек ничего написать невозможно в принципе. Последующие тезисы(динамическая линковка, раздельная сборка) в ту же сторону.
SergeyNovak Автор
28.01.2025 13:12Есть в Gentoo пакеты, которые позволяют установить несколько версий и выбрать через симлинк дефолтную версию.
java:
# eselect java-vm list Available Java Virtual Machines: [1] openjdk-bin-8 [2] openjdk-bin-11 [3] openjdk-bin-17 [4] openjdk-bin-21 system-vm
gcc:# eselect java-vm list Available Java Virtual Machines: [1] openjdk-bin-8 [2] openjdk-bin-11 [3] openjdk-bin-17 [4] openjdk-bin-21 system-vm
Но должно быть нечто, позволяющее выбрать необходимую версию. В основном это модули для eselect, который позволяет контролировать используемую версию. Именно такие пакеты с модулями для eselect обычно имеют слоты (до двоеточия версия, после - номер слота):sys-kernel/gentoo-sources-6.13.0:6.13.0
Ни boost ни cmake не относятся к подобным пакетам, у которых можно было бы установить одновременно несколько версий:# equery list -p cmake * Searching for cmake .... [-P-] [ ] dev-build/cmake-3.28.5:0 [-P-] [ ] dev-build/cmake-3.30.5:0 [-P-] [ ] dev-build/cmake-3.30.6:0 [-P-] [ ] dev-build/cmake-3.31.3:0 [-P-] [ ] dev-build/cmake-3.31.4-r1:0 [IP-] [ ] dev-build/cmake-3.31.5:0 [-P-] [ -] dev-build/cmake-9999:0
При непонимании темы лучше попросить объясненений, а не приправлять это непонимание хамством для пущей эффектности.никто не рассказал, что через "make install" можно поставить в отдельный префикс и не трогать всё остальное.
Как поведет себя FindPackage() если cmake тупо ляжет в отдельном префиксе? Или же немного нужно потрогать и даже поплясать чтобы сборщик смог им нормально воспользоваться?NightShad0w
28.01.2025 13:12CMake можно тыкать в префикс, где лежат отдельно собранные как надо библиотеки.
Практически каждый модуль CMake дополнительно имеет свой аргумент <Package>_ROOT, чтобы ему дополнительно ткнуть пальцем в конкретный модуль.
Конкретно связка Boost+CMake прошла тяжелый путь от CMake-модуля до CMake-конфига, и какие-то сочетания друг с другом плохо стыкуются. Это да. Ничего не мешает в отдельный префикс положить целый CMake нужной версии.
Собственно о статье - начали с нестыковок между системный окружением и требованиями конкретного проекта, а потом перешли на базовые принципы линковки, заметя проблему под ковер Docker-а. А после сборки в Ubuntu 20, как бинарь в хост-Gentoo использовать?
Опишу собственный подход к такой проблеме:
Системное окружение остается системным окружением, согласно завету пакетного менеджера. Проект или навязывает необходимые зависимости и окружение, или мы сами выбираем под что будем собирать и запускать. В отдельный префикс вручную по инструкциям собираем все то, что или недоступно в системе, или нас не устраивает. На этом шаге больше всего проблем, ибо даже одну систему сборки разные проекты используют по разному, а еще есть meson, autotools, make, и прочий зоопарк, и никто не читает инструкции для инструмента. После страданий с указанием префикса для тех зависимостей, которые не могут с префиксом и out-of-source сборку, берем свой проект, тыкаем CMake в нужные префикс и полетели. И это для разработки. Для релиза/деплоя все пройденные этапы упаковываем в скрипты сборки под целевую платформу, и объединяем со сборкой нашего проекта, кладя в тот же префикс. Итого, имеет почти самодостаточную директорию проекта со всеми зависимостями. Отсюда уже варианты - упаковка в контейнеры, в пакеты для целевой платформы, архивы и все такое.
Описанный подход прекрасно работает для Linux семейства, позволяя деволопить в одной системе, а деплоить в другие, где зависимости нужных версий не доступны. Чуть менее комфортно поддерживать или девелопить в Windows, Mingw сглаживает трудности, и если оставаться в пределах Mingw окружения - то все то же самое. А при намерении нативно собираться MSVC и следовать заветам Windows окружения, приходится собирать зависимости 'под Windows', уговаривать CMake дополнительными флагами и опциями, и страдать над GNU библиотеками, которым для MSVC сборки надо через Cygwin присовывать в autotools черте-что, и не косячить с путями и слэшами.
qweururu
28.01.2025 13:12Как поведет себя FindPackage() если cmake тупо ляжет в отдельном префиксе?
Также, как и не в отдельном. И да, все(или почти) сборщики имеют всякие конфигурации наподобие такой - https://cmake.org/cmake/help/latest/variable/CMAKE_PREFIX_PATH.html - там подобного десятки и никакой проблемы нет.
При непонимании темы лучше попросить объясненений, а не приправлять это непонимание хамством для пущей эффектности.
Я могу допустить свою неправоту насчёт генты(о чём сообщил сразу), но почему вы не попросили объяснений насчёт cmake/boost/прочего в отдельных префиксах, а вместо этого родили очередную статью "как же сложно собрать"? Почему вы пытаетесь собирать ho либу и при этом рассказываете кому-то про понимание/непонимание?
lrrr11
28.01.2025 13:12про cmake ничего не понял
Значит нужно собрать старый boost отдельно от системы, подключив к проекту через cmake. Опа! Но cmake тоже новый, и на старую версию boost он ругается ошибками.
что мешает скачать старый cmake с cmake.org или github (gitlab)? Распаковать в какую-нибудь папку, добавить в PATH и вуаля, я так делал.
Но не тут-то было. Оказалось, что в новых версиях такого мощного и любимого всеми сборщика проектов Cmake изменились политики поиска библиотек через функцию FindPackage, что он категорически отказался находить старые версии, как я ни плясал над ним
это видимо про политику CMP0167, но что мешает вернуть старое поведение через
cmake_policy
? Я посмотрел в версии 3.31,FindBoost.cmake
пока на месте.SergeyNovak Автор
28.01.2025 13:12Такой cmake будет использовать модули поиска пакетов из системной директории потому что понятия не будет иметь как найти эти скрипты "в какой-нибудь папке". Вот у меня он найдет и использует вот этот потому что PATH касается только основного бинаря:
/usr/share/cmake/Modules/FindBoost.cmake
Самый простой и надежный вариант изолировать сборщик и зависимости от системы это Docker.
wl2776
28.01.2025 13:12А какие версии boost и libcurl в итоге нужны? В conancenter около десятка разных имеется под разные ОС и архитектуры. Cкачиваются и ставятся одной командой, сonan install —requires=...
Эта команда также создает все нужные файлы, чтобы CMake нормально все собрала
vitaly_KF
28.01.2025 13:12За вторую часть статьи по библиотекам спасибо.
Но что касается первой части - все это давно решено пакетными менеджерами cpp, вот например: https://vcpkg.link/ports/tgbot-cpp/v/1.7.3/0
Соберет все что нужно со всеми зависимостями, хоть статик хоть динамик.
Также, насчет статической линковки, поправьте меня, но насколько я помню, в статье неправильно написано - из .a файла в бинарь слинкуются не все объектники, а только те, что реально используются.
Vad344
28.01.2025 13:12самое страшное случится, если все программы, которые собраны статически, попытаются загрузиться в память одновременно. Каждая из программ будет содержать копии кода используемых библиотек
А вы в реальности сталкивались с этой "проблемой"?
Я вот динамические разделяемые библиотеки использую, но совсем для другого: как средства расширения функциональности (плагины и т.п.), а также в случае, когда статическая линковка невозможна (С - библиотека и какой-нибудь Бэйсик-интерпретатор, например), или когда реально нужно сократить объем передаваемых по сети данных при обновлении софта (до сих пор есть весьма медленные каналы связи), для разделения логики, или по лицензионным причинам. С нехваткой памяти из-за объема кода ни разу не сталкивался.
SergeyNovak Автор
28.01.2025 13:12Не сталкивался потому что никому в голову не приходило собрать статически бинарь от какого-то браузера или аналогичной софтины. На практике часть библиотек даже невозможно слинковать статически. Вот берем chromium-browser:
ls -l /usr/lib64/chromium-browser/chrome -rwxr-xr-x 1 root root 272288544 Jan 20 18:45 /usr/lib64/chromium-browser/chrome
272 мегабайта с динамической линковкой, Карл!
А теперь смотрим список сколько библиотек он тянет из системы:ldd /usr/lib64/chromium-browser/chrome linux-vdso.so.1 (0x00007ffeebaff000) libgobject-2.0.so.0 => /usr/lib64/libgobject-2.0.so.0 (0x00007f6b05efa000) libglib-2.0.so.0 => /usr/lib64/libglib-2.0.so.0 (0x00007f6b05dac000) libsmime3.so => /usr/lib64/libsmime3.so (0x00007f6b05d7d000) libnss3.so => /usr/lib64/libnss3.so (0x00007f6b05c3d000) libnssutil3.so => /usr/lib64/libnssutil3.so (0x00007f6b05c0e000) libnspr4.so => /usr/lib64/libnspr4.so (0x00007f6b05bcd000) libdbus-1.so.3 => /usr/lib64/libdbus-1.so.3 (0x00007f6b05b7a000) libatk-bridge-2.0.so.0 => /usr/lib64/libatk-bridge-2.0.so.0 (0x00007f6b05b3b000) libatk-1.0.so.0 => /usr/lib64/libatk-1.0.so.0 (0x00007f6b05b14000) libcups.so.2 => /usr/lib64/libcups.so.2 (0x00007f6b05a83000) libgio-2.0.so.0 => /usr/lib64/libgio-2.0.so.0 (0x00007f6b05894000) libfontconfig.so.1 => /usr/lib64/libfontconfig.so.1 (0x00007f6b05846000) libz.so.1 => /usr/lib64/libz.so.1 (0x00007f6b05829000) libzstd.so.1 => /usr/lib64/libzstd.so.1 (0x00007f6b0576c000) libexpat.so.1 => /usr/lib64/libexpat.so.1 (0x00007f6b05742000) libpng16.so.16 => /usr/lib64/libpng16.so.16 (0x00007f6b05708000) libwebpdemux.so.2 => /usr/lib64/libwebpdemux.so.2 (0x00007f6b05701000) libwebpmux.so.3 => /usr/lib64/libwebpmux.so.3 (0x00007f6b056f4000) libwebp.so.7 => /usr/lib64/libwebp.so.7 (0x00007f6b05681000) libfreetype.so.6 => /usr/lib64/libfreetype.so.6 (0x00007f6b055b6000) libjpeg.so.62 => /usr/lib64/libjpeg.so.62 (0x00007f6b054fc000) libharfbuzz-subset.so.0 => /usr/lib64/libharfbuzz-subset.so.0 (0x00007f6b0538e000) libharfbuzz.so.0 => /usr/lib64/libharfbuzz.so.0 (0x00007f6b0524e000) libopenh264.so.7 => /usr/lib64/libopenh264.so.7 (0x00007f6b0514e000) libm.so.6 => /usr/lib64/libm.so.6 (0x00007f6b0509c000) libX11.so.6 => /usr/lib64/libX11.so.6 (0x00007f6b04f54000) libXcomposite.so.1 => /usr/lib64/libXcomposite.so.1 (0x00007f6b04f4f000) libXdamage.so.1 => /usr/lib64/libXdamage.so.1 (0x00007f6b04f4a000) libXext.so.6 => /usr/lib64/libXext.so.6 (0x00007f6b04f35000) libXfixes.so.3 => /usr/lib64/libXfixes.so.3 (0x00007f6b04f2d000) libXrandr.so.2 => /usr/lib64/libXrandr.so.2 (0x00007f6b04f1e000) libXtst.so.6 => /usr/lib64/libXtst.so.6 (0x00007f6b04f16000) libgbm.so.1 => /usr/lib64/libgbm.so.1 (0x00007f6b04f0f000) libxcb.so.1 => /usr/lib64/libxcb.so.1 (0x00007f6b04ee3000) libxkbcommon.so.0 => /usr/lib64/libxkbcommon.so.0 (0x00007f6b04e9a000) libffi.so.8 => /usr/lib64/libffi.so.8 (0x00007f6b04e8d000) libpango-1.0.so.0 => /usr/lib64/libpango-1.0.so.0 (0x00007f6b04e1f000) libcairo.so.2 => /usr/lib64/libcairo.so.2 (0x00007f6b04cdb000) libudev.so.1 => /usr/lib64/libudev.so.1 (0x00007f6b04c85000) libasound.so.2 => /usr/lib64/libasound.so.2 (0x00007f6b04b97000) libpulse.so.0 => /usr/lib64/libpulse.so.0 (0x00007f6b04b41000) libFLAC.so.12 => /usr/lib64/libFLAC.so.12 (0x00007f6b04adf000) libxml2.so.2 => /usr/lib64/libxml2.so.2 (0x00007f6b04985000) libatspi.so.0 => /usr/lib64/libatspi.so.0 (0x00007f6b0494c000) libminizip.so.1 => /usr/lib64/libminizip.so.1 (0x00007f6b0493e000) libxslt.so.1 => /usr/lib64/libxslt.so.1 (0x00007f6b048fb000) libgcc_s.so.1 => /usr/lib/gcc/x86_64-pc-linux-gnu/14/libgcc_s.so.1 (0x00007f6b048cd000) libc.so.6 => /usr/lib64/libc.so.6 (0x00007f6b046f3000) /lib64/ld-linux-x86-64.so.2 (0x00007f6b165fd000) libpcre2-8.so.0 => /usr/lib64/libpcre2-8.so.0 (0x00007f6b0464e000) libplc4.so => /usr/lib64/libplc4.so (0x00007f6b04647000) libplds4.so => /usr/lib64/libplds4.so (0x00007f6b04641000) libgnutls.so.30 => /usr/lib64/libgnutls.so.30 (0x00007f6b04443000) libgmodule-2.0.so.0 => /usr/lib64/libgmodule-2.0.so.0 (0x00007f6b0443c000) libmount.so.1 => /usr/lib64/libmount.so.1 (0x00007f6b043c7000) libsharpyuv.so.0 => /usr/lib64/libsharpyuv.so.0 (0x00007f6b043be000) libbz2.so.1 => /usr/lib64/libbz2.so.1 (0x00007f6b043a9000) libbrotlidec.so.1 => /usr/lib64/libbrotlidec.so.1 (0x00007f6b0439a000) libgraphite2.so.3 => /usr/lib64/libgraphite2.so.3 (0x00007f6b04375000) libstdc++.so.6 => /usr/lib/gcc/x86_64-pc-linux-gnu/14/libstdc++.so.6 (0x00007f6b040f7000) libXrender.so.1 => /usr/lib64/libXrender.so.1 (0x00007f6b040ea000) libdrm.so.2 => /usr/lib64/libdrm.so.2 (0x00007f6b040d3000) libXau.so.6 => /usr/lib64/libXau.so.6 (0x00007f6b040cd000) libXdmcp.so.6 => /usr/lib64/libXdmcp.so.6 (0x00007f6b040c5000) libfribidi.so.0 => /usr/lib64/libfribidi.so.0 (0x00007f6b040a3000) libxcb-render.so.0 => /usr/lib64/libxcb-render.so.0 (0x00007f6b04093000) libxcb-shm.so.0 => /usr/lib64/libxcb-shm.so.0 (0x00007f6b0408e000) libpixman-1.so.0 => /usr/lib64/libpixman-1.so.0 (0x00007f6b03fee000) libcap.so.2 => /usr/lib64/libcap.so.2 (0x00007f6b03fe1000) libpulsecommon-17.0.so => /usr/lib64/pulseaudio/libpulsecommon-17.0.so (0x00007f6b03f55000) libogg.so.0 => /usr/lib64/libogg.so.0 (0x00007f6b03f4b000) libicui18n.so.76 => /usr/lib64/libicui18n.so.76 (0x00007f6b03c15000) libicuuc.so.76 => /usr/lib64/libicuuc.so.76 (0x00007f6b03a10000) libXi.so.6 => /usr/lib64/libXi.so.6 (0x00007f6b039fc000) libidn2.so.0 => /usr/lib64/libidn2.so.0 (0x00007f6b039c7000) libunistring.so.5 => /usr/lib64/libunistring.so.5 (0x00007f6b037e1000) libtasn1.so.6 => /usr/lib64/libtasn1.so.6 (0x00007f6b037cc000) libhogweed.so.6 => /usr/lib64/libhogweed.so.6 (0x00007f6b0377f000) libnettle.so.8 => /usr/lib64/libnettle.so.8 (0x00007f6b0372d000) libgmp.so.10 => /usr/lib64/libgmp.so.10 (0x00007f6b03685000) libblkid.so.1 => /usr/lib64/libblkid.so.1 (0x00007f6b03625000) libbrotlicommon.so.1 => /usr/lib64/libbrotlicommon.so.1 (0x00007f6b03602000) libsndfile.so.1 => /usr/lib64/libsndfile.so.1 (0x00007f6b03576000) libasyncns.so.0 => /usr/lib64/libasyncns.so.0 (0x00007f6b03570000) libicudata.so.76 => /usr/lib64/libicudata.so.76 (0x00007f6b0170b000) libvorbis.so.0 => /usr/lib64/libvorbis.so.0 (0x00007f6b016db000) libvorbisenc.so.2 => /usr/lib64/libvorbisenc.so.2 (0x00007f6b01643000) libopus.so.0 => /usr/lib64/libopus.so.0 (0x00007f6b015e1000) libmpg123.so.0 => /usr/lib64/libmpg123.so.0 (0x00007f6b01598000) libmp3lame.so.0 => /usr/lib64/libmp3lame.so.0 (0x00007f6b01520000) libmvec.so.1 => /usr/lib64/libmvec.so.1 (0x00007f6b01425000)
Да это же половину всего дистрибутива нужно будет в память загрузить! И это только код не считая уже самого контента, для которого места уже совсем не останется.
CatAssa
28.01.2025 13:12При чем тут библиотеки, которые уже есть в системе. Явно же речь о причинах для создания собственных.
PS;
Ой, что это что такое хотя бы самая первая библиотека в вашем списке: linux-vdso.so.1? А где она находится ?
Ой, что это за циферки рядом с именем "файла" -
0x00007f6b05efa000? Неужели размер?
0x00007f6b05efa000 == 16 952 335 507 456 (dec) байт, серьезно? Почти 17 терабайт, какой кошмар!
SergeyNovak Автор
28.01.2025 13:12Странный комментарий. Я вообще не понимаю в чем вопрос и какие должны быть причины для создания собственных библиотек в контексте динамической или статической линковки.
Рядом с SO файлами в выводе ldd пишутся не террабайты, а контрольная сумма.И по поводу VDSO тоже немного ликбеза:
linux-vdso.so.1 — это виртуальный общий объект, предоставляемый ядром Linux. Это не физический файл, расположенный на диске, а механизм, предоставляемый ядром.
Цель vDSO (virtual dynamic shared object) — оптимизировать производительность определённых системных вызовов, позволяя им выполняться непосредственно в пользовательском пространстве, без необходимости переключения контекста в режим ядра.
Библиотека позволяет быстро выполнять определённые функции ядра, такие как функции времени, для доступа к которым не требуется какой-либо особый уровень привилегий. Вызов этих функций позволяет получить общедоступную системную информацию без фактического вызова системного вызова.
Имя vDSO отличается на разных архитектурах и часто его можно увидеть в выводе утилит, подобных ldd.
zloe_morkoffko
28.01.2025 13:12Рядом с SO файлами в выводе ldd пишутся не террабайты, а контрольная сумма.
А вот man говорит что это `For each dependency, ldd displays the location of the matching object and the (hexadecimal) address at which it is loaded`
DenSigma
28.01.2025 13:12Вот почему так-же нет в Java?! Один файл подправил - две трети файлов в проекте перекомпилируются.
SergeyNovak Автор
28.01.2025 13:12Не расстраивайся. Зато кофе можно будет пить несколько раз в день пока GC работает.
BulldozerBSG
28.01.2025 13:12Про динамические библиотеки в linux забыли рассказать довольно важную часть с soneme
SergeyNovak Автор
28.01.2025 13:12Расскажите что это
BulldozerBSG
28.01.2025 13:12soname - специальное поле записываемое в динамическую библиотеку, по которому на самом деле динамический линковщик ищет библиотеку. Для старта можно почитать статью на вики
Kelbon
понимаю, меня это натолкнуло написать свою библиотеку для тг ботов, где эта проблема решена https://github.com/bot-motherlib/TGBM