Для понимания того, что я буду рассказывать в дальнейшем, необходимо достаточно четкое представление о том, как работает make и что из себя представляет Makefile. Для тех, кто не в курсе темы и мануал курить не горит желанием, я расскажу основные моменты. Сразу предупреждаю, что в тонкости я не вдаюсь и вообще сильно упрощаю дело. Но суть я постараюсь передать.

Для чего нужен make

Не говоря уже о больших проектах, построение которых может занимать часы, сборка с нуля даже небольшого проекта может потребовать нескольких минут времени. По нескольку минут после каждого изменения, которое хочется проверить – и набегают часы, потраченные впустую на разглядывание монитора. Хорошо бы свести необходимое для построения (т.е. компиляции и линковки) время к секундам.

Сделать это можно только одним способом: разбить проект на много маленьких исходных файлов, каждый из которых компилируется отдельно и, по возможности, независимо от других. Результат компиляции (объектные файлы) сохраняется. Тогда изменение одного файла потребует перекомпиляции только этого файла и файлов, от него зависимых, поскольку для остальных исходных файлов подойдут объектные файлы от предыдущего построения. Такой подход займет заведомо меньше времени, чем полный ребилд. Для управления этим процессом и был придуман make.

Из вышеизложенного следует, что главное для make – это знать зависимости между файлами. Изменение каких файлов требует перекомпиляции некоторого файла? Зная ответы на эти вопросы, можно определить, что и в каком порядке нужно компилировать.

Кстати, не обязательно компилировать. Ту же логику можно приспособить к любому процессу, когда из одних файлов получаются другие. Например, когда из *.h файлов получаются *.cpp файлы с помощью moc.

Правила в Makefile

Зависимости в Makefile задаются с помощью правил (rules), которые связывают цели (targets) с их зависимостями (prerequisites), а также определяют действия, которые нужно выполнить для обновления целей. В простейшем случае правило выглядит следующим образом:

Простейший вид правила

цель: зависимость1 зависимость2 зависимость3 ... действие1 действие2 ....


1

2

3

4

5

6

 

цель: зависимость1 зависимость2 зависимость3 ...

    действие1

    действие2

    ....

 

И цели, и зависимости – это файлы. Наличие правила означает, что если какая-то из зависимостей оказалась свежее, чем цель (определяется по дате модификации файла), то цель нужно обновить с помощью указанных действий. Последние представляют собой просто команды оболочки операционной системы (shell).

Если не вдаваться в синтаксические детали, облегчающие написание Makefile (переменные, неявные действия и т.п.), то Makefile представляет собой набор такого рода правил. Эти правила не должны быть (и редко бывают) независимыми друг от друга. Цель в одном правиле спокойно может быть зависимостью в другом правиле. Определения того, какие правила выполнять и в каком порядке – задача make. В общем случае гарантируется, что для любого правила сначала отработают правила для зависимостей, если они указаны как цель в другом правиле.

Важный момент: правило для цели с действиями может быть только одно, но можно указывать дополнительные правила для той же цели, если они без действий. Такие “пустые” правила просто добавляют свои зависимости к правилу, у которого с действия есть.

Правило без действий

a.o: header.h a.o: a.c cc -c a.c # эти два правила есть то же самое, что это одно a.o: header.h a.c cc -c a.c


1

2

3

4

5

6

7

8

9

10

 

a.o: header.h

 

a.o: a.c

  cc -c a.c

 

# эти два правила есть то же самое, что это одно

a.o: header.h a.c

  cc -c a.c

 

Цели и зависимости не обязаны быть файлами

Самый простой пример – это традиционная цель clean, которая служит для удаления всех промежуточных файлов, получающихся при компиляции:

clean

clean: del main.o del app.exe


1

2

3

4

5

 

clean:

    del main.o

    del app.exe

 

При выполнении действий этого правила файл clean не создается. В подобных случаях make понимает, что он имеет дело с целью, которая не является файлом, и считает ее “пустышкой” (phony). Такая цель всегда считается “изменившейся” – до тех пор, пока правило цели не будет выполнено. Это значит, что если make доберется по зависимостям до обработки pnony цели, то ее действия обязательно выполнятся (только один раз, конечно). Есть тут, правда, один неприятный момент.

Неявный phony target

clean: del main.o del app.exe text.txt: clean echo some text > text.txt


1

2

3

4

5

6

7

8

 

clean:

    del main.o

    del app.exe

 

text.txt: clean

   echo some text > text.txt

 

Предположим, правило clean еще не отрабатывало. Тогда при выполнении правила text.txt вначале выполнится clean, верно? Да, но только в том случае, если файл clean реально не существует. Если же он вдруг существует, то он наверняка старый (т.к. правило clean его не обновляет), и поэтому ни правило clean, ни, в данном случае, правило text.txt не отработают вообще.

Самое распространенное решение для этой проблемы – использовать пустую цель, т.е. цель, которая не содержит ни зависимостей, ни действий. make обрабатывает такую цель специальным образом: даже ее выполнение не отмечает ее как обновленную, она всегда считается изменившейся. Таким образом, если некая цель зависит от подобной пустой цели (традиционно ее называют FORCE), то она всегда будет выполняться, т.к. содержит по крайней мере одну изменившуюся цель. А значит, гипотетически возможное существование файла clean не повлияет на конечный результат.

phony target через FORCE

FORCE: clean: FORCE del main.o del app.exe text.txt: clean echo some text > text.txt


1

2

3

4

5

6

7

8

9

10

 

FORCE:

 

clean: FORCE

    del main.o

    del app.exe

 

text.txt: clean

   echo some text > text.txt

 

GNU make поддерживает специальный способ указать, что некая цель есть “пустышка”. Для таких целей наличие файла с тем же именем не проверяется вообще.

Явный phony target

.PHONY: clean clean: del main.o del app.exe text.txt: clean echo some text > text.txt


1

2

3

4

5

6

7

8

9

10

 

.PHONY: clean

 

clean:

    del main.o

    del app.exe

 

text.txt: clean

   echo some text > text.txt

 

Порядок выполнения зависимостей

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

Неявный порядок обработки зависимостей

target: prereq1 prereq2 echo do target prereq1: echo do prereq1 prereq2: echo do prereq2 # вывод скорее всего будет следующий: # do prereq1 # do prereq2 # do target


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

 

target: prereq1 prereq2

    echo do target

 

prereq1:

    echo do prereq1

 

prereq2:

    echo do prereq2

 

# вывод скорее всего будет следующий:

# do prereq1

# do prereq2

# do target

 

Но есть тут подводный камень. Конкретная реализация make может обрабатывать Makefile в режиме параллельной обработки (загружая сразу все процессоры, что, естественно, ускоряет дело). В этом случае может получиться так, что, скажем, вторая зависимость отработает раньше первой. Поэтому использовать порядок обработки зависимостей для, по сути, неявного определения зависимости между ними (если для нас важен порядок зависимостей, значит они не независимы друг от друга) усиленно не рекомендуется. Лучше эту зависимость задать явно; такой вариант будет работать с любым режимом работы любой реализации make.

Явный порядок обработки зависимостей

target: prereq1 prereq2 echo do target prereq1: echo do prereq1 prereq2: prereq1 echo do prereq2 # вывод обязательно будет следующий: # do prereq1 # do prereq2 # do target


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

 

target: prereq1 prereq2

    echo do target

 

prereq1:

    echo do prereq1

 

prereq2: prereq1

    echo do prereq2

 

# вывод обязательно будет следующий:

# do prereq1

# do prereq2

# do target