#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
source "$(dirname -- "$0")/lib/ulib/auto" || exit 1
urequire ptools xmlsupport

SELF="$script"
TEMPLATEDIR="$scriptdir/lib/dkbuild/templates"

[ -n "$COMPOSE_V1" ] && DOCKER_COMPOSE=(docker-compose) || DOCKER_COMPOSE=(docker compose)

function get_default_phpbuilder_image() {
    echo "${REGISTRY:-pubdocker.univ-reunion.fr}/image/phpbuilder:${DIST:-d11}"
}
function get_default_javabuilder_image() {
    echo "${REGISTRY:-pubdocker.univ-reunion.fr}/image/javabuilder:d11"
}

##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## Aide

function display_help() {
    uecho "$scriptname: construire une image docker

USAGE
    $scriptname action [options]

OPTIONS
    --hdk, --help-dkbuild
        Afficher l'aide sur le format du fichier dkbuild
    --href, --help-reference
        Afficher la référence sur les commandes utilisables dans un fichier
        dkbuild
    --compose-v1
        Forcer l'utilisation de docker-compose v1

ACTIONS

$scriptname templates
    lister les templates valides pour 'init --template'

$scriptname init [OPTIONS] [PROJDIR [NAME [GROUP]]]
    initialiser un répertoire pour la construction d'une ou plusieurs images
    docker, selon un modèle prédéfini

    -j, --projdir PROJDIR
        Spécifier le répertoire de projet à créer/mettre à jour. le répertoire
        par défaut est le répertoire courant.
    -t, --template TEMPLATE
        Nom du modèle à utiliser. Utiliser 'default' par défaut
    -v, --var VAR=VALUE
        Spécifier une variable pour la génération des fichiers à partir du
        modèle. Le nom de la variable est mis en majuscule avant remplacement
        dans les fichiers
    -n, --name NAME
    -g, --group GROUP
        Raccourcis pour respectivement -vname=NAME et -vgroup=GROUP

$scriptname build [OPTIONS] [BUILDVARS...]
    construire les images. C'est l'action par défaut

    Les arguments BUILDVARS de la forme ARG=VALUE permettent de spécifier des
    arguments de build comme avec l'option --arg

    -m, --machine MACHINE
        Sélectionner la machine spécifiée avant de dérouler le script
    -j, --projdir PROJDIR
        Spécifier le répertoire de projet. Si cette option n'est pas spécifiée,
        remonter la hiérarchie et prendre le premier répertoire qui contient un
        fichier nommé dkbuild.
        Si PROJDIR est un fichier, c'est ce fichier qui est utilisé comme script
        de build. Le répertoire de projet sélectionné est le répertoire qui
        contient le script de build.
        Si le répertoire PROJDIR contient un fichier du même nom de base que le
        fichier de build avec l'extension '.env' (i.e dkbuild.env par défaut),
        ce fichier est lu de la même façon qu'un fichier de configuration
    -c, --config CONFIG
        Lire un fichier de configuration au format dkbuild. Si cette option
        n'est pas spécifiée, les fichiers ~/.dkbuild.env et /etc/dkbuild.env
        sont testés dans l'ordre et automatiquement sélectionnés s'ils existent.
        NB: cela veut dire que si cette option est spécifiée, les fichiers
        ~/.dkbuild.env et /etc/dkbuild.env sont ignorés
        L'ordre de priorité est le suivant:
        - d'abord les variables spécifiées avec --env et --arg,
        - puis les variables définies dans ce fichier de configuration,
        - puis celles définies dans dkbuild.env le cas échéant
        - puis celles définies dans le fichier de build courant.
        Utiliser la valeur spéciale 'none' pour indiquer qu'aucun fichier de
        configuration ne doit être chargé.

    -d, --dist DIST
    -0, --d10
    -1, --d11
    -2, --d12
    -3, --d13
    --r7, --rhel7
    --r8, --rhel8
    --r9, --rhel9
    --o7, --oracle7
    --o8, --oracle8
    --o9, --oracle9
        Ne faire le build que pour la distribution spécifiée. Par défaut, faire
        le build pour toutes les distributions définies. Si la distribution
        sélectionnée n'est pas valide pour ce build, elle est ignorée

    -g, --profile PROFILE
    -P, --prod
    -T, --test
    -E, --dtest
    --devel
        Spécifier le profil dans lequel construire l'image
    --all-profiles
        Construire l'image dans tous les profils définis

    -e, --env VAR=VALUE
        Spécifier la valeur d'une variable d'environnement. Cette valeur
        remplace la valeur par défaut spécifiée dans le fichier de build.
    --arg ARG=VALUE
        Spécifier la valeur d'un argument de build. Cette valeur remplace la
        valeur par défaut spécifiée dans le fichier de build.

    -u, --clean-update
        Avant de faire le build, faire un clean, suivi de git pull. C'est la
        méthode préférée pour mettre à jour le dépôt s'il y a des fichiers
        synchronisé avec la commande 'copy', parce que sinon les fichiers
        sources (mis à jour par git pull) sont désynchronisés d'avec les
        fichiers destination.
    --clone-src-only
        Ne faire que cloner les dépôts sources mentionnés avec la commande
        'checkout'
    --update-src-only
        Ne faire que mettre à jour les dépôts sources mentionnés avec la
        commande 'checkout'
    --update-src
        Avec la commande 'checkout', mettre à jour les dépôts avant de faire le
        build. C'est la valeur par défaut.
    --no-update-src
        Ne pas mettre à jour les dépôts avant de faire le build. La commande
        'checkout' devient un NOP si le dépôt existe déjà.
    -w, --update-devel-src
        Ne pas mettre à jour le dépôt, préférer la synchronisation depuis la
        version de développement d'un dépôt
    -s, --sync-src
        Avec la commande 'copy', effectuer la mise à jour des fichiers. C'est la
        valeur par défaut si on construit l'image
    --no-sync-src
        Ne pas mettre à jour les fichiers. La commande 'copy' devient un NOP si
        le fichier destination existe.
    -b, --build
        Construire les images
    --no-cache
        Ne pas utiliser le cache lors du build
    --plain-output
        Afficher la sortie complète des containers lors du build
    -U, --pull-image
        Essayer de récupérer une version plus récente de l'image source
    -p, --push-image
        Pousser les images construites vers la registry

$scriptname clean [OPTIONS] [DIRS...]
    nettoyer le projet des fichiers créés par 'copy gitignore=', en utilisant la
    commande 'git clean -dX'

    -j, --projdir PROJDIR
        Spécifier le répertoire de projet
    -X, --ignored
        Utiliser l'option -X de git clean pour ne supprimer que les fichiers
        ignorés par git. c'est l'option par défaut.
    -x, --untracked
        Utiliser l'option -x de git clean pour supprimer aussi les fichiers non
        suivis.
    -a, --all
        Supprimer aussi les fichiers listés par 'git status --ignored'. Cela
        permet de supprimer un maximum de fichiers qui ne font pas partie du
        projet. Cette option implique --untracked

$scriptname composer DESTDIR [ACTION [PARAMS] [ARGS]]
    lancer composer dans le répertoire spécifié

    cf la documentation de la commande 'composer' pour la description des
    paramètres

$scriptname mvn DESTDIR [ACTION [PARAMS] [ARGS]]
    lancer maven dans le répertoire spécifié

    cf la documentation de la commande 'mvn' pour la description des paramètres

$scriptname dump [OPTIONS]
    afficher les valeurs des variables

    Les options suivantes ont la même signification que pour l'action build:
    --machine, --projdir, --config, --dist, --profile, --all-profiles, --env,
    --arg"
}

function display_help_dkbuild() {
    uecho "\
OPTIONS
  --help  Aide générale
* --hdk   Aide sur le format du fichier dkbuild
  --href  Référence sur les commandes utilisables dans un fichier dkbuild

DKBUILD
=======

Un fichier dkbuild est un script shell utilisé pour construire une ou plusieurs
images docker.

A cause de l'implémentation utilisée pour les directives, le fichier doit être
parcouru (i.e exécuté) à de multiples reprises pour analyser les paramètres et
variables définis. il faut donc \"protéger\" les appels de scripts externes ou
de fonctions gourmandes avec les commandes 'run' et 'call' pour éviter que ces
commandes ne soient exécutées à plusieurs reprises.

Quand un fichier dkbuild est exécuté, le répertoire courant est toujours le
répertoire qui contient le fichier

## Distributions ###############################################################

Une distribution s'entend au sens de la distribution linux utilisée comme base
pour l'image construite. L'idée est de pouvoir construire des images similaires
qui ne diffèrent que par la version de base du système d'exploitation

Pour une même distribution, plusieurs versions d'une image peuvent être
construites. Une version est définie en ajoutant un préfixe à la distribution.

La commande 'setdists' permet de lister explicitement les distributions valides
(et les versions associées le cas échéant). Si la distribution sélectionnée par
l'utilisateur n'est pas dans la liste fournie, le script s'arrête sans erreur.

La première distribution listée est spéciale: c'est la distribution la plus
récente, celle qui reçoit le tag :latest

La distribution 'none' est spéciale aussi: elle n'est pas mentionnée dans les
tags automatiquement attribués aux images.

La commande 'dist' permet de tester si la distribution spécifiée en argument a
été sélectionnée par l'utilisateur. L'argument 'LATEST' permet de tester la
dernière distribution.

La commande 'version' permet de tester si la version spécifiée en argument a été
sélectionnée par l'utilisateur. On part du principe qu'une distribution a déjà
été testée au préalable avec 'dist'

Exemple:
    setdists 3.0-d11 3.1-d11 d10 d9
    if dist d11; then
        if version 3.0; then
            ...
        elif version 3.1; then
            ...
        fi
    elif dist d10; then
        ...
    elif dist d9; then
        ...
    fi
Dans une même distribution, les versions doivent être ordonnées de la plus
ancienne à la plus récente. ici, la version 3.1 est listée après la version 3.0
pour que l'image construite aie le tag :latest

Note: 'setdists' ne doit être utilisé qu'une seule fois. Les invocations
suivantes sont ignorées.

## Profils #####################################################################

Un profil correspond à l'environnement de destination de l'image: production,
test, développement.

La commande 'setprofiles' permet de lister explicitement les profils valides.
Si le profil sélectionné par l'utilisateur n'est pas dans la liste fournie, le
script s'arrête avec une erreur. Le premier profil listé est spécial: c'est le
profil par défaut.

La commande 'default_profile' permet de spécifier un profil par défaut à
utiliser, exactement comme s'il avait été spécifié avec l'option --profile.
Cette commande est particulièrement appropriée pour le fichier ~/.dkbuild.env
s'il s'agit de définir le profil à utiliser sur un hôte.

La commande 'profile' permet de tester si le profil spécifié en argument a été
sélectionné par l'utilisateur. L'argument 'DEFAULT' permet de tester le profil
par défaut.

Exemple:
    setprofiles prod devel
    if profile prod; then
        ...
    elif profile devel; then
        ...
    fi

Si le build est indépendant de la distribution, ou si la distribution est
utilisée sans version, alors il est possible de préfixer le profil d'une
version. Exemple:
    setprofiles v1-prod v2-prod
    if profile prod; then
        if version v1; then
            ...
        elif version v2; then
            ...
        fi
    fi
Dans un même profil, les versions doivent être ordonnées de la plus ancienne à
la plus récente.

Si les distributions sont utilisées avec des versions, alors c'est une erreur de
spécifier une version dans le profil

Note: 'setprofiles' et 'default_profile' ne peuvent être utilisés qu'une seule
fois. Les invocations suivantes sont ignorées.

## Versions ####################################################################

Si la version de l'image à construire n'est liée ni à la distribution, ni au
profil, il est possible de la spécifier avec la commande 'setversion'. La
version spécifiée avec 'setversion' est utilisée par défaut pour toutes les
images dont la version n'est pas spécifiée dans la distribution ou le profil.

La commande 'setversion' est évaluée en même temps que les commandes 'setenv',
ainsi il est possible d'utiliser la valeur d'une variable définie au préalable.

## Environnement ###############################################################

La commande 'machine' permet de tester si le build est fait sur la machine
spécifiée. Ce peut être la machine courante, ou la machine spécifiée avec
l'option --machine de la commande build

Exemple:
    if machine host{1,2,3}-prod; then
        setprofiles prod
    elif machine host{1,2,3}-test; then
        setprofiles test
    else
        setprofiles devel test prod
    fi

Les variables sont de deux types: argument de build ou variable d'environnement

La commande 'setenv' permet de définir une variable d'environnement. La commande
'setarg' permet de définir un argument de build. Ces valeurs sont alors
automatiquement utilisées à l'endroit approprié.

Ces commandes acceptent une liste d'argument de la forme VAR[=VALUE]

Si la valeur n'est pas spécifiée (e.g 'setarg DESTDIR'), alors la variable doit
être définie dans l'environnement courant. Si la variable n'est pas définie dans
l'environnement, alors le script s'arrête avec une erreur.

Une fois qu'une variable est définie, il n'est plus possible de la modifier. Les
commandes alternatives 'resetenv' et 'resetarg' permettent de pallier cette
limitation.

## Valeurs par défaut ##########################################################

Toutes les commandes ont des arguments requis, mais aussi des arguments
facultatifs qui sont fournis sous la forme d'une liste d'éléments VAR=VALUE

La commande 'default' permet de spécifier les valeurs par défaut de ces
arguments. Exemple:
    if profile prod; then
        default composer mode=production
    elif profile devel; then
        default composer mode=development
    fi
    composer install path/to/project

Une fois qu'une valeur par défaut est définie, il n'est plus possible de la
modifier. La commande alternative 'resetdefault' permet de pallier cette
limitation.

Définir des valeurs par défaut pour la commande 'docker' impacte la commande
'build' et toutes les commandes qui utilisent docker, comme 'composer' ou 'mvn'

## Synchronisation de fichiers #################################################

On peut vouloir s'assurer de la présence de certains fichiers à certains
endroits.

La commande 'checkout URL DESTDIR' permet de s'assurer qu'un checkout du dépôt
spécifié existe dans le répertoire DESTDIR. La branche ou le commit à utiliser,
la source en mode développement, etc. peuvent être spécifiés par des arguments
facultatifs.

La commande 'copy SRC DEST' permet de s'assurer que SRC et DEST sont
synchronisés. Si possible, des liens physiques sont créés pour conserver
l'espace disque. Par défaut, les fichiers ne sont créés que s'ils n'existent
pas.

## Support Composer, Maven, commandes génériques ###############################

La commande 'composer install' permet d'installer les dépendances Composer d'un
projet

La commande 'mvn package' permet de construire un projet Java.

La commande 'run' permet de lancer une commande quelconque. La commande est
cherchée dans le PATH et exécutée avec son chemin complet. celà permet de lancer
des commandes comme 'mvn' ou 'composer' ou dont le nom correspond à une fonction
déjà définie.

La commande 'call' permet de lancer une commande quelconque. La différence avec
'run' est que la commande est lancée telle quelle, sans modifications. Si une
fonction est définie, elle sera utilisée en priorité.

Les commandes 'runb' et 'callb' sont comme 'run' et 'call' respectivement, mais
elles ne sont exécutées que si build est activé.

Si elles sont utilisées sans argument, les commandes 'composer', 'mvn', 'run' et
'call' retournent vrai si la commande doit être exécutée dans le contexte
courant. Celà permet d'implémenter des traitements complexes. Ainsi le script
suivant:
    run cmd1
    run cmd2
est équivalent à:
    if run; then
        cmd1
        cmd2
    fi

En phase d'analyse, ces commandes retournent faux, donc cmd1 et cmd2 ne seront
lancés qu'une seule fois lors de l'invocation de dkbuild. Cela signifie qu'il ne
faut pas utiliser des directives de définition de variables à l'intérieur. Par
exemple, le script suivant ne produit pas forcément l'effet escompté:
    if run; then
        setenv var=value
        default 
        cmd args...
    fi

Bien entendu, si on veut être sûr que des commandes externes soient lancées, on
peut toujours utiliser 'run' à l'intérieur, e.g
    if composer; then
        run extcmd
        func
        composer args...
    fi

## Support Dockerfile et docker build ##########################################

La commande 'dockerfile OUTPUT' permet de construire de façon incrémentale et
dynamique un fichier Dockerfile. Toutes les commandes d'un fichier dockerfile
traditionnelles sont reconnues et elles doivent être spécifiées après la
commande 'dockerfile'.

La commande 'build' termine la construction du fichier Docker puis lance la
construction de l'image.
* arguments de build: les arguments de build définis dans le script sont passés
  à docker pour la construction de l'image
* nom de l'image et tag: par défaut, la variable d'environnement IMAGE est
  combinée le cas échéant avec DIST et VERSION pour déterminer les tags que
  reçoit l'image construite.

La commande 'cbuild' lance le build de toutes les images mentionnées dans les
fichiers 'docker-compose.yml' et 'docker-compose.PROFILE.yml' le cas échéant
* variables d'environnement: les arguments de build définis sont inscrits dans
  un fichier .env qui est utilisé ensuite par docker compose

Si aucune commande 'build' ou 'cbuild' ne figure dans le fichier, 'build' est
lancé automatiquement à la fin

## Autres commandes ############################################################

Il est possible d'organiser le script dans plusieurs fichiers qui sont inclus
avec la commande 'include'. Fonctionnellement, cela se passe comme si le contenu
du fichier inclus était inséré dans le script principal, à ceci près que le
répertoire courant devient temporairement celui du fichier inclus.

Exemple:
    setdists d11 d10 d9
    if dist d9; then
        include d9/dkbuild
    elif dist d10; then
        include d10/dkbuild
    elif dist d11; then
        include d11/dkbuild
    fi

La commande 'dkbuild' lance le build d'un répertoire destination. La différence
avec 'include' est que cela est fait dans un processus complètement différent,
comme si le build avait été lancé depuis la ligne de commande.

Les commandes 'section', 'note', 'info', 'debug' permettent d'afficher des
messages de différents niveaux

## Variables globales ##########################################################

PROJDIR est le chemin absolu du projet, dans lequel se trouve le fichier dkbuild
initial

La distribution actuellement sélectionnée se trouve dans la variable DIST. La
version actuellement sélectionnée se trouve dans la variable VERSION. Si la
commande 'setdists' n'est pas utilisée, alors ni DIST ni VERSION ne sont définis

Le profil actuellement sélectionné se trouve dans la variable PROFILE. La
version actuellement sélectionnée se trouve dans la variable VERSION (si les
distributions sont utilisées sans le support de la version). Si la commande
'setprofiles' n'est pas utilisée, alors ni PROFILE ni VERSION ne sont définis

IMAGE est le nom de l'image à construire. Si le nom de l'image contient le tag
(e.g IMAGE:TAG) alors le nom est utilisé tel quel. Sinon, DIST et VERSION sont
utilisés comme tags (i.e IMAGE:DIST et IMAGE:VERSION-DIST)"
}

