Makefile pour un projet Go
Vincent Bernat
Très tôt, j’ai pris en grippe le principe du GOPATH
mis en avant par
Go : je ne veux en aucun cas mélanger mon propre code avec celui
des dépendances. Je ne suis pas seul dans cette aversion et moults
outils ou des Makefile
ont été créés pour éviter d’organiser son
code autour du GOPATH
.
Heureusement, depuis Go 1.11, il est possible d’utiliser les
modules pour gérer les dépendances sans obligation d’utiliser le
GOPATH
. Tout d’abord, votre projet doit être converti en un
module1 :
$ go mod init hellogopher go: creating new go.mod: module hellogopher $ cat go.mod module hellogopher
Ensuite, vous pouvez invoquer les commandes habituelles, telles que
go build
ou go test
. La commande go
va résoudre les imports en
utilisant les versions spécifiées le fichier go.mod
. Lorsqu’il
rencontre un import pour un paquet non présent dans go.mod
, il
télécharge automatiquement la dernière version du module correspondant
et l’ajoute dans le fichier.
$ go test ./... go: finding github.com/spf13/cobra v0.0.5 go: downloading github.com/spf13/cobra v0.0.5 ? hellogopher [no test files] ? hellogopher/cmd [no test files] ok hellogopher/hello 0.001s $ cat go.mod module hellogopher require github.com/spf13/cobra v0.0.5
Si vous voulez une version spécifique, vous pouvez soit éditer
go.mod
ou invoquer go get
:
$ go get github.com/spf13/cobra@v0.0.4 go: finding github.com/spf13/cobra v0.0.4 go: downloading github.com/spf13/cobra v0.0.4 $ cat go.mod module hellogopher require github.com/spf13/cobra v0.0.4
Ajoutez go.mod
à votre système de gestion des versions. Vous pouvez
également ajouter go.sum
, ce qui procure une sécurité contre une
dépendance qui mute sa version2. Si vous voulez inclure les
dépendances (vendoring), vous pouvez lancer go mod vendor
et
ajouter le répertoire vendor/
à votre système de gestion des
versions.
Grâce aux modules, je pense que la gestion des dépendances en Go est
maintenant au niveau des autres langages, notamment Ruby. Bien qu’il
soit possible d’effectuer la plupart des opérations (construction et
tests) en utilisant uniquement la commande go
, un Makefile
est
toujours utile pour organiser les tâches les plus courantes, un peu
comme le setup.py
de Python ou le Rakefile
de Ruby. Je vais
décrire le mien.
Utilisation d’outils tierces#
La plupart des projets vont nécessiter quelques outils pour la construction ou les tests. Pour éviter à l’utilisateur d’avoir à les installer, je propose de les compiler à la volée. Par exemple, voici comment l’analyse statique du code est effectuée avec Golint :
BIN = $(CURDIR)/bin $(BIN): @mkdir -p $@ $(BIN)/%: | $(BIN) @tmp=$$(mktemp -d); \ env GO111MODULE=off GOPATH=$$tmp GOBIN=$(BIN) go get $(PACKAGE) \ || ret=$$?; \ rm -rf $$tmp ; exit $$ret $(BIN)/golint: PACKAGE=golang.org/x/lint/golint GOLINT = $(BIN)/golint lint: | $(GOLINT) $(GOLINT) -set_exit_status ./...
Le premier bloc définit comment l’outil tiers est fabriqué : go get
est invoqué avec le nom du paquet correspondant à l’outil à installer.
Nous ne voulons pas polluer notre gestion de dépendances pour cet
outil et nous travaillons donc dans un GOPATH
vide. Les binaires
générés sont placés dans bin/
.
Le second bloc étend la règle générique définie dans le premier bloc
en fournissant le nom du paquet pour golint
. Pour ajouter un autre
outil à compiler, une ligne similaire fait l’affaire.
Le dernier bloc définit la recette pour analyser le code. L’outil
utilisé par défaut est golint
tel que construit par le premier bloc.
Mais il est possible d’outrepasser ceci avec make
GOLINT=/usr/bin/golint
.
Mise à jour (11.2024)
Actuellement, l’approche recommandée consiste à créer un fichier tools.go
qui importe les paquets
contenant les outils pour gérer les dépendances via go.mod
. Cette méthode
garantit que tous les utilisateurs travaillent avec la même version des outils,
contrairement à l’approche précédente. J’ai mis à jour le dépôt
d’exemple pour utiliser cette méthode. À partir de Go 1.24 (sortie prévue
en février 2025), le fichier go.mod
inclura une nouvelle directive tool
pour
gérer nativement les dépendances des outils.
Tests#
Voici les règles permettant d’exécuter les tests :
TIMEOUT = 20 PKGS = $(or $(PKG),$(shell env GO111MODULE=on $(GO) list ./...)) TESTPKGS = $(shell env GO111MODULE=on $(GO) list -f \ '{{ if or .TestGoFiles .XTestGoFiles }}{{ .ImportPath }}{{ end }}' \ $(PKGS)) TEST_TARGETS := test-default test-bench test-short test-verbose test-race test-bench: ARGS=-run=__absolutelynothing__ -bench=. test-short: ARGS=-short test-verbose: ARGS=-v test-race: ARGS=-race $(TEST_TARGETS): test check test tests: fmt lint go test -timeout $(TIMEOUT)s $(ARGS) $(TESTPKGS)
L’utilisateur peut invoquer les tests de différente façon :
make test
lance tous les tests ;make test TIMEOUT=10
implique une durée limite de 10 secondes par test ;make test PKG=hellogopher/cmd
exécute les tests pour le paquetcmd
;make test ARGS="-v -short"
utilise les arguments fournis ;make test-race
exécute les tests en activant la détection des problèmes d’accès concurrents.
go test
permet également de déterminer la couverture des tests.
Malheureusement, l’outil est très rudimentaire et ne permet de gérer
qu’un seul paquet à la fois. Il faut également explicitement
lister tous les paquets à instrumenter. Dans le cas contraire,
seul le paquet testé l’est. De plus, les temps de compilation sont
prohibitifs si trop de paquets le sont. Enfin, afin d’obtenir un
rapport compatible avec Jenkins, quelques outils additionnels sont
nécessaires.
COVERAGE_MODE = atomic COVERAGE_PROFILE = $(COVERAGE_DIR)/profile.out COVERAGE_XML = $(COVERAGE_DIR)/coverage.xml COVERAGE_HTML = $(COVERAGE_DIR)/index.html test-coverage-tools: | $(GOCOVMERGE) $(GOCOV) $(GOCOVXML) # ❶ test-coverage: COVERAGE_DIR := $(CURDIR)/test/coverage.$(shell date -u +"%Y-%m-%dT%H:%M:%SZ") test-coverage: fmt lint test-coverage-tools @mkdir -p $(COVERAGE_DIR)/coverage @for pkg in $(TESTPKGS); do \ # ❷ go test \ -coverpkg=$$(go list -f '{{ join .Deps "\n" }}' $$pkg | \ grep '^$(MODULE)/' | \ tr '\n' ',')$$pkg \ -covermode=$(COVERAGE_MODE) \ -coverprofile="$(COVERAGE_DIR)/coverage/`echo $$pkg | tr "/" "-"`.cover" $$pkg ;\ done @$(GOCOVMERGE) $(COVERAGE_DIR)/coverage/*.cover > $(COVERAGE_PROFILE) @go tool cover -html=$(COVERAGE_PROFILE) -o $(COVERAGE_HTML) @$(GOCOV) convert $(COVERAGE_PROFILE) | $(GOCOVXML) > $(COVERAGE_XML)
En ❶, un certain nombre d’outils sont requis, de la même façon que
golint
vu précédemment :
gocovmerge
permet de combiner plusieurs profils en un seul,gocov-xml
convertit le rapport au format Cobertura pour Jenkins,gocov
convertit le rapport en un format utilisable pargocov-xml
.
En ❷, pour chaque paquet à tester, nous exécutons go test
avec
l’argument -coverprofile
. La liste des paquets à instrumenter est
donnée à l’argument -coverpkg
en utilisant go list
pour extraire
les dépendances du paquet en cours de test et en ne conservant que nos
propres paquets.
Mise à jour (09.2019)
Comme mentionné dans un
commentaire,
depuis Go 1.10, il est possible de tester plusieurs paquets tout en
collectant les informations de couverture. La recette test-coverage
peut donc être simplifiée et gocovmerge
n’est plus utile.
Construction#
Bien que l’on puisse simplement utiliser go build
pour obtenir notre
programme, il est assez courant d’avoir à fournir quelques arguments
supplémentaires ou de devoir exécuter des étapes additionnelles. Dans
l’exemple suivant, la version est extraite de l’étiquette Git la
plus proche ou d’un fichier .version
à la racine du projet. Elle
remplace la variable Version
dans le paquet hellogopher/cmd
.
VERSION ?= $(shell git describe --tags --always --dirty --match=v* 2> /dev/null || \ cat $(CURDIR)/.version 2> /dev/null || echo v0) all: fmt lint | $(BIN) go build \ -tags release \ -ldflags '-X hellogopher/cmd.Version=$(VERSION)' \ -o $(BIN)/hellogopher main.go
On en profite également pour effectuer le formatage et l’analyse statique du code.
Les extraits fournis ci-dessus sont un brin simplifiés. Jetez un œil sur le résultat final pour plus de détails !
Mise à jour (09.2019)
Il y a un fil intéressant à propos de cet article sur Reddit. Il contient des indices pour bloquer la version des outils tierces. Plusieurs personnes ont également mis en avant Mage, un logiciel de construction utilisant Go. Il nécessite cependant une étape de construction non triviale.
-
Pour une application qui n’est pas destinée à servir de dépendance, je préfère utiliser un nom court plutôt qu’un nom dérivé d’une URL, comme
github.com/vincentbernat/hellogopher
. Cela rend plus aisé à lire les blocs d’import :↩︎import ( "fmt" "os" "hellogopher/cmd" "github.com/pkg/errors" "github.com/spf13/cobra" )
-
Depuis Go 1.16, sans fichier
go.sum
, une étape supplémentaire est nécessaire pour le générer. Il semble plus simple de l’ajouter à votre système de gestion des versions. ↩︎