Дело было в субботу, проснулся я как-то с утра под звуки стройки и... решил, что точно надо написать что-то в бложик, ибо давно таким не занимался.
Итак, Makefile для гошных приложений.
Можно спросить - зачем нам makefile, если в принципе в проекте на гошке всё просто, мы говорим go build, и он собирается. Кроме того, по-сути инициализация проекта таким makefile-ом не охватывается? Ответ довольно простой - чтобы не напрягаться. Как и в сишке, всё должно быть просто и одинаково для обслуживания.
Итак, начнём. Прежде всего, вспомним, что хорошим тоном будет соблюдать folder layout, которого придерживаются проекты на гошке. Как вариант, это main.go с приложением в каталоге cmd/имя-приложения и именно этот файл мы собираем для сборки приложения. go.mod и go.sum в основном каталоге. Либы (как правило, основная либа приложения) в каталоге internal/имяЛибы. Пакеты, которые можно запользовать в других приложениях, в каталоге pkg/имяПакета. Далее, можно к этому доложить данные в каталоге data, примеры в каталоге examples, какие-то вещи, специфичные для дистрибутивов линукса (и не только) в каталоге etc, что-то, что было бы полезно, но совсем не обязательно в каталоге contrib и так далее.
С формальностями покончено, давайте уже к сути вопроса.
#!/usr/bin/env gmake -f
GOOPTS=CGO_ENABLED=0
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath -buildvcs=false
MYNAME=aleesa-webapp-go
BINARY=${MYNAME}
UNIX_BINARY=${MYNAME}
WINDOWS_BINARY=${MYNAME}.exe
MYNAME2=flickr_init
BINARY2=${MYNAME2}
UNIX_BINARY2=${MYNAME2}
WINDOWS_BINARY2=${MYNAME2}.exe
MYNAME3=flickr_populate
BINARY3=${MYNAME3}
UNIX_BINARY3=${MYNAME3}
WINDOWS_BINARY3=${MYNAME3}.exe
MYNAME4=flickr_test
BINARY4=${MYNAME4}
UNIX_BINARY4=${MYNAME4}
WINDOWS_BINARY4=${MYNAME4}.exe
RMCMD=rm -rf
# На windows имя бинарника может зависеть не только от платформы, но и от выбранной цели, для linux-а суффикс .exe
# не нужен
ifeq ($(OS),Windows_NT)
ifdef GOOS
ifeq ($(GOOS),windows)
BINARY=${WINDOWS_BINARY}
BINARY2=${WINDOWS_BINARY2}
BINARY3=${WINDOWS_BINARY3}
BINARY4=${WINDOWS_BINARY4}
else # not ifeq ($(GOOS),windows)
BINARY=${MYNAME}
BINARY2=${MYNAME2}
BINARY3=${MYNAME3}
BINARY4=${MYNAME4}
endif # ifeq ($(GOOS),windows)
else # not ifdef GOOS
BINARY=${WINDOWS_BINARY}
BINARY2=${WINDOWS_BINARY}
BINARY3=${WINDOWS_BINARY}
BINARY4=${WINDOWS_BINARY}
endif # ifdef GOOS
ifeq ($(SHELL), sh.exe)
RMCMD=DEL /Q /F
endif
endif
# Явно определяем символ новой строки, чтобы избежать неоднозначности на windows
define IFS
endef
all: clean build
build:
ifeq ($(OS),Windows_NT)
# Looks like on windows gnu make explicitly set SHELL to sh.exe, if it was not set.
ifeq ($(SHELL), sh.exe)
# # Vanilla cmd.exe / powershell.
SET "CGO_ENABLED=0"
go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME}
else ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
# # git-bash
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME}
else # not ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
# # Some other shell.
# # TODO: handle it.
$(info "-- Dunno how to handle this shell: ${SHELL}")
endif # ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
else # not ($(OS),Windows_NT)
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME}
endif # ifeq ($(OS),Windows_NT)
buildutils:
ifeq ($(OS),Windows_NT)
# Looks like on windows gnu make explicitly set SHELL to sh.exe, if it was not set.
ifeq ($(SHELL), sh.exe)
# # Vanilla cmd.exe / powershell.
SET "CGO_ENABLED=0"
go build ${BUILDOPTS} -o ${BINARY2} ./cmd/${MYNAME2}
go build ${BUILDOPTS} -o ${BINARY3} ./cmd/${MYNAME3}
go build ${BUILDOPTS} -o ${BINARY4} ./cmd/${MYNAME4}
else ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
# # git-bash
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY2} ./cmd/${MYNAME2}
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY3} ./cmd/${MYNAME3}
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY4} ./cmd/${MYNAME4}
else # not ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
# # Some other shell.
# # TODO: handle it.
$(info "-- Dunno how to handle this shell: ${SHELL}")
endif # ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
else # not ($(OS),Windows_NT)
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY2} ./cmd/${MYNAME2}
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY3} ./cmd/${MYNAME3}
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY4} ./cmd/${MYNAME4}
endif # ifeq ($(OS),Windows_NT)
clean:
ifeq ($(OS),Windows_NT)
ifeq ($(SHELL),sh.exe)
# # Vanilla cmd.exe / powershell.
if exist ${WINDOWS_BINARY} ${RMCMD} ${WINDOWS_BINARY}
if exist ${UNIX_BINARY} ${RMCMD} ${UNIX_BINARY}
if exist ${WINDOWS_BINARY2} ${RMCMD} ${WINDOWS_BINARY2}
if exist ${UNIX_BINARY2} ${RMCMD} ${UNIX_BINARY2}
if exist ${WINDOWS_BINARY3} ${RMCMD} ${WINDOWS_BINARY3}
if exist ${UNIX_BINARY3} ${RMCMD} ${UNIX_BINARY3}
if exist ${WINDOWS_BINARY4} ${RMCMD} ${WINDOWS_BINARY4}
if exist ${UNIX_BINARY4} ${RMCMD} ${UNIX_BINARY4}
else # not ifeq ($(SHELL),sh.exe)
${RMCMD} ./${WINDOWS_BINARY}
${RMCMD} ./${UNIX_BINARY}
${RMCMD} ./${WINDOWS_BINARY2}
${RMCMD} ./${UNIX_BINARY2}
${RMCMD} ./${WINDOWS_BINARY3}
${RMCMD} ./${UNIX_BINARY3}
${RMCMD} ./${WINDOWS_BINARY4}
${RMCMD} ./${UNIX_BINARY4}
endif # ifeq ($(SHELL),sh.exe)
else # not ifeq ($(OS),Windows_NT)
${RMCMD} ./${BINARY}
${RMCMD} ./${BINARY2}
${RMCMD} ./${BINARY3}
${RMCMD} ./${BINARY4}
endif
upgrade:
go get -u ./...
go mod tidy
go mod vendor
# vim: set ft=make noet ai ts=4 sw=4 sts=4:
Собственно, на этом можно закончить :) Далее, по-сути, идут пояснения относительно этого Makefile-а.
Но, возможно, пояснения окажутся интересными.
Итак, конечно же сам по себе Makefile для удобства можно сделать исполняемым, именно для этого в первой строке стоит shebang. То есть, прям с первой строки у нас идёт охрененный quality of life trick. И, важный момент - на некоторых системах make это не gnu make, к которому мы все привыкли, а, например, всратый bsd make, который и сильно более примитивный и сильно более другой. То есть, технически, конечно, возможно извратиться и написать что-то совместимое с bsd make, но зачем страдать? именно поэтому мы, в качестве названия бинарника make, берём gmake. Это устояшееся имя для gnu make. А привычный нам make по сути альяс к этому gmake.
Следующим пунктом у нас переменные окружения. Они специфичны для конкретного проекта или их может даже вовсе не быть.
В Makefile-е, взятом для примера, это переменная CGO_ENABLED выставленная в 0. Да, в этой строке ДВА знака "равно", это нормально, make интерпретирует как присвоение значения только первый знак "равно" в этой конструкции. Кроме того, строку не надо брать в кавычки, лексер справляется со всзятием значения для присвоения переменной GOOPTS и без них. Эта переменная работает в области видимости makefile и конкретно в данном случае не является переменной окружения (make не bash, в конце концов). Что значит CGO_ENABLED на текущий момент неважно, это можно загуглить (да-да, автор тот ещё засранец и делает некрасиво, отсылая читателя гуглить, это нормально).
Следующая переменная окружения BUILDOPTS - это опции сборки. У компилятора гошки их не так много и большинство из них обычно не задействуются. Здесь взяты опции, которые делают бинарник меньше без потери возможности получать нормальные trace-ы в случае падения приложения.
Далее идёт блок переменных MYNAME, BINARY, UNIX_BINARY, WINDOWS_BINARY и они регламентируют наименование получаемого бинарника. В данном случае у приложения один основной бинарник, поэтому описывается, как он будет именоваться под windows и под остальные поддерживаемые системы.
За этим блоком идут три блока с описанием имён бинарников вспомогательных утилит.
Затем мы задаём начальное значение переменной RMCMD, в которой содержится команда удаления файлов. Дело в том, что в разных оболчках и разных средах эта команда разная. В gnu make даже встроена команда удаления файлов, но она не портабельная, то есть под windows она не работает, несмотря на то, что есть порт команды make под windows.
Далее, идёт большой блок, в котором мы таки определяемся, какие имена будут у бинарников в зависимости от текущей системы и переменной окружения GOOS. И заодно, если нужно переопределяем команду удаления бинарника.
Следующим шагом мы явно задаём значение переменной, которая используется make-ом для определения символа новой строки. Существует не один порт команды make под windows и в разных портах используется разные значения символа новой строки. Так вот, мы хотим, чтобы он был одинаковым и таким как на unix-системах и мы его явно определяем.
С подготовительными работами покончено, все вводные у нас на руках, можно начинать описывать цели сборки.
Цели сборки, это то, что идёт после слова make когда мф пытаемся заставить make собрать проект. Допустим, ничего - в нашем случае, это эквивалент make all. Или make clean, чтобы убрать все файлы, которые были сгенерированы в процессе сборки.
В разбираемом примере у нас есть цель "ничего", all, build, buildutils, clean и upgrade.
Итак, начнём с начала - цель по-умолчанию, она же "ничего", она же all. В этой цели определяется, что мы будем собирать другие цели - вначале clean, за ней build.
Следующая цель build. Это самостоятельная цель и для её достижения не надо собирать никаких других целей. Поэтому после двоеточия мы переходим сразу к инструкциям. Обратите внимание, что условные операторы начинаются с начала строки, а вот, собственно, команды с символа tab. И здесь важно, что это именно символ tab, make именно его требует для того, чтобы отделить команды цели от всего остального. (И в этом месте питонисты наверняка упали на пол, забились в конвульсиях и начали пускать пену изо рта.)
Поскольку, основная логика сконцентрирована именно в основной программе, а вспомогательные программы затрагивают лишь небольшую часть проекта, для утилит есть отдельная цель buildutils. Подразумевается, что в процессе разработки не так часто надо будет билдить утилиты, а вот основной бинарь придётся билдить регулярно, для проверки того, что сделано.
Следующая цель - clean. Она используется в цели по-умолчанию, чтобы убедиться, что перед сборкой бинаря всё почищено. По сути, в рассматриваемом проекте нет ничего генерённого при сборке, кроме бинарников, в этой цели мы как раз занимаемся удалением всех возможных бинарников.
И наконец последняя цель - upgrade. Этот проект простой, не предполагающий использование часто меняющихся методов и функций задействованных библиотек, поэтому апгрейд зависимостей здесь вынесен в отдельную цель. Но вообще говоря, хорошей практикой является ревью всего и вся на предмет изменений, поэтому предполагается, что разработчик не слепо пытается апнуть все зависимости, а вначале смотрит, что имеет смысл обновить, а что нет. И в идеале обновлет точечно ровно то, что имеет смысл обновить. Либо обновлять всё и потом смотреть, что ничего не сломалось и работает как должно.
На этом, пожалуй, всё.
Утилита make умеет ощутимо больше, чем нужно для гошных проектов, но наш опус был посвящён конкретному use case-у, поэтому остальные возможности gnu make-а остались за рамками повествования.