Отладка makefile — это что-то из черной магии. К несчастью, не существует такой вещи как makefile отладчик, чтобы изучить ход выполнения конкретного правила или как разворачивается переменная. Большую часть отладки можно выполнить с помощью обычных print’ов и проверкой makefile. Конечно, GNU make немного помогает своими встроенными методами и опциями командной строки. Один из лучших методов отладки makefile это добавить отладочные перехваты (hooks) и использовать техники безопасного программирования, на которые можно будет опереться, когда дела пойдут совсем плохо. Далее представлено несколько основных техник отладки и практик безопасного программирования, которые будут, на мой взгляд, наиболее полезными.

Отладочные возможности make

Очень полезная для отладки не работающего makefile — функция warning . Так как функция warningразворачивается в пустую строку, ее можно использовать везде в makefile: на верхнем уровне, в имени цели, в списке зависимостей и в командных скриптах. Это позволяет печатать значения переменных там, где это наиболее подходит для их проверки. Например:

$(warning A top-level warning)
FOO := $(warning Right-hand side of a simple variable)bar
BAZ = $(warning Right-hand side of a recursive variable)boo
$(warning A target)target: $(warning In a prerequisite list)makefile $(BAZ)
   $(warning In a command script)
   ls
$(BAZ):

Дает вывод:

$ make
makefile:1: A top-level warning
makefile:2: Right-hand side of a simple variable
makefile:5: A target
makefile:5: In a prerequisite list
makefile:5: Right-hand side of a recursive variable
makefile:8: Right-hand side of a recursive variable
makefile:6: In a command script
ls
makefile

Заметим, что выполнение функции warning следует нормальному ходу алгоритма make для немедленных и отложенных вычислений. Также, присваивание к BAZ содержит warning и сообщение не печатается, пока BAZ не будет развернут в списке зависимостей.

Возможность вставки warning вызов куда угодно делает его очень полезным инструментом отладки.

Опции командной строки

Есть три очень полезные опции командной строки для отладки: --just-print (-n), --print-data-base (-p) и --warn-undefined-variables.

--just-print

Первое испытание новой цели в makefile — это вызвать make с опцией --just-print (-n). Будучи вызванным с этой опцией make прочитает makefile и напечатает каждую команду, которую в обычном режиме он бы выполнил для обновления цели. Для удобства, GNU make также выведет команды помеченные собачкой (@) - заглушающим модификатором.

Опция предназначена для полного подавления выполнения всех команд. Однако, есть исключения. В то время как make не выполняет командные скрипты целей, он выполнит вызовы shell функции, которые будут в немедленном контексте выполнения. Для примера:

REQUIRED_DIRS = ...
_MKDIRS := $(shell for d in $(REQUIRED_DIRS); \
             do                               \
                [[ -d $$d ]] || mkdir -p $$d; \
             done)

$(objects) : $(sources)

Смысл переменной _MKDIRS в инициировании создания нужных директорий. Если выполнить это с --just-print опцией, команда оболочки будет выполнена как обычно в момент чтения makefile. Затем, make выведет (без исполнения) каждую команду компиляции необходимую для обновления списка файлов $(objects).

--print-data-base

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

Секция Variables выводит список переменных с описательным комментарием:

# automatic
<D = $(patsubst %/,%,$(dir $<))
# environment
EMACS_DIR = C:/usr/emacs-21.3.50.7
# default
CWEAVE = cweave
# makefile (from `../mp3_player/makefile', line 35)
CPPFLAGS = $(addprefix -I ,$(include_dirs))
# makefile (from `../ch07-separate-binaries/makefile', line 44)
RM := rm -f
# makefile (from `../mp3_player/makefile', line 14)
define make-library
 libraries += $1
 sources += $2
 $1: $(call source-to-object,$2)
 $(AR) $(ARFLAGS) $$@ $$^
endef

Авто-переменные не выводятся, но выводятся другие, зависящие от них, полезные переменные, такие как $(<D). В комментарии пишется вывод функции origin (см. make manual). Если переменная определена в файле, будет выведено имя файла и номер строки объявления. Простые и рекурсивные переменные определяются по оператору присваивания. Вычисленное значение простых переменных также печатается в правой части выражения.

