Назад |
При разработке крупных и даже средних программ, их исходный код размещается в нескольких исходных файлах. Деление больших программных комплексов на меньшие, более управляемые части, является обычной практикой и позволяет упростить процесс отладки приложения. Разделенная на части программа располагается в отдельных файлах, которые, в свою очередь, могут быть помещены в отдельные директории.
Сложная программная система, как правило, состоит не только из набора c- и h-файлов, но и из файлов, содержащих рабочую и справочную документацию по разрабатываемому программному обеспечению, исходные данные для работы или отладки модулей, и многое другое.
Все эти части могут требовать совершенно различной обработки. Файлы, содержащие исходные тексты программ, должны быть откомпилированы. Выполняемый код должен быть скомпонован вместе с определенными библиотеками и т.д. Действия по разработке комплекса программ включают также прогон тестов и включение соответствующей отладочной информации. Кроме того, иногда есть необходимость осуществить замену некоторой компоненты в системе, подготовить документацию и т.д.
Файлы, составляющие разрабатываемый комплекс, зависят друг от друга. Так объектные модули зависят от соответствующих файлов, содержащих исходные тексты. Последние зависят от файлов-заголовков. При отладке программ легко забыть, какие файлы после изменений компилировались, а какие нет. Попытка же перекомпилировать весь проект - непозволительная роскошь.
Решить проблему помогает утилита make. Она позволяет задавать связь модулей, умеет делать сравнения времен их модификации и, на этой основе, выполняет реконструкцию (например, перекомпиляцию) системы.
При использовании интерпретатора make, рекомендуется следующая технология разработки программного комплекса:
редактор -> make -> проверка -> редактор
Такая технология существенно повышает производительность труда программиста, так как освобождает его от забот по "ручной" сборке программ. make "следит" за тем, чтобы при многократных компиляциях и отладках приложений "не делалось то, что можно не делать".
Задание для утилиты make представляет собой программу на специальном языке (ее мы будем называть make-программой). make-программа содержит структуру зависимостей файлов и действия над ними, оформленные в виде списка правил. Выполнение действий приводит к требуемой перестройке комплекса. Текст программы может содержать определения переменных, используемых затем в описаниях правил и специальных директив, понимаемых утилитой.
Правило описывается следующим образом: сначала указывается имя файла, который необходимо создать (цель (target )), затем двоеточие, затем имена файлов, от которых зависит цель (подцели). Эта строка правила называется "строкой зависимостей". После этого идут строки, описывающие действия, которые надо осуществить, если обнаружено, что хотя бы одна подцель модифицировалась позже, чем файл-цель. Действие - это инструкция командному процессору, предписывающая, например, скопировать файлы, вызвать ту или иную программу (например компилятор) или сделать что-либо еще.
Таким образом, правило make-программы в общем виде выглядит так:
имя_цели . . . : [имя_подцели] [действие] . . [действие]
Cинтаксис строк, задающих действия, соответствует синтаксису командных строк shell. Первым символом такой строки в make-программе должен быть символ табуляции - это обязательное условие! Если строка слишком длинная, то ее можно разбить на подстроки. В конце каждой из них, кроме последней, ставится символ '\'. Все последовательности символов, начиная от символа "#"' и до конца строки, являются комментарием. Пустые строки и лишние пробелы игнорируются. Интерпретатор make передает строку, задающую действие, на выполнение shell без ведущего символа табуляции.
Следует заметить, что, если строка начинается с символа табуляции, то она относится к списку действий в правиле, иначе, если строка начинается с первой позиции, она может быть либо "строкой зависимостей", либо комментарием, либо определением переменной.
Если make-программа размещена в файле с именем "Makefile" или "makefile", то при запуске утилиты имя файла с программой можно не указывать в командой строке. В противном случае его надо задать при помощи опции '-f'. Таким образом, вызов
make
выполняет программу из файла "Makefile" или "makefile", а
make -f mymakeprog
программу из файла "mymakeprog".
В командной строке при вызове make можно также указать имя цели, список зависимостей которой надо проверить и, возможно выполнить соответствующие действия. Иначе выполняться будет первое правило make-программы.
Рассмотрим пример, иллюстрирующий применения make для создания программы, исходный текст которой содержится в файлах: "file1.c" и "file2.c". Текст make-программы выглядит следующим образом:
prog: file1.o file2.o cc -o proj file1.o file2.o file1.o: file1.c cc -c file1.c file2.o: file2.c cc -c file2.c
Первая строка программы определяет, что целью программы является получение файла "prog", зависящего от файлов "file1.o" и "file2.o". Во второй строке указывается действие, по которому получается целевой файл "prog". Остальные строки содержат описание правил получения подцелей "file1.o" и "file2.o".
Для того чтобы облегчить создание сложных make-программ, утилита позволяет задавать и использовать в них переменные. Создается переменная с помощью конструкции
<идентификатор>=<значение>
Здесь <идентификатор> - имя переменной, <значение> - это произвольная строка, возможно пустая. В последнем случае переменная считается неопределенной. Чтобы использовать переменную в make-программе, надо поставить ее идентификатор в скобки, перед которыми находится символ '$'. В следующем примере иллюстрируется применение переменных.
PROGNAME = prog OBJS = file1.o \ file2.o $(PROGNAME) : $(OBJS) cc -o $(PROGNAME) $(OBJS)
Переменные можно определять не только в тексте программы, но и в командной строке при запуске утилиты. Например:
make PROGNAME=prog1
Если в этом случае значение переменной состоит из нескольких слов, то его надо обрамить двойными кавычками.
make составляет список имен переменных и присваивает им значения в процессе чтения программы. Если значение переменной задается в командной строке при запуске утилиты, то все ее определения в make-программе игнорируются и используется значение, взятое из командной строки. Рекурсия при присвоении значения переменным не допускается.
Существуют переменные с предопределенными именами, значения которых устанавливаются при выполнении make-программы. К ним относятся: переменная с именем '@', ее значение - имя текущей цели; переменная с именем '?', принимает значение имен тех файлов-подцелей, которые "моложе" файла-цели; переменная '<' - имя текущей обрабатываемой подцели (используется в правилах, описывающих "неявные" зависимости (см. ниже)); '*' - имя текущей цели без расширения. Предопределенные переменные '@', '?' и '<' используются только в списке действий правила и в каждом правиле имеют свои значения. При ссылках на все перечисленные переменные, имена последних скобками не обрамляются.
Ниже демонстрируется употребление предопределенных переменных:
CC = cc LIBS = -lgen -lm OBJS = file1.o file2.o prog: $(OBJS) $(CC) -o $@ $(OBJS) $(LIBS)
Кроме описанных выше явных зависимостей между файлами, make позволяет определять так называемые "неявные" зависимости. Они "говорят", каким образом файлы с одним расширением создаются из файлов с другим расширением. Так, например, можно сообщить make, как из исходных ( .c ) файлов получить объектные модули ( .o ):
.o.c : cc -c $<
В приведенном примере make сравнивает времена модификации файлов, имеющих одинаковые имена и расширения, соответственно ".o" и ".c". Если o-файл "старше", то выполняется перекомпиляция. При этом в указанной в правиле команде перекомпиляции переменная '<', в соответствии с ее смыслом, заменяется на имя c-файла.
Из примера виден общий синтаксис правил с "неявными" зависимостями:
<расширение1>. <расширение2> : [командная строка, задающая действие] . . [командная строка, задающая действие]
Проверяется зависимость файлов с расширением 1 от файлов с расширением 2.
Некоторые "неявные" зависимости определяются make по умолчанию. Таковой, в частности, является зависимость ".o" от ".c". Учитывая сказанное, приведенный в начале раздела пример можно переписать так:
prog: file1.o file2.o cc -o proj file1.o file2.o
Здесь указывается лишь из каких объектных модулей и с помощью какой команды производится компоновка программы. Перекомпиляция же осуществляется согласно правилу с "неявной" зависимостью по умолчанию.
В заключении отметим еще несколько особенностей программы make. Как правило, при выполнении действий соответствующие команды печатаются на экране. Запретить это можно, если поставить перед ней символ '@'.
Если текущая выполняемая команда возвращает не нулевой код завершения, то make выводит сообщение об ошибке и останавливается. Чтобы этого не происходило, перед командой должен стоять символ '-'.
В следующем комплексном примере показаны make-программы, используемые для сборки проекта, разбитого на поддиректории. Предполагается, что проект располагается в каталоге "proj". Последний имеет два подкаталога: "lib" с файлами, образующими библиотеку (пусть это "lib1.c", "lib2.c", а имя самой библиотеки "libproj.a") и "main" с файлами, образующими программу (пусть это "main1.c" и "main2.c", а имя самой программы "main").
make-программа для сборки проекта.
# Makefile проекта SUBDIRS = lib main # Создание проекта all: @for i in $(SUBDIRS); \ do \ cd $$i ; \ make ; \ cd . . ; \ done # "Очистка" проекта - удаление порождаемых ".a" и ".o" файлов clean: @for i in $(SUBDIRS); \ do \ cd $$i ; \ make clean; \ cd . . ; \ done
Она "пробегает" по подкаталогам проекта, вызывая make для выполнения находящихся там программ.
make-программа для сборки библиотеки.
# Makefile для библиотеки. Располагается в директории # proj/lib. OFILES = lib1.o lib2.o # Создание библиотеки lib : $(OFILES) ar rv libproj.a $(OFILES) # "Очистка" - удаление порождаемых ".a" и ".o" файлов clean : rm -rf *.o *.a
make-программа для сборки программы.
# Makefile для создания программы. Располагается в директории # proj/main LIBDIR = . ./lib LIB = proj OFILES = main1.o main2.o PROG = main # Создание программы prog : $(OFILES) cc -o $(PROG) $(LIBDIR) $(OFILES) -l$(LIB) #"Очистка" - удаление порождаемых ".o" файлов и программы clean : rm -rf *.o $(PROG)
В приведенных make-программах файлы ".o" порождаются по соответствующим ".c" файлам в соответствии с "неявными" зависимостями, принятыми make по умолчанию.
Описанные характеристики make являются общими для большинства версий UNIX. Но отдельные ее диалекты могут иметь дополнительные свойства. Так, make фирмы SunSoft (OC Solaris 2.x) имеет специальную цель .KEEP_STATE. Если она встречается в make-программе, то утилита автоматически создает список зависимостей исходных файлов ( .c ) от файлов-заголовков ( .h ).