function display_help_reference() {
    uecho "\
OPTIONS
  --help  Aide générale
  --hdk   Aide sur le format du fichier dkbuild
* --href  Référence sur les commandes utilisables dans un fichier dkbuild

REFERENCE
=========

## fonctions d'affichage

USAGE:
    section TITLE
    note MESSAGE
    info MESSAGE
    debug MESSAGE

## machine         -- vérifier la machine courante

## setdists        -- spécifier les distributions valides

## dist            -- vérifier la distribution courante

## setprofiles     -- spécifier les profils valides

## default_profile -- spécifier le profil à utiliser si aucun profil n'est sélectionné

## profile         -- vérifier le profil courant

## setversion      -- spécifier la version par défaut

USAGE: setversion VERSION

Les paramètres optionnels sont
* from-file=FILE
  prendre la version depuis le fichier spécifié. les formats pom.xml et
  VERSION.txt sont supportés
* from-repo=REPO
  calculer la version depuis le dépôt git spécifié
* from-glob=GLOB
  calculer la version depuis le chemin spécifié
* extract=REGEXP
  à partir de la valeur calculée par l'un des paramètres from-*, matcher
  l'expression régulière au format AWK, et prendre comme version la valeur de
  l'expression \$1
  Si from-glob est spécifié, la valeur par défaut de extract est calculée en
  remplaçant '*' par '(.*)'
* add-prefix=PREFIX
  Ajouter le préfixe spécifié à la version extraite
* add-suffix=SUFFIX
  Ajouter le suffixe spécifié à la version extraite

## version         -- vérifier la version courante

## setenv          -- spécifier une variable d'environnement

## resetenv        -- spécifier une variable d'environnement

## setarg          -- spécifier une variable de build

## resetarg        -- spécifier une variable de build

## default         -- spécifier des arguments par défaut

## resetdefault    -- spécifier des arguments par défaut

## checkout        -- faire et vérifier checkout d'un dépôt git

USAGE: checkout URL [DESTDIR] [PARAMS]

Les paramètres optionnels sont
* checkout
  mettre à jour dépôt. c'est la valeur par défaut. utiliser checkout= pour
  désactiver la mise à jour du dépôt. utiliser checkout=devel pour synchroniser
  depuis le répertoire de développement.
* origin=ORIGIN
  spécifier l'origine. Par défaut, prendre 'origin'
* branch=BRANCH
  spécifier la branche à utiliser dans l'origine spécifiée. La valeur par défaut
  est 'master' si les profils ne sont pas utilisé. Si les profils sont utilisés,
  la valeur par défaut est 'develop' pour les profils 'test' et 'devel', sinon
  c'est 'master'.
  Utiliser la syntaxe ^COMMIT pour ignorer l'origine et sélectionner un commit
  en particulier.
* develdir=DIR
  spécifier l'emplacement du répertoire de développement, utilisé avec
  checkout=devel
* develtype=TYPE
  spécifier le type de projet pour la synchronisation depuis le répertoire de
  développement. certains projets, notamment les projets composer avec des
  dépendances de type 'path', nécessitent une méthode de synchronisation
  particulière. Si ce paramètre n'est pas spécifié, il est auto-détecté. Les
  valeurs supportées sont:
  * composer -- détecté de par la présence d'un fichier composer.json
  * maven -- détecté de par la présence d'un fichier pom.xml
  * none -- valeur par défaut: pas de type particulier

## copy            -- synchroniser des fichiers

USAGE: copy SRC DEST [PARAMS]

Si SRC est un fichier, alors DEST doit être un chemin vers le fichier
destination. Si SRC est un répertoire, alors DEST doit être un chemin vers le
répertoire destination. On peut forcer à considérer SRC et/ou DEST comme un
répertoire en les suffixant de '/'

Par exemple:
* copy SRCDIR/ DESTDIR/
  échoue si SRCDIR et/ou DESTDIR sont des fichiers
* les commandes suivantes sont équivalentes:
  copy SRC DESTDIR/
  copy SRC DESTDIR/SRCNAME

Les paramètres optionnels sont
* copy
  synchroniser les fichiers. c'est la valeur par défaut. utiliser copy= pour
  désactiver la synchronisation des fichiers.
* overwrite
  autoriser l'écrasement des fichiers destination, ce qui permet de rendre la
  destination identique à la source. par défaut, un fichier destination est
  laissé en place, ce qui permet d'avoir le cas échéant des fichiers locaux
  différents de la source.
* gitignore=BASEDIR
  maintenir le fichier .gitignore de BASEDIR, qui doit être un répertoire parent
  de DESTDIR. Les fichiers synchronisés sont rajouté le cas échéant dans
  .gitignore, sauf si le répertoire qui les contient est déjà exclu.
  NB: Un fichier n'est considéré pour l'ajout dans .gitignore que s'il a été
  copié au préalable. Ainsi, un fichier déjà existant dans la destination ne
  sera pas ajouté dans le fichier .gitignore si overwrite=

## genfile         -- créer un fichier générique

USAGE: genfile OUTPUT [INPUT] [PARAMS]

Si le fichier INPUT est spécifié, il est utilisé pour créer le contenu initial
du fichier. Sinon, l'entrée standard *doit* être redirigée depuis un fichier, et
elle est lue pour générer le contenu initial du fichier à générer.

Les paramètres optionnels sont
* context=DIR
  générer le fichier dans le répertoire spécifié
* sed=SCRIPT
  script sed à appliquer au fichier

## dockerfile      -- créer un fichier Dockerfile

USAGE: dockerfile [OUTPUT [INPUT]] [PARAMS]

Si le fichier INPUT est spécifié, il est utilisé pour créer le contenu initial
du fichier. Sinon, *si* l'entrée standard est redirigée depuis un fichier, elle
est lue pour générer le contenu initial du fichier.

Les paramètres optionnels sont
* context=DIR
  générer le fichier dans le répertoire spécifié
* sed=SCRIPT
  script sed à appliquer au fichier initial, construit à partir de l'entrée
  standard. Le fichier n'est plus modifié par la suite.

## build           -- construire une image avec docker

USAGE: build [PARAMS]

Les paramètres optionnels sont
* build
  construire les images, c'est la valeur par défaut. utiliser build= pour
  désactiver la construction.
* context=DIR
  répertoire de contexte. cette valeur est en principe générée automatiquement
  par la commande 'dockerfile'
* dockerfile=FILE
  fichier de build à utiliser. cette valeur est en principe générée automatiquement
  par la commande 'dockerfile'
* no-cache
  ne pas utiliser le cache
* pull
  forcer la mise à jour des images dépendantes
* host-mappings=MAPPINGS
  définir des mappings d'hôte. si ce paramètre n'est pas spécifié, consulter
  aussi la valeur par défaut 'docker host-mappings='
* set-tag=TAGS... ou set-tags=TAGS...
  spécifier des tags à rajouter au nom de l'image, séparés par un espace. cette
  liste remplace celle calculée automatiquement. ce paramètre est ignoré pour
  les noms d'images comportant un tag.
  Utiliser le tag spécial LATEST pour rajouter :latest si c'est approprié
* add-tag=TAGS... ou add-tags=TAGS...
  spécifier des tags à rajouter à la liste calculée automatiquement, séparés par
  un espace. ce paramètre est ignoré pour les noms d'images comportant un tag
* image=IMAGES... ou images=IMAGES...
  liste de nom d'images, séparés par un espace. si les noms n'ont pas de tag, le
  tag est construit à partir de DIST et VERSION sous la forme [VERSION-]DIST
* push
  pousser les images vers le registry après les avoir construites

## cbuild          -- construire une image avec docker compose

USAGE: cbuild [SERVICE] [PARAMS]

Les paramètres optionnels sont
* files=FILES...
  spécifier les fichiers docker-compose à utiliser. Par défaut, prendre
  docker-compose.yml et docker-compose.PROFILE.yml
* project-name=PROJECT_NAME
  spécifier le nom du projet
* no-cache
  ne pas utiliser le cache
* pull
  forcer la mise à jour des images dépendantes

## include         -- inclure un autre fichier dkbuild

## dkbuild         -- lancer un builder externe

La commande dkbuild qui traite le script de build courant est lancée telle
quelle, avec la même syntaxe qu'en ligne de commande. L'intérêt de cette
commande est qu'on est assuré d'utiliser le même dkbuild que celui qui traite le
script de build courant.

Les variables DKBUILD_CMD_ARGS, DKBUILD_CONFIGS, DKBUILD_ENVS et DKBUILD_ARGS
peuvent être utilisées pour reprendre les valeurs spécifiées en ligne de
commande, e.g"'
    dkbuild -c "${DKBUILD_CONFIGS[0]}" "${DKBUILD_CMD_ARGS[@]}"'"

La variable DKBUILD_CONFIGS contient les noms des fichiers de configuration
utilisés.
Les variables DKBUILD_ENVS et DKBUILD_ARGS sont indexées sur le nom des
variables (resp. arguments). Il est donc possible de reprendre une valeur
particulière, e.g"'
    dkbuild -c "${DKBUILD_CONFIGS[0]}" -e "${DKBUILD_ENVS[myenv]}"'"

## composer        -- gérer projet composer

USAGE: composer DESTDIR [ACTION [PARAMS] [ARGS]]

La destination est obligatoire. Sans arguments, cette commande retourne
simplement vrai

Les actions valides sont
* install -- installer les dépendances. c'est l'action par défaut
* update -- mettre à jour les dépendances
* rshell -- lancer un shell root dans le répertoire du projet
* shell -- lancer un shell utilisateur dans le répertoire du projet
* none -- ne rien faire

L'action est exécutée sur DESTDIR. Juste avant de lancer l'action, le répertoire
courant est modifié pour être DESTDIR, ce qui permet d'utiliser des chemins
relatifs le cas échéant.

La commande 'rshell' lance un shell bash avec l'utilisateur root au lieu de
lancer la commande composer, ce qui permet de faire des opérations plus
complexes si le besoin s'en fait sentir. La commande alternative 'shell' lance
le shell avec le compte utilisateur. Ces commandes sont particulièrement utiles
si composer est paramétré pour être lancé dans un container

Les paramètres optionnels sont
* args=ARGS...
  arguments à rajouter à la commande composer. La valeur par défaut dépend du
  profil:
  * prod: --no-dev -o
  * test: --no-dev -o
  * autres profils: (pas d'arguments)
* php=VERSION
  version de PHP en dessous de laquelle image= est utilisé. En d'autres termes,
  c'est la version minimum de PHP nécessaire pour faire tourner composer. L'idée
  est que si la version de PHP installée est suffisante, il n'est pas nécessaire
  de passer par une image docker.
  Cette valeur doit être spécifiée avec le format de PHP_VERSION_ID i.e 70300
  pour PHP 7.3
  * Spécifier 'any' ou 'force' pour forcer l'utilisation de l'image docker.
  * Spécifier 'none' ou 'system' pour lancer directement composer sans passer
    par une image docker.
  Si php n'est pas disponible dans le PATH, ce paramètre prend par défaut la
  valeur 'force'
* php-max=VERSION
  version de PHP à partir de laquelle image= est utilisé. En d'autres termes,
  c'est la version maximum de PHP, à partir de laquelle il faut passer par une
  image docker. L'idée est que si la version de PHP installée est trop récente,
  ça peut poser problème avec le calcul des dépendances.
  Cette valeur doit être spécifiée avec le format de PHP_VERSION_ID i.e 70300
  pour PHP 7.3
  Si la valeur n'est pas spécifiée ou vaut 'none', elle est ignorée.
* image=COMPOSER_IMAGE
  image docker utilisée pour lancer composer. La valeur par défaut est la valeur
  de la variable d'environnement \$COMPOSER_IMAGE
  Spécifier 'none' pour lancer directement composer sans passer par une image
  docker.
  L'image spécifiée doit disposer de la commande 'su-exec' afin de pouvoir
  lancer la commande avec l'utilisateur courant. Le répertoire \$HOME est monté
  à l'intérieur du container et le script composer.phar du projet est utilisé le
  cas échéant.
* machine=MACHINE
  nom de la docker machine sur laquelle se connecter pour lancer l'image docker.
  La valeur par défaut est -u, ce qui force l'utilisation de l'instance docker
  locale. Spécifier 'current' pour ne pas modifier la valeur courante le cas
  échéant
* host-mappings=MAPPINGS
  définir des mappings d'hôte. si ce paramètre n'est pas spécifié, consulter
  aussi la valeur par défaut 'docker host-mappings='
* composer=PATH/TO/COMPOSER
  chemin vers l'exécutable composer. Par défaut, utiliser composer.phar s'il
  existe dans le répertoire du projet. Sinon utiliser /usr/bin/composer
* setup=CMDS...
  liste de commandes à lancer pour configurer le container. Un container ayant
  pour base COMPOSER_IMAGE et nommé d'après le nom du projet est préparé et les
  commandes spécifiées y sont lancées. Ce container est réutilisé à chaque fois.
  Ce paramétrage est utilisé par exemple pour installer certains packages
  nécessaire au projet.
* setup-image=SETUP_IMAGE
  forcer le nom de l'image pour setup= (la valeur de project-name= est ignorée)
* project-name=PROJECT_NAME
  si setup= est défini, nommer l'image sur la base de ce nom. par défaut, le nom
  est calculé automatiquement

Si un fichier .composer.yaml existe dans le répertoire du projet, il est analysé
pour obtenir les valeurs par défaut des paramètres suivants:
* composer_php_min: valeur par défaut de php=
* composer_php_max: valeur par défaut de php-max=
* composer_registry: ET composer_image: valeur par défaut de image=
* composer_setup: valeur par défaut de setup=
* composer_setup_image: valeur par défaut de setup-image=

Sinon, si un fichier .composer.conf existe dans le répertoire du projet, il est
sourcé pour obtenir les valeurs par défaut des paramètres:
* COMPOSER_PHP -- valeur par défaut de php=
* COMPOSER_PHP_MAX -- valeur par défaut de php-max=
* COMPOSER_IMAGE -- valeur par défaut de image=
* COMPOSER_MACHINE -- valeur par défaut de machine=
* COMPOSER_CMD -- valeur par défaut de composer=
* COMPOSER_SETUP -- valeur par défaut de setup=
* COMPOSER_SETUP_IMAGE -- valeur par défaut de setup-image=

## mvn             -- construire projet maven

USAGE: mvn DESTDIR [ACTION [PARAMS] [ARGS]]

Le répertoire de destination est obligatoire. Sans arguments, cette commande
retourne simplement vrai. L'action par défaut est 'package'

Les actions valides sont
* install -- lance mvn avec les commandes 'clean package install'
* package -- lance mvn avec les commandes 'clean package'
* package_only -- lance mvn avec uniquement la commande 'package'
* rshell -- lancer un shell root dans le répertoire du projet
* shell -- lancer un shell utilisateur dans le répertoire du projet
* java -- lancer java au lieu de lancer mvn. il est possible de spécifier la
  version de java directement, e.g java7, java8, java11

L'action est exécutée sur DESTDIR. Juste avant de lancer l'action, le répertoire
courant est modifié pour être DESTDIR, ce qui permet d'utiliser des chemins
relatifs le cas échéant.

La commande 'rshell' lance un shell bash avec l'utilisateur root au lieu de
lancer la commande mvn, ce qui permet de faire des opérations plus complexes si
le besoin s'en fait sentir. La commande alternative 'shell' lance le shell avec
le compte utilisateur. Ces commandes sont particulièrement utiles si mvn est
paramétré pour être lancé dans un container

Les paramètres optionnels sont
* args=ARGS...
  arguments à rajouter à la commande mvn
* java=VERSION
  version de Java à sélectionner à l'intérieur de l'image docker
  * Spécifier 'any' ou 'force' pour prendre la valeur par défaut
  * Spécifier 'none' ou 'system' pour ne pas utiliser l'image docker
  Si java ou mvn ne sont pas disponibles dans le PATH, ce paramètre prend par
  défaut la valeur 'force'
* image=MAVEN_IMAGE
  image docker utilisée pour lancer mvn. La valeur par défaut est la valeur
  de la variable d'environnement \$MAVEN_IMAGE
  Spécifier 'none' pour lancer directement mvn sans passer par une image
  docker, même si java= est renseigné
  L'image spécifiée doit disposer de la commande 'su-exec' afin de pouvoir
  lancer la commande avec l'utilisateur courant. Le répertoire \$HOME est monté
  à l'intérieur du container.
* machine=MACHINE
  nom de la docker machine sur laquelle se connecter pour lancer l'image docker.
  La valeur par défaut est -u, ce qui force l'utilisation de l'instance docker
  locale. Spécifier 'current' pour ne pas modifier la valeur courante le cas
  échéant
* host-mappings=MAPPINGS
  définir des mappings d'hôte. si ce paramètre n'est pas spécifié, consulter
  aussi la valeur par défaut 'docker host-mappings='
* mvn=PATH/TO/MVN
  chemin vers l'exécutable mvn. Par défaut, utiliser la commande trouvée dans le
  PATH
* setup=CMDS...
  liste de commandes à lancer pour configurer le container. Un container ayant
  pour base MAVEN_IMAGE et nommé d'après le nom du projet est préparé et les
  commandes spécifiées y sont lancées. Ce container est réutilisé à chaque fois.
  Ce paramétrage est utilisé par exemple pour installer certains packages
  nécessaire au projet.
* setup-image=SETUP_IMAGE
  forcer le nom de l'image pour setup= (la valeur de project-name= est ignorée)
* project-name=PROJECT_NAME
  si setup= est défini, nommer l'image sur la base de ce nom. par défaut, le nom
  est calculé automatiquement

Si un fichier .maven.conf existe dans le répertoire du projet, il est sourcé
pour obtenir les valeurs par défaut des paramètres:
* MAVEN_JAVA -- valeur par défaut de java=
* MAVEN_IMAGE -- valeur par défaut de image=
* MAVEN_MACHINE -- valeur par défaut de machine=
* MAVEN_CMD -- valeur par défaut de mvn=
* MAVEN_SETUP -- valeur par défaut de setup=
* MAVEN_SETUP_IMAGE -- valeur par défaut de setup-image=

## run             -- lancer des commandes

## runb            -- lancer des commandes

## call            -- lancer des commandes ou des fonctions

## callb           -- lancer des commandes ou des fonctions
"
}

##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## shared

declare -A PROTECTED_VARS=(
    [PROTECTED_VARS]=1
    [SELF]=1
    [SHARED_LOCALS1]=1
    [SHARED_ARGS1]=1
    [SHARED_LOCALS2]=1
    [SHARED_ARGS2]=1
    [TFUNCTIONS]=1
    [FFUNCTIONS]=1
    [PROJDIR]=1
    [DKBUILD]=1
    [CONFIG]=1
    [MACHINE]=1
    [SETDISTS_DONE]=1
    [SETDISTS]=1
    [SETPROFILES_DONE]=1
    [SETPROFILES]=1
    [DEFAULT_PROFILE]=1
    [SETVERSION_DONE]=1
    [SETVERSION]=1
    [AUTOBUILD]=1
    [DISTS]=1
    [PROFILES]=1
    [_ENVIRON]=1
    [ENVIRON]=1
    [ARGS]=1
    [DEFAULTS]=1
    [DKBUILD_CMD_ARGS]=1
    [fill_DKBUILD_CONFIGS]=1 [DKBUILD_CONFIGS]=1
    [fill_DKBUILD_ENVS]=1 [DKBUILD_ENVS]=1
    [fill_DKBUILD_ARGS]=1 [DKBUILD_ARGS]=1
)

SHARED_LOCALS1="local PROJDIR DKBUILD CONFIG"
SHARED_ARGS1=(
    -j:,--projdir: PROJDIR=
    -c:,--config: CONFIG=
)

SHARED_LOCALS2="local DIST PROFILE ALL_PROFILES; local -a TMPENVIRON TMPARGS"
SHARED_ARGS2=(
    -d:,--dist: DIST=
    -0,--d10 DIST=d10
    -1,--d11 DIST=d11
    -2,--d12 DIST=d12
    -3,--d13 DIST=d13
    --r7,--rhel7 DIST=rhel7
    --r8,--rhel8 DIST=rhel8
    --r9,--rhel9 DIST=rhel9
    --ol7,--oracle7 DIST=ol7
    --ol8,--oracle8 DIST=ol8
    --ol9,--oracle9 DIST=ol9

    -g:,--profile: PROFILE=
    -P,--prod PROFILE=prod
    -T,--test PROFILE=test
    -E,--dtest PROFILE=dtest
    -J,--jclain PROFILE=jclain
    --devel PROFILE=devel
    --all-profiles ALL_PROFILES=1

    -e:,--env: '$TMPENVIRON+=("$value_")'
    --arg: '$TMPARGS+=("$value_")'
)

declare -a DKBUILD_CMD_ARGS
fill_DKBUILD_CONFIGS=; declare -a DKBUILD_CONFIGS
fill_DKBUILD_ENVS=; declare -A DKBUILD_ENVS
fill_DKBUILD_ARGS=; declare -A DKBUILD_ARGS

TFUNCTIONS=(
    # dkbuild
    section note info debug
    setdists dist
    setprofiles profile
    default_profile
    setversion version
    setenv resetenv
    setarg resetarg
    default resetdefault
    checkout
    copy
    genfile
    dockerfile
    build
    cbuild
    # dockerfile
    FROM
    RUN
    CMD
    LABEL
    MAINTAINER
    EXPOSE
    ENV
    ADD
    COPY
    ENTRYPOINT
    VOLUME
    USER
    WORKDIR
    ARG
    ONBUILD
    STOPSIGNAL
    HEALTHCHECK
    SHELL
)
FFUNCTIONS=(
    # dkbuild
    composer
    mvn
    run
    runb
    call
    callb
    dkbuild
)

function set_machine() {
    local machine="$1"
    if [ "$machine" == -u ]; then
        # déselectionner la machine courante
        local -x DOCKER_TLS_VERIFY= DOCKER_HOST= DOCKER_CERT_PATH= DOCKER_MACHINE_NAME=
        machine=
    fi
    if [ -n "$machine" ]; then
        if [ -f ~/etc/default/dk ]; then
            machine="$(
               CLUSTERDIRS=()
               DM_ALIASES=()
               source ~/etc/default/dk
               for alias_machine in "${DM_ALIASES[@]}"; do
                   if [ "${alias_machine%%:*}" == "$machine" ]; then
                       echo "${alias_machine#*:}"
                       exit
                   fi
               done
               for clusterdir in "${CLUSTERDIRS[@]}"; do
                   if [ -f "$clusterdir/0config/configure.conf" ]; then
                       DM_ALIASES=()
                       source "$clusterdir/0config/configure.conf"
                       for alias_machine in "${DM_ALIASES[@]}"; do
                           if [ "${alias_machine%%:*}" == "$machine" ]; then
                               echo "${alias_machine#*:}"
                               exit
                           fi
                       done
                   fi
               done
               echo "$machine"
            )"
        fi
        eval "$(docker-machine env "$machine" || echo false)" || die
    else
        machine="$DOCKER_MACHINE_NAME"
        [ -n "$machine" ] || machine="${HOSTNAME%%.*}"
    fi
    MACHINE="$machine"
}

