Makefile pour un projet Go

Vincent Bernat

Très tôt, j’ai pris en grippe le prin­cipe du GOPATH mis en avant par Go : je ne veux en au­cun cas mé­lan­ger mon propre code avec ce­lui des dé­pen­dances. Je ne suis pas seul dans cette aver­sion et moults ou­tils ou des Makefile ont été créés pour évi­ter d’or­ga­ni­ser son code au­tour du GOPATH.

Heu­reu­se­ment, de­puis Go 1.11, il est pos­sible d’uti­li­ser les mo­dules pour gé­rer les dé­pen­dances sans obli­ga­tion d’uti­li­ser le GOPATH. Tout d’abord, votre pro­jet doit être converti en un module1 :

$ go mod init hellogopher
go: creating new go.mod: module hellogopher
$ cat go.mod
module hellogopher

En­suite, vous pou­vez in­vo­quer les com­mandes ha­bi­tuelles, telles que go build ou go test. La com­mande go va ré­soudre les im­ports en uti­li­sant les ver­sions spé­ci­fiées dans le fi­chier go.mod. Lorsqu’il ren­contre un im­port pour un pa­quet non pré­sent dans go.mod, il té­lé­charge au­to­ma­ti­que­ment la der­nière ver­sion du mo­dule cor­res­pon­dant 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 vou­lez une ver­sion spé­ci­fique, vous pou­vez soit édi­ter go.mod ou in­vo­quer 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

Ajou­tez go.mod à votre sys­tème de ges­tion des ver­sions. Vous pou­vez éga­le­ment ajou­ter go.sum, ce qui pro­cure une sé­cu­rité contre une dé­pen­dance qui mute sa ver­sion2. Si vous vou­lez in­clure les dé­pen­dances (ven­do­ring), vous pou­vez lan­cer go mod vendor et ajou­ter le ré­per­toire vendor/ à votre sys­tème de ges­tion des versions.

Grâce aux mo­dules, je pense que la ges­tion des dé­pen­dances en Go est main­te­nant au ni­veau des autres lan­gages, no­tam­ment Ruby. Bien qu’il soit pos­sible d’ef­fec­tuer la plu­part des opé­ra­tions (construc­tion et tests) en uti­li­sant uni­que­ment la com­mande go, un Makefile est tou­jours utile pour or­ga­ni­ser les tâches les plus cou­rantes, un peu comme le setup.py de Py­thon ou le Rakefile de Ruby. Je vais dé­crire le mien.

Utilisation d’outils tiers#

La plu­part des pro­jets vont né­ces­si­ter quelques ou­tils pour la construc­tion ou les tests. Pour évi­ter à l’uti­li­sa­teur d’avoir à les ins­tal­ler, je pro­pose de les com­pi­ler à la vo­lée. Par exemple, voici com­ment l’ana­lyse sta­tique du code est ef­fec­tué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 pre­mier bloc dé­fi­nit com­ment l’ou­til tiers est fa­bri­qué : go get est in­vo­qué avec le nom du pa­quet cor­res­pon­dant à l’ou­til à ins­tal­ler. Nous ne vou­lons pas pol­luer notre ges­tion de dé­pen­dances pour cet ou­til et nous tra­vaillons donc dans un GOPATH vide. Les bi­naires gé­né­rés sont pla­cés dans bin/.

Le se­cond bloc étend la règle gé­né­rique dé­fi­nie dans le pre­mier bloc en four­nis­sant le nom du pa­quet pour golint. Pour ajou­ter un autre ou­til à com­pi­ler, une ligne si­mi­laire fait l’affaire.

Le der­nier bloc dé­fi­nit la re­cette pour ana­ly­ser le code. L’ou­til uti­lisé par dé­faut est golint tel que construit par le pre­mier bloc. Mais il est pos­sible d’ou­tre­pas­ser ceci avec make GOLINT=/usr/bin/golint.

Mise à jour (03.2025)

