четверг, 4 февраля 2010 г.

Bash в примерах, часть 3

Подробно об основах программирования в Bash.
Изучение Ebuild системы

Резюме: В своей последней статье "Bash в примерах", Daniel Robbins показывает Ebuild систему Gentoo Linux, которая является прекрасным примером использования Bash. Шаг за шагом, он показывает, как была реализована сборочная система и затрагивает многие удобные методы Bash и разработку стратегии. К концу этой статьи, вы будете хорошо понимать то, как подготовить полномасштабные Bash-приложения, а также как при кодировать собственные ebuild`s.

Введение в Ebuild систему

Я действительно с нетерпением ждал этой третьей и заключительной статьи, потому что теперь мы уже знаем основы Bash программирования, и можем сосредоточиться на более сложных темах, таких как разработка приложений и разработка программ. В этой статье я дам вам практический, реальный опыт глубоких знаний Bash, представляя проект, над которым я провел много часов кодирования и переработки: сборочная Gentoo Linux система "ebuild".

Я главный архитектор Gentoo Linux, нового поколения ОС Linux в настоящее время бета-тестер. Одна из моих главных задач является, убедиться, что все бинарные пакеты (по аналогии с пакетами RPM) создаются надлежащим образом и работают вместе. Как вы, наверное, знаете, стандартная система Linux состоит не из одного единого дерева исходных текстов (например, BSD), но на самом деле состоит из примерно 25 (и более) базовых пакетов, которые работают вместе. Некоторые пакеты включают:

Package     Description
linux       The actual kernel
util-linux  A collection of miscellaneous Linux-related programs
e2fsprogs   A collection of ext2 filesystem-related utilities
glibc       The GNU C library

Каждый пакет находится в своем архиве и поддерживается отдельными независимыми разработчиками или группами разработчиков. Чтобы создать распределения, каждый архив должен быть отдельно загружен, скомпилирован и упакован. Каждый раз, когда пакет должен быть установлен, обновлено или улучшен, сборка и упаковка должны повториться (а это стареет очень быстро). Чтобы помочь устранить повторяющиеся этапы создания и обновления пакетов, я создал сборочную систему, написанной почти полностью на Bash. Для повышения Ваших знаний Bash, я покажу вам, как я распаковал и собирал части Ebuild система, шаг за шагом. Я объясню на каждом шаге, почему были сделаны определенные дизайнерские решения. К концу этой статьи, у вас будет отличное понимание крупномасштабных проектов Bash, но вы также поймете значительную часть полной автоматической системы сборки.

Почему Bash?

Bash является важным компонентом в сборочной системе Gentoo Linux. Она была выбрана в качестве основного языка Ebuild по ряду причин. Во-первых, она имеет простой и знакомый синтаксис, который особенно хорошо подходит для вызова внешних программ. Автоматическое создание системы "glue code", которая автоматизирует вызов внешних программ, а Bash очень хорошо подходит для приложений такого типа. Во-вторых, поддержка в Bash функций позволило Ebuild системе имеют модульную структуру, легкую для понимания кода. В-третьих, сборочная система пользуется поддержкой в Bash переменных среды, позволяющих сопровождающим пакет и разработчикам легко настроить ее на лету.

Обзор процесса сборки

Прежде чем мы посмотрим на Ebuild систему, давайте рассмотрим, как пакет будет получен, скомпилирован и установлен. Для нашего примера, мы будем рассмотрим "sed" пакет стандартная текстовая GNU утилита потокового редактирования, которая является частью всех дистрибутивов Linux. Во-первых, скачайте архив с исходными кодами (sed-3.02.tar.gz) (см. Ресурсы).  Мы сохраним этот пакет в /usr/src/distfiles, к которому мы будем обращаться при помощи переменной окружения "$DISTDIR". "$DISTDIR" это каталог, в котором хранятся все наши оригинальные архивы (исходники), это большое хранилище исходного кода.

Наш следующий шаг заключается в создании временного каталога "work", в который мы будем распаковывать архив. Мы будем ссылаться на этот каталог позднее с помощью "$WORKDIR". Для этого перейдите в каталог, где у Вас есть права на запись и введите следующую команду:

Uncompressing sed into a temporary directory

$ mkdir work
$ cd work
$ tar xzf /usr/src/distfiles/sed-3.02.tar.gz

Архив (tarball) будет распакован, и будет создан каталог "sed-3.02", который содержит все исходники. Мы будем ссылаться на "sed-3.02", используя переменную окружения "$SRCDIR". Для компиляции программы, введите следующую команду:

Uncompressing sed into a temporary directory

$ cd sed-3.02
$ ./configure --prefix=/usr
(autoconf generates appropriate makefiles, this can take a while)

$ make
(the package is compiled from sources, also takes a bit of time)

Мы пропустим шаг "make install", поскольку мы просто освещаем шаги распаковки и собирания пакета в этой статье. Если мы хотим написать Bash скрипт для выполнения всех этих шагов, он мог бы выглядеть следующим образом:

Sample bash script to perform the unpack/compile process

#!/usr/bin/env bash

if [ -d work ]
then
# remove old work directory if it exists
        rm -rf work
fi
mkdir work
cd work
tar xzf /usr/src/distfiles/sed-3.02.tar.gz
cd sed-3.02
./configure --prefix=/usr
make

Обобщая код


Хотя этот сценарий работает автоматически, это не очень гибко. В принципе, Bash сценарий просто содержит список всех команд, которые ввели в командной строке. Хотя это решение работает, но было бы неплохо сделать общий сценарий, который может быть настроен так, чтобы быстро распаковать и собрать любой пакет, просто изменив несколько строк. Таким образом, это потребует гораздо меньше усилий для сопровождающего пакет, для добавления новых пакетов дистрибутива. Давайте для начала используем множество различных переменных окружения, что сделает наш сценарий построения более гибким и общим:

# A new, more general script

#!/usr/bin/env bash

# P is the package name

P=sed-3.02

# A is the archive name

A=${P}.tar.gz

export ORIGDIR=`pwd`
export WORKDIR=${ORIGDIR}/work
export SRCDIR=${WORKDIR}/${P}

if [ -z "$DISTDIR" ]
then
        # set DISTDIR to /usr/src/distfiles if not already set
        DISTDIR=/usr/src/distfiles
fi
export DISTDIR

if [ -d ${WORKDIR} ]
then
        # remove old work directory if it exists
        rm -rf ${WORKDIR}
fi

mkdir ${WORKDIR}
cd ${WORKDIR}
tar xzf ${DISTDIR}/${A}
cd ${SRCDIR}
./configure --prefix=/usr
make

Мы добавили множество переменных окружения в код, но он все равно в принципе тот же самый. Тем не менее, сейчас, чтобы собирать информацию о любых стандартных исходных архивах, можно просто скопировать этот файл в новый файл (с соответствующим именем, чтобы отразить название нового пакета, который он компилирует), а затем изменить значение "$A"и" $P". Все остальные переменные окружения автоматически настраиваются на правильные настройки, и скрипт работает, как ожидалось. Хотя это и удобно, есть дальнейшие улучшения, которые могут быть внесены в код. Этот конкретный код гораздо больше, чем первоначальный сценарий, который мы создали. Поскольку одной из целей программирования для любого проекта, должно быть сокращение сложности для потребителя, было бы неплохо, чтобы значительно сократить код, или по крайней мере организовать его лучше. Мы можем сделать это за счет аккуратного трюка - мы разделим код на два отдельных файла. Сохраните этот файл как "sed-3.02.ebuild":

sed-3.02.ebuild

#the sed ebuild file -- very simple!
P=sed-3.02
A=${P}.tar.gz

Наш первый файл банален, и содержит только те переменные окружения, которые должны быть настроены для каждого пакета. Вот второй файл сложнее, он содержит многие операции. Сохраните его как "ebuild" и сделайте исполняемым:

The ebuild script

#!/usr/bin/env bash


if [ $# -ne 1 ]
then
        echo "one argument expected."
        exit 1
fi

if [ -e "$1" ]
then
        source $1
else
        echo "ebuild file $1 not found."
        exit 1
fi

export ORIGDIR=`pwd`
export WORKDIR=${ORIGDIR}/work
export SRCDIR=${WORKDIR}/${P}

if [ -z "$DISTDIR" ]
then
        # set DISTDIR to /usr/src/distfiles if not already set
        DISTDIR=/usr/src/distfiles
fi
export DISTDIR

if [ -d ${WORKDIR} ]
then
        # remove old work directory if it exists
        rm -rf ${WORKDIR}
fi Fi

mkdir ${WORKDIR}
cd ${WORKDIR}
tar xzf ${DISTDIR}/${A}
cd ${SRCDIR}
./configure --prefix=/usr
make

Теперь, когда мы разделили нашу систему сборки на два файла, я уверен, вам интересно, как он работает. Для компиляции sed, наберите:

$ ./ebuild sed-3.02.ebuild

Когда "ebuild" выполняется, он сначала пытается определить переменную "$1".  Что это значит? Из моей предыдущей статьи, вспомним, что "$1" является первым аргументом командной строки - в этом случае, "sed-3.02.ebuild". В Bash команда "source" читает команды из файла и выполняет их, как если бы они были записаны вместо команды "source". Так "source ${1}" в "ebuild" скрипте выполняет команды из "sed-3.02.ebuild", которые определяют "$P" and "$A". Это действительно удобно, потому что если мы хотим собрать другую программу вместо sed, мы можем просто создать новый .ebuild-файл и передать его в качестве аргумента для нашего "сценария ebuild". Таким образом,.ebuild-файлами может оказаться очень простой, а сложная Ebuild-система хранится в одном месте - наш ebuild-сценарий. Таким образом, мы можем модернизировать Ebuild-систему просто редактируя Ebuild-сценарий, сохраняя детали реализации за пределами Ebuild-файлов. Вот пример .ebuild-файла для gzip:

gzip-1.2.4a.ebuild

#another really simple ebuild script!
P=gzip-1.2.4a
A=${P}.tar.gz

Добавление функциональности


ОК, мы добились некоторых успехов. Однако, есть некоторые дополнительные функциональные возможности, которые я хотел бы добавить. Я бы хотел, чтобы Ebuild-сценарий принимал второй аргумент командной строки, которая будет "compile", "unpack", или "all". Этот второй аргумент командной строки говорит Ebuild-скрипту, какой шаг в процессе сборки выполнять. Таким образом, я могу сказать Ebuild распаковать архив, а не компилировать его (на всякий случай я должен проверить источник архива перед компиляцией). Чтобы сделать это, я добавлю case условие, которое будет проверять переменную "$2", и делать разные вещи в зависимости от ее значения. Вот как код выглядит сейчас:

ebuild, revision 2

#!/usr/bin/env bash

if [ $# -ne 2 ]
then
        echo "Please specify two args - .ebuild file and unpack, compile or all"
        exit 1
fi

if [ -z "$DISTDIR" ]
then
        # set DISTDIR to /usr/src/distfiles if not already set
        DISTDIR=/usr/src/distfiles
fi Fi
export DISTDIR

ebuild_unpack()
        #make sure we're in the right directory
        cd ${ORIGDIR}

        if [ -d ${WORKDIR} ]
        then
                rm -rf ${WORKDIR}
        fi

        mkdir ${WORKDIR}
        cd ${WORKDIR}
        if [ ! если [! -e ${DISTDIR}/${A} ]
        then
            echo "${DISTDIR}/${A} does not exist.  Please download first."
            exit
        fi
        tar xzf ${DISTDIR}/${A}
        echo "Unpacked ${DISTDIR}/${A}."
        #source is now correctly unpacked
}


ebuild_compile()

        #make sure we're in the right directory
        cd ${SRCDIR}
        if [ ! -d "${SRCDIR}" ]
        then
                echo "${SRCDIR} does not exist -- please unpack first."
                exit 1
        fi
        ./configure --prefix=/usr
        make
}

export ORIGDIR=`pwd`
export WORKDIR=${ORIGDIR}/work

if [ -e "$1" ]
then
        source $1
else
        echo "Ebuild file $1 not found."
        exit 1
fi

export SRCDIR=${WORKDIR}/${P}

case "${2}" in
        unpack)
                ebuild_unpack
                ;;
        compile)
                ebuild_compile
                ;;
        all)
                ebuild_unpack
                ebuild_compile
                ;;
        *)
                echo "Please specify unpack, compile or all as the second arg"
                exit 1
                ;;
esac

Мы внесли много изменений, поэтому рассмотрим их. Во-первых, мы расставили compile и unpack в их собственные функции, и назвали ebuild_compile() и ebuild_unpack(), соответственно. Это хороший шаг, так как код становится более сложным, и новые функции обеспечивают некоторую модульность, которая помогает писать скрипт более организовано. В первой строке каждой функции, я делаю "cd" в каталог, в котором я хочу быть, поскольку, как наш код становится все более модульными, а не линейным, то более вероятно мы могли бы ошибиться и выполнить функцию в неправильном текущем рабочем каталоге. "cd" команда явно ставит нас в нужное место, и помешает нам сделать ошибку позднее - важный шаг - особенно если вы будете удалять файлы внутри функции.

Так же, я добавил проверку до начала ebuild_compile() функции. Теперь он проверяет, что "$SRCDIR" существует, и если нет, то выводит сообщение об ошибке пользователю, что надо распаковать архив и затем выходит. Если хотите, вы можете изменить это поведение, так что если "$SRCDIR" не существует, наш Ebuild скрипт распакует архив исходных текстов автоматически. Вы можете сделать это, заменив ebuild_compile() следующим кодом:

# A new spin on ebuild_compile()

ebuild_compile() {
        #make sure we're in the right directory
        if [ ! -d "${SRCDIR}" ]
        then
                ebuild_unpack
        fi
        cd ${SRCDIR}
        ./configure --prefix=/usr
        make
}

Одним из наиболее существенных изменений в нашей второй версии Ebuild сценария нового заявление case в конце кода. Case просто проверяет значение второго аргумента командной строки, и выполняет правильные действия, в зависимости от этого. Если мы теперь наберем:

$ ebuild sed-3.02.ebuild

Мы действительно получим сообщение об ошибке. Ebuild теперь хочет, чтобы ему указали, что делать, а именно:

$ ebuild sed-3.02.ebuild unpack

или

$ ebuild sed-3.02.ebuild compile

или

$ ebuild sed-3.02.ebuild all

Если вы подставляете другой второй аргумент командной строки, кроме перечисленных выше, вы получите сообщение об ошибке ("*" значение), и программа завершается.

Модуляризация кода

Теперь, когда код достаточно развит и функционален, вы можете захотеть создать еще несколько Ebuild файлов, чтобы распаковать и собрать ваши любимые программы. Если это так, то рано или поздно вы столкнулись с некоторыми программами, которые не используют Autoconf ("./configure") или, возможно, имеющие другие нестандартные процессы компиляции. Нам нужно сделать несколько изменений в сборочной системе для этих программ. Но прежде, чем мы это сделаем, подумайте о том, как достичь этого.

Одной из замечательных особенностей "./configure --prefix=/usr; make" в нашем шаге компиляции является то, что большую часть времени, это работает. Но мы также должны иметь в системе сборки программ, которые не используют autoconf или нормальный Makefiles. Чтобы решить эту проблему, я предлагаю, чтобы наш Ebuild сценарий, по умолчанию, выполнял следующие действия:

   1. Если есть скрипт configure в "${SRCDIR})", выполнить его следующим образом:
      ./configure --prefix=/usr
      В противном случае, пропустить этот шаг.
   2. Выполнить следующую команду: make

Ebuild выполнит configure, только если он действительно существует, мы можем теперь автоматически собрать программы, которые не используют autoconf и имеют стандартный make-файл. Но что, если "make" нет у некоторых программ? Нам нужно найти способ переопределить наши разумные значения по умолчанию с некоторым конкретным кодом для обработки этих ситуаций. Чтобы сделать это, мы разделим нашу ebuild_compile() функцию на две. Первая функция, которую можно рассматривать в качестве "родителя", все еще будет называться ebuild_compile(). Но мы также будем иметь новую функцию, называемую user_compile(), которая содержит только наши разумные действия по умолчанию:

ebuild_compile() split into two functions ebuild_compile

user_compile() {
        #we're already in ${SRCDIR}
        if [ -e configure ]
        then
                #run configure script if it exists
                ./configure --prefix=/usr
        fi
        #run make
        make
}

ebuild_compile()
        if [ ! -d "${SRCDIR}" ]
        then
                echo "${SRCDIR} does not exist -- please unpack first."
                exit 1
        fi Fi
        #make sure we're in the right directory
        cd ${SRCDIR}
        user_compile
}

Хотя код работает практически идентично предыдущей версии Ebuild, мы можем сейчас сделать то, что мы не могли делать раньше - мы можем переопределить user_compile() в sed-3.02.ebuild. Так, если по умолчанию user_compile() функция не соответствует нашим потребностям, мы можем определить новую в нашем .ebuild файле, которая содержит команды, необходимые для компиляции пакета. Например, вот .ebuild файл для пакета e2fsprogs-1.18, который требует немного другой "./configure" строки:

e2fsprogs-1.18.ebuild

#this ebuild file overrides the default user_compile()
P=e2fsprogs-1.18
A=${P}.tar.gz

user_compile() {
       ./configure --enable-elf-shlibs
       make
}

Теперь, e2fsprogs будет составлен именно так, как мы хотим. Однако, для большинства пакетов, мы можем пропустить пользовательскую user_compile() функцию в .ebuild файле, и будет использован user_compile() из ebuild сценария .

Откуда же ebuild сценарий знает, какую user_compile() использовать? На самом деле это очень просто. В ebuild сценарии, по умолчанию user_compile() функция определена до e2fsprogs-1.18.ebuild файла. Если есть user_compile() в e2fsprogs-1.18.ebuild она перезаписывает версию по умолчанию, которая определена ранее. Если нет, то используется user_compile() функция, определенная в ebuild скрипте.

Это отличная вещь, мы добавили большую гибкость без какого-либо сложного кода, если он не нужен. Мы не будем рассматривать это здесь, но вы могли бы также внести аналогичные изменения в ebuild_unpack(), так что пользователи могут изменить процесс распаковки по умолчанию. Это может пригодиться, если надо внести исправления (patches), или если файлы, содержатся в нескольких архивах. Это также является хорошей идеей, чтобы изменить код распаковки, так как по умолчанию считается, что архив сжат bzip2.

Файлы конфигурации

Мы рассмотрели много хитрых методов Bash, и теперь пришло время для еще одного. Зачастую, удобно для программы, иметь глобальный файл конфигурации, который находится в /etc. К счастью, это легко сделать с помощью Bash.  Просто создайте следующий файл и сохраните его как /etc/ebuild.conf:

/ect/ebuild.conf

# /etc/ebuild.conf: set system-wide ebuild options in this file

# MAKEOPTS are options passed to make
MAKEOPTS="-j2"

В этом примере, я включил одну опцию конфигурации, но вы могли бы включать многие другие. Одной из самых прекрасных вещей в Bash является то, что этот файл может быть проанализирован простым включением (source) его в скрипт.  После source /etc/ebuild.conf, "$MAKEOPTS" определяется в пределах нашего Ebuild сценария. Мы будем использовать это, чтобы позволить пользователю передавать параметры make. Как правило, этот вариант используется, чтобы позволить Ebuild делать параллельные make.  Что такое параллельные make?

Чтобы ускорить компиляцию на многопроцессорных системах, make поддерживает параллельную компиляцию программы. Это означает, что вместо компиляции одного исходного файла, сделать компиляцию указанного количества исходных файлов одновременно (используя дополнительные процессоры в многопроцессорной системе). Параллельная компиляция включается опцией -j# и делается следующим образом:

make -j4

Этот код указывает сделать компиляцию четырех файлов одновременно. MAKE="make -j4" аргумент указывает на необходимость пройти -j4 опцию любому дочернему процессу, который он запускает.

Вот окончательный вариант нашей Ebuild программы:

ebuild, the final version

#!/usr/bin/env bash

if [ $# -ne 2 ]
then
        echo "Please specify ebuild file and unpack, compile or all"
        exit 1
fi

source /etc/ebuild.conf

if [ -z "$DISTDIR" ]
then
        # set DISTDIR to /usr/src/distfiles if not already set
        DISTDIR=/usr/src/distfiles
fi
export DISTDIR

ebuild_unpack() {
        #make sure we're in the right directory
        cd ${ORIGDIR}

        if [ -d ${WORKDIR} ]
        then
                rm -rf ${WORKDIR}
        fi

        mkdir ${WORKDIR}
        cd ${WORKDIR}
        if [ ! -e ${DISTDIR}/${A} ]
        then
                echo "${DISTDIR}/${A} does not exist.  Please download first."
                exit 1
        fi
        tar xzf ${DISTDIR}/${A}
        echo "Unpacked ${DISTDIR}/${A}."
        #source is now correctly unpacked
}

user_compile() {
        #we're already in ${SRCDIR}
        if [ -e configure ]
        then
                #run configure script if it exists
                ./configure --prefix=/usr
        fi
        #run make
        make $MAKEOPTS MAKE="make $MAKEOPTS"
}

ebuild_compile() {
        if [ ! -d "${SRCDIR}" ]
        then
                echo "${SRCDIR} does not exist -- please unpack first."
                exit 1
        fi
        #make sure we're in the right directory
        cd ${SRCDIR}
        user_compile
}

export ORIGDIR=`pwd`
export WORKDIR=${ORIGDIR}/work

if [ -e "$1" ]
then
        source $1
else
        echo "Ebuild file $1 not found."
        exit 1
fi

export SRCDIR=${WORKDIR}/${P}

case "${2}" in
        unpack)
                ebuild_unpack
                ;;
        compile)
                ebuild_compile
                ;;
        all)
                ebuild_unpack
                ebuild_compile
                ;;
        *)
                echo "Please specify unpack, compile or all as the second arg"
                exit 1
                ;;
esac

Заключение

Мы рассмотрели много методов Bash программирования в этой статье, но мы лишь коснулись поверхности Bash. Например, Gentoo Linux Ebuild система не только самостоятельно распаковывает и собирает каждый пакет, но она может также:

* Автоматическая загрузка источников, если они не находятся в "$DISTDIR"
* Убедиться, что источники не повреждены с помощью MD5
* При необходимости установить приложение в файловой системе, записывая все установленные файлы, так что этот пакет может быть легко удален позднее
* По желанию, пакет может быть собран и заархивирован, так что его можно установить позже, на другом компьютере, или в основе инсталяционного компакт-диска (если вы делаете distribution CD)

Кроме того, система Ebuild имеет несколько других глобальных параметров конфигурации, позволяющих пользователю указать, например, какие флаги оптимизации использовать во время компиляции, и является ли опциональной поддержка пакетов, например, как GNOME или slang, или должен быть включен по умолчанию в тех пакетах, которые поддерживают это.

Понятно, что с Bash можно достичь гораздо большего, чем я затронул в этой
серии статей. Я надеюсь, что вы многое узнали об этом невероятном средстве, и будете рады использовать Bash для ускорения и повышения уровня решения ваших задач.

Комментариев нет:

Отправить комментарий