function get_project_name() {
    local project_name
    setx project_name=basename -- "$(pwd)"
    if [ "${project_name%.service}" != "$project_name" ]; then project_name="${project_name%.service}"
    elif [ "${project_name%.stack}" != "$project_name" ]; then project_name="${project_name%.stack}"
    elif [ "${project_name%.network}" != "$project_name" ]; then project_name="${project_name%.network}"
    fi
    echo "$project_name"
}

function get_container_name() {
    local container_name="${1//[^a-zA-Z0-9_-]/}"
    [ -n "$PROFILE" ] && container_name="${container_name}_$PROFILE"
    echo "$container_name"
}

function reset_functions() {
    local func
    for func in "${TFUNCTIONS[@]}"; do
        eval "function $func() { : echo \"$func \$*\"; return 0; }"
    done
    for func in "${FFUNCTIONS[@]}"; do
        eval "function $func() { : echo \"$func \$*\"; return 1; }"
    done
    function include() {
        edebug "include $(qvals "$@")"

        local file="$1" cwd="$(pwd)"
        [ -d "$file" ] && file="$file/dkbuild"
        [ -f "$file" ] || die "$file: fichier introuvable"
        setx file=abspath "$file"
        cd "$(dirname "$file")"
        source "$file"
        cd "$cwd"
    }
    function machine() {
        local machine version
        for machine in "$@"; do
            [ "$machine" == "$MACHINE" ] && return
        done
        return 1
    }
}

function _runcmd() {
    edebug "\$ $(qvals "$@")"
    "$@"
}

