Un script pour builder son projet Go pour toutes les plateformes ! 😎

Lors des mes différentes avancées sur mon projet Meteor, j'ai dû le builder et le tester sur différentes plateformes. Un point pour le compilateur Go qui permet de faire de la cross-compilation. 😇

Il est possible de lister la liste des OS et archs supportées en utilisant la commande :

go tool dist list

D'ailleurs cette dernière va mettre très utile pour la suite.

Résultat obtenu avec la commande du dessus.

Habituellement, ce que font beaucoup, c'est d'écrire un Makefile qui va contenir les différentes plateformes à builder, sauf que moi, je voulais pouvoir builder pour toutes les plateformes disponibles... 😥

Ainsi, dans certains projets on peut retrouver des lignes du genre ...

compile: main.go # Compile this program
	GOOS=linux GOARCH=arm go build -o $(BUILD_DIR)/$(PROJECT_NAME)-$(PROJECT_VERSION)-linux-arm $<
	GOOS=linux GOARCH=arm64 go build -o $(BUILD_DIR)/$(PROJECT_NAME)-$(PROJECT_VERSION)-linux-arm64 $<
	GOOS=linux GOARCH=amd64 go build -o $(BUILD_DIR)/$(PROJECT_NAME)-$(PROJECT_VERSION)-linux-amd64 $<
	GOOS=freebsd GOARCH=386 go build -o $(BUILD_DIR)/$(PROJECT_NAME)-$(PROJECT_VERSION)-freebsd-386 $<
Makefile

Dans mon cas, comme je souhaite builder pour toutes les plateformes, c'est pas super opti... C'est alors que j'ai décidé de m'écrire un petit script aux petits oignons pour faire ce que je souhaite, à savoir :

  • Builder pour toutes les plateformes (merci captain obvious, ça fait 3 fois que tu le redis...), sauf certaines (exemple : js, android, ios).
  • Classer mes builds proprement dans un répertoire en fonction du type d'OS.
  • Supprimer le cache de go-build avant chaque nouveau build pour éviter les soucis de compatibilité avec la glibc. 😅

Bon avant de commencer, je vais un peu expliquer l'arborescence du petit projet créé rien que pour cet article (ohhh que je suis gentil)...

.
├── build.sh
├── Makefile
├── src
│  └── main.go
└── VERSION

Alors :

  • Le script build.sh que je vais vous présenter;
  • Le Makefile qui fait appel au script pour builder;
  • Le package main.go (un simple "Hello World");
  • Le fichier VERSION qui contient le numéro de version du programme à builder, je fais ça car ça m'évite de devoir spécifier le numéro de version à 15 000 endroits, je préfère que chaque programme vienne lire le fichier qui contient le numéro de version. 🤯

Au niveau du contenu, pour le fichier VERSION nous avons :

0.0.1
VERSION

Dans notre package src/main.go, nous avons :

package main

import "fmt"

func main() {
	fmt.Println("hello from go!")
}
src/main.go

Comme vous pouvez le constater, le programme ne fait pas grand chose ! 😅

Comme je l'ai spécifié plus haut, j'utilise un Makefile pour lancer le projet ou encore le builder.

# Store project version
PROJECT_VERSION="$(shell cat VERSION)"
PROJECT_ENTRYPOINT="src/main.go"

# Defines the default target that `make` will to try to make,
# or in the case of a phony target, execute the specified commands
# This target is executed whenever we just type `make`
.DEFAULT_GOAL = help