Следующий раздел Directories более полезен разработчикам make, а не пользователям make. Представляет собой список папок просмотренных make, включая SCCS и RCS под-папки, которые могут быть, но обычно отсутствуют. Для каждой папки выводится детали реализации: номер устройства, inode и статистика по совпадениям шаблонов файлов.

Секция Implicit rules содержит все встроенные и пользовательские шаблоны в базе данных make. Опять же, для тех правил, которые определены в файле, выводится имя файла и строка определения:

%.c %.h: %.y
# commands to execute (from `../mp3_player/makefile', line 73):
   $(YACC.y) --defines $<
   $(MV) y.tab.c $*.c
   $(MV) y.tab.h $*.h
%: %.c
# commands to execute (built-in):
   $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@
%.o: %.c
# commands to execute (built-in):
   $(COMPILE.c) $(OUTPUT_OPTION) $<

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

Следующая секция это каталог зависящих от конкретного шаблона переменных определенных в makefile. Описание этих переменных — это их значения, которые будут подставлены во время выполнения при выполнении соответствующих им правил. Например, для переменной определенной как:

%.c %.h: YYLEXFLAG := -d
%.c %.h: %.y
 $(YACC.y) --defines $<
 $(MV) y.tab.c $*.c
 $(MV) y.tab.h $*.h

будет выведено:

# Pattern-specific variable values

%.c :
# makefile (from `Makefile', line 1)
# YYLEXFLAG := -d
# variable set hash-table stats:
# Load=1/16=6%, Rehash=0, Collisions=0/1=0%
%.h :
# makefile (from `Makefile', line 1)
# YYLEXFLAG := -d
# variable set hash-table stats:
# Load=1/16=6%, Rehash=0, Collisions=0/1=0%

# 2 pattern-specific variable values

Далее следует Files секция, которая выводит все явные и суффикс- правила, которые относятся к конкретным файлам:

# Not a target:
.p.o:
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
# commands to execute (built-in):
 $(COMPILE.p) $(OUTPUT_OPTION) $<
lib/ui/libui.a: lib/ui/ui.o
# Implicit rule search has not been done.
# Last modified 2004-04-01 22:04:09.515625
# File has been updated.
# Successfully updated.
# commands to execute (from `../mp3_player/lib/ui/module.mk', line 3):
 ar rv $@ $^
lib/codec/codec.o: ../mp3_player/lib/codec/codec.c ../mp3_player/lib/codec/codec.c ..
/mp3_player/include/codec/codec.h
# Implicit rule search has been done.
# Implicit/static pattern stem: `lib/codec/codec'
# Last modified 2004-04-01 22:04:08.40625
# File has been updated.
# Successfully updated.
# commands to execute (built-in):
 $(COMPILE.c) $(OUTPUT_OPTION) $<

Промежуточные файлы и суффикс-правила обозначены как "Not a target"; остальное — цели. Каждый файл включает комментарии, показывающие как make будет обрабатывать это правило. У файлов, которые найдены через обычный vpath поиск, показан найденный путь до них.

Последняя секция называется VPATH Search Paths и перечисляет значение VPATH и все vpath шаблоны.

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

--warn-undefined-variables

Эта опция заставляет make выводить предупреждение при вычислении неопределенной переменной. Так как неопределенные переменные вычисляются в пустую строку, зачастую, опечатки остаются необнаруженными долгое время. Проблема с этим ключом в том, что многие встроенные правила используют неопределенные переменные, которые нужны для перехвата пользовательских значений. Поэтому запуск make с этой опцией неизбежно выведет много предупреждений, которые не являются ошибками и не связаны с makefile'ом пользователя, Например:

$ make --warn-undefined-variables -n
makefile:35: warning: undefined variable MAKECMDGOALS
makefile:45: warning: undefined variable CFLAGS
makefile:45: warning: undefined variable TARGET_ARCH
...
makefile:35: warning: undefined variable MAKECMDGOALS
make: warning: undefined variable CFLAGS
make: warning: undefined variable TARGET_ARCH
make: warning: undefined variable CFLAGS
make: warning: undefined variable TARGET_ARCH
...
make: warning: undefined variable LDFLAGS
make: warning: undefined variable TARGET_ARCH
make: warning: undefined variable LOADLIBES
make: warning: undefined variable LDLIBS