function ensure_projdir() {
    if [ -z "$PROJDIR" ]; then
        local found=
        if [ ! -f dkbuild ]; then
            # NB: on teste $PROJDIR != $scriptdir parce qu'il ne faut pas qu'on
            # prenne le présent script comme un script de build...
            PROJDIR="$(pwd)"
            if [ "${PROJDIR#$HOME/}" != "$PROJDIR" ]; then
                while [ "$PROJDIR" != "$HOME" ]; do
                    if [ -f "$PROJDIR/dkbuild" -a "$PROJDIR" != "$scriptdir" ]; then
                        found=1
                        break
                    fi
                    setx PROJDIR=dirname "$PROJDIR"
                done
            else
                while [ "$PROJDIR" != / ]; do
                    if [ -f "$PROJDIR/dkbuild" -a "$PROJDIR" != "$scriptdir" ]; then
                        found=1
                        break
                    fi
                    setx PROJDIR=dirname "$PROJDIR"
                done
            fi
        fi
        if [ -n "$found" ]; then
            enote "Sélection du répertoire de projet $(relpath "$PROJDIR")"
        else
            PROJDIR=.
        fi
    fi

    if [ -f "$PROJDIR" ]; then
        DKBUILD="$PROJDIR"
        setx PROJDIR=dirname "$PROJDIR"
    else
        DKBUILD="$PROJDIR/dkbuild"
    fi
    [ -d "$PROJDIR" ] || die "$PROJDIR: répertoire de projet introuvable"

    setx PROJDIR=abspath "$PROJDIR"
    setx DKBUILD=abspath "$DKBUILD"
    cd "$PROJDIR" || die
    [ -f "$DKBUILD" ] || die "$(ppath "$DKBUILD"): fichier de build introuvable"

    if [ "$CONFIG" == none ]; then
        edebug "no default config used"
    elif [ -n "$CONFIG" ]; then
        setx CONFIG=abspath "$CONFIG"
        edebug "using config $CONFIG"
    else
        local config found
        for config in ~/.dkbuild.env /etc/dkbuild.env; do
            if [ -f "$config" ]; then
                found=1
                CONFIG="$config"
                edebug "using default config $CONFIG"
                break
            fi
        done
        [ -n "$found" ] || CONFIG=none
    fi
}

function load_dkbuild() {
    local dkbuildenv="$PROJDIR/$(basename "$DKBUILD").env"
    cd "$PROJDIR"

    set_defaults dkbuild

    if [ -n "$CONFIG" ]; then
        if [ "$CONFIG" != none ]; then
            edebug "loading $CONFIG"
            source "$CONFIG"
        fi
        [ -n "$fill_DKBUILD_CONFIGS" ] && DKBUILD_CONFIGS+=("$CONFIG")
    fi
    if [ -f "$dkbuildenv" ]; then
        edebug "loading $dkbuildenv"
        source "$dkbuildenv"
        [ -n "$fill_DKBUILD_CONFIGS" ] && DKBUILD_CONFIGS+=("$dkbuildenv")
    fi

    edebug "loading $DKBUILD"
    source "$DKBUILD"
}

function load_environ() {
    declare -g -A _ENVIRON
    eval "$(declare -p -x | sed -r 's/^declare -x ([^=]+)=/_ENVIRON[\1]=/')"
}

function from_glob() {
    local vvalue=value value vfile=file file
    [[ "$1" != *=* ]] && { vvalue="$1"; shift; }
    [[ "$1" != *=* ]] && { vfile="$1"; shift; }

    local basedir path extract add_prefix add_suffix
    while [ $# -gt 0 ]; do
        case "$1" in
        basedir=*) basedir="${1#basedir=}";;
        path=*) path="${1#path=}";;
        extract=*) extract="${1#extract=}";;
        add-prefix=*) add_prefix="${1#add-prefix=}";;
        add-suffix=*) add_suffix="${1#add-suffix=}";;
        *=*) ewarn "path: $1: argument ignoré";;
        *) break;;
        esac
        shift
    done
    [ -n "$basedir" ] || basedir=.
    value="$(list_all "$basedir" "$path" | sort -rn | head -1)"
    file="$basedir/$value"
    [ -n "$extract" ] || extract="${path//\*/(.*)}"
    if [ -n "$extract" ]; then
        extract="${extract//\//\\/}"
        value="$add_prefix$(awk -v version="$value" "BEGIN {
          if (match(version, /$extract/, vs)) { print vs[1] }
          else { print version }
        }")$add_suffix"
    fi

    local "$vvalue" "$vfile"; upvars "$vvalue" "$value" "$vfile" "$file"
}
function define_functions_env() {
    function setversion() {
        [ -n "$SETVERSION_DONE" ] && return
        # sans argument, retourner 0
        [ $# -eq 0 ] && return

        local from_file from_repo from_glob extract add_prefix add_suffix
        while [ $# -gt 0 ]; do
            case "$1" in
            from-file|file) from_file=.;;
            from-file=*|file=*)
                from_file="${1#from-}"; from_file="${from_file#file=}"
                ;;
            from-repo|repo) from_repo=.;;
            from-repo=*|repo=*)
                from_repo="${1#from-}"; from_repo="${from_repo#repo=}"
                ;;
            from-glob=*|glob=*)
                from_glob="${1#from-}"; from_glob="${from_glob#glob=}"
                ;;
            extract=*) extract="${1#extract=}";;
            add-prefix=*) add_prefix="${1#add-prefix=}";;
            add-suffix=*) add_suffix="${1#add-suffix=}";;
            *=*) ewarn "setversion: $1: argument ignoré";;
            *) break;;
            esac
            shift
        done
        if [ -n "$from_file" ]; then
            if [ -d "$from_file" ]; then
                setx SETVERSION=pver --sw "$from_file" || die
            elif [[ "$from_file" == *.xml ]]; then
                setx SETVERSION=pver -E "$from_file" || die
            else
                setx SETVERSION=pver -F "$from_file" || die
            fi
        elif [ -n "$from_repo" ]; then
            die "setversion from-repo: pas encore implémenté" #XXX
        elif [ -n "$from_glob" ]; then
            SETVERSION="$(list_all . "$from_glob" | sort -rn | head -1)"
            [ -n "$extract" ] || extract="${from_glob//\*/(.*)}"
        else
            SETVERSION="$1"
        fi
        if [ -n "$extract" ]; then
            extract="${extract//\//\\/}"
            SETVERSION="$add_prefix$(awk -v version="$SETVERSION" "BEGIN {
              if (match(version, /$extract/, vs)) { print vs[1] }
              else { print version }
            }")$add_suffix"
        fi
        SETVERSION_DONE=1
    }
    function dist() {
        local dist version latest
        for dist in "$@"; do
            if [ "$dist" == LATEST ]; then
                latest=1
                continue
            fi
            parse_dist "$dist" dist version
            [ "$dist" == "$DIST" ] || continue
            [ -z "$version" -o "$version" == "$DVERSION" ] || continue
            return 0
        done
        if [ -n "$latest" ]; then
            # trouver la dernière occurence de la première distribution
            # mentionnée. en effet, les versions doivent être ordonnées de la
            # plus ancienne à la plus récente.
            local first_dist last_version
            for dist in "${SETDISTS[@]}"; do
                parse_dist "$dist" dist version
                if [ -z "$first_dist" ]; then
                    first_dist="$dist"
                    last_version="$version"
                elif [ "$dist" == "$first_dist" ]; then
                    last_version="$version"
                fi
            done
            if [ "$first_dist" == "$DIST" ]; then
                if [ -z "$last_version" -o "$last_version" == "$DVERSION" ]; then
                    return 0
                fi
            fi
        fi
        return 1
    }
    function version() {
        local version
        for version in "$@"; do
            [ "$version" == "$VERSION" ] && return 0
        done
        return 1
    }
    function profile() {
        local profile version default
        for profile in "$@"; do
            if [ "$profile" == DEFAULT ]; then
                default=1
                continue
            fi
            parse_profile "$profile" profile version
            [ "$profile" == "$PROFILE" ] || continue
            [ -z "$version" -o "$version" == "$PVERSION" ] || continue
            return 0
        done
        if [ -n "$default" ]; then
            # trouver la dernière occurence du premier profil mentionné. en
            # effet, les versions doivent être ordonnées de la plus ancienne à
            # la plus récente.
            local first_profile last_version
            for profile in "${SETPROFILES[@]}"; do
                parse_profile "$profile" profile version
                if [ -z "$first_profile" ]; then
                    first_profile="$profile"
                    last_version="$version"
                elif [ "$profile" == "$first_profile" ]; then
                    last_version="$version"
                fi
            done
            if [ "$first_profile" == "$PROFILE" ]; then
                if [ -z "$last_version" -o "$last_version" == "$PVERSION" ]; then
                    return 0
                fi
            fi
        fi
        return 1
    }
    declare -g -A ENVIRON
    function setenv() {
        local name value
        for name in "$@"; do
            if [[ "$name" == *=* ]]; then
                value="${name#*=}"
                name="${name%%=*}"
            else
                value="${_ENVIRON[$name]-__UNDEFINED__}"
                [ "$value" == __UNDEFINED__ ] && die "la variable d'environnement $name doit être définie"
            fi
            if [ "${ENVIRON[$name]-__UNDEFINED__}" == __UNDEFINED__ ]; then
                # Ne spécifier la valeur que si elle n'a pas déjà été définie
                _ENVIRON["$name"]="$value"
                ENVIRON["$name"]="$value"
                if [ -z "${PROTECTED_VARS[$name]}" ]; then
                    _setv "export $name" "$value"
                    if [ -n "$fill_DKBUILD_ENVS" ]; then
                        DKBUILD_ENVS["$name"]="$name=$value"
                        DKBUILD_CMD_ARGS+=(-e "$name=$value")
                    fi
                fi
            fi
        done
    }
    function resetenv() {
        local name value
        for name in "$@"; do
            if [[ "$name" == *=* ]]; then
                value="${name#*=}"
                name="${name%%=*}"
            else
                value="${_ENVIRON[$name]-__UNDEFINED__}"
                [ "$value" == __UNDEFINED__ ] && die "la variable d'environnement $name doit être définie"
            fi
            _ENVIRON["$name"]="$value"
            ENVIRON["$name"]="$value"
            [ -z "${PROTECTED_VARS[$name]}" ] && _setv "export $name" "$value"
        done
    }
    declare -g -A ARGS
    function setarg() {
        local name value
        for name in "$@"; do
            if [[ "$name" == *=* ]]; then
                value="${name#*=}"
                name="${name%%=*}"
            else
                value="${_ENVIRON[$name]-__UNDEFINED__}"
                [ "$value" == __UNDEFINED__ ] && die "la variable d'environnement $name doit être définie"
            fi
            if [ "${ARGS[$name]-__UNDEFINED__}" == __UNDEFINED__ ]; then
                # Ne spécifier la valeur que si elle n'a pas déjà été définie
                ARGS["$name"]="$value"
                if [ -n "$fill_DKBUILD_ARGS" ]; then
                    DKBUILD_ARGS["$name"]="$name=$value"
                    DKBUILD_CMD_ARGS+=(--arg "$name=$value")
                fi
            fi
        done
    }
    function resetarg() {
        local name value
        for name in "$@"; do
            if [[ "$name" == *=* ]]; then
                value="${name#*=}"
                name="${name%%=*}"
            else
                value="${_ENVIRON[$name]-__UNDEFINED__}"
                [ "$value" == __UNDEFINED__ ] && die "la variable d'environnement $name doit être définie"
            fi
            ARGS["$name"]="$value"
        done
    }
    declare -g -A DEFAULTS
    function default() {
        local command="$1"; shift
        local name value
        for name in "$@"; do
            if [[ "$name" == *=* ]]; then
                value="${name#*=}"
                name="${name%%=*}"
            else
                value=1
            fi
            name="${command}_$name"
            if [ "${DEFAULTS[$name]-__UNDEFINED__}" == __UNDEFINED__ ]; then
                # Ne spécifier la valeur que si elle n'a pas déjà été définie
                DEFAULTS["$name"]="$value"
            fi
        done
    }
    function resetdefault() {
        local command="$1"; shift
        local name value
        for name in "$@"; do
            if [[ "$name" == *=* ]]; then
                value="${name#*=}"
                name="${name%%=*}"
            else
                value=1
            fi
            name="${command}_$name"
            DEFAULTS["$name"]="$value"
        done
    }
}

function parse_dist() {
    local dist="$1" version
    if [[ "$dist" == *-* ]]; then
        version="${dist%-*}"
        dist="${dist##*-}"
    fi
    local "${2:-dist}"; upvar "${2:-dist}" "$dist"
    local "${3:-version}"; upvar "${3:-version}" "$version"
}

function parse_profile() {
    local profile="$1" version
    if [[ "$profile" == *-* ]]; then
        version="${profile%-*}"
        profile="${profile##*-}"
    fi
    local "${2:-profile}"; upvar "${2:-profile}" "$profile"
    local "${3:-version}"; upvar "${3:-version}" "$version"
}

