Как создавать, собирать, устанавливать и использовать пакеты с программами и библиотеками для UNIX-подобных систем

в 20:38, , рубрики: C, c++, GNU/Linux, open source, UNIX, Разработка под Linux

Речь пойдёт о программах и библиотеках для UNIX-подобных систем, распространяемых в виде исходного кода (в том числе в виде тарболлов), написанных обычно на C и C++ (хотя этот же порядок работы может применяться к софту на любом языке). Многие вещи в этой статье написаны применительно конкретно к GNU/Linux, хотя многое из статьи может быть обобщено и на другие UNIX-подобные ОС.

Под словом «пакет» я понимаю в этой статье пакет с исходными текстами, причём не пакет конкретного дистрибутива GNU/Linux, а просто пакет, исходящий от оригинальных авторов софта.

В этой статье я разберу следующие вопросы:

  • Вот скачал программу или библиотеку. Как её собрать и установить? Как воспользоваться библиотекой?
  • Что такое префикс (prefix) установки? В чём разница между сборкой и установкой? Куда обычно устанавливают программы?

Я разберу только совсем базовые вещи. Те, которые типичные участники сообщества свободного ПО, программирующие на C и C++ под UNIX-подобные системы, обычно уже знают. Как создавать тарболлы (на примере «голого» make) и как устанавливать чужие тарболлы. Advanced советы по созданию «хороших» пакетов я не дам. «Продвинутые» вещи читайте в документации систем сборки, в замечательной статье «Upstream guide» от Debian (в её конце есть ещё куча ссылок о создании «хороших» пакетов). Многое в этой статье можно было сделать по-другому, моя цель: дать хотя бы один способ, не пытаться объять необъятное.

Предупрежу: начало будет совсем простым, но ближе к концу будет всё поинтереснее.

И ещё одно предупреждение. Я написал эту статью наскоро для одного человека. А после написания подумал, мол, раз уж написал, выложу уж на Хабр, чтоб не пропала. Поэтому в статье есть недочёты типа нечёткого структуирования статьи, фраз типа "не осуществляет сама инклудивание" и пр. Я это исправлять не буду. Может когда-нибудь в следующей жизни, когда руки дойдут. Выбор был между тем, чтобы публиковать так или не публиковать вовсе.

Итак, начнём мы с того, что создадим пакет с программой Hello, world. Для начала определимся с системой сборки.

Собственно, сама сборка пакета обычно осуществляется с помощью программы make (хотя это не единственный путь). Конфиг для make обычно лежит в файле под названием Makefile.

Есть несколько вариантов: просто использовать make либо использовать совместно с make некую высокоуровневую систему сборки (обычно autotools или cmake), которая будет, собственно, генерировать конфиги для make.

Мы в нашем примере будет использовать только make для простоты. Итак, создаём hello.c:

#include <stdio.h>

int
main (void)
{
  printf ("Hello, world!n");
  return 0;
}

Теперь Makefile:

Внимание! Работает на GNU/Linux. Работа под macOS не гарантируется! Этот Makefile не является портируемым вообще на все UNIX-подобные системы! Подробнее будет дальше.

<------> здесь означает символ табуляции.

PREFIX=/usr/local
CC=cc
CFLAGS=

all: hello

hello: hello.c
<------>$(CC) $(CFLAGS) -o hello hello.c

install: all
<------>mkdir -p $(PREFIX)/bin
<------>install hello $(PREFIX)/bin/

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

Также в этой статье я предполагаю, что вы знаете уж совсем базовые вещи про make. Я имею в виду, что вы знаете, что Makefile — это описание дерева зависимостей, и что он нужен, чтобы собрать ровно те файлы, которые нуждаются в сборке. В этой статье я расскажу про использование make при создании пакетов, про «виртуальные» цели make такие как install и all. Ещё я предполагаю, что вы уже знаете совсем базовые вещи про то, как запускать компилятор из командной строки, т. е. вы знаете, что такое cc -o hello hello.c.