Тем не менее, эта опция может быть крайне полезна в поиске ошибок такого типа.

--debug опция

Когда нужно узнать как make анализирует твой граф зависимостей, используй --debug опцию. Она предоставляет самую детальную доступную информацию без запуска отладчика. Есть пять опций вывода отладки и один модификатор: basic, verbose, implicit, jobs, all, и makefile, соответственно.

Если опция указана в форме --debug, используется basic - краткий вывод для отладки. Если опция указана в виде -d, используется all. Для выбора других комбинаций можно использовать список разделенный запятыми: --debug=option1,option2, где option может быть одно из следующих значений (на самом деле, make смотрит только на первую букву):

  • basic
    Наименьшая детализированность. Когда включена, make выводит каждую цель, которая нуждается в обновлении и статус действия обновления.

  • verbose
    Эта опция включает basic вывод и дополнительную информацию о проанализированных файлах и о зависимостях, которые не нуждаются в обновлении.

  • implicit
    Эта опция включает basic вывод и дополнительную информацию о неявных правилах, просмотренных в поиске нужного для каждой выполняемой цели.

  • jobs
    Эта опция выводит детали о запущенных make'ом подпроцессах. Она не включает basic вывод.

  • all
    Включает все выше перечисленное и является значением по умолчанию для -d опции.

  • makefile
    Обычно, отладочная информация включается после того, как все makefileы будут обновлены. Обновление включает в себя и обновление всех импортированных файлов, таких как файлы со списками зависимостей. С этим модификатором make выведет выбранную опциями информацию в ходе обновления makefile'ов и импортированных файлов. Он включает basic информацию, а также включается при указании all опции.

Пишем код, удобный для отладки

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

Правила в этом разделе, в несколько произвольном виде, основаны на практиках кодирования, защитном программировании и техник отладки. Тогда как определенные правила, такие как проверка статуса выхода команд, можно поместить как в хорошие практики кодирования так и в защитное программирование, эти три категории отражают общую шкалу. Кодируй хорошо свои makefile, но без излишеств в срезании углов. Используй защитное программирование, чтобы защитить makefile от неожиданных событий и изменчивого внешнего окружения. В конце концов, когда баги все-таки будут возникать, используй каждый приём, доступный тебе, чтобы их размозжить.

Принцип «KISS» — залог всех хороших систем. makefile очень быстро могут становится сложными, даже для повседневных задач, таких как генерация графа зависимостей. Борись с тенденцией включать все больше и больше функционала в твою систему сборки. Ибо ты проиграешь, но не так плохо, если будешь при каждом случае добавлять функционал.

Хорошие практики кодирования

По моему опыту, большая часть программистов не рассматривает написание makefile'ов как программирование, и, из-за этого, не уделяют такое же внимание, как при разработке на C++ или Java. Но ведь язык make это полный по Тьюрингу декларативный язык! И если надежность и легкость сопровождения твоей системы сборки важна, пиши ее тщательно и используй все доступные тебе лучшие практики.

Один из важнейших аспектов в программировании безотказных makefile это проверка статуса выхода команд. Конечно, make проверяет для простых команд это дело сам, но makefile часто содержит составные команды, которые могут не сработать молча:

do:
   cd i-dont-exist; \
   echo *.c

При выполнении этот makefile не прервется с ошибкой, тогда как ошибка несомненно произойдет:

$ make
cd i-dont-exist; \
echo *.c
/bin/sh: line 1: cd: i-dont-exist: No such file or directory
*.c

И далее, выражение подстановки не найдет никаких файлов .c, и молча вернет выражение подстановки. Ой. Способ по-лучше, это использовать возможности командной оболочки по проверке и предотвращению ошибок:

SHELL = /bin/bash
do:
   cd i-dont-exist && \
   shopt -s nullglob &&
   echo *.c

Теперь ошибка cd правильно передастся make, команда echo никогда не исполнится и make прервётся со статусом ошибки. В дополнение, после установки nullglob опции bashа подстановка вернет пустую строку если не будут найдены такие файлы. (Конечно, в твоем конкретном случае могут быть другие предпочтения.)