function resolve_dists_profiles() {
    ## construire d'abord la liste des distributions et profils
    edebug "Calcul de la liste des distributions et des profils"
    reset_functions
    SETDISTS_DONE=
    SETDISTS=()
    SETPROFILES_DONE=
    SETPROFILES=()
    DEFAULT_PROFILE=
    SETVERSION_DONE=
    SETVERSION=
    AUTOBUILD=1

    function setdists() {
        [ -n "$SETDISTS_DONE" ] && return
        SETDISTS=("$@")
        SETDISTS_DONE=1
    }
    function setprofiles() {
        [ -n "$SETPROFILES_DONE" ] && return
        SETPROFILES=("$@")
        SETPROFILES_DONE=1
    }
    function default_profile() {
        [ -n "$DEFAULT_PROFILE" ] || DEFAULT_PROFILE="$1"
    }
    function build() {
        AUTOBUILD=
    }
    function cbuild() {
        AUTOBUILD=
    }
    load_dkbuild

    if [ -z "$PROFILE" -a -z "$ALL_PROFILES" -a -n "$DEFAULT_PROFILE" ]; then
        enote "Auto-sélection du profil $DEFAULT_PROFILE"
        PROFILE="$DEFAULT_PROFILE"
    fi
    local MANUAL_SETPROFILES=
    if [ -z "$SETPROFILES_DONE" -a -n "$PROFILE" ]; then
        # Si l'utilisateur spécifie un profil mais qu'aucun profil n'a été
        # défini dans la configuration, considérer que c'est cet unique profil
        # qui a été défini
        SETPROFILES=("$PROFILE")
        SETPROFILES_DONE=1
        MANUAL_SETPROFILES=1
    fi

    ## ensuite vérifier si on est dans la bonne distribution
    edebug "Calcul de la distribution courante"
    reset_functions
    DISTS=()
    function setdists() {
        local dist version found
        # construire la liste des distributions à considérer
        if [ -n "$DIST" ]; then
            # on a spécifié une distribution en argument
            for dist in "${SETDISTS[@]}"; do
                if [ "$dist" == "$DIST" ]; then
                    # matcher avec la version éventuellement
                    found=1
                    break
                fi
                parse_dist "$dist"
                if [ "$dist" == "$DIST" ]; then
                    # ou matcher uniquement la distribution
                    found=1
                    break
                fi
            done
            # si aucune distribution ne correspond, arrêter le script
            [ -n "$found" ] || exit 0
            # forcer à ne construire que cette distribution
            DISTS=("$DIST")
        else
            DISTS=("${SETDISTS[@]}")
        fi
    }
    load_dkbuild

    ## puis vérifier si on est dans le bon profil
    edebug "Calcul du profil courant"
    reset_functions
    PROFILES=()
    function setprofiles() {
        local profile version found
        # construire la liste des distributions à considérer
        if [ -n "$PROFILE" ]; then
            # on a spécifié une distribution en argument
            for profile in "${SETPROFILES[@]}"; do
                if [ "$profile" == "$PROFILE" ]; then
                    # matcher avec la version éventuellement
                    found=1
                    break
                fi
                parse_profile "$profile"
                if [ "$profile" == "$PROFILE" ]; then
                    # ou matcher uniquement le profil
                    found=1
                    break
                fi
            done
            # si aucune distribution ne correspond, arrêter le script
            [ -n "$found" ] || die "$PROFILE: profil invalide"
            # forcer à ne construire que cette distribution
            PROFILES=("$PROFILE")
        elif [ -n "$ALL_PROFILES" ]; then
            # prendre tous les profils comme indiqué
            for profile in "${SETPROFILES[@]}"; do
                parse_profile "$profile"
                PROFILES+=("$profile")
            done
        else
            # prendre le profil par défaut
            parse_profile "${SETPROFILES[0]}"
            PROFILES=("$profile")
        fi
    }
    load_dkbuild

    if [ -n "$MANUAL_SETPROFILES" ]; then
        setprofiles "$PROFILE"
    fi

    ## Si pas de distribution ou de profil, remplacer par valeur vide
    if [ ${#DISTS[*]} -eq 0 ]; then
        SETDISTS=("")
        DISTS=("")
    fi
    if [ ${#PROFILES[*]} -eq 0 ]; then
        SETPROFILES=("")
        PROFILES=("")
    fi

    ## puis calculer la version par défaut
    edebug "Calcul de la version par défaut"
    reset_functions
    define_functions_env

    fill_DKBUILD_ENVS=1 setenv "${TMPENVIRON[@]}"
    fill_DKBUILD_ARGS=1 setarg "${TMPARGS[@]}"
    setarg "$@"
    fill_DKBUILD_CONFIGS=1 load_dkbuild
}

function foreach_dists_profiles() {
    local each="$1" before="$2" after="$3"

    local version dist dversion profile pversion
    local VERSION DIST DVERSION PROFILE PVERSION
    local HAVE_VERSION LAST_VERSION
    declare -A dones

    if [ -n "$before" ]; then
        "$before"
    fi
    for dist in "${DISTS[@]}"; do
        parse_dist "$dist" dist dversion
        # y a-t-il une version dans cette distribution ou ce profil, et si oui, laquelle?
        HAVE_VERSION=
        LAST_VERSION=
        for DIST in "${SETDISTS[@]}"; do
            parse_dist "$DIST" DIST DVERSION
            [ "$DIST" == "$dist" ] || continue
            VERSION="$DVERSION"
            for profile in "${PROFILES[@]}"; do
                parse_profile "$profile" profile pversion
                for PROFILE in "${SETPROFILES[@]}"; do
                    parse_profile "$PROFILE" PROFILE PVERSION
                    [ "$PROFILE" == "$profile" ] || continue
                    [ -n "$DVERSION" ] && PVERSION= || VERSION="$PVERSION"
                    [ -z "$VERSION" -a -n "$SETVERSION" ] && VERSION="$SETVERSION"
                    if [ -n "$VERSION" ]; then
                        HAVE_VERSION=1
                        LAST_VERSION="$VERSION"
                    fi
                done
            done
        done

        for DIST in "${SETDISTS[@]}"; do
            parse_dist "$DIST" DIST DVERSION
            [ "$DIST" == "$dist" ] || continue
            [ -z "$dversion" -o "$DVERSION" == "$dversion" ] || continue
            VERSION="$DVERSION"

            for profile in "${PROFILES[@]}"; do
                parse_profile "$profile" profile pversion
                for PROFILE in "${SETPROFILES[@]}"; do
                    parse_profile "$PROFILE" PROFILE PVERSION
                    [ "$PROFILE" == "$profile" ] || continue
                    if [ -z "$DVERSION" ]; then
                        [ -z "$pversion" -o "$PVERSION" == "$pversion" ] || continue
                        VERSION="$PVERSION"
                    else
                        PVERSION=
                    fi

                    [ -n "${dones[$PVERSION-$PROFILE-$DVERSION-$DIST]}" ] && continue
                    dones["$PVERSION-$PROFILE-$DVERSION-$DIST"]=1

                    [ -z "$VERSION" -a -n "$SETVERSION" ] && VERSION="$SETVERSION"

                    "$each"
                done
            done
        done
    done
    if [ -n "$after" ]; then
        "$after"
    fi
}

function define_functions_cmd() {
    _IN_SECTION=
    function section() {
        [ -n "$_IN_SECTION" ] && eend
        etitle "$*"
        _IN_SECTION=1
    }
    function note() {
        enote "$*"
    }
    function info() {
        estep "$*"
    }
    function debug() {
        edebug "$*"
    }
    function checkout() {
        edebug "checkout $(qvals "$@")"

        local url destdir
        [[ "$1" != *=* ]] && { url="$1"; shift; }
        [[ "$1" != *=* ]] && { destdir="$1"; shift; }

        local checkout="${DEFAULTS[checkout_checkout]-1}"
        local origin="${DEFAULTS[checkout_origin]}"
        local branch="${DEFAULTS[checkout_branch]}"
        local develdir="${DEFAULTS[checkout_develdir]}"
        local develtype="${DEFAULTS[checkout_develtype]}"
        while [ $# -gt 0 ]; do
            case "$1" in
            checkout) checkout=1;;
            checkout=*) checkout="${1#checkout=}";;
            origin=*) origin="${1#origin=}";;
            branch=*) branch="${1#branch=}";;
            develdir=*) develdir="${1#develdir=}";;
            develtype=*) develtype="${1#develtype=}";;
            *) ewarn "checkout: $1: argument ignoré";;
            esac
            shift
        done

        [ -n "$checkout" ] || return
        [ -n "$url" -a -n "$destdir" ] || die "checkout: Vous devez spécifier l'url du dépôt et la destination"
        [ -n "$origin" ] || origin=origin
        if [ -z "$branch" ]; then
            case "$PROFILE" in
            test|devel) branch=develop;;
            *) branch=master;;
            esac
        fi

        if [ "$checkout" == devel ]; then
            # synchronisation depuis le répertoire de développement
            [ -n "$develdir" ] || die "checkout: vous devez spécifier le répertoire de développement"
            [ -d "$develdir" ] || die "checkout: répertoire de développement introuvable"

            die "Pas encore implémenté" #XXX

        elif [ -d "$destdir" -a -d "$destdir/.git" ]; then
            # maj du dépôt
            local cwd="$(pwd)"

            estep "checkout: maj du dépôt $url --> $destdir (origin=$origin, branch=$branch)"
            cd "$destdir"
            git fetch --all -p -f || die
            if [ "${branch#^}" != "$branch" ]; then
                git reset --hard "${branch#^}" || die
            else
                git reset --hard "$origin/$branch" || die
            fi
            cd "$cwd"

        else
            # reliquat checkout=devel?
            [ -d "$destdir" ] && rm -rf "$destdir"

            # clone
            estep "checkout: clone du dépôt $url --> $destdir (origin=$origin, branch=$branch)"
            if [ "${BRANCH#^}" != "$BRANCH" ]; then
                local cwd="$(pwd)"
                git clone -o "$origin" "$url" "$destdir" || die
                cd "$destdir"
                git reset --hard "${branch#^}" || die
                cd "$cwd"
            else
                git clone -o "$origin" -b "$branch" "$url" "$destdir" || die
            fi
        fi
    }
    function copy() {
        edebug "copy $(qvals "$@")"

        local src dest
        [[ "$1" != *=* ]] && { src="$1"; shift; }
        [[ "$1" != *=* ]] && { dest="$1"; shift; }

        local copy="${DEFAULTS[copy_copy]-1}"
        local overwrite="${DEFAULTS[copy_overwrite]}"
        local gitignore="${DEFAULTS[copy_gitignore]}"
        while [ $# -gt 0 ]; do
            case "$1" in
            copy) copy=1;;
            copy=*) copy="${1#copy=}";;
            overwrite) overwrite=1;;
            overwrite=*) overwrite="${1#overwrite=}";;
            gitignore=*) gitignore="${1#gitignore=}";;
            *) ewarn "copy: $1: argument ignoré";;
            esac
            shift
        done

        [ -n "$copy" ] || return
        [ -n "$src" -a -n "$dest" ] || die "copy: Vous devez spécifier la source et la destination de la copie"
        [ -e "$src" ] || {
            ewarn "copy: $src: fichier ou répertoire introuvables"
            return 1
        }

        local srcdir destdir
        if [ "${src%/}" != "$src" ]; then
            [ -d "$src" ] || die "copy: $src: doit être un répertoire"
            setx srcdir=abspath "$src"
            src=
        elif [ -d "$src" ]; then
            setx srcdir=abspath "$src"
            src=
        else
            setx src=abspath "$src"
        fi
        if [ "${dest%/}" != "$dest" ]; then
            [ -f "$dest" ] && die "copy: $dest: doit être un répertoire"
            setx destdir=abspath "$dest"
            dest=
        elif [ -d "$dest" ]; then
            setx destdir=abspath "$dest"
            dest=
        elif [ -f "$dest" ]; then
            [ -n "$srcdir" ] && die "copy: $dest: doit être un répertoire"
            setx dest=abspath "$dest"
        elif [ -n "$srcdir" ]; then
            setx destdir=abspath "$dest"
            dest=
        else
            setx dest=abspath "$dest"
        fi

        local -a srcs dests
        if [ -n "$srcdir" -a -n "$destdir" ]; then
            # copie de répertoire à répertoire
            local destpath="$(relpath "$destdir" "$PROJDIR")"
            [ -n "$destpath" ] || destpath=.
            estep "copy $(relpath "$srcdir" "$PROJDIR")/ --> $destpath/"

            array_from_lines srcs "$(find "$srcdir/" -type f -o -type l)"
            for src in "${srcs[@]}"; do
                dest="$destdir/${src#$srcdir/}"
                if [ -n "$overwrite" -o ! -f "$dest" ]; then
                    mkdirof "$dest" || die
                    cp -dfl "$src" "$dest" || die
                    dests+=("$dest")
                fi
            done

        elif [ -n "$src" ]; then
            # transformer copie de fichier à répertoire en copie de fichier à fichier
            [ -n "$dest" ] || dest="$destdir/$(basename "$src")"

            if [ -n "$overwrite" -o ! -f "$dest" ]; then
                # copie de fichier à fichier
                estep "copy $(relpath "$src" "$PROJDIR") --> $(relpath "$dest" "$PROJDIR")"
                mkdirof "$dest" || die
                cp -fl "$src" "$dest" || die

                setx destdir=dirname "$dest"
                dests+=("$dest")
            fi

        else
            # en réalité, on ne devrait jamais arriver ici
            die "copy: impossible de copier un répertoire dans un fichier"
        fi

        if [ -n "$gitignore" ]; then
            setx gitignore=abspath "$gitignore"
            [ -d "$gitignore" ] && gitignore="$gitignore/.gitignore"

            local basedir
            setx basedir=dirname "$gitignore"
            if [ "${destdir#$basedir/}" == "$destdir" -a "$destdir" != "$basedir" ]; then
                ewarn "copy: gitignore ignoré parce que le répertoire n'est pas un parent de destdir"
            else
                [ -f "$gitignore" ] || { mkdir -p "$basedir"; touch "$gitignore"; }

                declare -A ignored_dirs
                local ignored_dir
                for dest in "${dests[@]}"; do
                    dest="/${dest#$basedir/}"
                    if grep -q "^$dest\$" "$gitignore"; then
                        ignored=1
                    else
                        ignored=
                        setx ignored_dir=dirname "$dest"
                        while [ "$ignored_dir" != / ]; do
                            ignored="${ignored_dirs[$ignored_dir/]-compute}"
                            if [ "$ignored"  == compute ]; then
                                grep -q "^$ignored_dir/\$" "$gitignore" && ignored=1 || ignored=
                                ignored_dirs["$ignored_dir/"]="$ignored"
                            fi
                            if [ -n "$ignored" ]; then
                                # un répertoire parent est déjà ignoré, on peut
                                # passer au fichier suivant
                                break
                            fi
                            setx ignored_dir=dirname "$ignored_dir"
                        done
                    fi

                    if [ -z "$ignored" ]; then
                        # le fichier n'est pas ignoré, ni directement, ni via un
                        # répertoire parent. il faut donc l'ajouter à .gitignore
                        echo "$dest" >>"$gitignore"
                    fi
                done
            fi
        fi
    }
    function genfile() {
        edebug "genfile $(qvals "$@")"

        local output input
        [[ "$1" != *=* ]] && { output="$1"; shift; }
        [[ "$1" != *=* ]] && { input="$1"; shift; }

        local context="${DEFAULTS[genfile_context]}"
        local sed="${DEFAULTS[genfile_sed]}"
        while [ $# -gt 0 ]; do
            case "$1" in
            context=*) context="${1#context=}";;
            sed=*) sed="${1#sed=}";;
            *) ewarn "genfile: $1: argument ignoré";;
            esac
            shift
        done

        [ -n "${DEFAULTS[build_build]-1}" ] || return 0
        [ -n "$output" ] || die "genfile: Vous devez spécifier le fichier en sortie"
        if [ -n "$context" ]; then
            mkdir -p "$context" || die
            output="$context/$output"
        fi

        if [ -n "$input" ]; then
            cat "$input" >"$output" || die
        elif ! tty -s; then
            cat >"$output"
        else
            die "genfile: Vous devez spécifier une source pour le fichier $output"
        fi
        if [ -n "$sed" ]; then
            sed -i "$sed" "$output"
        fi
    }
    function dockerfile() {
        edebug "dockerfile $(qvals "$@")"

        local input
        [[ "$1" != *=* ]] && { DOCKERFILE="$1"; shift; }
        [[ "$1" != *=* ]] && { input="$1"; shift; }

        local context="${DEFAULTS[dockerfile_context]}"
        local sed="${DEFAULTS[dockerfile_sed]}"
        while [ $# -gt 0 ]; do
            case "$1" in
            context=*) context="${1#context=}";;
            sed=*) sed="${1#sed=}";;
            *) ewarn "dockerfile: $1: argument ignoré";;
            esac
            shift
        done

        [ -n "${DEFAULTS[build_build]-1}" ] || return 0
        [ -n "$DOCKERFILE" ] || DOCKERFILE=Dockerfile
        DOCKERCONTEXT=.
        if [ -n "$context" ]; then
            mkdir -p "$context" || die
            DOCKERCONTEXT="$context"
            DOCKERFILE="$context/$DOCKERFILE"
        fi

        setx DOCKERFILE=abspath "$DOCKERFILE"
        if [ -n "$input" ]; then
            cat "$input" >"$DOCKERFILE" || die
        elif ! tty -s; then
            cat >"$DOCKERFILE" || die
        else
            echo "# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8" >"$DOCKERFILE"
        fi
        if [ -n "$sed" ]; then
            sed -i "$sed" "$DOCKERFILE"
        fi
    }
    function add2dockerfile() {
        edebug "$(qvals "$@")"

        [ -n "${DEFAULTS[build_build]-1}" ] || return 0
        [ -n "$DOCKERFILE" ] || return
        echo "$*" >>"$DOCKERFILE"
    }
    function FROM() { add2dockerfile FROM "$@"; }
    function RUN() { add2dockerfile RUN "$@"; }
    function CMD() { add2dockerfile CMD "$@"; }
    function LABEL() { add2dockerfile LABEL "$@"; }
    function MAINTAINER() { add2dockerfile MAINTAINER "$@"; }
    function EXPOSE() { add2dockerfile EXPOSE "$@"; }
    function ENV() { add2dockerfile ENV "$@"; }
    function ADD() { add2dockerfile ADD "$@"; }
    function COPY() { add2dockerfile COPY "$@"; }
    function ENTRYPOINT() { add2dockerfile ENTRYPOINT "$@"; }
    function VOLUME() { add2dockerfile VOLUME "$@"; }
    function USER() { add2dockerfile USER "$@"; }
    function WORKDIR() { add2dockerfile WORKDIR "$@"; }
    function ARG() { add2dockerfile ARG "$@"; }
    function ONBUILD() { add2dockerfile ONBUILD "$@"; }
    function STOPSIGNAL() { add2dockerfile STOPSIGNAL "$@"; }
    function HEALTHCHECK() { add2dockerfile HEALTHCHECK "$@"; }
    function SHELL() { add2dockerfile SHELL "$@"; }
    function build() {
        edebug "build $(qvals "$@")"

        local build="${DEFAULTS[build_build]-1}"
        local no_cache="${DEFAULTS[build_no-cache]}"
        local pull="${DEFAULTS[build_pull]}"
        local host_mappings="${DEFAULTS[build_host-mappings]-__UNDEFINED__}"
        [ "$host_mappings" == __UNDEFINED__ ] && host_mappings="${DEFAULTS[docker_host-mappings]}"
        local set_tags="${DEFAULTS[build_set-tags]}"
        local add_tags="${DEFAULTS[build_add-tags]}"
        local images="${DEFAULTS[build_images]}"
        local push="${DEFAULTS[build_push]}"
        while [ $# -gt 0 ]; do
            case "$1" in
            build) build=1;;
            build=*) build="${1#build=}";;
            context=*) DOCKERCONTEXT="${1#context=}";;
            dockerfile=*) DOCKERFILE="${1#dockerfile=}";;
            no-cache) no_cache=1;;
            no-cache=*) no_cache="${1#no-cache=}";;
            pull) pull=1;;
            pull=*) pull="${1#pull=}";;
            host-mappings=*) host_mappings="${1#host-mappings=}";;
            set-tag=*) set_tags="${1#set-tag=}";;
            set-tags=*) set_tags="${1#set-tags=}";;
            add-tag=*) add_tags="${1#add-tag=}";;
            add-tags=*) add_tags="${1#add-tags=}";;
            image=*) images="${1#image=}";;
            images=*) images="${1#images=}";;
            push) push=1;;
            push=*) push="${1#push=}";;
            *) ewarn "build: $1: argument ignoré";;
            esac
            shift
        done

        estep "build options:" ${build:+build} ${no_cache:+no-cache} ${pull:+pull} ${push:+push}

        [ -n "$images" ] || images="$IMAGE"
        eval "set_tags=($set_tags)"
        eval "add_tags=($add_tags)"
        eval "images=($images)"
        local tag imagetag autotag=1
        local -a imagetags
        if [ ${#set_tags[*]} -gt 0 ]; then
            autotag=
            for imagetag in "${images[@]}"; do
                if [[ "$imagetag" == *:* ]]; then
                    # le tag est déjà spécifié
                    imagetags+=("$imagetag")
                else
                    for tag in "${set_tags[@]}" "${add_tags[@]}"; do
                        if [ "$tag" == LATEST ]; then
                            if [ -n "$HAVE_VERSION" -a "$VERSION" == "$LAST_VERSION" ]; then
                                tag=latest
                            else
                                # ignorer le tag LATEST s'il n'est pas applicable
                                continue
                            fi
                        fi
                        imagetags+=("$imagetag:$tag")
                    done
                fi
            done
        else
            for imagetag in "${images[@]}"; do
                if [[ "$imagetag" == *:* ]]; then
                    # le tag est déjà spécifié
                    autotag=
                    imagetags+=("$imagetag")
                else
                    for tag in "${add_tags[@]}"; do
                        if [ "$tag" == LATEST ]; then
                            # toujours ignorer le tag LATEST dans add_tags
                            continue
                        fi
                        imagetags+=("$imagetag:$tag")
                    done
                    [ -n "$VERSION" ] && imagetags+=("$imagetag:$VERSION-$DIST")
                    if [ -n "$DIST" -a "$DIST" != none -a -z "$HAVE_VERSION" ]; then
                        imagetags+=("$imagetag:$DIST")
                    fi
                fi
            done
        fi
        if [ -n "$autotag" ]; then
            if [ -n "$DIST" ]; then
                if [ -z "$HAVE_VERSION" ]; then
                    dist LATEST && imagetags+=("$imagetag:latest")
                elif [ "$VERSION" == "$LAST_VERSION" ]; then
                    [ "$DIST" != none ] && imagetags+=("$imagetag:$DIST")
                    dist LATEST && imagetags+=("$imagetag:latest")
                fi
            elif [ -n "$PROFILE" ]; then
                profile DEFAULT && imagetags+=("$imagetag:latest")
            else
                imagetags+=("$imagetag:latest")
            fi
        fi

        local avar
        local -a args; args=(
            ${no_cache:+--no-cache}
            ${progress:+--progress "$progress"}
            ${pull:+--pull}
        )
        eval "host_mappings=($host_mappings)"
        for host_mapping in "${host_mappings[@]}"; do
            args+=(--add-host "$host_mapping")
        done
        for avar in "${!ARGS[@]}"; do
            args+=(--build-arg "$avar=${ARGS[$avar]}")
        done
        for imagetag in "${imagetags[@]}"; do
            args+=(-t "$imagetag")
            estep "tag $imagetag"
        done

        [ -n "$DOCKERCONTEXT" ] || DOCKERCONTEXT="${DEFAULTS[build_context]:-.}"
        [ -n "$DOCKERFILE" ] || DOCKERFILE="${DEFAULTS[build_dockerfile]:-Dockerfile}"
        if [ -n "$build" ]; then
            etitle build
            _runcmd docker build "${args[@]}" -f "$DOCKERFILE" "$DOCKERCONTEXT" || die
            eend
        fi

        if [ -n "$push" ]; then
            etitle push
            for imagetag in "${imagetags[@]}"; do
                _runcmd docker push "$imagetag" || die
            done
            eend
        fi

        DOCKERCONTEXT=
        DOCKERFILE=
        [ -n "$build" -o -n "$push" ]
    }
    function cbuild() {
        edebug "cbuild $(qvals "$@")"

        local build="${DEFAULTS[build_build]-1}"
        local files="${DEFAULTS[cbuild_files]}"
        local project_name="${DEFAULTS[cbuild_project-name]}"
        local no_cache="${DEFAULTS[cbuild_no-cache]}"
        local pull="${DEFAULTS[cbuild_pull]}"
        while [ $# -gt 0 ]; do
            case "$1" in
            files=*) files="${1#files=}";;
            project-name=*) project_name="${1#project-name=}";;
            no-cache) no_cache=1;;
            no-cache=*) no_cache="${1#no-cache=}";;
            pull) pull=1;;
            pull=*) pull="${1#pull=}";;
            *=*) ewarn "cbuild: $1: argument ignoré";;
            *) break;;
            esac
            shift
        done

        [ -n "$build" ] || return
        if [ -n "$files" ]; then
            eval "files=($files)"
        else
            files=(docker-compose.yml)
            if [ -f "docker-compose.override.yml" ]; then
                files+=("docker-compose.override.yml")
            fi
            if [ -n "$PROFILE" -a -f "docker-compose.$PROFILE.yml" ]; then
                files+=("docker-compose.$PROFILE.yml")
            fi
        fi
        [ -n "$project_name" ] || setx project_name=get_project_name

        estep "cbuild files:" ${files[@]}
        estep "cbuild options:" project_name="$project_name" ${no_cache:+no-cache} ${pull:+pull}

        local file; local -a args
        args=(
            -p "$project_name"
        )
        for file in "${files[@]}"; do
            args+=(-f "$file")
        done
        local avar evar; local -a bargs
        bargs=(
            ${no_cache:+--no-cache}
            ${progress:+--progress "$progress"}
            ${pull:+--pull}
        )
        for avar in "${!ARGS[@]}"; do
            bargs+=(--build-arg "$avar=${ARGS[$avar]}")
        done

        if [ -f .shared_env -o -f ".${MACHINE}_env" -o ${#ENVIRON[*]} -gt 0 ]; then
            echo >.env "## fichier auto-généré. ne pas modifier ##"
        fi
        [ -f .shared_env ] && cat .shared_env >>.env
        [ -f ".${MACHINE}_env" ] && cat ".${MACHINE}_env" >>.env
        for evar in "${!ENVIRON[@]}"; do
            echo_setv "$evar=${ENVIRON[$evar]}" >>.env
        done

        _runcmd "${DOCKER_COMPOSE[@]}" "${args[@]}" build "${bargs[@]}" "$@" || die
    }
    function _local_composer() {
        case "$action" in
        rootshell|rshell|rootbash|rbash)
            shift
            estep "Lancement d'un shell root"
            sudo bash "$@"
            return $?
            ;;
        usershell|shell|userbash|bash)
            shift
            estep "Lancement d'un shell utilisateur"
            bash "$@"
            return $?
            ;;
        *)
            estep "composer $action"
            if [ -n "$composer" ]; then :
            elif [ -x composer.phar ]; then composer=./composer.phar
            elif [ -x /usr/bin/composer ]; then composer=/usr/bin/composer
            else die "Impossible de trouver composer"
            fi
            "$composer" "$action" $args "$@"
        esac
    }
    function _docker_composer() {
        local user group projdir actualcmd
        setx user=id -un; setx user=getent passwd "$user"
        setx group=id -gn; setx group=getent group "$group"
        setx projdir=pwd
        case "$action" in
        rootshell|rshell|rootbash|rbash)
            action=rshell
            shift
            actualcmd='eval "bash $args"'
            ;;
        usershell|shell|userbash|bash)
            action=shell
            shift
            actualcmd='eval "su-exec \"$user\" bash $args"'
            ;;
        *)
            actualcmd='eval "su-exec \"$user\" \"$composer\" $args"'
            args="$action${args:+ $args}"
            ;;
        esac
        setx args=qvals $args "$@"

        local -a basecmd setupscript runscript cmd
        basecmd=(
            -e user="$user"
            -e group="$group"
            -e projdir="$projdir"
            -e setup="$setup"
            -e setup_image="$setup_image"
            -e composer="$composer"
            -e args="$args"
        )
        eval "host_mappings=($host_mappings)"
        for host_mapping in "${host_mappings[@]}"; do
            basecmd+=(--add-host "$host_mapping")
        done
        basecmd+=(-v "$HOME:$HOME")
        if [ "${projdir#$HOME/}" == "$projdir" ]; then
            # si le répertoire de projet ne se trouve pas dans $HOME, le monter aussi
            cmd+=(-v "$projdir:$projdir")
        fi
        setupscript='eval "$setup"'
        runscript='
