Немного о makefile

Среди программистов freebasic (да и других бейсиков), как правило не обсуждается тема makefile , в частности на платформе Windows. Наверно это связано с тем, что лень залезать в дебри документации по GNU утилитам. Конечно, когда дело касается одного, двух или даже 10 ".bas" файлов, проще создать проект в FbEdit или написать простенький bat файл. Но вот у меня проект из ~300 файлов . Да, да... моя библиотека насчитывает 290 отдельных файлов bas. И все их надо собрать в одну статическую библиотеку. Для такого кол-ва проект FbEdit не подойдет (автор просто не внес такую возможность и редактор вылетает). До какого-то времени я использовал bat файл . Но батник себя оправдывает, когда действительно нужно скомпилировать\перекомпилировать все файлы. Какого же приходится, когда нужно внести правки лишь в отдельные файлы... Ожидание компиляции становится тягостным! К примеру компиляция проходит так:

1) каждый из 300 файлов .bas сначала компилируется в объектный файл
2) все 300 объектных файлов собираются в единый файл библиотеки

Первый пункт выполняется долго (несколько минут). Второй пункт 2-3 секунды. Очевидно, что первый пункт желательно оптимизировать, в особенности при малых правках. И действительно, если мы внесли правки в один , два файла, зачем нам делать перекомпиляцию остальных 298 файлов? Нам просто нужно перекомпилировать эти два файла и затем выполнить 2 пункт. Конечно это работает, если объектные файлы после компиляции не удаляются!!!

И вот тут , как раз пригодится утилита make. Она умеет автоматически определять измененные файлы и выполнять определенные вами команды.

Но здесь встают два вопроса:

1) Надо специально ставить пакет MinGW+MSYS
2) Нужно изучать нехилую документацию по утилите MAKE.

На самом деле, 1 пункт никогда не будет лишним. Вам пригодится данный пакет , хотя бы для компиляции самого компилятора freebasic или любых других сишных библиотек.

Что же касается 2 пункта, то тут может и не стоит учить все? Мне например достаточно малой толики знаний по этой утилите. Нужно просто знать как внутри выглядит простейший makefile и основные правила.

Простейший формат записи примерно такой:

target: prerequisite
    commands

target - это цель которую надо достигнуть, например в качестве цели можно указать конечный файл библиотеки. Например так: libmylib.a :

prerequisite - это файл\файлы , которые нужны для достижения target . Несколько файлов записываются через пробел. Пример: 1.bas 2.bas 3.bas

commands - это команда которую нужно выполнить для достижения target , например: fbc -lib 1.bas 2.bas 3.bas -x libmylib.a

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

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

Конечный продукт: первая_заготовка вторая_заготовка третья_заготовка
    собрать из заготовок конечный_продукт
Первая заготовка: первое_исходное_сырье
    собрать из сырья первую_заготовку
Вторая заготовка: второе_исходное_сырье
    собрать из сырья вторую_заготовку
Третья заготовка: третье_исходное_сырье
    собрать из сырья третью_заготовку

#######следующие строки по умолчанию выполняться не будут#######
Другой продукт: четвертая_заготовка пятая_заготовка шестая_заготовка
    собрать из заготовок другой_продукт

Четвертая заготовка: исходное_сырье
    собрать из сырья четвертую_заготовку
Пятая заготовка: исходное_сырье
    собрать из сырья пятую_заготовку
Шестая заготовка: исходное_сырье
    собрать из сырья шестую_заготовку

Утилита MAKE при таком подходе читает цель по умолчанию (Конечный продукт) и смотрит зависимости , которые нужно выполнить. Должно быть понятно, что заготовок пока не существует, поэтому утилита сначала выполнит работу по их созданию, а потом уже выполнит работу по их объедению в конечный продукт. Если мы первые 2 строчки:

Конечный продукт: первая_заготовка вторая_заготовка третья_заготовка
    собрать из заготовок конечный_продукт

запишем в конец makefile , то тогда выполняются строчки:

Первая заготовка: первое_исходное_сырье
    собрать из сырья первую_заготовку

и make завершит работу, потому что работа по данной цели самодостаточна и не требует никаких зависимостей.

Если нам нужно выполнить цель "Другой продукт" вместо цели по умолчанию, то в командной строке нужно после make ввести имя данной цели.

В таком варианте, утилита MAKE всегда проверяет сырье на предмет изменения. Если не найден какой нибудь объектный файл, то выполняется команда по его компиляции. 

А теперь представьте, что у нас есть 3 файла: 1.bas , 2.bas , 3.bas и нам нужно создать makefile для создания 1.exe , 2.exe и удаления всех созданных файлов. Файл 1.exe компилируется сразу из трех модулей (1.bas , 2.bas , 3.bas) , а файл 2.exe только из двух (1.bas , 2.bas). Тогда makefile может выглядеть примерно так:

#цель по умолчанию
1.exe: 1.o 2.o 3.o
    fbc -x 1.exe 1.o 2.o 3.o
    
#цель опционально (задается в командной строке, например так: make 2.exe)
2.exe: 2.o 1.o
    fbc -x 2.exe 2.o 1.o
    
#цель опционально удаление файлов (задается в командной строке например так: make clean)
clean:
    rm -f *.exe *.o
    
#вспомогательная цель 1.o
1.o: 1.bas
    fbc -m 1 -c 1.bas
    
#вспомогательная цель 2.o
2.o: 2.bas
    fbc -c 2.bas
    
#вспомогательная цель 3.o
3.o: 3.bas
    fbc -c 3.bas


Как можно заметить, есть три главных цели (1.exe, 2.exe, clean) и три вспомогательные (1.o, 2.o, 3.o). При чем цель 1.exe (по умолчанию) выполняется , когда в командной строке набирается make. Цель 1.exe зависит от 3 вспомогательных целей 1.o, 2.o, 3.o. Пока Make не создаст 1.o, 2.o, 3.o, нет возможности выполнить цель 1.exe, поэтому вспомогательные цели выполняются в первую очередь. Если мы наберем make clean , то выполнится цель clean , которая кстати не имеет зависимостей (просто удаление имеющихся файлов по маске) и make завершит работу. Если мы наберем make 2.exe , то выполнится цель 2.exe , которая зависит от 2 вспомогательных целей 1.o, 2.o и make завершит работу.

Как видно символ # определяет комментарии.

Для того, чтобы напечатать какую-нибудь строку есть команда echo. Пример ее использования:

#цель по умолчанию
1.exe: 1.o 2.o 3.o
    @echo Выполняется сборка
    fbc -x 1.exe 1.o 2.o 3.o


Символ @ запрещает печатать символьное представление команды, но конечно же строка в параметре команды echo выведется.

Для переноса длинных строк есть символ \

И последнее, что реально может понадобится, это переменные. Объявляются переменные так:

имя = значение

или

имя := значение

В реальности это два разных присваивания, с разными тонкостями, но для простых makefile , я думаю не нужно придавать этому значение.

Обращение к переменной происходит так:

$(имя)

И прошлый пример с использованием переменной:

#переменная
OBJ = 1.o 2.o 3.o
 
#цель по умолчанию
1.exe: $(OBJ)
    @echo Выполняется сборка объектых файлов
    fbc -x 1.exe $(OBJ)
    
#цель опционально (задается в командной строке, например так: make 2.exe)
2.exe: 2.o 1.o
    fbc -x 2.exe 2.o 1.o
    
#цель опционально удаление файлов (задается в командной строке например так: make clean)
clean:
    rm -f *.exe *.o
    
#вспомогательная цель 1.o
1.o: 1.bas
    fbc -m 1 -c 1.bas
    
#вспомогательная цель 2.o
2.o: 2.bas
    fbc -c 2.bas
    
#вспомогательная цель 3.o
3.o: 3.bas
    fbc -c 3.bas

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