Итак, теперь давайте разбирать наш Makefile.

Для начала скажу следующее. Допустим, пользователь скачал пакет. Ему нужно сперва собрать его, т. е. получить бинарники в каталоге с самим пакетом (либо в случае out of tree build получить их в неком другом каталоге, что не меняет сути), а затем установить, т. е. скопировать полученные бинарники в место их окончательного хранения в системе.

То есть смотрите. Вы залогинены под юзером user. Ваш домашний каталог /home/user. Вы скачали пакет, распаковали его, скажем в /home/user/Desktop/foo. Далее вы его собрали. Собрали в этой же папке, /home/user/Desktop/foo. Либо, если речь идёт об out of tree build, вы создали ещё одну папку /home/user/Desktop/foo-build и собрали в ней. Теперь, после сборки вы решаете установить её в /usr/local. Вот тут уже вам нужны права root. Вы при помощи sudo устанавливаете эту программу в /usr/local.

Пара слов о размещении каталогов в системе. Разберу лишь некоторые каталоги. Более подробную информацию можно найти в Filesystem Hierarchy Standard (поддержан многими дистрибутивами GNU/Linux) и по команде «man 7 hier» в GNU/Linux и, возможно, других ОС. Во всяком случае я расскажу, как они были устроены, скажем, лет пять назад (т. е. где-то в 2012-м году), всякие новые веяния типа недавнего нововведения в Fedora «давайте-ка мы всё переместим в /usr» я рассматривать не буду.

/bin — это бинарники (т. е. испоняемые файлы), важные для работы системы на ранних стадиях загрузки.

/sbin — это то же самое с тем отличием, что эти бинарники обычно может запускать только root.

/lib — бинарные файлы библиотек, нужные для программ из /bin и /sbin

/usr — «вторая иерархия файлов», т. е. это как бы «ещё один /»

/usr/bin — то же, что /bin, но на этот раз это бинарники, некритичные для загрузки

/usr/sbin — то же, что /sbin, но опять-таки, некритичные для загрузки

/usr/lib — бинарные файлы библиотек, которые не нужны для программ из /bin и /sbin

/usr/include — хедеры библиотек

/usr/local — «третья иерархия файлов», она опять содержит /usr/local/bin, /usr/local/sbin, /usr/local/lib и /usr/local/include, на этот раз эти каталоги предназначены для «локального администратора». Что это значит? Это значит, что (в случае дистрибутивов GNU/Linux с пакетным менеджером) каталоги, о которых речь шла до этого, находятся под контролем пакетного менеджера. А вот каталоги внутри /usr/local находятся в распоряжении «локального администратора», т. е. вас. Если вы хотите поставить что-то из сорцов, в обход менеджера пакетов, то можно поставить туда.

Теперь по поводу prefix. Когда вы устанавливаете какой-нибудь пакет, вы должны указать ему так называемый prefix, т. е. каталог, в который всё будет установлено. В этом каталоге будет создан подкаталог bin, в него будет установлены бинарники, будет создан подкаталог lib, туда будут установлены бинарные файлы библиотек, и т. д.

То есть, например, вы устанавливаете пакет, указывая ему prefix /usr/local. Тогда бинарники пойдут в /usr/local/bin, бинарные файлы библиотек — в /usr/local/lib и так далее.

Теперь вернусь к разбору Makefile. PREFIX — это и есть тот prefix, про который я сейчас говорил. В качестве дефолтного префикса (пользователь сможет его переопределить) у нас указан /usr/local — хороший дефолтный выбор. Обычно его всегда указывают в качестве дефолтного префикса при создании пакетов (к сожалению, некоторые пакеты этого всё же не делают, дальше будет подробнее). Далее идёт CC. Это стандартное название для переменной make, в которую кладут компилятор, в данном случае cc. cc — это в свою очередь обычно используемая команда для запуска компилятора по умолчанию в данной системе. Это может быть gcc или clang. На некоторых системах команда cc может отсутствовать. CFLAGS — стандартное название для переменной с флагами компиляции.

