Date Редакция Категория comp Теги C / make

Если собирается проект, состоящий из нескольких исходных файлов, например,

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