$ make
cd i-dont-exist && \
echo *.c
/bin/sh: line 1: cd: i-dont-exist: No such file or directory
make: *** [do] Error 1

Другой хорошей практикой является форматирование своего кода для максимальной читабельности. Большая часть makefile'ов плохо отформатированы, и как следствие, их трудно читать. Что тут кажется легче прочитать?

_MKDIRS := $(shell for d in $(REQUIRED_DIRS); do [[ -d $$d \
]] || mkdir -p $$d; done)

или:

_MKDIRS := $(shell                           \
             for d in $(REQUIRED_DIRS);      \
             do                              \
               [[ -d $$d ]] || mkdir -p $$d; \
             done)

Если ты как все люди, то тебе будет труднее прочитать первое, точку с запятой сложно найти, и количество операндов сосчитать еще сложнее. И это не шутки. Огромный процент синтаксических ошибок, которые встречаются в командных скриптах будут из-за пропущенных точек с запятой, символов экранирования, других разделителей, таких как разделитель программного конвейера или логические операторы.

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

TAGS:
        cd src \
        ctags --recurse

disk_free:
        echo "Checking free disk space..." \
        df . | awk '{ print $$4 }'

С форматированием команд для читабельности такого рода ошибки станет легко ловить. Делай отступы при работе в пользовательских функциях. Если же пробелы станут проблемой при работе макроса, форматирование можно обернуть через вызов strip функции. Форматируй длинные списки значений по одному на строку. Добавь комментарий перед каждой целью, дай краткое пояснение, и задокументируй параметры.

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

Защитное программирование

Защитный код это код, который выполняется когда одно из твоих предположений или ожиданий неверно — if проверяет то, что всегда истинно, assert функция никогда не упадет, и конечно, значимость такого кода в том, что внезапно (обычно когда меньше всего этого ждешь), он выполняется и дает предупреждение или ошибку, или ты включаешь трассировку кода, чтобы увидеть внутреннюю работу make.

Валидация — это отличный пример защитного программирования. Следующий код проверяет что текущая запущенная версия make — 3.80:

NEED_VERSION := 3.80
$(if $(filter $(NEED_VERSION),$(MAKE_VERSION)),,             \
 $(error You must be running make version $(NEED_VERSION).))

Для приложений Java полезно включить проверку файлов в CLASSPATH.

Код валидации также может просто проверять что что-то истинно.

Другой отличный пример защитного программирования это использование assert функций, например таких:

# $(call assert,condition,message)
define assert
   $(if $1,,$(error Assertion failed: $2))
endef
# $(call assert-file-exists,wildcard-pattern)
define assert-file-exists
   $(call assert,$(wildcard $1),$1 does not exist)
endef
# $(call assert-not-null,make-variable)
define assert-not-null
   $(call assert,$($1),The variable "$1" is null)
endef

Небольшое количество assert вызовов в makefile будет дешевым и эффективным путём обнаружения пропавших переменных или переменных с опечатками в названии, а также и других ожиданий.

Также можно определить пару функций для трассировки разворачивания пользовательских функций:

# $(debug-enter)
debug-enter = $(if $(debug_trace),\
                $(warning Entering $0($(echo-args))))

# $(debug-leave)
debug-leave = $(if $(debug_trace),$(warning Leaving $0))
comma := ,
echo-args = $(subst ' ','$(comma) ',\
              $(foreach a,1 2 3 4 5 6 7 8 9,'$($a)'))

Можно добавить вызовы этих макросов в свою функцию и оставить их выключенными до тех пор, пока они не потребуются для отладки. Для включения их нужно установить debug_trace в любое не пустое значение:

$ make debug_trace=1

И еще одно средство защитного программирования это легкое и простое выключение действия @ командного модификатора, если использовать его через переменную, а не буквально:

QUIET := @
…
target:
   $(QUIET) some command

Используя эту технику можно увидеть ход выполнения заглушенных команд просто переопределив переменную в командной строке:

$ make QUIET=

Часть 2 тут - Отладка Makefile /часть 2/.