Если просто набрать make, то будет выполнена та цель, которая идёт в Makefile первой. Обычная практика в том, чтобы называть её all. И такая цель обычно собирает весь проект, но ничего не устанавливает. Эта цель обычно «виртуальная», т. е. у нас нет файла под названием all. Т. к. наша задача собрать лишь один бинарник hello, то мы просто делаем all зависящим от hello. Далее идёт описание цели сборки hello. Его можно было в принципе разбить на два этапа: сборка hello.o из hello.c и сборка hello из hello.o. Я так делать не стал для простоты.

Далее идёт install, установка. Это тоже виртуальная цель. Она зависит от all. Это сделано на случай, если пользователь сразу наберёт «make install», без «make». В этой цели мы сперва создаём папку, куда будем устанавливать, т. е. как и следовало ожидать, $(PREFIX)/bin, а затем устанавливаем в неё утилитой install.

Что делает install? Это почти то же, что и cp. Точные отличия я и сам не знаю. Для установки программ нужно использовать install, а не cp.

Бинарник устанавливается в $(PREFIX)/bin, потому что именно так и нужно делать. Бинарники идут в подкаталог bin в prefix, бинарные файлы библиотек — в lib в PREFIX и т. д.

Здесь я предполагаю, что у вас на системе установлен так называемый BSD install. В GNU/Linux'е он есть. В некоторых системах его может не быть. Может быть какой-нибудь другой install, который в этой ситуации не сработает. Именно это я имел в виду, когда сказал, что работа в разных ОС не гарантируется.

Здесь я не рассматривал DESTDIR, который, между прочим, крайне рекомендуется использовать в Makefile. Я даже не рассматривал цель clean.

Окей, давайте теперь создадим окончательный тарболл, так называют файл с расширением .tar.gz, .tar.xz и так далее. Поместите файлы hello.c и Makefile в папку hello-1.0 (принято указывать номер версии при создании тарболла). Затем установите в качестве текущего каталога тот, который содержит hello-1.0 и наберите, например:

tar --xz -cf hello-1.0.tar.xz hello-1.0

Это создаст архив, внутри которого есть каталог hello-1.0, который содержит hello.c и Makefile. Таков способ распространения пакетов.

C++-вариант пакета. Исходник будет тем же, его нужно будет назвать hello.cpp. Makefile будет таким:

PREFIX=/usr/local
CXX=c++
CXXFLAGS=

all: hello

hello: hello.cpp
<------>$(CXX) $(CXXFLAGS) -o hello hello.cpp

install: all
<------>mkdir -p $(PREFIX)/bin
<------>install hello $(PREFIX)/bin/

Обратите внимание, что стандартное название переменной для флагов к компилятору C++ — это CXXFLAGS, а не CPPFLAGS. CPPFLAGS же — это название переменной для флагов к препроцессору C.

Теперь разберём, как устанавливать пакеты из сорцов (любые, программы и библиотеки). Независимо от системы сборки. Этот алгоритм будет пригоден в том числе для тарболла, который мы создали только что. Допустим, что пакет, который нужно собрать, тоже называется hello.

Первый шаг: скачать и зайти в папку с сорцами. Тут два варианта: скачиваем из системы контроля версий (я разберу для примера git) или скачиваем тарболл.

Первый вариант. git. Делаем clone:

git clone <репозиторий>

Это создаст папку hello. Без номера версии, т. к. мы скачали из системы контроля версий. Делаем cd в созданную папку, т. е. cd hello.