echo "$user" >>/etc/passwd; user="${user%%:*}"
echo "$group" >>/etc/group; group="${group%%:*}"

cd "$projdir"
if [ -n "$composer" ]; then :
elif [ -x composer.phar ]; then composer=./composer.phar
elif [ -x /usr/bin/composer ]; then composer=/usr/bin/composer
else
    echo "ERROR: Impossible de trouver composer"
    exit 1
fi
'"$actualcmd"

        if [ -n "$setup" ]; then
            local project_name container_name ctid
            local setup_image="$setup_image"

            # lancement dans un container docker à préparer
            [ -n "$project_name" ] || setx project_name=get_project_name
            setx container_name=get_container_name "$project_name"
            [ -n "$setup_image" ] || setup_image="dkbuild_composer_${container_name}_image"

            # vérifier l'existence de l'image
            setx ctid=docker image ls --format '{{.ID}}' "$setup_image"

            # créer le container le cas échéant
            if [ -z "$ctid" ]; then
                estep "Création de l'image $setup_image à partir de $image"
                cmd=(
                    docker create -it --name "${setup_image}_tmpct"
                    "${basecmd[@]}"
                    "$image"
                    bash -c "$setupscript"
                )
                setx ctid="${cmd[@]}" &&
                docker container start -ai "$ctid" &&
                docker container commit "$ctid" "$setup_image" &&
                docker container rm "$ctid" || die
            fi

            # prendre comme image le container créé
            image="$setup_image"
        fi

        case "$action" in
        rshell) estep "Lancement d'un shell root (avec l'image $image)";;
        shell) estep "Lancement d'un shell utilisateur (avec l'image $image)";;
        *) estep "composer $action (avec l'image $image)";;
        esac
        cmd=(
            docker run -it --rm
            "${basecmd[@]}"
            "$image"
            bash -c "$runscript"
        )
        "${cmd[@]}"
    }
    function composer() {
        edebug "composer $(qvals "$@")"
        [ $# -eq 0 ] && return 0

        local action destdir
        [[ "$1" != *=* ]] && { destdir="$1"; shift; }
        [[ "$1" != *=* ]] && { action="$1"; shift; }

        [ -n "$destdir" ] || destdir=.
        [ -d "$destdir" ] || die "composer: $destdir: répertoire introuvable"
        local cwd="$(pwd)"
        cd "$destdir" || die

        [ -n "$action" ] || action=install
        if [ "$action" == none ]; then
            cd "$cwd"
            return
        fi

        local build="${DEFAULTS[build_build]-1}"
        local args
        case "$PROFILE" in
        prod|test) args="${DEFAULTS[composer_args]---no-dev -o}";;
        *) args="${DEFAULTS[composer_args]}";;
        esac
        local php="${DEFAULTS[composer_php]}"
        local php_max="${DEFAULTS[composer_php-max]}"
        local image="${DEFAULTS[composer_image]}"
        local machine="${DEFAULTS[composer_machine]}"
        local host_mappings="${DEFAULTS[composer_host-mappings]-__UNDEFINED__}"
        [ "$host_mappings" == __UNDEFINED__ ] && host_mappings="${DEFAULTS[docker_host-mappings]}"
        local composer="${DEFAULTS[composer_composer]}"
        local setup="${DEFAULTS[composer_setup]}"
        local setup_image="${DEFAULTS[composer_setup-image]}"
        local project_name="${DEFAULTS[composer_project-name]}"
        if [ -f "$destdir/.composer.yaml" ]; then
            eval "$(
                COMPOSER_PHP=
                COMPOSER_PHP_MAX=
                COMPOSER_IMAGE="$COMPOSER_IMAGE"
                COMPOSER_SETUP=
                COMPOSER_SETUP_IMAGE=
                eval "$(<"$destdir/.composer.yaml" grep ^composer_ |
                    sed 's/^composer_php_min: /COMPOSER_PHP=/
                    s/^composer_php_max: /COMPOSER_PHP_MAX=/
                    s/^composer_registry: /registry=/
                    s/^composer_image: \(.*\)/COMPOSER_IMAGE="${registry:-$REGISTRY}\/\1"/
                    s/^composer_setup: /COMPOSER_SETUP=/
                    s/^composer_setup_image: /COMPOSER_SETUP_IMAGE=/')"
                [ -z "$php" ] && echo_setv php="$COMPOSER_PHP"
                [ -z "$php_max" ] && echo_setv php_max="$COMPOSER_PHP_MAX"
                [ -z "$image" ] && echo_setv image="$COMPOSER_IMAGE"
                [ -z "$setup" ] && echo_setv setup="$COMPOSER_SETUP"
                [ -z "$setup_image" ] && echo_setv setup_image="$COMPOSER_SETUP_IMAGE"
            )"
        elif [ -f "$destdir/.composer.conf" ]; then
            eval "$(
                COMPOSER_PHP=
                COMPOSER_PHP_MAX=
                COMPOSER_IMAGE="$COMPOSER_IMAGE"
                COMPOSER_MACHINE=-u
                COMPOSER_CMD=
                COMPOSER_SETUP=
                COMPOSER_SETUP_IMAGE=
                source "$destdir/.composer.conf"
                [ -z "$php" ] && echo_setv php="$COMPOSER_PHP"
                [ -z "$php_max" ] && echo_setv php_max="$COMPOSER_PHP_MAX"
                [ -z "$image" ] && echo_setv image="$COMPOSER_IMAGE"
                [ -z "$machine" ] && echo_setv machine="$COMPOSER_MACHINE"
                [ -z "$composer" ] && echo_setv composer="$COMPOSER_CMD"
                [ -z "$setup" ] && echo_setv setup="$COMPOSER_SETUP"
                [ -z "$setup_image" ] && echo_setv setup_image="$COMPOSER_SETUP_IMAGE"
            )"
        fi

        while [ $# -gt 0 ]; do
            case "$1" in
            build) build=1;;
            build=*) build="${1#build=}";;
            args=*) args="${1#args=}";;
            php=*) php="${1#php=}";;
            php-max=*) php_max="${1#php-max=}";;
            image=*) image="${1#image=}";;
            machine=*) machine="${1#machine=}";;
            host-mappings=*) host_mappings="${1#host-mappings=}";;
            composer=*) composer="${1#composer=}";;
            setup=*) setup="${1#setup=}";;
            setup-image=*) setup_image="${1#setup-image=}";;
            project-name=*) project_name="${1#project-name=}";;
            *=*) ewarn "composer: $1: argument ignoré";;
            *) break;;
            esac
            shift
        done

        if [ -z "$build" ]; then
            cd "$cwd"
            return
        fi

        if [ "$php" != force -a "$php" != any ]; then
            # Si php n'est pas disponible dans le PATH, forcer l'utilisation de
            # l'image
            progexists php || php=force
        fi

        local use_image
        if [ "$php" == force -o "$php" == any ]; then
            use_image=1
        elif [ "$php" == none -o "$php" == system ]; then
            php=none
            use_image=
        elif [ -n "$php_max" -a "$php_max" != none ]; then
            # Vérifier la version de PHP
            php -r '
