© Георгиевский Анатолий, Евгений Загидуллин, 28.10.2005 - 22.12.2008

Справка по составлению Makefile

Зачем это нужно

Студенты привыкают к использованию графических интерфейсов для разработки программного обеспечения и часто не понимают, как это делается без кнопочки "билд". В целях облегчения жизни нужно снижать трудозатраты на рутинную работу, перекладывать её на плечи умных утилит, но как бы мы не обрастали графическими оболчками и средствами разработки проектов не стоит забывать на чем основыается процесс сборки проектов.

Необходимость стандартизации правил сборки проекта связано с требованиями переносимости программного обеспечения между различными аппаратными платформами, различными операционными системами и различными компиляторами. Такой набор требований возникает, например, если программное обеспечение разрабатывается для использования одновременно под Windows и Linux (кросс-платформенное ПО). Требование стандартизации правил сборки на уровне Makefile возникает, когда группа разработчиков использует различные среды разработки проектов (IDE). Если ПО разрабатывается сообществом, вы не можете навязывать требования стандартизации среды разработки и навязывать коммерческие компиляторы.

В наших задачах, программное обеспечение создается для встроенных систем и должно обладать переносимостью между различными процессорными архитектурами, требуется поддержка использования нескольких кросс-компиляторов в одном проекте. Разработчики используют Windows и GNU/Linux для сборки проекта и различные среды разработки проектов.

Высокопроизводительные расчетные задачи, с которыми сталкиваются физики и прикладные математики на нашем факультете, обычно выдвигают ряд требований по переносимости исходных кодов между процессорными архитектурами, в т.ч. 32-битными и 64-битными платформами. Правила сборки должны поддерживать условное подключение различных версий библиотек. В некоторых случаях требуется подключение разных библиотек под разные условия компиляции. Проекты могут собираться различными компиляторами, в частности GCC, Intel C/C++. Проекты должны собираться под Windows и Linux. В условиях вычислительного кластера ПО должно собираться из командной строки без привлечения среды разработки, через терминальное соединение. Все эти требования не должны ограничивать разработчика в выборе среды разработки проекта. Тот же проект должен собираться на персональном компьютере с использованием привычной среды разработки проектов.

Прежде всего стоит обратить внимание на справку по утилите make: man make. В справке вы найделте формальное описание всех возможностей утилиты. В качестве среды разработки проектов нашим студентам мы рекомендуем использовать Code::Blocks или Eclipse, которые позволяют использовать утилиту make и правила в Makefile для сборки проекта.

Задача утилиты make - автоматически определять, какие файлы проекта были изменены и требуют компиляции, и применять необходимые для этого команды. Хотя примеры применения относятся к использованию утилиты для описания процесса компиляции пограмм на языке С/С++, утилита может использоваться для описания сценариев обновления любых файлов.

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

Правила компиляции составных частей проекта заносятся в Makefile. Затем все необходимые действия по компиляции и сборке проекта могут быть выполнены автоматически при запуске утилиты make из рабочей директории проекта. Необходимость выполнения команд для обновления объектных и исполняемых кодов программ определяется исходя из даты и времени обновления исходных файлов проекта.

The GNU make

Стандартизация процедуры сборки программ с использованием утилиты make позволяет собирать пакеты программ из исходных кодов не имея представления о структуре и составных частях исходных кодов. Для сборки проекта распространяемого с исходными кодами достаточно выполнить команду "make" в корневой директории проекта. Имено так собирается открытое программное обеспечение.

Структура Makefile

Мakefile состоит из так называемых "правил", имеющих вид:

имя-результата: исходные-имена ...
       команды
        ...
        ...

имя-результата - это обычно имя файла, генерируемого программой, например, исполняемый или объектный файл. "Результатом" может быть действие никак не связанное с процессом компиляции, например, clean - очистка.

исходное-имя - это имя файла, используемого на вводе, необходимое, чтобы создать файл с именем-результата. команда - это действие, выполняемое утилитой make. Правило может включать более одной команды, В начале каждой команды надо вставлять отступ (символ "Tab"). команда выполняется, если если один из файлов в списке исходные-имена изменился. Допускается написание правила содержащего команду без указания зависимостей. Например, можно создать правило "clean", удаляющее объектные файлы проекта, без указания имен.

Итак, правила объясняют как и в каком случае надо пересобирать определённые файлы проекта.

Стандартные првила

К числу стандартных правил относятся:
all - основная задача, компиляция программы.
install - копирует исполняемые коды программ, библиотеки настройки и всё что необходимо для последующего использования
uninstall - удаляет компоненты программы из системы
clean - удаляет из директории проекта все временные и вспомогательные файлы.

Пример простого makefile

CC = mpicc
CFLAGS = -Wall

all: hello

hello: hello.o

hello.o: hello.c

