Содержание


Jam - система создания программ из файлов исходного кода

Часть 1. Общий обзор функциональности системы

Comments

Серия контента:

Этот контент является частью # из серии # статей: Jam - система создания программ из файлов исходного кода

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Jam - система создания программ из файлов исходного кода

Следите за выходом новых статей этой серии.

Автором Jam является Кристофер Сейуолд [Christopher Seiwald], который позиционирует свою систему, как "замену make, упрощающую сборку простых программ и сохраняющую управляемость при создании сложных программ". Кроме того, в качестве основных достоинств автор называет компактность записи Jamfile-ов (это аналоги Makefile-ов), автоматизацию обработки заголовочных файлов "на лету" (их можно не указывать явно), переносимость Jam (работает на многих платформах: Linux, BSD, Solaris, Mac, AIX, HPUX и др.), возможность компоновать крупные проекты, распределённые по многим подкаталогам, за один проход.

Заявления эффектные, но попробуем сами разобраться в преимуществах и недостатках системы Jam.

1. Как работает Jam

После запуска Jam проходит четыре фазы функционирования: начальная настройка (start-up), синтаксический анализ (parsing), установление зависимостей (binding) и обновление целевых файлов (updating).

1.1. Первая фаза - настройка

Первая фаза является подготовительной: переменные операционной среды импортируются в Jam-переменные. Если необходимо при запуске Jam изменить значение переменной, то для этого используется ключ командной строки:

jam -s имя_переменной=значение

1.2. Вторая фаза - синтаксический анализ

Здесь считывается и выполняется файл Jambase. По умолчанию используется стандартный, "встроенный" в систему файл. При необходимости встроенный файл можно заменить:

jam -fмой_jambase

Последняя операция в файле Jambase - считывание (при помощи правила "include") пользовательского файла с именем Jamfile.

Общей целью файлов Jambase и Jamfile является определение имён целевых и исходных файлов, создание схемы зависимостей (dependency graph) между ними и назначение операций создания для всех целей. В Jambase определяются типовые, шаблонные правила и выполняются присваивания переменным значений, а Jamfile использует эти правила и переменные для того, чтобы описать действительные отношения между целевыми и исходными файлами.

1.3. Третья фаза - установление зависимостей

После завершения синтаксического анализа Jam выполняет рекурсивный проход по схеме зависимостей и связывает каждый целевой файл с конкретным местоположением в текущей файловой системе.

Идентификатором цели чаще всего является имя файла, которое может быть задано в абсолютной форме (полное имя файла, начиная с корневого каталога), в относительной форме (относительно каталога, в котором вызвана программа jam) или как файл в текущем каталоге (имя файла без указания каталога).

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

1.4. Четвёртая фаза - обновление целевых файлов

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

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

jam -jN

Здесь N - это количество одновременно обрабатываемых целей (по умолчанию 1).

Обычно обработка целей выполняется в том порядке, в котором они записаны в Jamfile. Ключ -g изменяет порядок обработки таким образом, что в первую очередь обновляются цели с самыми новыми исходными файлами. Ключ -a позволяет принудительно выполнить обновление всех целей, даже если для них не требуется обновление.

2. Структура файлов Jambase и Jamfile

Прежде чем рассматривать структуру файлов Jambase и Jamfile, необходимо отметить, что в Jam используется интерпретируемый процедурный язык, специально ориентированный на сборку программ. Командами являются определения правил, вызовы этих правил, определения операций обновления. Кроме того, имеются структуры управления потоком выполнения и поддерживаются присваивания значений переменным. Jam интерпретирует поток ввода, как последовательность литеральных элементов, разделённых любыми "пробельными" символами (с помощью двойных кавычек можно создать элемент, содержащий пробелы), причём некоторые литеральные элементы зарезервированы в качестве ключевых слов языка (о них речь пойдёт позже). Единственный тип данных в Jam - одномерный список произвольных строк (литеральных элементов).

2.1. Правила

Основой языка Jam является правило (rule). Вообще говоря, правило - это определение процедуры, тело которой составляют команды Jam. Эти команды выполняются при вызове правила. Правило может принимать до 9 аргументов - от $(1) до $(9) - и возвращать одно значение. Если вызов правила заключён в квадратные скобки, то возвращаемое значение преобразовывается в список литеральных элементов.