# The @ makes sure that the command itself isn't echoed in the terminal
help: # Print help on Makefile
	@echo "Sample project version $(PROJECT_VERSION)"
	@echo ""
	@echo "Please use 'make <target>' where <target> is one of"
	@echo ""
	@grep '^[^.#]\+:\s\+.*#' Makefile | \
	sed "s/\(.\+\):\s*\(.*\) #\s*\(.*\)/`printf "\033[93m"`  \1`printf "\033[0m"`	\3 [\2]/" | \
	expand -35
	@echo ""
	@echo "Check the Makefile to know exactly what each target is doing."

exec: # Execute this program
	@go run $(PROJECT_ENTRYPOINT)

build: # Build this program for all platforms
	@bash build.sh $(PROJECT_ENTRYPOINT)
Makefile

Comme vous pouvez le constater, celui-ci fait appel aux fichiers :

  • VERSION pour récupérer le numéro de version
  • build.sh pour builder le projet 😻

Si vous vous demandez ce que fait la target help, n'hésitez pas à aller regarder mon dernier article qui traite de ce sujet ici.

Maintenant il ne reste plus qu'à voir le contenu du fichier build.sh :

#!/usr/bin/env bash

# Script to build your Golang project for all available platforms
# Written by Julien Briault <dev[at]jbriault.fr>

package=$1
if [[ -z "$package" ]]; then
  echo "usage: $0 <package-name>"
  echo "example: $0 main.go"
  exit 1
fi


function pprint() {
    # Function to colorize the output
    # levels: 'error', 'success', 'info', 'warning'
    local level=$1
    local message=$2

    # Colors for CLI output
    local yellow='\033[1;33m'
    local green='\033[0;32m'
    local red='\033[0;31m'
    local orange='\033[0;33m'
    local resetcolor='\033[0m'

    case ${level} in
        "error")
            echo -e "${red}[!] ${message}${resetcolor}"
            ;;
        "warning")
            echo -e "${orange}[!] ${message}${resetcolor}"
            ;;
        "success")
            echo -e "${green}[+] ${message}${resetcolor}"
            ;;
        "info")
            echo -e "${yellow}[*] ${message}${resetcolor}"
            ;;
        *)
            echo "[+] ${message}"
            ;;
    esac
}

# Output build directory
build_dir="build/"

platforms=$(go tool dist list)

# Remove '.go' from package name
package_split=(${package%.*})
package_name=${package_split[-1]}

# Get project version
package_version=$(cat VERSION)

IFS=$'\n\t'

# Clean go-build cache
pprint "" "Clean go-build cache"
rm -rf ~/.cache/go-build

for platform in ${platforms[@]}; do 
    platform_split=(${platform/\// })
    os_name=$(echo ${platform_split} | awk '{ print $1 }')
    os_arch=$(echo ${platform_split} | awk '{ print $2 }')

    output_name=${package_name}'-'${package_version}'-'${os_name}'-'${os_arch}
    
    # Add '.exe' extension for Windows binary file
    if [ "$os_name" = "windows" ]; then
      output_name+='.exe'
    fi

    if [ ! -d "build" ]; then 
        mkdir -p ${build_dir}
    else 
        mkdir -p ${build_dir}${os_name}/
    fi 

    case ${os_name} in 
        "android" | "ios" | "js") # You can add the OS you want if you don't want it to be built.
            pprint "info" "Skip building package ${package} for ${platform}"
            ;;
        *)
            env CGO_ENABLED=1 GOOS=${os_name} GOARCH=${os_arch} go build -o ${build_dir}${os_name}/${output_name} $package
            if [ $? -ne 0 ]; then
                pprint "error" "[!] An error has occurred! Aborting the script execution..."
                exit 1
            else
                pprint "success" "Building ${package} package for ${platform}"
            fi
            ;;
    esac
done
build.sh

Si vous souhaitez ne pas builder pour certains OS, vous pouvez le faire en les rajoutant à la ligne "android" | "ios" | "js".

C'est cool Julien mais, comment ça s'utilise? Eh bien c'est tout bête, si vous souhaitez appeler le script (hors Makefile), vous pouvez l'appeler en faisant un :

chmod u+x ./build.sh # lui donner les droits d'exécution (pour notre user)
./build.sh src/main.go

# Via Makefile
make build

Automatiquement, il va vous créer le répertoire build/ dans lequel seront placés les différents builds de manière hiérarchique. 🤠

Et voilà, c'est tout! Tout ça a donc l'avantage d'être rapide, propre et rangé. Ne me remerciez pas. Si vous souhaitez retrouver le code, vous pouvez le cloner ici.

Bisous.