.PHONY: clean

clean:
        rm -f hello hello.o

В начале файла следует определение констант. СС и CFLAGS - стандартные константы.
СС - определяет имя программы-компилятора языка С. В нашем примере в качестве компилятора указана программма mpicc.
CFLAGS - параметры командной строки при обращении к компилятору. Ключ -Wall разрешает вывод всех замечаний о качестве исходного кода. В структуре зависимостей не указаны правила компиляции программ. Видимо, по расширению имени утилита make сама может догадаться, какое действие надо произвести для получения нужного результата.

Структура зависимостей может составляться из явно описанных правил сборки и неявных правил. В данном примере, для получения объектного файла, компилятор с языка С запускается в соответствии с неявным правилом:

hello.o: hello.c
	$(CC) $(CFLAGS) -c -o hello.o hello.c

Makefile для сборки программ на языке С++ и Fortran

# Компилятор для программ на С
CC = mpicc
# Компилятор для программ на C++
CXX= mpiCC
# Компилятор g77 для программ на языке GNU Fortran 77, 
FC = mpif77
# Компилятор GNU g95 для программ на языке Fortran 95
# FC = mpif95

CXXFLAGS = -Wall
CFLAGS = -Wall

all: hello

hello: hello.o
	$(CXX) $(LINKLAGS) hello.o -o hello
hello.o: hello.cpp

.PHONY: clean

clean:
	rm -f hello hello.o

Для разработки программ на C/С++ под Windows и Linux, мы предлагаем пользоваться интегированной средой разработки Code::Blocks коллекцией компиляторов GNU. Процесс компиляции программ сможет основываться на использовании готового сценария Makefile (make -f Makefile).

Оптимизация программ под архитектуру вычислительного кластера

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

Ключи компилятора GNU

-I"путь/поиска/include"
добавляет директорию к списку путей поиска файлов заголовков(.h).

-Wall -Werror
эти опции заставляют компилятор реагирвать на множество замечаний, которые не вызывают ошибок, но потенциально могут приводить к некорректной работе программ. Студентам мы настоятельно рекомендуем использовать эти опции при компиляции программ. Наш опыт показывает, что применние такой строгости позволяет избежать большей части проблем утечки памяти, некорректного преобразования типов и сократить период отладки программы.

-O1, -O2, -O3, -Os
Ключи -On определяют уровень оптимизации программ. Не рекомендуется использование ключей оптимизации кода на этапе отладки. В частности, при использовании наиболее глубокой оптимизации -O3 изменяется порядок следования инструкций и теряется привязка исполняемого кода к исходному, что делает невозможным пошаговую отладку программы. Ключ -Os вызывает оптимизацию программы по размеру исполняемого кода. Есть мнение, что хорошо написаная программа при использовании оптимизации "по размеру" имеет показатели производительности не хуже, чем при оптимизации "по производительности". Во всех случаях оптимизация не дает существеной прибавки производительности. Основной выигрыш производительности получается только за счет правильного выбора метода решения задачи.

-march=архитектура
Часще всего речь идет о запуске программ на однородном кластере состоящем из N-вычислительных узлов. Можно довольно существенно ускорить выполнение программ, указав при компиляции целевую архитектуру процессоров. В нашем случае используются процессоры Celeron (c ядром P-4), Pentium-4, AMD Opteron и Intel Xeon EM64T Nocona.
Ключи: -march=opteron, -march=athlon-4, -march=nocona, -march=prescott, -march=pentium4. Все возможные варианты выбора архитектуры можно посмотреть в документации по GCC: команда man gcc или на сайте GCC manual. Следует также обратить внимание, на совместимость ключей компилятора и библиотек. Библиотеки, в частности libc, должны быть скомпилированы под ту же целевую архитектуру.

-o имя-объектного-файла.o
указывает компилятору имя объектного файла, который получается в результате компиляции.

Ключи сборки

-llibrary
указывает линковшику использовать библиотеку library при сборке программы. Линковщик просматривает список директорий, в которых установлены библиотеки, в поисках файла с именем liblibrary.a

-s
не включает симольные таблицы и информацию о размещении функций в испольняемый файл. Использование этого ключа позволяет существенно ужать исполняемые файлы.

-L"путь/поиска/библиотек"
добавляет директорию к списку путей поиска библиотек.

Универсальный файл сборки

Приведем ещё один пример описания сборки (Makefile)

CFLAGS = -Wall -g3
CPPFLAGS = $(shell pkg-config gtk+-2.0 gthread-2.0 --cflags)
LDLIBS = $(shell pkg-config gtk+-2.0 gthread-2.0 --libs)

SOURCES = cnc.c cnc_hpgl.c cnc_dxf.c gcodes.c grafix.c main.c
APP = cnc-view

all: $(APP)