Второй вариант. Тарболл. Скачиваем тарболл. Потом набираем команду, скажем, tar -xf hello-1.0.tar.xz. Это создаст папку, например, hello-1.0. Обычно, если создатель пакета сделал всё правильно, имя папки будет содержать номер версии. Потом делаем cd в полученную папку, например, cd hello-1.0.

Теперь нужно собрать. Будем считать для простоты, что мы не будем делать out of tree build (если автор пакета требует out of tree build, там обычно будут написаны инструкции, как это сделать). А значит, собирать мы будем в этой же папке, где сорцы. Т. е. в этой папке, в которую мы сейчас сделали cd.

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

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

Обязательно указывайте prefix при сборке (что бы там не писал в инструкции автор). Если вы не укажите, то будет выбран дефолтный. Обычно это /usr/local, и это достаточно хороший выбор. А если нет? Что если автор пакета указал какой-то другой дефолтный префикс? Вы установите непонятно куда. В частности libqglviewer использует в качестве дефолтного префикса /usr, что совершенно неправильно (я отправил автору баг репорт). Итак, совершенно всегда указывайте префикс. Читайте инструкции, которые автор указывает на своём сайте и прикидывайте, куда впихнуть prefix.

Итак, какие могут быть системы сборки. Во-первых, может быть просто make. Такой вариант с голым make встречается редко. Один из немногих пакетов с «голым» make — bzip ( www.bzip.org ). В случае с нашим hello-1.0.tar.xz, который мы создали, у нас именно такой вариант.

Итак, собирать в случае голого make нужно так:

make PREFIX=/префикс-который-вы-хотите

(Конкретно в случае с bzip на этапе сборки указывать PREFIX не надо. Но теоретически можно представить себе пакет, который захардкоживает PREFIX внутрь бинарника. Поэтому в общем случае PREFIX нужен.)

Следующий вариант — это autotools. В этом случае собираем так:

./configure --prefix=/префикс-который-вы-хотите
make

Следующий вариант — cmake. Собираем так (обратите внимание на точку в конце команды cmake):

cmake -DCMAKE_INSTALL_PREFIX=/префикс-который-вы-хотите .
make

Откуда точка в конце? Дело в том, что cmake'у нужно передавать путь к сорцам. А поскольку у нас не out of tree build, мы собираем здесь же. Сорцы находятся там же, где находимся мы. Поэтому точка, т. е. текущий каталог.

В случае с autotools и cmake команда, генерирующая Makefile (т. е. ./configure или cmake) записывает prefix в конфиги к make, поэтому в команде make (и в команде make install, про которую речь пойдёт далее) указывать prefix уже не нужно.

Итак, собрали одним из этих способов. Что дальше? Теперь нужно установить.

В случае голого make это делается так:

make PREFIX=/префикс-который-вы-хотите install

Нужно будет указать тот же prefix, который вы указывали при сборке.

В случае autotools и cmake так:

make install

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

Окей, теперь посмотрим, что мы натворили. Допустим, что ставили мы не что-нибудь, а тот hello-1.0.tar.xz, который создавали до этого. Допустим также, что prefix, который мы указали, был /foo. Тогда в нашей системе появятся папки /foo, /foo/bin (если их не было до этого) и файл /foo/bin/hello. Что произошло? Указанная в командной строке при сборке переменная PREFIX=/foo переопределила заданную в Makefile PREFIX=/usr/local. В результате указанные в Makefile команды mkdir -p и install стали такими:

mkdir -p /foo/bin
install hello /foo/bin/

В результате бинарник положился в /foo/bin.

Теперь хочу ещё немного поговорить про префиксы. Какие префиксы вообще есть?

Префикс /. Вряд ли когда-нибудь вам придётся его выбирать. Он используется для программ, критичных для ранних стадий загрузки ОС (т. е. критичные элементы для загрузки находятся в /bin, /lib и т. д.) (впрочем, даже если вам нужно установить программу в /, её сперва устанавливают в /usr, т. е. собирают и устанавливают с префиксом /usr, а потом перемещают необходимое в / [т. е. перемещают из /usr/bin в /bin, скажем], во всяком случае именно так поступают авторы Linux From Scratch 7.10 с пакетом, скажем, bash).