Общая форма определения правила:

rule имя_правила [ : параметры ] { команды }

Общая форма вызова правила:

имя_правила [ параметр1 : параметр2 : ... : параметр9 ] ;

Следует обратить особое внимание на необходимость выделения пробелами двоеточия (:) и точки с запятой (;) - они также являются отдельными литеральными элементами языка.

2.2. Операции обновления

Если правило содержит команды, выполняющие операции обновления, то аргумент $(1) трактуется как список целей (обозначение-синоним $(<)), а аргумент $(2) трактуется как список исходных файлов (обозначение-синоним $(>)). Операции обновления фактически представляют собой команды оболочки (shell), требуемые для создания (компиляции, компоновки и т.п.) целевого файла.

Общая форма определения операции обновления:

actions [ модификаторы ] имя_операции { команды_shell }

2.3. Простейший пример использования Jam

Для того, чтобы всё вышеизложеннное стало более понятным, рассмотрим небольшой пример, в котором определяется одно правило, одна операция создания целевого файла, затем правило вызывается. Запишем весь этот "проект" в файл с именем test_jam:

rule TestRule
{
  CreateFile $(1) ;
}

actions CreateFile
{
  touch $(1)
}

TestRule test_file.1 ;
TestRule test_file.2 ;
TestRule test_file.3 ;

До начала испытаний скажу ещё пару слов. В первом разделе статьи я отметил, что "целью" обычно является элемент файловой системы. Но Jam также распознаёт так называемые "символические цели", не связанные с какими-либо файлами или каталогами и используемые только для организации зависимостей. Одна из таких символических целей носит название "all". Если при вызове в командной строке не задана цель, то Jam попытается создать все предполагаемые цели. Для демонстрации такой попытки выполним следующую команду:

jam -ftest_jam

Результат выполнения этой команды показан на рисунке 1.

Рисунок 1. Попытка Jam создать все (all) цели
Рисунок 1. Попытка Jam создать все (all) цели
Рисунок 1. Попытка Jam создать все (all) цели

Попытка не удалась. Попробуем указать конкретную цель. См. рисунок 2.

Рисунок 2. Создание указанной цели
Рисунок 2. Создание указанной цели
Рисунок 2. Создание указанной цели

Теперь заданная цель создана. Первая попытка была неудачной, потому что при вызове Jam с ключом -f мы фактически отменили стандартный набор правил Jambase, а своё правило для цели "all" не определили. Но это легко исправить:

rule TestRule
{
  CreateFile $(1) ;
  DEPENDS all : $(1) ;
}

actions CreateFile
{
  touch $(1)
}

TestRule test_file.1 ;
TestRule test_file.2 ;
TestRule test_file.3 ;

Здесь директива DEPENDS устанавливает прямую зависимость между целью "all" и списком целей (см.п.2.2 данной статьи; правило можно было записать и так: DEPENDS all : $(<) ;). По рисунку 3 можно понять, что теперь Jam "знает", как достичь цели "all".

Рисунок 3. Создание всех (all) целей
Рисунок 3. Создание всех (all) целей
Рисунок 3. Создание всех (all) целей

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

3. Цели и зависимости

Вообще говоря, в большинстве случаев замена встроенного набора правил и переменных Jambase не требуется. В файле Jambase, включённом в дистрибутивный пакет Jam, определены правила и переменные, которые поддерживают стандартные операции создания программ (компиляция, компоновка и т.п.). Таким образом, при обычном запуске (без ключа -f) правила считываются из встроенного Jambase, а после этого открывается файл Jamfile в текущем каталоге. Вот это и есть основной пользовательский файл, в котором описано, что нужно делать с исходными файлами в данном каталоге. Кроме того, в нём могут содержаться директивы чтения файлов Jamfile в других каталогах.

3.1. Простой Jamfile

В файлах Jamfile содержатся вызовы правил, записываемые в следующем формате:

ИмяПравила цел(и) : список_исходных_файлов ;

Например:

Main my_prog : main.c io_sys.c sched.c drv.c util.c ;

Здесь предполагается, что файлы main.c, io_sys.c, sched.c, drv.c и util.c расположены в том же каталоге, что и данный Jamfile, и эти файлы необходимо скомпилировать и скомпоновать (связать) в выполняемый файл с именем my_prog. Создание my_prog выполняется с помощью команды:

jam

3.2. Jamfile с несколькими целями

Рассмотрим следующий Jamfile:

Main my_prog : main.c util.c ;
LinkLibraries my_prog : libsupport ;
Library libsupport : io_sys.c sched.c drv.c ;

Правило Main определяет, что выполняемый файл my_prog будет скомпонован из объектных файлов, являющихся результатом компиляции main.c и util.c. Правило LinkLibraries определяет, что при сборке my_prog также будет "прилинкована" библиотека libsupport. Правило Library задаёт исходные файлы, которые будут скомпилированы и собраны в библиотеку libsupport.

В приведённом выше Jamfile имеются две цели: my_prog и libsupport. Обе эти цели являются реально создаваемыми, то есть, каждой соответствует конкретный объект файловой системы. Но к "реально создаваемым" относятся также псевдоцели (pseudotarget), которые являются символическими (нет какого бы то ни было объекта файловой системы, соответствующего псевдоцели) и представляют набор некоторых других целей. В предыдущем разделе мы уже рассматривали одну такую псевдоцель all, которой соответствуют все цели. В Jambase также определена псевдоцель "lib", объединяющая все цели, создаваемые с помощью правила Library. В нашем примере ей соответствует библиотека libsupport.

Любую реально создаваемую цель можно создать независимо от других целей. В приведённом выше примере можно ограничиться только созданием библиотеки без компоновки программы в целом:

jam libsupport

или с учётом того, что было сказано о псевдоцелях:

jam lib

4. Обработка дерева каталогов

Правила SubDir применяются для определения иерархии каталогов, содержащих файлы исходного кода. С помощью SubDir и SubInclude вы можете выполнять сборку программ из исходных файлов, распределённых по нескольким каталогам, что часто имеет место в больших проектах. Правила SubDir упорядочивают всё "дерево исходных кодов" таким образом, что Jam может считать файлы Jamfile из всех каталогов за один проход и построить зависимости для всего проекта в целом.

Для того, чтобы воспользоваться правилами SubDir, необходимо:

  1. Подготовить Jamfile в каждом каталоге, который будет включён в правило SubDir.
  2. Поместить в корне дерева исходных файлов файл с именем Jamrules. Этот файл может быть пустым, но на практике в него записывают определяемые пользователем правила и переменные, которые предполагается использовать во всём дереве.
  3. Определить переменную среды, указывающую на корневой каталог дерева исходных файлов (это не является обязательным требованием). Имя этой переменной может быть произвольным, но чаще всего для удобства используется имя TOP.

4.1. Правило SubDir

Правило SubDir обязательно должно быть вызвано до вызова других правил, в которых содержатся ссылки на содержимое устанавливаемого каталога. Лучше всего записать правило SubDir в самом начале каждого Jamfile. Например:

# Jamfile в каталоге $(TOP)/src/util
SubDir TOP src util ;
Main my_prog : main.c util.c ;
LinkLibraries my_prog : libsupport ;
Library libsupport : io_sys.c sched.c drv.c ;

По этим правилам компилируются пять файлов в каталоге $(TOP)/src/util. Три из пяти полученных объектных файлов объединяются в библиотеку libsupport, которая затем вместе с двумя оставшимися объектными файлами компонуется в выполняемый файл my_prog. Все целевые и промежуточные файлы также размещаются в каталоге $(TOP)/src/util.

И в чём отличие от предыдущего примера Jamfile, который давал тот же результат, но без всяких SubDir-правил? Внешних различий не наблюдается, тем не менее, при выполнении второго примера происходят два "скрытых" события.

Во-первых, правило SubDir заставляет Jam прочитать файл $(TOP)/Jamrules, который может содержать определения правил и переменных, специализированных для данного проекта. Это позволяет полностью перенастроить среду сборки без переписывания встроенного Jambase. Файл Jamrules считывается только один раз, во время первого вызова SubDir.

Во-вторых, правило SubDir инициализирует набор переменных, которые в дальнейшем используются правилом Main и другими правилами для однозначной идентификации исходных файлов в данном каталоге и для определения мест расположения указанных целевых файлов.