clean:
	rm -f $(APP) *.o

$(APP): $(SOURCES:.c=.o)
	$(LINK.o) $^ $(LDLIBS) -o $@

Сложно объяснить почему это вообще работает. В описании вообще отсутствует связь между исходными и исполняемыми файлами. Утилита использует неявные правила для компиляции файлов. Попробуем пояснить, что есть что в этом примере и как модифицировать этот файл для своих целей. При составлении команд утилита make на выводе будет содержать строки вида:

cc  -Wall -g3 `pkg-config ... --cflags` -c main.c
cc main.o `pkg-config ... --libs` -o cnc-view

Прежде всего обратите внимание на строчки в обратных кавычках (``). Интерперетатор командной строки заменяет содержимое обратных кавычек на результат исполнения строки. Утилита pkg-config в зависимости от ключа (--libs или --cflags) выдает список параметров необходимых для сборки проекта с использованием библиотек указанных в качестве аргументов. Утилита pkg-config входит в состав дистрибутивов Linux и может быть установлена под Windows. Утилита pkg-config разработана для поиска зависимостей и установленных компонет библиотек, и может быть использована в сценариях сборки программ как под Linux так и под Windows. Подробности использования утилиты pkg-config, см. man.

В самом файле определение APP задает имя приложения на выходе, а в определении SOURCES перечислены исходные файлы проекта.

Для нормальной работы сценариев сборки программ под Windows вам, кроме самой утилиты make, понадобиться также утилита rm, для удаления файлов, котороая входит в дистрибутив MinGW и поставляется в составе Code::Blocks

Автоматическая подстановка имен в правилах

$@
Имя цели правила
$^
Список зависимостей, подставляет весь список
$<
Имя первого элемента в списке зависимостей. Правило последовательно применяется для каждого исходного файла.

Список зависимостей может содержать имена файлов с указанием директории, в которой расположен файл. В этой связи, возникает необходимость выделения имени файла или выделения имени директории, в которой расположен файл.

$(@F)
Имя файла цели правила,
$(@D)
Имя директории цели правила,
$(<F)
Имя исходного файла без имени директории,
$(<D)
Имя директории, в которой расположен исходный файл,

Условия и определения

Как бы мы не старались сделать сценарий универсальным набор исходных файлов и правила сборки проекта под разные платформы и разные архитектуры и разные компиляторы могу отличаться. В таком случае предлагается использовать нотации для условного исполнения сценария.

ifndef DEVICE
DEVICE = default
endif

FEATURESET = USE_UNICODE
ifeq ($(DEVICE),BENDER)
	FEATURESET += USE_PROGMAN USE_UI
endif

Значение параметра может быть задано в окружении или в качестве параметра запуска сценария

make DEVICE=BENDER

Развернутый пример

Приведем пример раздутого файла сборки, который описывает процедуру сборки модульного ядра системы. Модули - это куски кода, которые встраиваются в основную программу по определенным правилам. Один Makefile описывает целый спектр целевых платформ с разным набором программных возможносетей (модулей). Набор программных возможностей (Feature Set) выбирается путем указания целевой платформы, каждой платформе соответствует свой набор модулей. Кроме того есть возможность переопределения кросс-компилятора для сборки программы под разные архитектуры процессоров. Пример взят из рабочего проекта, ориентированного на встроенные системы.

Выбор кросс-компилятора

	
# Префикс имени компилятора выбирается 
# исходя из настроек параметра CROSS
ifndef CROSS
CROSS = arm-elf
endif

# Для работы необходимо определить имя компилятора C, 
# ассемблер и утилиты перобразования объектных кодов
CC = $(CROSS)-gcc
AS = $(CROSS)-gcc
SIZE = $(CROSS)-size
OBJCOPY = $(CROSS)-objcopy

Выбор набора программных возможностей, привязка аппаратуры

ifeq ($(DEVICE),BENDER)
	FEATURESET = USE_CNC
	CHIP  = AT91SAM9260
	ARCH  = ARM926
else ifeq ($(DEVICE),SH32LS2)
	CHIP = AT91SAM7S64
	ARCH  = ARM7TDMI
else
	CHIP  = AT91SAM9260
	ARCH  = ARM926
	FEATURESET = USE_CNC USE_OHCI
endif

Выбор параметров оптимизации

ifeq ($(ARCH), ARM926)
	OPTIMIZATION += -march=armv5te
else
	OPTIMIZATION += -march=armv4t
endif
OPTIMIZATION +=-Os

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

FEATURESET := $(filter-out $(FEATURE_DEL),$(FEATURESET))
FEATURESET += $(FEATURE_ADD)

FEATURESET_CFLAGS = $(patsubst %,-D%,$(FEATURESET))
$(foreach USE,$(FEATURESET),$(eval $(USE) = yes))