Префикс /usr. Стандартный префикс, используемый обычно для программ, установленных через менеджер пакетов. То есть если вы установили программу через менеджер пакетов, она ведёт себя так, словно она собрана и установлена на вашей системе с префиксом /usr. Самому устанавливать пакеты с префиксом /usr нельзя.

Префикс /usr/local. Отличный префикс для установки туда программ самостоятельно. Хорош тем, что /usr/local/bin есть в дефолтном PATH (во всяком случае в дебиане). То есть сразу после установки программы вы сможете просто запускать программу по названию. Потому что бинарник лежит в /usr/local/bin, а /usr/local/bin есть в PATH. Плох тем, что там все программы лежат смешанно. Вот допустим, вы установили библиотеку foo, а потом библиотеку bar. Обе в этот prefix. Тогда дерево может выглядеть так (в совсем упрощённом виде):

/usr/local/include/foo.h
/usr/local/include/bar.h
/usr/local/lib/foo.so
/usr/local/lib/bar.so

Видите? Всё смешано. Нет единой папки, которая бы содержала «всё, связанное с foo» и другой папки, которая бы содержала «всё, связанное с bar». (Хотя это ваше дело, считать, что это действительно плохо или нет). Понятное дело, что такая же проблема присутствует при любой установке разных пакетов в один префикс. То есть префикс /usr страдает от того же: пакеты «размазаны» по системе (здесь уже речь идёт о пакетах, поставленных через менеджер пакетов, т. е. тех, которые, собственно, составляют систему). Собственно, это и есть одно из бросающихся отличий большинства UNIX-подобных систем от Windows. В Windows каждая программа находится в своей папке в Program Files. В большинстве UNIX-подобных систем она «размазана» по системе. Существуют дистибутивы GNU/Linux, «решающие» эту проблему, например, GoboLinux. Там каждый пакет в своём каталоге, как в Windows.

Префиксы вида /opt/XXX. Папку /opt предполагается использовать следующим образом: в ней нужно создавать подкаталоги, называть их названиями пакетов и использовать эти подкаталоги как префиксы. При таком подходе указанная выше проблема /usr/local (если считать её проблемой) исчезает. Каждый пакет будет установлен в свой каталог. Приведённый выше пример с foo и bar будет выглядеть так (я бы посоветовал в названии подкаталогов в /opt указывать ещё и номер версии):

/opt/foo-1.0/include/foo.h
/opt/foo-1.0/lib/foo.so
/opt/bar-2.0/include/bar.h
/opt/bar-2.0/lib/bar.so

Недостаток у такого решения тоже есть. Вам придётся самому добавлять все эти бесчисленные каталоги /opt/foo-1.0/bin (для каждого пакета) в PATH.

Префиксы, соответствующие домашним каталогам. Т. е., скажем, /home/user. Советую в случае, когда хочется поставить «только для себя», т. е. только для одного юзера. Или когда нет прав root. Возможно, ваши конфиги, поставляемые с ОС уже настроены таким образом, чтобы помещать ~/bin в PATH при условии, что такой каталог есть. Так что PATH будет настроен как надо.

Каждый префикс может содержать в себе свои bin, sbin, lib, include и т. д.

Итак, что из этого выбрать? Если нужно поставить на всей системе, то я бы посоветовал /opt/XXX. Я сам так обычно ставлю.

Теперь о сборке, установке библиотеки и её использовании. Собирается и ставится библиотека так же, как и любой другой пакет, я это уже рассказывал выше. Так что перейдём сразу к использованию. Вот мы установили библиотеку в некий префикс, допустим, /foo. Теперь в /foo/include появились хедеры этой библиотеки, а в /foo/lib — бинарные файлы библиотеки (.so — динамическая библиотека, либо .a — статическая, либо и то, и то).