$version = $argv[1];
if (strpos($version, ".") !== false) {
  $version = explode(".", $version);
  $version = $version[0] * 10000 + $version[1] * 100 + (isset($version[2])? $version[2]: 0);
}
exit((PHP_VERSION_ID > $version)? 0: 1);
' -- "$php_max"
            case $? in
            0) use_image=1;;
            1) use_image=;;
            *) ewarn "Erreur lors du lancement de PHP: est-il installé? Sinon, utilisez php=any";;
            esac
        fi
        if [ -n "$use_image" -o "$php" == none ]; then
            : # ok, on a déjà décidé
        elif [ -z "$php" ]; then
            # pas de version minimum, tester simplement la valeur de image
            [ "$image" != none ] && use_image=1
        else
            # Vérifier la version de PHP
            php -r '
$version = $argv[1];
if (strpos($version, ".") !== false) {
  $version = explode(".", $version);
  $version = $version[0] * 10000 + $version[1] * 100 + (isset($version[2])? $version[2]: 0);
}
exit((PHP_VERSION_ID < $version)? 0: 1);
' -- "$php"
            case $? in
            0) use_image=1;;
            1) use_image=;;
            *) ewarn "Erreur lors du lancement de PHP: est-il installé? Sinon, utilisez php=any";;
            esac
        fi

        if [ -n "$use_image" ]; then
            if [ -z "$image" ]; then
                # Si l'image n'est pas définie, calculer une valeur par défaut à
                # partir REGISTRY et DIST
                image="$(get_default_phpbuilder_image)"
            fi
            local orig_machine
            [ "$image" != none ] || die "Vous devez spécifier l'image à utiliser pour composer"
            if [ -n "$machine" -a "$machine" != current -a "$DOCKER_MACHINE_NAME" != "$machine" ]; then
                orig_machine="$DOCKER_MACHINE_NAME"
                set_machine "$machine"
            fi
            _docker_composer "$@"
            if [ -n "$_orig_machine" ]; then
                set_machine "$orig_machine"
            fi
        else
            _local_composer "$@"
        fi

        # restaurer le répertoire courant
        cd "$cwd"
    }
    function _local_mvn() {
        if [ -n "$java" ]; then
            urequire java
            select_java_exact "$java" || die "mvn: Java $java introuvable"
            export MVN_JAVA_VERSION="$java"
        fi

        case "$action" in
        rootshell|rshell|rootbash|rbash)
            shift
            estep "Lancement d'un shell root"
            sudo bash "$@"
            return $?
            ;;
        usershell|shell|userbash|bash)
            shift
            estep "Lancement d'un shell utilisateur"
            bash "$@"
            return $?
            ;;
        java)
            shift
            estep "java"
            java "$@"
            ;;
        *)
            estep "mvn $action"

            [ -n "$mvn" ] || setx mvn=which mvn 2>/dev/null
            [ -n "$mvn" ] || die "mvn: commande introuvable"

            set -- "$action" $args "$@"
            case "$1" in
            install) set clean package install "${@:2}";;
            package) set clean package "${@:2}";;
            package_only) set package "${@:2}";;
            esac
            "$mvn" "$@"
        esac
    }
    function _docker_mvn() {
        local user group projdir actualcmd
        setx user=id -un; setx user=getent passwd "$user"
        setx group=id -gn; setx group=getent group "$group"
        setx projdir=pwd
        case "$action" in
        rootshell|rshell|rootbash|rbash)
            action=rshell
            shift
            actualcmd='eval "bash $args"'
            ;;
        usershell|shell|userbash|bash)
            action=shell
            shift
            actualcmd='eval "su-exec \"$user\" bash $args"'
            ;;
        java)
            shift
            actualcmd='eval "su-exec \"$user\" java $args"'
            ;;
        *)
            actualcmd='eval "su-exec \"$user\" \"$mvn\" $args"'

            set -- "$action" $args "$@"
            case "$1" in
            install) set clean package install "${@:2}";;
            package) set clean package "${@:2}";;
            package_only) set package "${@:2}";;
            esac
            args=
            ;;
        esac
        setx args=qvals $args "$@"

        local -a basecmd setupscript runscript cmd
        basecmd=(
            -e user="$user"
            -e group="$group"
            -e projdir="$projdir"
            -e setup="$setup"
            -e setup_image="$setup_image"
            -e mvn="$mvn"
            -e args="$args"
            ${java:+-e JAVA="$java"}
        )
        eval "host_mappings=($host_mappings)"
        for host_mapping in "${host_mappings[@]}"; do
            basecmd+=(--add-host "$host_mapping")
        done
        basecmd+=(-v "$HOME:$HOME")
        if [ "${projdir#$HOME/}" == "$projdir" ]; then
            # si le répertoire de projet ne se trouve pas dans $HOME, le monter aussi
            cmd+=(-v "$projdir:$projdir")
        fi
        setupscript='eval "$setup"'
        runscript='
echo "$user" >>/etc/passwd; user="${user%%:*}"
echo "$group" >>/etc/group; group="${group%%:*}"

[ -n "$mvn" ] || mvn=mvn