Тут следует дать разъяснения по используемым записям. В наших примерах часто встречаются конструкции вида $(функция списк-аргументов) или просто $(список-аргументов). Не будем углубляться в описание всех возможных функций. Приведем краткий скисок функций. Если вас заинтересует одна из этих возможностей, описание работы функции можно найти в документации по утилите make.

$(patsubst шаблон,замена, список)
функция замены строк по шаблону
$(переменная:шаблон=замена)
функция замены строк по шаблону, выражение эквивалентно patsubst
strip
удаляет начальные и конечные пробелы
findstring
производит поиск фрагмента
filter-out
удаляет все слова, которые подходят под шаблон
sort
производит сортировку слов
$(dir список)
выделяет имена директорий из списка файлов
$(sort список)
производит сортировку по имени и удаляет дублирующиеся имена
$(addprefix префикс, список)
добавляет префикс к каждому элементу списка
suffix
выделяет суффиксы файлов
join
объединение строк
word
выделяет слово из текста
$(wildcard шаблон)
формирует список файлов по шаблону, например *.с
foreach
цикл выполняется для каждого слова из списка
$(shell команда)
подставляет результат выполнения системной команды

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

Определение цели и вспомогательных папок

# Output file basename
OUTPUT = $(DEVICE)-$(CHIP)-test

Правила включения директорий, флаги компиляции

# Flags
INCLUDES = -I./ -I../ -Ihal/ -Icore/ -Iboard/$(BOARD)/

CFLAGS += -Wall
CFLAGS += $(OPTIMIZATION) $(INCLUDES) 
CFLAGS += -D$(CHIP) -D$(BOARD) $(FEATURESET_CFLAGS)

ASFLAGS = $(OPTIMIZATION) -D$(CHIP) -D__ASSEMBLY__ -c
LDFLAGS += -s $(OPTIMIZATION)

Подключение файлов в проект

C_SOURCES += main.c
C_SOURCES += hal/aic.c hal/pio.c hal/pit.c hal/usart.c
C_SOURCES += core/config.c 

ifdef USE_OHCI
C_SOURCES += core/usb_ohci.c
endif

ifdef USE_CNC
C_SOURCES += cnc_machine.c
endif

Правила сборки по типам файлов

all: $(OBJ)

$(OBJ): $(ASM_SOURCES:.S=.o) $(C_SOURCES:.c=.o)
	$(CC) $(LDFLAGS) -n -o $(OUTPUT).elf $^
	$(SIZE) $(OUTPUT).elf

$(ASM_SOURCES:.S=.o): $(ASM_SOURCES)

clean:
	rm -rfv $(C_SOURCES:.c=.o) $(ASM_OBJECTS:.S=.o) $(OUTPUT).elf

Правила компиляции объектных файлов из исходных кодов используются по умолчанию. Утилита make по-умолчанию использует константы CC, CFLAGS, AS и ASFLAGS для составления команд.

Необычный пример

Приведем пример составления файла сборки для построения документации к проекту. Непревычным является то, что утилита make используется для компоновки и преобразования форматов текстовых и графических форматов, не имеющих прямого отношения к языкам программирования. Для сборки документации по функциям и составным частям проекта ПО используется утилита Doxygen. Программа Doxygen просматривает исходные коды проекта и создает файлы описания в формате html. index.html создаётся doxygen'ом всегда вне зависимости от состава документации. Целью сборки является обновление документации т.е. обновление index.html. Среди прочих правил указывается правило, по которому создается папка для записи документации и генерируется документация с вставками диаграмм описанных в приложении Dia. Диаграммы предварительно должны быть перобразованы в графический формат PNG. Обратите внимание, что преобразование форматов выполняется только при обновлении исходных файлов .dia. Обработке подлежат все файлы в текущей директории.

# =========================================
DIA = dia
SOURCES = $(wildcard *.dia)
DESTDIR = html
DOC_INDEX = $(DESTDIR)/index.html
# =========================================

PNG = $(SOURCES:%.dia=$(DESTDIR)/%.png)

.PHONY: all clean doc

all: $(PNG)

$(PNG): $(DESTDIR)/%.png: %.dia
	$(DIA) -t png-libart -e $@ $<

$(DOC_INDEX): $(PNG)
	doxygen

doc: $(DOC_INDEX)

clean:
	$(RM) $(DESTDIR)/*	

Описанный сценарий герерации документации является универсальным и не требует дополний в Makefile после внесения изменений в исходные файлы проекта, не требует адаптации при переносе в другой проект.


Для получения более подробной информации по написанию Makefile надо изучать документацию по make (man make) и раздел GNU make manual.

Описание утилиты Make в стандарте POSIX, IEEE Std 1003.1, 2004 Edition

Оставить комментарий

(28 октября 2005 г. - 22 декабря 2008 г.)