Допустим, нужно собрать некий файл a.c с этой библиотекой. Как это сделать?

Во-первых, вверху файла нужно написать #include, соответствующий подключаему хедеру. Ну а собирать нужно так:

cc -c -I/foo/include a.c
cc -L/foo/lib -o a a.o -lfoo

Давайте разбираться. Для начала скажу, что I (большая английская и) и l (маленькая английская эль) — это две разные буквы. Не перепутайте их в приведённых командах.

Первая команда производит компиляцию, то есть создаёт a.o на основе a.c. Вторая — линковку, то есть окончательный бинарник на основании a.o.

В первой команде мы указали -I/foo/include. Это указание той папки, где нужно искать хедеры. Путь из этой опции соединится с файлом, указанным в #include. То есть если в командной строке указано -I/foo/include, а в файле написано #include <foo.h>, то получится /foo/include/foo.h, он-то и будет заинклуден.

Здесь опция -I/foo/include не осуществляет сама инклудивание. Она лишь указывает папку, где нужно искать, поэтому нужен ещё и #include. То есть нужен и -I/foo/include, и #include, одного из них недостаточно.

Линковка. -L/foo/lib — это указание папки, где нужно искать бинарные файлы библиотеки, т. е. файлы .so и .a. -lfoo — это указание на то, что нужно, собственно, прилинковать эту библиотеку к результирующему бинарнику. Имя библиотеки, указанное в опции -lfoo, соединится с папкой, указанной в -L/foo/lib и получится /foo/lib/foo, затем сюда прибавится .so (или .a) и опционально номер версии и получится /foo/lib/foo.so или, скажем, /foo/lib/foo.so.1. Это и будет тем именем .so-файла, который будет искаться.

Так же, как и при компиляции (a.o из a.c), нужны обе опции -L/foo/lib и -lfoo. -L/foo/lib указывает, где искать. А -lfoo даёт окончательную команду на прилинковку.

Вместо -lfoo можно прямо написать целиком путь до файла библиотеки, который нужно слинковать, например, /foo/lib/foo.so.1. Тогда опция -L/foo/lib не нужна. Получится так:

cc -o a a.o /foo/lib/foo.so.1

Библиотеку (будь то полный путь к библиотеке или опция вида -lfoo) нужно указывать после «своих» объектных файлов, в данном случае a.o. (Может, и не нужно, но на всякий случай лучше так делать.)

Можно соединить две наши команды в одну, тогда будет что-то такое:

cc -I/foo/include -L/foo/lib -o a a.c -lfoo

В случае, если библиотека установлена с префиксом /usr (то есть просто установлена через менеджер пакетов), то опции -I и -L не нужны, считайте, что -I/usr/include и -L/usr/lib у вас как бы уже есть. То же, возможно, относится к /usr/local.

Если существует некая библиотека под названием foo, то она обычно запаковывается для дебиан в пакеты с названиями libfoo (или libfoo1, libfoo2) и libfoo-dev. libfoo содержит файлы .so, а libfoo-dev — хедеры. То есть хитрые разработчики дебиан умеют из одного пакета сделать несколько. На сборочных машинах дебиана пакет собирается и расфасовывается в несколько пакетов.

Если вы установите у себя на машине libfoo и libfoo-dev, то результат будет как если бы вы сами собрали пакет foo из исходных кодов с префиксом /usr. У вас на системе будут, скажем, файлы:

/usr/include/foo.h (из пакета libfoo-dev)
/usr/lib/foo.so.1 (из пакета libfoo)

Напомню, что список файлов в данном пакете можно посмотреть по команде, скажем, dpkg -L libfoo. Ну а найти пакет по файлу с помощью dpkg -S /usr/include/foo.h.

Автор: safinaskar

Источник

Поделиться

* - обязательные к заполнению поля