Если собирается проект, состоящий из нескольких исходных файлов, например,
g++ main.c hello.c factorial.c -o hello
то изменение любого из них заставит перекомпилировать все заново, включая и файлы, которые не изменялись. Эти избыточные операции увеличивают время компиляции, что для больших проектов может быть весьма существенно. Здесь на помощь приходит программа make, которая автоматически определяет, какие файлы были изменены и требуют компиляции, и применяет необходимые для этого команды. Для использования make необходимо описать сценарий сборки проекта в файле Makefile. Этим мы сейчас и займемся.
Рассмотрим составление Makefile'а на примере следующего проекта:
main.c
#include <stdio.h>
#include <stdlib.h>
#include "factorial.h"
#include "hello.h"
int main(int argc, char **argv)
{
int a = atoi(argv[1]);
print_hello();
printf("%d! = %d\n", a, fact(a));
return 0;
}
hello.h
#ifndef HELLO_H
#define HELLO_H
#include <stdio.h>
void print_hello();
#endif
hello.c
#include "hello.h"
void print_hello()
{
printf("Hello! Lets go calculate factorial...\n");
}
factorial.h
#ifndef FACTORIAL_H
#define FACTORIAL_H
int fact(int);
#endif
factorial.c
#include "factorial.h"
int fact(int num)
{
if (num != 1)
return num * fact(num - 1);
else
return 1;
}
Все эти файлы лучше поместить в отдельный каталог.
Готовая программа будет приветствовать пользователя и вычислять факториал числа, заданного в командной строке. Впрочем, для нас важно лишь то, что этот проект состоит из нескольких исходных файлов.
Запуск make
Программа, запущенная без аргументов
make
будет искать в текущем каталоге файл сценария Makefile и выполнять инструкции из него. Если в текущем каталоге находится несколько сценариев, то указать нужный можно следующим образом:
make -f MyMakefile
Простейший сценарий
Сценарий Makefile -- это обычный текстовый файл, состоящий из следующих блоков:
цель: зависимости
[tab] команда_1
[tab] команда_2
[tab] ...
Простейший сценарий, полностью аналогичный сборке проекта из командной строки, имеет вид:
all:
g++ -Wall main.c hello.c factorial.c -o hello
В нашем примере цель называется all. Это -- цель по умолчанию, которая будет выполняться, если никакая другая цель не указана. Пока никаких зависимостей у нас нет, так что make сразу приступает к выполнению команды по сборке программы.
Обратите внимание, что строка с командой обязательно должна начинаться с табуляции! Сохраните сценарий в файле Makefile-1 и запустите сборку
make -f Makefile-1
Использование зависимостей
Простейший сценарий не дает никаких преимуществ при сборке проекта. Для того, чтобы при изменении одного файла не понадобилось пересобирать весь проект, необходимо использовать несколько целей. Например:
Makefile-2
all: hello
hello: main.o factorial.o hello.o
g++ main.o factorial.o hello.o -o hello
main.o: main.c
g++ -c -Wall main.c
factorial.o: factorial.c
g++ -c -Wall factorial.c
hello.o: hello.c
g++ -c -Wall hello.c
clean:
rm -rf *.o hello
Теперь у цели all есть только зависимость, но нет команды. В этом случае make последовательно выполнит все указанные в файле зависимости этой цели.
Цель all зависит только от цели hello -- создания исполняемого файла проекта. hello, в свою очередь, зависит от наличия трех объектных файлов. Создание каждого из этих файлов является отдельной целью, и требует выполнения команды компиляции. Так, цели и зависимости между файлами анализируются в сценарии "сверху вниз", а команды, направленные на достижение целей выполняются "снизу вверх".
Цель и зависимости, как правило, представляют собой имена файлов: создание исполняемого файла зависит от наличия объектных файлов, а их создание -- от файлов исходных. Особняком стоит цель clean. Она традиционно используется для очистки результатов сборки проекта:
make -f Makefile-2 clean
Переменные и комментарии
Наш сценарий позволяет компилировать только изменившиеся файлы. Но если мы изменим компилятор или используем другие опции компиляции, его придется переписывать. Чтобы этого избежать используются переменные make:
Makefile-3
# Переменная CC хранит имя используемого компилятора
CC=g++
# CFLAGS содержит опции, передаваемые компилятору
CFLAGS=-c -Wall
all: hello
hello: main.o factorial.o hello.o
$(CC) main.o factorial.o hello.o -o hello
main.o: main.c
$(CC) $(CFLAGS) main.c
factorial.o: factorial.c
$(CC) $(CFLAGS) factorial.c
hello.o: hello.c
$(CC) $(CFLAGS) hello.c
clean:
rm -rf *.o hello
Переменные обозначаются прописными буквами (VAR). Им нужно присвоить значение до момента их использования и затем можно подставлять это значение в нужное место сценария следующим образом: $(VAR)
Строка комментария начинается с символа '#'.
Что делать дальше
Теперь мы знаем достаточно, чтобы составлять простейшие сценарии. Но это только вершина айсберга -- дальше надо читать серьезные учебники и руководства. Попробуйте, например, самостоятельно разобраться как работает следующий сценарий, который можно легко адаптировать под практически любой проект:
Makefile-4
CC=g++
CFLAGS=-c -Wall
LDFLAGS=
SOURCES=main.c hello.c factorial.c
OBJECTS=$(SOURCES:.c=.o)
EXECUTABLE=hello
all: $(SOURCES) $(EXECUTABLE)
$(EXECUTABLE): $(OBJECTS)
$(CC) $(LDFLAGS) $(OBJECTS) -o $@
.c.o:
$(CC) $(CFLAGS) $< -o $@
clean:
rm -rf *.o $(EXECUTABLE)
Строка 5 означает, что OBJECTS
-- это тот же список, что и SOURCES
, но расширения .c
в нем заменены на .o
Переменные $
@ и $<
называются автоматическими. $
@ заменяется на текущую цель; $<
заменяется на первую зависимость из списка (например, на имя файла из списков SOURCES
или OBJECTS
).
Следует отметить, что хотя приведенные примеры сценариев относятся к использованию make для компиляции проектов на языке С, этим отнюдь не исчерпывается сфера применения программы. make может использоваться для описания сценариев обновления любых файлов, например, для обработки документов LaTeX и т. п.
Комментарии
comments powered by Disqus