SubDir формирует пути к файлам, начинающиеся с каталога, задаваемого переменной $(TOP). Эта переменная передаётся в правило SubDir в качестве первого параметра. Все последующие параметры интерпретируются, как подкаталоги в формируемом пути.

4.2. Правило SubInclude

Правило SubInclude используется для того, чтобы организовать считывание другого файла Jamfile из текущего. Параметры для этого правила задаются в том же формате, что и для SubDir.

При составлении правил SubInclude рекомендуется ограничиваться одним уровнем подкаталогов; Jamfale-ы более низких уровней будут содержать команды считывания из своих подкаталогов и т.д. Это позволяет более гибко управлять любыми ветвями подкаталогов большого проекта. Вот пример Jamfile, расположенного на самом верхнем уровне:

# Файл $(TOP)/Jamfile - корневой для проекта huge_prog
SubInclude TOP src ;
SubInclude TOP engine ;
SubInclude TOP misc ;
SubInclude TOP util ;

Если в некотором каталоге должны размещаться целевые файлы, а кроме того имеются и подкаталоги, то правила SubInclude должны быть записаны либо до правила SubDir, либо в самом конце текущего Jamfile, но никак не между SubDir и вызовами прочих правил. Например:

# Файл $(TOP)/src/Jamfile
SubDir TOP src ;
Main huge_prog : huge_prog.c ;
LinkLibraries huge_prog : libengine libutil ;
SubInclude TOP src engine ;
SubInclude TOP src util ;

Jam обрабатывает все считываемые Jamfile-ы как один большой Jamfile. Правила создания целей, такие как Main и LinkLibraries, основываются на предшествующем правиле SubDir при установке мест расположения исходных, промежуточных и целевых файлов. Считываемые в соотвествии с правилами SubInclude файлы также могут содержать собственные правила SubDir. Поэтому если вы запишете правило SubInclude между SubDir и Main, то может возникнуть путаница в определении местонахождения исходных файлов, необходимых для выполнения правила Main.

5. Переменные, используемые при работе с деревом каталогов

Следующие переменные устанавливаются правилом SubDir и используются правилами из файла Jambase при определении имён целевых файлов.

SEARCH_SOURCE - Цели SubDir (например, "TOP src util") применяются для формирования путевого имени ($(TOP)/src/util), которое присваивается переменной $(SEARCH_SOURCE). Такие правила, как Main и Library, используют эту переменную для установки путей поиска исходных файлов.

LOCATE_SOURCE - Правило SubDir инициализирует эту переменную тем же значением, что и переменную $(SEARCH_SOURCE), если не определена переменная ALL_LOCATE_TARGET. $(LOCATE_SOURCE) используется правилами, которые задают генерацию исходных файлов (например, с помощью yacc и lex), чтобы установить место расположения сгенерированных файлов.

LOCATE_TARGET - Правило SubDir инициализирует эту переменную тем же значением, что и переменную $(SEARCH_SOURCE), если не определена переменная ALL_LOCATE_TARGET. $(LOCATE_TARGET) используется правилами, которые определяют создание бинарных объектов (например, Main и Library), чтобы установить место расположения создаваемых файлов.

ALL_LOCATE_TARGET - Если эта переменная определена, то переменным LOCATE_SOURCE и LOCATE_TARGET присваивается её значение вместо значения переменной SEARCH_SOURCE. Это можно использовать для организации записи целевых файлов в каталоги, расположенные за пределами дерева исходных файлов, а также при создании программ из исходных файлов, размещённых в каталогах, защищённых от записи.

Заключение

Сначала, в применении к простым программам, собираемым из 2-3 файлов, система Jam кажется невероятно простой. По мере усложнения проектов возникают вопросы, поиск ответов на которые требуют определённого времени. Появляется ощущение излишней усложнённости Jam. После сборки нескольких более или менее крупных проектов всё приходит в некую "усреднённую норму" - не то, чтобы Jam вызывал восхищение, но полного его неприятия тоже не происходит. Впрочем, у читателей есть возможность самим попробовать этот "Джем" и определить собственное отношение к данному продукту.

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


Ресурсы для скачивания


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=513278
ArticleTitle=Jam - система создания программ из файлов исходного кода: Часть 1. Общий обзор функциональности системы
publish-date=08262010