À par­tir de Go 1.24 (sorti en fé­vrier 2025), le fi­chier go.mod in­clut une nou­velle di­rec­tive tool pour gé­rer na­ti­ve­ment les dé­pen­dances des ou­tils. J’ai mis à jour le dé­pôt d’exemple pour uti­li­ser cette mé­thode. Pré­cé­dem­ment, l’ap­proche re­com­man­dée consis­tait à créer un fi­chier tools.go qui im­porte les pa­quets conte­nant les ou­tils pour gé­rer les dé­pen­dances via go.mod. Cette mé­thode ga­ran­tit que tous les uti­li­sa­teurs tra­vaillent avec la même ver­sion des ou­tils, contrai­re­ment à l’ap­proche pré­cédente.

Tests#

Voici les règles per­met­tant d’exé­cu­ter 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’uti­li­sa­teur peut in­vo­quer les tests de dif­fé­rente façon :

  • make test lance tous les tests ;
  • make test TIMEOUT=10 im­plique une du­rée li­mite de 10 se­condes par test ;
  • make test PKG=hellogopher/cmd exé­cute les tests pour le pa­quet cmd ;
  • make test ARGS="-v -short" uti­lise les ar­gu­ments fournis ;
  • make test-race exé­cute les tests en ac­ti­vant la dé­tec­tion des pro­blèmes d’ac­cès concurrents.

go test per­met éga­le­ment de dé­ter­mi­ner la cou­ver­ture des tests. Mal­heu­reu­se­ment, l’ou­til est très ru­di­men­taire et ne per­met de gé­rer qu’un seul pa­quet à la fois. Il faut éga­le­ment ex­pli­ci­te­ment lis­ter tous les pa­quets à ins­tru­men­ter. Dans le cas contraire, seul le pa­quet testé l’est. De plus, les temps de com­pi­la­tion sont pro­hi­bi­tifs si trop de pa­quets le sont. En­fin, afin d’ob­te­nir un rap­port com­pa­tible avec Jen­kins, quelques ou­tils ad­di­tion­nels 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 cer­tain nombre d’ou­tils sont re­quis, de la même fa­çon que golint vu pré­cé­demment :

  • gocovmerge per­met de com­bi­ner plu­sieurs pro­fils en un seul,
  • gocov-xml conver­tit le rap­port au for­mat Co­ber­tura pour Jenkins,
  • gocov conver­tit le rap­port en un for­mat uti­li­sable par gocov-xml.

En ❷, pour chaque pa­quet à tes­ter, nous exé­cu­tons go test avec l’ar­gu­ment -coverprofile. La liste des pa­quets à ins­tru­men­ter est don­née à l’ar­gu­ment -coverpkg en uti­li­sant go list pour ex­traire les dé­pen­dances du pa­quet en cours de test et en ne conser­vant que nos propres paquets.

Mise à jour (09.2019)

Comme men­tionné dans un com­men­taire, de­puis Go 1.10, il est pos­sible de tes­ter plu­sieurs pa­quets tout en col­lec­tant les in­for­ma­tions de cou­ver­ture. La re­cette test-coverage peut donc être sim­pli­fiée et gocovmerge n’est plus utile.

Construction#

Bien que l’on puisse sim­ple­ment uti­li­ser go build pour ob­te­nir notre pro­gramme, il est as­sez cou­rant d’avoir à four­nir quelques ar­gu­ments sup­plé­men­taires ou de de­voir exé­cu­ter des étapes ad­di­tion­nelles. Dans l’exemple sui­vant, la ver­sion est ex­traite de l’éti­quette Git la plus proche ou d’un fi­chier .version à la ra­cine du pro­jet. Elle rem­place la va­riable Version dans le pa­quet 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 pro­fite éga­le­ment pour ef­fec­tuer le for­ma­tage et l’ana­lyse sta­tique du code.


Les ex­traits four­nis ci-des­sus sont un brin sim­pli­fiés. Je­tez un œil sur le ré­sul­tat fi­nal pour plus de détails !

Mise à jour (09.2019)

Il y a un fil in­té­res­sant à pro­pos de cet ar­ticle sur Red­dit. Il contient des in­dices pour blo­quer la ver­sion des ou­tils tiers. Plu­sieurs per­sonnes ont éga­le­ment mis en avant Mage, un lo­gi­ciel de construc­tion uti­li­sant Go. Il né­ces­site ce­pen­dant une étape de construc­tion non triviale.