Les outils d’inclusion de ressources comme pkger
permettent d’embarquer dans
l’exécutable aussi bien les assets statiques que des fichiers non publiés, comme
les templates.
Voyons comment.
Pourquoi utiliser un “bundler” comme markbates/pkger
?
Une des différences entre le déploiement d’applications Web classiques utilisant le stack LAMP ou des technologies similaires comme Node.JS, Python, ou Ruby on Rails, est le fait que les applications Go sont compilées en un programme exécutable.
Pour ce qui est de la logique métier, cela n’a que peu d’impact, mais les applications
Web utilisent aussi des fichiers autres que Go et le contenu des bases de données.
Ce sont en particulier les assets statiques transférés directement au navigateur.
Ceux-ci peuvent être aussi bien servis directement par l’application, sur une
route associée au Handler
natif http.FileServer,
que servis par un serveur Web placé devant l’application Go, voire par un service
de fichiers Cloud totalement indépendant comme
Amazon S3,
Google Cloud Storage, ou
Azure Files.
Un cas quelque peu différent est celui des templates : non seulement, leur organisation sur disque est souvent hiérarchique et totalement décorrélée de la structure des URLs de l’applications - à la différence des assets - mais ils ne sont pas transférés au navigateur client, et n’ont donc pas de raison d’exister séparément du programme lui-même1. Le fait qu’ils n’existent pas en-dehors du binaire peut même être souhaité pour accroître leur sécurité.
Pour tous ces cas, comme pour toute autre ressource à intégrer à un exécutable, la technique éprouvée consiste, lors d’une étape de construction du programme, à les transformer en code source qui sera compilé et lié comme n’importe quel autre fragment de code.
De multiples outils Go existent à cette fin, à tel point que l’inclusion d’une solution standardisée dans Go est à l’étude dans le ticket 35950 , utilisant des méthodes variées.
Jusqu’à l’automne 2019, l’outil packr
du framework web Buffalo se détachait du lot,
ne fût-ce qu’en raison de l’usage important dudit framework. Mais il présentait
divers inconvénients et son concepteur, Mark Bates, lui a créé un successeur, le
module markbates/pkger
(cf. Ressources additionnelles
en bas d’article).
Malheureusement, les exemples fournis utilisent le moteur de templates
plush
, spécifiques à Buffalo.
Cet article montre comment construire un exemple indépendant de Buffalo, avec
des templates Go traditionnels, situés dans deux répertoires différents.
Comment utiliser pkger
dans son projet
Préparer les dépendances
Durant le développement:
- Il est nécessaire d’utiliser Go 1.13 ou plus récent.
- Le projet doit utiliser les modules VGO, dont
pkger
dépend pour la résolution des chemins, donc penser à initialiser le système de modules. Le nom du module est facultatif si le répertoire de travail est dans leGOPATH
.
$ go mod init [module]
- La commande
pkger
doit être installée sur le poste de développement, afin de pouvoir être exécutée durant le développement.
$ go get github.com/markbates/pkger/cmd/pkger
Le projet n’a aucune dépendance à l’exécution.
Travailler en mode développement
Durant le développement pkger
accède aux ressources au travers du système de
fichier, en ne les embarquant pas dans l’exécutable.
Pour cela, il faut s’assurer que le fichier pkged.go
est bien absent du
répertoire racine du projet. N’hésitez pas à la supprimer, il sera régénéré à
chaque construction en mode déploiement.
Le fait que pkger
accède aux fichiers au moyen du système de fichiers permet,
en particulier dans le cas des templates, de ne pas avoir besoin de recompiler à
chaque changement des templates, jusqu’à la préparation d’une version à déployer.
La commande pkger list
produit une liste des fichiers identifiés comme devant
être inclus dans le bundle de déploiement. Elle a en outre comme effet de bord
de supprimer le fichier pkged.go
s’il est présent2.
Ceci permet d’identifier ce que pkger
va embarquer, en particulier pour ajouter
les éléments qu’il n’aurait pas choisi d’inclure.
Préparer une compilation pour déploiement
Si la liste d’inclusion est correcte, il est possible de créer le fichier pkged.go
avec la commande pkger
sans arguments additionnels.
La commande n’affiche aucun résultat, mais crée le fichier pkged.go
à la racine
du module.
$ ls -F ; pkger ; ls -F
LICENSE README.md go.mod go.sum main.go templates/
LICENSE README.md go.mod go.sum main.go pkged.go templates/
Il est possible d’utiliser la commande go generate
si le fichier source a été
rédigé en conséquence, pour obtenir un processus de construction plus idiomatique:
$ ls -F ; go generate ; ls -F
LICENSE README.md go.mod go.sum main.go templates/
LICENSE README.md go.mod go.sum main.go pkged.go templates/
Afin de permettre le déploiement sur un poste qui n’aurait pas la commande pkger
,
comme un serveur d’intégration, il est préférable de commettre le fichier pkged.go
dans le dépôt avec le reste des sources.
À ce stade il ne reste plus qu’à compiler comme vous le souhaitez (par exemple
un simple go build
) : la seule particularité introduite par pkger
est le
fichier additionnel pkged.go
qui est ajouté au projet.
Comment intégrer ses ressources avec pkger
Pour permettre à pkger
d’inclure les ressources dans le bundle, les sources du
module doivent être rédigés en conséquence. Voyons comment.
Les exemples illustrés dans cet article sont extraites du mini-projet https://github.com/fgm/pkger_demo qui est prêt à compiler: vous pouvez le récupérer pour suivre plus facilement dans votre IDE:
$ go get github.com/fgm/pkger_demo
Rendre le projet compatible avec go generate
Pour permettre l’utilisation de go generate
, une instruction de génération doit
être incluse dans un fichier. Dans cet exemple, ce sera dans le fichier principal
main.go
à la racine du projet:
|
|
De cette façon, en développement, la commande standard go generate
aura pour
effet de créer le fichier pkged.go
, prêt à compiler.
L’API de pkger
: les principes
Pour identifier les ressources qu’il doit embarquer, pkger
procède par une analyse
syntaxique du code Go, au cours de laquelle il identifie les appels à ses fonctions
propres, donc il extrait les arguments déclarés comme correspondant à des chemins.
Lorsque ce sont des chaînes constantes, ces arguments sont résolus par rapport
à l’emplacement du fichier, en considérant les chemins absolus ("/foo"
) comme
étant relatifs au répertoire racine du module. Ainsi :
- pour la démo, extraite dans
/Users/fgm/go/src/github.com/fgm/pkger_demo
- …un argument passé comme
"/templates/layout.gohtml"
- …est traduit par
pkger
en
/Users/fgm/go/src/github.com/fgm/pkger_demo/templates/layout.gohtml
En mode développement, c’est ce chemin absolu qui est utilisé pour les appels aux fonctions de fichier standard3.
En mode déploiement, les chemins sur disque ne sont plus utilisés, et les
fonctions de pkger
résolvent les arguments à partir des données compilées
dans l’exécutable, permettant au programme d’être complètement autonome.
L’API de pkger
: les fonctions
La grande idée de l’API de pkger
consiste à renvoyer des données très similaires
à l’API native de Go pour les entrées/sorties, pour un sous-ensemble des paquets
os
et filepath
. Elle se résume à la liste suivante.
Runtime Go | pkger |
---|---|
os.Create(name string) (*os.File, error) | .Create(name string) (pkging.File, error) |
os.MkdirAll(path string, perm os.FileMode) error | .MkdirAll(path string, perm os.FileMode) error |
os.Open(name string) (*os.File, error) | .Open(name string) (pkging.File, error) |
os.Remove(name string) error | .Remove(name string) error |
os.RemoveAll(path string) error | .RemoveAll(path string) error |
os.Stat(name string) (os.FileInfo, error) | .Stat(name string) (os.FileInfo, error) |
filepath.Walk(root string, walkFn filepath.WalkFunc) error | .Walk(root string, walkFn filepath.WalkFunc) error |
Quelques fonctions plus avancées existent pour manipuler les calculs d’inclusion :
Include(name string) string
est une fonction identité n’opérant aucun traitement.Current()
,Info(string)
, etParse(string)
servent à manipuler les chemins et implémentationspkger
pour des opérations avancées.
La fonction pkger.Include
est de loin la plus utile : elle se contente de renvoyer
le chemin qui lui est passé comme argument, sans aucun traitement, mais permet
à la commande pkger
d’identifier que la chaîne qui lui est passée correspond à
un chemin de ressource à inclure dans le bundle.
Comme le montre le tableau ci-dessus, la principale différence entre les fonctions
de pkger
et celles du runtime Go est le fait qu’elles renvoient une valeur
de type pkging.File
et non *os.File
.
Cette différence est en pratique peu significative: la plupart des fonctions du
runtime ne dépendent pas du type concret os.File
mais des interfaces du paquet
io
qu’il implémente ; et le type interface pkging.File
en implémente la vaste majorité:
Interface | os.File | pkging.File | Interface | os.File | pkging.File | |
---|---|---|---|---|---|---|
io.Closer | X | X | io.Seeker | X | X | |
io.ReadCloser | X | X | io.StringWriter | X | ||
io.Reader | X | X | io.WriteCloser | X | X | |
io.ReaderAt | X | io.Writer | X | X | ||
io.ReadSeeker | X | X | io.WriterAt | X | ||
io.ReadWriteCloser | X | X | io.WriteSeeker | X | X | |
io.ReadWriter | X | X | ||||
io.ReadWriteSeeker | X | X |
Chargement de templates dans une arborescence
Le programme de démo pkger_demo est une application Web basique, renvoyant une page mise en form par deux templates, reflétant une disposition courante.
templates/page.gohtml
:
|
|
templates/layout/footer.gohtml
|
|
Examinons le source du fichier main.go
point par point. Les lignes 1 à 22
contiennent le préambule du programme (package
, import
) et la définition du
type PageData
qui sera passé aux templates lors de leur exécution. Rien de
particulier sur ce point.
Le programme utilise les templates présents dans le répertoire templates
et
tous ses sous-répertoires.
Il va donc parcourir l’arborescence située sous /templates
, et ouvrir les
fichiers templates qu’il y trouvera:
|
|
- ligne 29: le fonctionnement est identique à celui de
filepath.Walk
: la fonctionWalk
parcourt l’arborescence et invoque une fonction de rappelwalkFn
pour chaque fichier et sous-répertoire rencontré. Dans notre exemple, c’est la fonction anonyme des lignes 29-42. - lignes 30-33: la fonction ignore les fichiers ne correspondant pas au format des noms de templates attendus.
- ligne 36, elle ouvre le fichier avec
pkger.Open
. - ligne 37, elle en lit le contenu avec la fonction
ioutil.ReadAll
du runtime. - ligne 40, elle compile la chaîne ainsi obtenu en tant que template Go.
En fin d’exécution, le résultat est un jeu de templates compilés valide, ou une erreur.
Lors de la construction du programme, la commande pkger
va identifier la présence
de l’appel à pkger.Open
et tenter d’en déduire le nom de la ressource à ajouter
au bundle. Mais il y a un problème…
En effet, pkger
est un analyseur statique, mais l’argument de pkger.Open
est
une variable et non une constante. De ce fait, sa valeur ne peut pas être
déterminée avant l’exécution, et cet appel est donc ignoré.
Si le programme se limitait à ce mécanisme, les templates ne seraient
pas encodés dans le fichier pkged.go
et le programme échouerait à les exécuter.
Il est donc nécessaire d’utiliser un autre moyen pour informer pkger
, et ce
moyen est la fonction identité pkger.Include
, à la ligne 50:
|
|
Lorsqu’il rencontre cette fonction lors de l’analyse statique du code, pkger
constante que son argument est une chaîne constante, qu’il est donc en mesure
d’évaluer. Il va donc parcourir le répertoire indiqué et ajouter les
fichiers et répertoires qu’il va y rencontrer. Vérifions le résultat:
$ pkger list
github.com/fgm/pkger_demo
> github.com/fgm/pkger_demo:/templates
> github.com/fgm/pkger_demo:/templates/layout
> github.com/fgm/pkger_demo:/templates/layout/footer.gohtml
> github.com/fgm/pkger_demo:/templates/page.gohtml
Les templates étant incorporés à l’exécutable, les appels à pkger.Open
dans
la fonction de rappel vont cette fois réussir et les templates pourront être
compilés et renvoyés.
À la différence de la fonction de rappel de Walk
, pkger
n’a rien pu filtrer:
il est donc important avec ce procédé que l’arborescence ne contienne aucun fichier
annexe qui ne soit pas destiné à être manipulé à l’exécution, car cela augmenterait
inutilement la taille et l’occupation mémoire de l’exécutable.
Une fois le jeu de templates compilé obtenu à la ligne 52, les handlers de requête HTTP peuvent l’utiliser comme si de rien n’était:
|
|
Ressources additionnelles par Mark Bates
- Article d’introduction (11/2019)
https://blog.gobuffalo.io/introducing-pkger-static-file-embedding-in-go-1ce76dc79c65 - Vidéo d’introduction de
pkger
(11/2019)
La vidéo ci-dessous présente les idées suivant lesquelles Mark Bates a conçu ce nouvel outil, en comparant en particulier les améliorations qu’il apporte par rapport àgobuffalo/packr/v2
.
- En résumé, l’API de
pkger
est idiomatique, calquée sur les paquetsio
etfilepath
, alors que celle depackr2
était spécifique ; et elle est sans perte (lossless), préservant les informations fichier alors que celle depackr2
perdait une partie de l’information.
Si l’application est conçue pour accepter des templates modifiables après le déploiement, il est alors nécessaire qu’ils soient présents, et la situation est la même que pour les assets statiques. ↩︎
Il est possible d’obtenir une vue plus détaillée de l’analyse, au format JSON, avec le drapeau associé :
pkger list -json
↩︎Avec un effet qui peut surprendre : il est possible de lancer le fichier compilé depuis n’importe quel répertoire courant et les fichiers sont toujours référencés à leur emplacement dans les sources du projet, et non depuis le répertoire courant, en raison de ces chemins absolus. ↩︎