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

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

В Go можно установить значение переменных на этапе компиляции, используя параметр -ldflags <flags>, предоставленный для команды go build.

Флаги сборки

Параметр - ldflags позволяет указать аргументы, которые затем будут переданы в go link инструмент, используемый в процессе сборки. В нашем случае особенно интересен один из аргументов:

-X importpath.name=value
	Set the value of the string variable in importpath named name to value.

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

  • Переменная не должна быть инициализирована или инициализирована постоянной строкой. -X не будет работать, если значение инициализировано с помощью:
var (
     	// Call to external function
 and variable
	Build    = fmt.Sprintf("rc-%d", someIntegerVar)
)

но работает с:

var (
     	// Call to external function
	Build    = "rc-4"
)

Давайте создадим скрипт сборки, который автоматически установит номер версии и зафиксирует хеш

Мы создадим простую программу с одной функцией - выводом версии и строкой сборки (фиксации).

Создайте каталог проекта и инициализируйте репозиторий git внутри:

mkdir ~/go-build-variables
cd ~/go-build-variables
git init

Не создавайте простой main.go файл с простым содержанием:

package main
import (
	"fmt"
)
var (
	Version  = "0.0.1"
	Build    = "000000"
)
func main() {
	fmt.Printf("version %s, build: %s\n", Version, Build)
}

На этапе сборки мы будем заменять переменные Version и Build и изменять их значения по умолчанию.

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

go build -ldflags "-X main.Version=0.1.0 -X main.Build=12345" main.go

main.Version указывают на наш пакет, который в данном случае main, и имена переменных Version и Build.

Если вы сейчас запустите скомпилированный main файл, вы должны увидеть результат, подобный следующему:

$: ./main 
version 0.1.0, build: 12345

Но мы можем сделать больше. Давайте создадим сценарий-оболочку для автоматизации процесса сборки!

Создадим удобный bash скрипт-оболочку для процесса сборки. Такой сценарий может использоваться вашими инструментами CI / CD. Наш скрипт принимает следующие аргументы:

  • -v (обязательно) для предоставления версии
  • -e (необязательно), чтобы предоставить целевую среду, для которой мы хотим создать двоичный файл (разработка или выпуск)
  • -o (необязательно) для указания имени выходного двоичного файла

Несколько слов об окружающей среде. Почему это здесь? Возможно, вы работаете над проектом, который требует других настроек для целей разработки (например, добавление информации к номеру версии, сборка с разными флагами, отключение некоторых проверок…), но абсолютно не может присутствовать в версии, выпущенной для клиентов (или production версии). Таким образом, хорошей практикой было бы создание release сборки по умолчанию и development сборки по специальному запросу (наоборот, в зависимости от ваших бизнес-потребностей).

Итак, теперь мы создадим bash скрипт для нашего процесса сборки:

#!/usr/bin/env bash
function print_usage {
  printf "Usage: build.sh -v [-e] SRC_FILE\n"
  printf "\t-v\tProvide binary version, example: -v 0.0.1\n"
  printf "\t-e\tProvide target environment, any value different than \"development\" will result in release build, example: -v release\n"
  printf "\t-o\tOutput binary filename\n"
  printf "\t-h\tPrint help and exit\n"
  exit 1
}
# Iterate through arguments list and process arguments
while getopts "v:e:o:h" o; do
    case "${o}" in
        v)
            VERSION=${OPTARG}
            ;;
        e)
            ENV=${OPTARG}
            ;;
        o)
            OUTPUT_FILE=${OPTARG}
            ;;
        h)
            print_usage
            ;;
        *)
            printf "Error: Option undefined\n\n"
            print_usage
            ;;
    esac
done
# Discard processed arguments from arguments list ("OPTIND-1" is index of first unprocessed argument - source filename in this case)
shift $((OPTIND-1))
SRC_FILE="${BASH_ARGV[0]}"
# Version number and source filename is required
if [[ -z $VERSION ]]
then
  print_usage
fi
if [[ -z $SRC_FILE ]]
then
  print_usage
fi
# If output filename is not set, we will mimic how go by default handles this situation - source main filename without .ext
if [[ -z $OUTPUT_FILE ]]
then
    OUTPUT_FILE="${SRC_FILE%.*}"
fi
# Any value other than "development" will produce production build.
# This is to prevent building and accidental release of development builds which can be unsafe or unstable.
# Here we are appending \"rc-\" to version number and setting output filename to indicate if this is release or dev build,
# but it can be more here, for example setting another variable to indicate development build or building with different flags.
if [[ "$ENV" == "development" ]]
then
    VERSION="rc-$VERSION"
    OUTPUT_FILE="$OUTPUT_FILE-development"
else
    OUTPUT_FILE="$OUTPUT_FILE-stable"
fi

# Enable strict checking for variables initialization and exit codes.
set -euo pipefail
# Format \"%h\" for git log prints only short commit hashes in reverse chronological order,
# -n 1 returns first row - last commit
BUILD_HASH=$(git log --pretty=format:"%h" -n 1|head -1)
echo "Building: $SRC_FILE version: $VERSION from commit: $BUILD_HASH to file: $OUTPUT_FILE"
go build -ldflags "-X main.Version=$VERSION -X main.Build=$BUILD_HASH" -o "$OUTPUT_FILE" "$SRC_FILE"

Код прокомментирован, но я добавлю несколько слов:

  • Сначала мы перебираем аргументы, переданные скрипту.
  • shift $((OPTIND-1)) здесь, чтобы отбросить обработанные аргументы. Нам это нужно, потому что имя файла - это последний аргумент, который нужно передать скрипту.
  • OUTPUT_FILE="${SRC_FILE%.*}" будет выводить исходное имя файла без расширения - если выходное имя файла не было указано (по умолчанию в go)

Теперь, чтобы получить последнюю фиксацию (которую мы будем использовать в качестве информации о сборке), нам нужно зафиксировать изменения:

git add main.go build.sh
git commit -m "Initial application and build script"

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

chmod 744 build.sh
./build.sh -v 0.3.0 main.go

Он создаст основной стабильный файл, который является нашим исполняемым файлом. Если вы запустите его, вы должны увидеть:

version 0.3.0, build: <commit hash>

Вы можете проверить свой хеш фиксации с помощью команды (она должна быть первой):

git log --oneline

Теперь давайте построим еще несколько вариантов. Эта команда создаст разрабатываемую версию 0.4.0 нашего приложения под именем api-server-development:

./build.sh -v 0.4.0 -e development -o api-server main.go

Резюме

Вот и все, мы создали основы для процесса развертывания с управлением версиями. Спасибо за чтение!

Если вам понравилась эта статья, подумайте о подписке на мою рассылку здесь