cd "$projdir"
'"$actualcmd"

        if [ -n "$setup" ]; then
            local project_name container_name ctid
            local setup_image="$setup_image"

            # lancement dans un container docker à préparer
            [ -n "$project_name" ] || setx project_name=get_project_name
            setx container_name=get_container_name "$project_name"
            [ -n "$setup_image" ] || setup_image="dkbuild_maven_${container_name}_image"

            # vérifier l'existence de l'image
            setx ctid=docker image ls --format '{{.ID}}' "$setup_image"

            # créer le container le cas échéant
            if [ -z "$ctid" ]; then
                estep "Création de l'image $setup_image à partir de $image"
                cmd=(
                    docker create -it --name "${setup_image}_tmpct"
                    "${basecmd[@]}"
                    "$image"
                    bash -c "$setupscript"
                )
                setx ctid="${cmd[@]}" &&
                docker container start -ai "$ctid" &&
                docker container commit "$ctid" "$setup_image" &&
                docker container rm "$ctid" || die
            fi

            # prendre comme image le container créé
            image="$setup_image"
        fi

        case "$action" in
        rshell) estep "Lancement d'un shell root (avec l'image $image)";;
        shell) estep "Lancement d'un shell utilisateur (avec l'image $image)";;
        java) estep "java (avec l'image $image)";;
        *) estep "mvn $action (avec l'image $image)";;
        esac
        cmd=(
            docker run -it --rm
            "${basecmd[@]}"
            "$image"
            bash -c "$runscript"
        )
        "${cmd[@]}"
    }
    function mvn() {
        edebug "mvn $(qvals "$@")"
        [ $# -eq 0 ] && return 0

        local action destdir
        [[ "$1" != *=* ]] && { destdir="$1"; shift; }
        [[ "$1" != *=* ]] && { action="$1"; shift; }

        [ -n "$destdir" ] || destdir=.
        [ -d "$destdir" ] || die "mvn: $destdir: répertoire introuvable"
        local cwd="$(pwd)"
        cd "$destdir" || die

        [ -n "$action" ] || action=package
        if [ "$action" == none ]; then
            cd "$cwd"
            return
        fi

        local build="${DEFAULTS[build_build]-1}"
        local args="${DEFAULTS[mvn_args]}"
        local java="${DEFAULTS[mvn_java]}"
        local image="${DEFAULTS[mvn_image]}"
        local machine="${DEFAULTS[mvn_machine]}"
        local host_mappings="${DEFAULTS[mvn_host-mappings]-__UNDEFINED__}"
        [ "$host_mappings" == __UNDEFINED__ ] && host_mappings="${DEFAULTS[docker_host-mappings]}"
        local mvn="${DEFAULTS[mvn_mvn]}"
        local setup="${DEFAULTS[mvn_setup]}"
        local setup_image="${DEFAULTS[mvn_setup-image]}"
        local project_name="${DEFAULTS[mvn_project-name]}"
        if [ -f "$destdir/.maven.conf" ]; then
            eval "$(
                MAVEN_JAVA=
                MAVEN_IMAGE="$MAVEN_IMAGE"
                MAVEN_MACHINE=-u
                MAVEN_CMD=
                MAVEN_SETUP=
                MAVEN_SETUP_IMAGE=
                source "$destdir/.maven.conf"
                [ -z "$java" ] && echo_setv java="$MAVEN_JAVA"
                [ -z "$image" ] && echo_setv image="$MAVEN_IMAGE"
                [ -z "$machine" ] && echo_setv machine="$MAVEN_MACHINE"
                [ -z "$mvn" ] && echo_setv mvn="$MAVEN_CMD"
                [ -z "$setup" ] && echo_setv setup="$MAVEN_SETUP"
                [ -z "$setup_image" ] && echo_setv setup_image="$MAVEN_SETUP_IMAGE"
            )"
        fi

        while [ $# -gt 0 ]; do
            case "$1" in
            build) build=1;;
            build=*) build="${1#build=}";;
            args=*) args="${1#args=}";;
            java=*) java="${1#java=}";;
            image=*) image="${1#image=}";;
            machine=*) machine="${1#machine=}";;
            host-mappings=*) host_mappings="${1#host-mappings=}";;
            mvn=*) mvn="${1#mvn=}";;
            setup=*) setup="${1#setup=}";;
            setup-image=*) setup_image="${1#setup-image=}";;
            project-name=*) project_name="${1#project-name=}";;
            *=*) ewarn "mvn: $1: argument ignoré";;
            *) break;;
            esac
            shift
        done

        if [ -z "$build" ]; then
            cd "$cwd"
            return
        fi

        local version
        case "$action" in
        java*)
            version="${action#java}"
            [ -n "$version" ] && java="$version"
            set java "${@:2}"
            ;;
        esac

        if [ "$java" != force -a "$java" != any ]; then
            # Si Java et mvn ne sont pas disponibles dans le PATH, forcer
            # l'utilisation de l'image
            progexists java || java=force
            progexists mvn || java=force
        fi

        local use_image
        if [ "$java" == force -o "$java" == any ]; then
            java=
            use_image=1
        elif [ "$java" == none -o "$java" == system ]; then
            java=
            use_image=
        elif [ -n "$image" -a "$image" != none ]; then
            use_image=1
        fi

        if [ -n "$use_image" ]; then
            if [ -z "$image" ]; then
                # Si l'image n'est pas définie, calculer une valeur par défaut à
                # partir REGISTRY et DIST
                image="$(get_default_javabuilder_image)"
            fi
            local orig_machine
            [ "$image" != none ] || die "Vous devez spécifier l'image à utiliser pour mvn"
            if [ -n "$machine" -a "$machine" != current -a "$DOCKER_MACHINE_NAME" != "$machine" ]; then
                orig_machine="$DOCKER_MACHINE_NAME"
                set_machine "$machine"
            fi
            _docker_mvn "$@"
            if [ -n "$_orig_machine" ]; then
                set_machine "$orig_machine"
            fi
        else
            _local_mvn "$@"
        fi

        # restaurer le répertoire courant
        cd "$cwd"
    }
    function run() {
        edebug "run $(qvals "$@")"
        [ $# -eq 0 ] && return 0

        local cmd="$1"; shift
        if [ "${cmd#/}" != "$cmd" ]; then :
        elif [ "${cmd#./}" != "$cmd" ]; then :
        elif [ "${cmd#../}" != "$cmd" ]; then :
        else
            local abscmd
            setx abscmd=which "$cmd" 2>/dev/null
            [ -n "$abscmd" ] || die "run: $cmd: commande introuvable"
            edebug "'$cmd' resolved as '$abscmd'"
            cmd="$abscmd"
        fi
        "$cmd" "$@" || die
    }
    function runb() {
        local build="${DEFAULTS[build_build]-1}"
        if [ -z "$build" ]; then
            [ $# -eq 0 ] && return 1 || return 0
        fi
        run "$@"
    }
    function call() {
        edebug "call $(qvals "$@")"
        [ $# -eq 0 ] && return 0

        "$@"
    }
    function callb() {
        local build="${DEFAULTS[build_build]-1}"
        if [ -z "$build" ]; then
            [ $# -eq 0 ] && return 1 || return 0
        fi
        call "$@"
    }
    function dkbuild() {
        edebug "dkbuild $(qvals "$@")"

        "$SELF" "$@" || die
    }
}

##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## init

function templates_action() {
    declare -A TEMPLATES
    [ -f "$TEMPLATEDIR/templates.conf" ] && source "$TEMPLATEDIR/templates.conf"

    etitle "Templates valides"
    local -a templates
    setx -a templates=list_dirs "$TEMPLATEDIR"
    for template in "${templates[@]}"; do
        [ "$template" == default ] && continue
        desc="${TEMPLATES[$template]}"
        estep "$template${desc:+ - "$desc"}"
    done
    eend
}

##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## init

function init_action() {
    eval "$SHARED_LOCALS1"
    local template; local -a defaultvars addvars
    local -a args; args=(
        "${SHARED_ARGS1[@]}"
        -t:,--template: template=
        -v:,--var: '$addvars+=("$value_")'
        -n:,--name: '$addvars+=(name="$value_")'
        -g:,--group: '$addvars+=(group="$value_")'
    )
    parse_args "$@"; set -- "${args[@]}"

    defaultvars=(name=name group=group)
    [ $# -gt 0 ] && { [ -n "$1" ] && PROJDIR="$1"; shift; }
    [ $# -gt 0 ] && { [ -n "$1" ] && addvars+=(name="$1"); shift; }
    [ $# -gt 0 ] && { [ -n "$1" ] && addvars+=(group="$1"); shift; }

    declare -A vars
    local name value
    for name in "${defaultvars[@]}" "${addvars[@]}"; do
        if [[ "$name" == *=* ]]; then
            value="${name#*=}"
            name="${name%%=*}"
        else
            value=
        fi
        name="${name^^}"
        vars[$name]="$value"
    done

    [ -n "$template" ] || template=default
    [ -d "$TEMPLATEDIR/$template" ] || die "$template: template introuvable"

    [ -n "$PROJDIR" ] || PROJDIR=.
    setx PROJDIR=abspath "$PROJDIR"
    if [ ! -d "$PROJDIR" ]; then
        ask_yesno "Voulez-vous créer le nouveau projet $(ppath "$PROJDIR")?" O || die
    fi
    mkdir -p "$PROJDIR"

    local sedscript
    for name in "${!vars[@]}"; do
        value="${vars[$name]}"
        [ -n "$sedscript" ] && sedscript="$sedscript; "
        sedscript="${sedscript}s/${name//\//\\\/}/${value//\//\\\/}/g"
    done

    local src mode dest link
    template="$TEMPLATEDIR/$template"
    enote "Initialisation de $(ppath "$PROJDIR")"
    find "$template/" -type f -o -type l | while read src; do
        dest="$PROJDIR/${src#$template/}"
        if [ -L "$src" ]; then
            setx link=readlink "$src"
            if [ -L "$dest" ]; then
                if [ "$(readlink "$dest")" != "$link" ]; then
                    estep "${src#$template/} [link, updated]"
                    ln -sf "$link" "$dest"
                else
                    edebug "${src#$template/} [exists, ignored]"
                fi
            elif [ -e "$dest" ]; then
                estepe "${src#$template/} [destination is not a link]"
            else
                estep "${src#$template/} [link]"
                ln -s "$link" "$dest"
            fi
        elif [ -f "$dest" ]; then
            edebug "${src#$template/} [exists, ignored]"
        else
            estep "${src#$template/}"
            mkdirof "$dest"
            sed <"$src" >"$dest" "$sedscript"
            setx mode=stat -c %a "$src"
            chmod "$mode" "$dest"
        fi
    done
}

##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## build

function build_action() {
    eval "$SHARED_LOCALS1; $SHARED_LOCALS2"
    local action=build
    local machine
    local clean_update clean_opt=-X
    local clone_src_only update_src_only update_src sync_src
    local build no_cache progress pull_image
    local push_image
    local -a args; args=(
        "${SHARED_ARGS1[@]}" "${SHARED_ARGS2[@]}"
        -m:,--machine: machine=
        -u,--clean-update clean_update=1
        -X,--clean-ignored clean_opt=-X
        -x,--clean-untracked clean_opt=-x
        --clone-src-only clone_src_only=1
        --update-src-only update_src_only=1
        --update-src update_src=1
        --no-update-src update_src=no
        -w,--update-devel-src update_src=devel
        -s,--sync-src sync_src=1
        --no-sync-src sync_src=no
        -b,--build build=1
        --no-cache no_cache=1
        --plain-output progress=plain
        -U,--pull-image pull_image=1
        -p,--push-image push_image=1
    )
    parse_args "$@"; set -- "${args[@]}"

    if [ -n "$clone_src_only" ]; then
        action=clone_src
    elif [ -n "$update_src_only" ]; then
        action=update_src
    else
        action=build
        if [ -z "$clean_update" -a -z "$sync_src" -a -z "$build" -a -z "$push_image" ]; then
            sync_src=1
            build=1
        fi
        if [ -n "$build" ]; then
            [ -n "$update_src" ] || update_src=1
            [ "$update_src" == no ] && update_src=
        else
            update_src=
        fi
        [ "$sync_src" == no ] && sync_src=
    fi

    edebug "build_action"
    set_machine "$machine"

    ensure_projdir
    resolve_dists_profiles "$@"

    if [ -n "$clean_update" ]; then
        edebug "clean"
        _clean_git_clean -f || die

        edebug "update"
        git pull || die

        edebug "sync"
        [ -n "$build" ] && sync_src=1
    fi

    case "$action" in
    clone_src)
        die "Pas encore implémenté" #XXX
        ;;
    update_src)
        die "Pas encore implémenté" #XXX
        ;;
    build)
        default checkout checkout="$update_src"
        default copy copy="$sync_src"
        default build build="$build" ${no_cache:+no-cache} ${pull_image:+pull} ${push_image:+push}
        [ $# -gt 0 ] && default build "$@"
        define_functions_cmd

        foreach_dists_profiles _build_each _build_before _build_after
        ;;
    esac
}

function _build_before() {
    PREV_DIST=
    PREV_PROFILE=
}
function _build_each() {
    if [ -n "$DIST" -a "$DIST-$DVERSION" != "$PREV_DIST" ]; then
        [ -n "$PREV_PROFILE" ] && eend
        PREV_PROFILE=

        [ -n "$PREV_DIST" ] && eend
        PREV_DIST="$DIST-$DVERSION"
        etitle "Distribution ${DVERSION:+$DVERSION-}$DIST"
    fi
    if [ -n "$PROFILE" -a "$PROFILE-$PVERSION" != "$PREV_PROFILE" ]; then
        PREV_PROFILE="$PROFILE-$PVERSION"
        etitle "Profil ${PVERSION:+$PVERSION-}$PROFILE"
    fi

    load_dkbuild
    if [ -n "$AUTOBUILD" ]; then
        if [ -f docker-compose.yml ]; then
            cbuild
        else
            build
        fi
    fi
}
function _build_after() {
    if [ -n "$PREV_PROFILE" ]; then eend; fi
    if [ -n "$PREV_DIST" ]; then eend; fi
}

##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## clean

function _clean_git_clean() {
    LANG=C git clean -d $clean_opt "$@" |
        grep -vE '^(Would skip|Skipping) ' |
        sed -r 's/^Would remove //'
}
function _clean_git_status() {
    git status --porcelain --ignored |
        grep '^!! ' |
        sed 's/^...//'
}

function clean_action() {
    eval "$SHARED_LOCALS1"
    local clean_opt=-X all=
    local -a args; args=(
        "${SHARED_ARGS1[@]}"
        -X,--ignored clean_opt=-X
        -x,--untracked clean_opt=-x
        -a,--all all=1
    )
    parse_args "$@"; set -- "${args[@]}"

    [ -n "$all" ] && clean_opt=-x

    edebug "clean_action"
    ensure_projdir

    local cleans
    setx cleans=_clean_git_clean -n "$@"
    if [ -n "$cleans" ]; then
        if check_interaction -c; then
            einfo "via git clean"
            eecho "$cleans"
            ask_yesno "Voulez-vous supprimer ces fichiers?" O || die
        fi

        _clean_git_clean -f "$@" || die
    fi

    if [ -n "$all" ]; then
        setx cleans=_clean_git_status
        if [ -n "$cleans" ]; then
            if check_interaction -c; then
                einfo "via git status"
                eecho "$cleans"
                ask_yesno "Voulez-vous supprimer ces fichiers supplémentaires?" O || die
            fi

            sed 's/^/Removing /' <<<"$cleans"
            eval "cleans=($cleans);"' rm -rf "${cleans[@]}"' || die
        fi
    fi
}

##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## composer

function composer_action() {
    local -a args; args=(
    )
    parse_args "$@"; set -- "${args[@]}"

    [ $# -gt 0 ] || set .

    edebug "composer_action"

    define_functions_cmd
    composer "$@"
}

##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## mvn

function mvn_action() {
    local -a args; args=(
    )
    parse_args "$@"; set -- "${args[@]}"

    [ $# -gt 0 ] || set .

    edebug "mvn_action"

    define_functions_cmd
    mvn "$@"
}

##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## dump

function dump_action() {
    eval "$SHARED_LOCALS1; $SHARED_LOCALS2"
    local machine
    local -a args; args=(
        "${SHARED_ARGS1[@]}" "${SHARED_ARGS2[@]}"
        -m:,--machine: machine=
    )
    parse_args "$@"; set -- "${args[@]}"

    edebug "dump_action"
    set_machine "$machine"

    ensure_projdir
    resolve_dists_profiles "$@"

    foreach_dists_profiles _dump_each _dump_before _dump_after
}

function _dump_before() {
    PREV_DIST=
    PREV_PROFILE=
}
function _dump_each() {
    if [ -n "$DIST" -a "$DIST-$DVERSION" != "$PREV_DIST" ]; then
        [ -n "$PREV_PROFILE" ] && eend
        PREV_PROFILE=

        [ -n "$PREV_DIST" ] && eend
        PREV_DIST="$DIST-$DVERSION"
        etitle "Distribution ${DVERSION:+$DVERSION-}$DIST"
    fi
    if [ -n "$PROFILE" -a "$PROFILE-$PVERSION" != "$PREV_PROFILE" ]; then
        PREV_PROFILE="$PROFILE-$PVERSION"
        etitle "Profil ${PVERSION:+$PVERSION-}$PROFILE"
    fi

    load_dkbuild

    etitle "Variables d'environnement"
    for name in "${!ENVIRON[@]}"; do
        estep "$name=${ENVIRON[$name]}"
    done
    eend

    etitle "Variables de build"
    for name in "${!ARGS[@]}"; do
        estep "$name=${ARGS[$name]}"
    done
    eend

    etitle "Valeurs par défaut"
    for name in "${!DEFAULTS[@]}"; do
        estep "$name=${DEFAULTS[$name]}"
    done
    eend
}
function _dump_after() {
    if [ -n "$PREV_PROFILE" ]; then eend; fi
    if [ -n "$PREV_DIST" ]; then eend; fi
}

##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
##

# faire une copie de l'environnement original
load_environ

# si aucune action n'est spécifiée, il faut inférer build pour que ses options
# soient reconnues
args=()
while [ $# -gt 0 ]; do
    case "$1" in
    --help) args+=("$1"); shift; continue;;
    --hdk|--help-dkbuild) args+=("$1"); shift; continue;;
    --href|--help-reference) args+=("$1"); shift; continue;;
    --compose-v1) args+=("$1"); shift; continue;;
    -*|*=*) # option quelconque: inférer build
        args+=(build)
        break
        ;;
    *) # argument quelconque: on s'arrête ici
        break
        ;;
    esac
done
set -- "${args[@]}" "$@"

args=(+
    --help '$exit_with display_help'
    --hdk,--help-dkbuild '$exit_with display_help_dkbuild'
    --href,--help-reference '$exit_with display_help_reference'
    --compose-v1 '$DOCKER_COMPOSE=(docker-compose)'
)
parse_args "$@"; set -- "${args[@]}"

# aliases
case "$1" in
ci) set composer "$2" install "${@:3}";;
cu) set composer "$2" update "${@:3}";;
cr) set composer "$2" rshell "${@:3}";;
cs) set composer "$2" shell "${@:3}";;
mvr) set mvn "$2" rshell "${@:3}";;
mvs) set mvn "$2" shell "${@:3}";;
java) set mvn "$2" java "${@:3}";;
esac

# actions
action="${1:-build}"; shift
case "$action" in
templates|t) templates_action "$@";;
init|i|0) init_action "$@";;
build|b) build_action "$@";;
clean|k) clean_action "$@";;
composer|c) composer_action "$@";;
maven|mvn|m) mvn_action "$@";;
dump|d) dump_action "$@";;
*) die "$action: action invalide";;
esac