Files
FWS-ISO/build-offi.sh
T
nocode 7730ac7b00 feat(build): add preflight package resolution and fws-dnd package
Add preflight package resolution step in build-offi.sh to validate all profile
packages against pacman repositories before mkarchiso execution. This prevents
'target not found' errors during ISO creation by catching missing packages early
with helpful error messages and recovery instructions.

Introduce fws-dnd package (D&D 5e manager for FWS with GTK4 interface) to the
local package build process in setup-aur.sh. Include PKGBUILD definition and
source tarball.

When package resolution fails, the build now exits with clear feedback pointing
users to run setup-aur.sh to build missing dependencies like fws-dnd.
2026-07-07 21:39:29 +02:00

409 lines
19 KiB
Bash
Executable File

#!/bin/bash
# ============================================================
# FWS — Build OFFICIEL versionné de l'ISO (archiso)
#
# IDENTIQUE à build.sh (même bascule conteneur/WSL, --network=host, fusion
# baseline, MÊME injection local-repo → donc même paquet anaconda avec le champ
# Hostname), avec EN PLUS uniquement le RENOMMAGE :
# - demande la version (major.minor.build) + le nom de variante, le fichier
# VERSION servant de défaut,
# - nomme l'ISO fws-<version>-<nom>.iso (ex. fws-1.5.0-cli.iso),
# - range l'ISO dans out/vMAJOR/vMAJOR.MINOR/.
#
# Fonctionne sur :
# - Windows 10 / 11 (Git Bash → bascule auto dans WSL Arch)
# - Arch Linux natif (exécution directe)
# - WSL Arch lancé directement (exécution directe)
#
# Nom de la distro WSL : "Arch" par défaut, ou $ARCH_WSL_DISTRO,
# ou auto-détecté parmi les noms courants.
# ============================================================
set -e
trap 'rc=$?; echo -e "\e[31mÉchec (build-offi.sh) ligne $LINENO, code $rc.\e[0m" >&2; exit $rc' ERR
DISTRO_NAME="${ARCH_WSL_DISTRO:-Arch}"
# ============================================================
# Bascule Git Bash (Windows 10/11) → WSL si besoin
# ============================================================
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || -n "$WINDIR" ]]; then
echo -e "\e[36mGit Bash détecté — bascule dans WSL ($DISTRO_NAME)...\e[0m"
export MSYS_NO_PATHCONV=1
export MSYS2_ARG_CONV_EXCL="*"
# WSL doit être présent (sinon message clair plutôt que command not found).
command -v wsl.exe >/dev/null 2>&1 || {
echo -e "\e[31mWSL introuvable. Installe WSL2 puis une distro Arch :\e[0m" >&2
echo " 1) PowerShell admin : wsl --install" >&2
echo " 2) Installe Arch (ArchWSL, ou 'wsl --install -d archlinux') puis relance." >&2
exit 1
}
# Choix de la distro Arch : override $ARCH_WSL_DISTRO, sinon nom exact,
# sinon 1re distro « arch » réellement joignable. On énumère via
# `wsl -l -q` (sortie UTF-16LE → on retire NUL/CR) au lieu d'une liste
# figée, mais on garde la sonde « echo ok » (une distro listée peut
# être en cours d'install / cassée).
pick_arch_distro() {
wsl.exe -d "$DISTRO_NAME" -u root -- echo ok >/dev/null 2>&1 && return 0
local d distros=()
mapfile -t distros < <(wsl.exe -l -q 2>/dev/null | tr -d '\0\r' | sed 's/[[:space:]]*$//;/^$/d')
for d in "${distros[@]}"; do
[[ "$d" == "$DISTRO_NAME" ]] || continue
wsl.exe -d "$d" -u root -- echo ok >/dev/null 2>&1 && { DISTRO_NAME="$d"; return 0; }
done
for d in "${distros[@]}"; do
[[ "$d" =~ [Aa][Rr][Cc][Hh] ]] || continue
wsl.exe -d "$d" -u root -- echo ok >/dev/null 2>&1 && { DISTRO_NAME="$d"; return 0; }
done
return 1
}
if ! pick_arch_distro; then
echo -e "\e[31mAucune distribution WSL Arch joignable.\e[0m" >&2
echo "Distros WSL installées :" >&2
wsl.exe -l -v 2>/dev/null | tr -d '\0' >&2 || true
echo "" >&2
echo "Installe Arch dans WSL puis relance :" >&2
echo " - ArchWSL (yuk7) — s'enregistre sous 'Arch' : https://github.com/yuk7/ArchWSL/releases/latest" >&2
echo " - ou image officielle : wsl --install -d archlinux (nom 'archlinux')" >&2
echo "Distro sous un autre nom ? export ARCH_WSL_DISTRO=<nom> (cf. 'wsl -l -v' ci-dessus)." >&2
exit 1
fi
echo -e "\e[36m==> Distro WSL : $DISTRO_NAME\e[0m"
# Traduit le dossier du script en chemin WSL.
SCRIPT_DIR_WIN="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -W)"
SCRIPT_PATH_WSL=$(MSYS_NO_PATHCONV=1 wsl.exe -d "$DISTRO_NAME" -u root -- \
wslpath "$(echo "$SCRIPT_DIR_WIN" | sed 's|/|\\\\|g')/build-offi.sh" \
2>/dev/null | tr -d '\r\n')
[ -n "$SCRIPT_PATH_WSL" ] || {
echo -e "\e[31mTraduction du chemin vers WSL échouée (wslpath). Vérifie ARCH_WSL_DISTRO ($DISTRO_NAME).\e[0m" >&2
exit 1
}
wsl.exe -d "$DISTRO_NAME" -u root -- test -f "$SCRIPT_PATH_WSL" || {
echo -e "\e[31mScript introuvable dans WSL : $SCRIPT_PATH_WSL\e[0m" >&2
exit 1
}
# Relance ce même script dans WSL, en root, depuis son vrai chemin
# (BASH_SOURCE → WORK_DIR correct). Les fins de ligne sont garanties LF
# par .gitattributes (*.sh text eol=lf) ; le profil copié est en plus
# passé à dos2unix plus loin.
echo "==> Script à exécuter dans WSL : $SCRIPT_PATH_WSL"
MSYS_NO_PATHCONV=1 wsl.exe -d "$DISTRO_NAME" -u root -- bash "$SCRIPT_PATH_WSL"
exit 0
fi
# ============================================================
# Bascule conteneur : hôte non-Arch (Fedora, Ubuntu, …) →
# build dans un conteneur Arch via podman.
#
# mkarchiso exige pacman + des mounts/chroot/loop privilégiés que
# seul un environnement Arch fournit. Sur un hôte non-Arch on relance
# CE script à l'identique dans docker.io/library/archlinux:latest, le
# dépôt monté au MÊME chemin absolu (→ WORK_DIR et out/ inchangés).
# podman ROOTFUL + --privileged obligatoire : rootless ne gère pas les
# mounts/chroot de mkarchiso (et --privileged relâche aussi SELinux,
# enforcing sur Fedora, sur le bind-mount sans avoir besoin de :Z).
# ============================================================
host_is_arch() {
# Fedora empaquette aussi « pacman » → sa seule présence ne suffit pas ;
# on exige en plus un ID/ID_LIKE « arch » dans /etc/os-release.
command -v pacman >/dev/null 2>&1 \
&& grep -qiE '^ID(_LIKE)?=.*arch' /etc/os-release 2>/dev/null
}
if [ -z "$FWS_IN_CONTAINER" ] && ! host_is_arch; then
echo -e "\e[36mHôte non-Arch détecté — build officiel dans un conteneur Arch (podman)...\e[0m"
command -v podman >/dev/null 2>&1 || {
echo -e "\e[31mpodman introuvable.\e[0m" >&2
echo " Fedora : sudo dnf install -y podman" >&2
echo " Debian/Ubuntu : sudo apt install -y podman" >&2
exit 1
}
# La version + le nom sont DEMANDÉS sur l'hôte (avant le conteneur) et passés
# via -e, pour que le prompt s'affiche au bon endroit (terminal de l'hôte).
# (Définis FWS_ISO_VERSION / FWS_ISO_TAG ; lus plus bas, prompt sauté si déjà
# fournis — c.-à-d. dans le conteneur.)
ask_iso_meta_host() {
[ -n "$FWS_ISO_VERSION" ] && [ -n "$FWS_ISO_TAG" ] && return 0
local def=""
if [ -f "$(dirname "${BASH_SOURCE[0]}")/VERSION" ]; then
def="$(tr -d ' \t\r\n' < "$(dirname "${BASH_SOURCE[0]}")/VERSION")"
[[ "$def" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || def=""
fi
while :; do
if [ -n "$def" ]; then
read -rp "==> Version (major.minor.build) [Entrée = $def] : " FWS_ISO_VERSION || exit 1
[ -n "$FWS_ISO_VERSION" ] || FWS_ISO_VERSION="$def"
else
read -rp "==> Version (major.minor.build, ex. 1.5.0) : " FWS_ISO_VERSION || exit 1
fi
[[ "$FWS_ISO_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && break
echo -e "\e[31m Format attendu : major.minor.build (ex. 1.5.0)\e[0m" >&2
done
while :; do
read -rp "==> Nom de la variante (ex. cli, gnome, kde) : " FWS_ISO_TAG || exit 1
FWS_ISO_TAG="$(printf '%s' "$FWS_ISO_TAG" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9-')"
[ -n "$FWS_ISO_TAG" ] && break
echo -e "\e[31m Nom requis (lettres, chiffres, tirets)\e[0m" >&2
done
export FWS_ISO_VERSION FWS_ISO_TAG
}
ask_iso_meta_host
# Rootful : préfixe sudo si on n'est pas déjà root.
SUDO=(); [ "$EUID" -eq 0 ] || SUDO=(sudo)
# -it seulement si on est dans un terminal (sinon podman casse en CI).
TTY=(); [ -t 0 ] && TTY=(-it)
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SELF="$(basename "${BASH_SOURCE[0]}")"
IMAGE="docker.io/library/archlinux:latest"
# pull explicite : « run » ne re-résout jamais le tag « latest » une fois
# l'image en cache → sans ça on build indéfiniment sur une image qui
# vieillit (puis partial upgrades pacman). Le -Syu côté conteneur (plus
# bas) complète la parade.
echo "==> Pull de l'image $IMAGE..."
"${SUDO[@]}" podman pull "$IMAGE"
echo "==> Relance dans le conteneur ($SELF)..."
# --network=host : le réseau bridge de podman ne route plus vers l'extérieur
# sur ce wifi (firewalld/NAT cassé après changement de réseau → « core.db :
# Resolving timed out », même avec --dns). En partageant la pile réseau de
# l'hôte (qui, lui, a internet + DNS via 127.0.0.53/systemd-resolved joignable
# dans son propre netns), le conteneur récupère une connectivité identique.
"${SUDO[@]}" podman run --rm "${TTY[@]}" --network=host --privileged \
-e FWS_IN_CONTAINER=1 \
-e FWS_ISO_VERSION -e FWS_ISO_TAG \
-v "$REPO_DIR":"$REPO_DIR" \
-v fws-pacman-cache:/var/cache/pacman/pkg \
-w "$REPO_DIR" \
"$IMAGE" \
bash "$REPO_DIR/$SELF"
# podman rootful → les fichiers produits (out/) appartiennent à root.
# On les rend au propriétaire du dépôt pour confort côté hôte.
OWNER="$(stat -c '%u:%g' "$REPO_DIR" 2>/dev/null || true)"
if [ -n "$OWNER" ]; then
for d in out local-repo; do
[ -e "$REPO_DIR/$d" ] && "${SUDO[@]}" chown -R "$OWNER" "$REPO_DIR/$d" 2>/dev/null || true
done
fi
exit 0
fi
# ============================================================
# Exécution native (Arch Linux bare-metal, WSL, OU conteneur Arch)
# ============================================================
command -v pacman >/dev/null \
|| { echo -e "\e[31mpacman introuvable — lance ce script depuis Arch / WSL.\e[0m"; exit 1; }
# mkarchiso exige root. Sur Arch natif on s'élève (sudo de préférence,
# sinon su) ; depuis la bascule Git Bash on est déjà root (-u root).
# $0 est cité pour gérer les chemins à espaces (ex. "1. FWS").
if [ "$EUID" -ne 0 ]; then
if command -v sudo >/dev/null 2>&1; then
# -E : conserve FWS_ISO_VERSION / FWS_ISO_TAG (demandés avant) à travers sudo.
exec sudo -E bash "$0" "$@"
else
exec su -c "bash \"$0\"" root
fi
fi
if grep -qi 'microsoft\|wsl' /proc/version 2>/dev/null; then
echo -e "\e[36mBuild officiel dans WSL ($DISTRO_NAME) en tant que root...\e[0m"
IS_WSL=1
else
echo -e "\e[36mBuild officiel sur Arch Linux natif en tant que root...\e[0m"
IS_WSL=0
fi
# Le dossier de travail = dossier qui contient ce script (gère les
# espaces du chemin, ex. "1. FWS").
WORK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "==> Dossier de travail : $WORK_DIR"
# ============================================================
# Version (major.minor.build) + nom de la variante.
# Déjà demandés sur l'hôte (FWS_ISO_VERSION/FWS_ISO_TAG passés via -e) ; sinon
# (exécution native Arch / WSL directe) on les demande ici. Le fichier VERSION
# sert de défaut (Entrée = le reprendre). Produit fws-<version>-<nom>.iso.
# ============================================================
VERSION="$FWS_ISO_VERSION"
ISO_TAG="$FWS_ISO_TAG"
VERSION_DEFAULT=""
if [ -f "$WORK_DIR/VERSION" ]; then
VERSION_DEFAULT="$(tr -d ' \t\r\n' < "$WORK_DIR/VERSION")"
[[ "$VERSION_DEFAULT" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || VERSION_DEFAULT=""
fi
while [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; do
if [ -n "$VERSION_DEFAULT" ]; then
read -rp "==> Version (major.minor.build) [Entrée = $VERSION_DEFAULT] : " VERSION \
|| { echo -e "\e[31mEntrée requise — lance le script en interactif.\e[0m" >&2; exit 1; }
[ -n "$VERSION" ] || VERSION="$VERSION_DEFAULT"
else
read -rp "==> Version (major.minor.build, ex. 1.5.0) : " VERSION \
|| { echo -e "\e[31mEntrée requise — lance le script en interactif.\e[0m" >&2; exit 1; }
fi
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \
|| echo -e "\e[31m Format attendu : major.minor.build (ex. 1.5.0)\e[0m" >&2
done
while [ -z "$ISO_TAG" ]; do
read -rp "==> Nom de la variante (ex. cli, gnome, kde) : " ISO_TAG \
|| { echo -e "\e[31mEntrée requise — lance le script en interactif.\e[0m" >&2; exit 1; }
ISO_TAG="$(printf '%s' "$ISO_TAG" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9-')"
[ -n "$ISO_TAG" ] || echo -e "\e[31m Nom requis (lettres, chiffres, tirets)\e[0m" >&2
done
IFS='.' read -r MAJOR MINOR BUILD <<< "$VERSION"
BUILD_DATE="$(date +%Y.%m.%d)"
# iso_version interne (passé à mkarchiso) ; l'ISO finale est renommée plus bas.
ISO_VERSION="${VERSION}-${ISO_TAG}"
OUT_DIR="$WORK_DIR/out/v${MAJOR}/v${MAJOR}.${MINOR}"
# Date ajoutée À LA FIN du nom, avant .iso → fws-<version>-<nom>-<AAAA.MM.JJ>.iso
ISO_FILE="$OUT_DIR/fws-${VERSION}-${ISO_TAG}-${BUILD_DATE}.iso"
echo "==> Version : ${VERSION} · variante : ${ISO_TAG} · date : ${BUILD_DATE}"
echo "==> ISO finale : fws-${VERSION}-${ISO_TAG}-${BUILD_DATE}.iso"
echo "==> Dossier : $OUT_DIR"
# pacman ≥ 7 sandboxe les ops FS via landlock, non supporté par le kernel
# WSL ni (selon le noyau) par le conteneur → désactivation. Idempotent,
# sans effet sur Arch natif.
if { [ "$IS_WSL" -eq 1 ] || [ -n "$FWS_IN_CONTAINER" ]; } \
&& ! grep -qE '^[[:space:]]*DisableSandbox' /etc/pacman.conf; then
sed -i '/^\[options\]/a DisableSandbox' /etc/pacman.conf
echo "==> WSL/conteneur détecté → DisableSandbox ajouté à /etc/pacman.conf"
fi
echo "==> Mise à jour des miroirs et installation de archiso..."
pacman -Sy --noconfirm reflector || true
reflector --verbose --latest 10 --sort rate --save /etc/pacman.d/mirrorlist || true
if [ -n "$FWS_IN_CONTAINER" ]; then
# Image de base qui vieillit → upgrade complet AVANT d'ajouter archiso,
# sinon partial upgrade (archiso tire des deps plus récentes que la base).
pacman -Syu --noconfirm archiso
else
pacman -Sy --noconfirm archiso
fi
echo "==> Préparation de l'environnement de build..."
rm -rf /tmp/fws-build
mkdir -p /tmp/fws-build/releng
# Copie configs/releng/ en base
if [ -d "$WORK_DIR/configs/releng" ]; then
cp -ar "$WORK_DIR/configs/releng/"* /tmp/fws-build/releng/
echo "==> configs/releng/ copié"
fi
# Fusionne configs/baseline/ par dessus (priorité aux fichiers baseline)
if [ -d "$WORK_DIR/configs/baseline" ]; then
cp -ar "$WORK_DIR/configs/baseline/"* /tmp/fws-build/releng/
echo "==> configs/baseline/ fusionné (prioritaire)"
fi
# ── Injection de la version dans profiledef.sh (copie temporaire) ──
# mkarchiso nomme l'ISO ${iso_name}-${iso_version}-${arch}.iso ; on force
# iso_version pour obtenir le nom voulu (l'ISO est renommée fws-… plus bas).
if [ -f /tmp/fws-build/releng/profiledef.sh ]; then
sed -i "s|^iso_version=.*|iso_version=\"${ISO_VERSION}\"|" \
/tmp/fws-build/releng/profiledef.sh
echo "==> iso_version forcé à '${ISO_VERSION}' dans profiledef.sh"
else
echo -e "\e[31mprofiledef.sh introuvable dans le profil de build.\e[0m"
exit 1
fi
echo "==> Activation de NetworkManager.service sur le live (symlink réel)..."
mkdir -p /tmp/fws-build/releng/airootfs/etc/systemd/system/multi-user.target.wants
ln -sf /usr/lib/systemd/system/NetworkManager.service \
/tmp/fws-build/releng/airootfs/etc/systemd/system/multi-user.target.wants/NetworkManager.service
echo "==> Vérification des services systemd..."
ls /tmp/fws-build/releng/airootfs/etc/systemd/system/ 2>/dev/null || echo " (aucun service)"
ls /tmp/fws-build/releng/airootfs/etc/systemd/system/multi-user.target.wants/ 2>/dev/null || echo " (aucun symlink)"
# Copie le repo local (winboat-bin + deps Anaconda packagées)
if ls "$WORK_DIR/local-repo/"*.pkg.tar.zst &>/dev/null 2>&1; then
echo "==> Copie du repo local fws-local..."
mkdir -p /tmp/fws-build/local-repo
cp -a "$WORK_DIR/local-repo/"* /tmp/fws-build/local-repo/
echo "==> Injection [fws-local] dans pacman.conf..."
sed -i '/^\[fws-local\]/,/^$/d' /tmp/fws-build/releng/pacman.conf
PACMAN_TMP=$(mktemp)
printf '[fws-local]\nSigLevel = Optional TrustAll\nServer = file:///tmp/fws-build/local-repo\n\n' \
> "$PACMAN_TMP"
cat /tmp/fws-build/releng/pacman.conf >> "$PACMAN_TMP"
mv "$PACMAN_TMP" /tmp/fws-build/releng/pacman.conf
echo "==> Vérification pacman.conf :"
head -5 /tmp/fws-build/releng/pacman.conf
else
echo "==> Aucun repo local détecté."
fi
echo "==> Correction des retours à la ligne Windows (dos2unix)..."
pacman -S --needed --noconfirm dos2unix
find /tmp/fws-build/releng -type f -exec dos2unix {} + 2>/dev/null
# ── Pré-vol : résolution de TOUS les paquets du profil ──
# « error: target not found » ne doit plus surgir en plein mkarchiso : on
# résout la liste complète (packages.x86_64 fusionné) contre le pacman.conf
# du profil (fws-local injecté inclus) dans une base temporaire. S'il manque
# un paquet, on échoue ICI avec la liste + le geste correctif.
echo "==> Pré-vol : résolution des paquets du profil..."
PREFLIGHT_DB="$(mktemp -d)"
pacman -Sy --config /tmp/fws-build/releng/pacman.conf --dbpath "$PREFLIGHT_DB" >/dev/null
mapfile -t PROFILE_PKGS < <(sed -e 's/#.*//' -e 's/[[:space:]]//g' -e '/^$/d' \
/tmp/fws-build/releng/packages.x86_64)
# -Sp n'installe rien (affiche les URLs) ; LC_ALL=C fige le motif d'erreur.
# stderr → capture, stdout → poubelle ; le pipeline sed sort toujours 0.
MISSING="$(LC_ALL=C pacman -Sp --config /tmp/fws-build/releng/pacman.conf \
--dbpath "$PREFLIGHT_DB" "${PROFILE_PKGS[@]}" 2>&1 >/dev/null \
| sed -n 's/^error: target not found:[[:space:]]*//p')"
rm -rf "$PREFLIGHT_DB"
if [ -n "$MISSING" ]; then
echo -e "\e[31mPaquets introuvables dans les dépôts du profil :\e[0m" >&2
printf ' \e[31m%s\e[0m\n' $MISSING >&2
echo -e "\e[33mDépôt fws-local absent/incomplet ? Lance ./setup-aur.sh (construit" >&2
echo -e "anaconda, winboat-bin, fws-dnd, …) puis relance ce script.\e[0m" >&2
exit 1
fi
echo "==> Pré-vol OK : ${#PROFILE_PKGS[@]} paquets résolus."
echo "==> Lancement de mkarchiso..."
mkdir -p "$OUT_DIR"
# mkarchiso fait un « rm » interne de l'ISO cible avant de la (ré)écrire ; si
# une ISO du même nom est VERROUILLÉE par Windows (VM encore ouverte dessus,
# ISO montée, antivirus), ce rm échoue et tout le build casse à la toute fin.
# Parade : mkarchiso écrit dans un dossier de staging neuf, puis on déplace
# l'ISO. Si la cible (cette même version) est verrouillée, on bascule sur un
# nom horodaté → on ne touche jamais aux ISO des autres versions.
STAGE="$OUT_DIR/.staging-$$"
rm -rf "$STAGE"; mkdir -p "$STAGE"
mkarchiso -v -w /tmp/fws-build/work -o "$STAGE" /tmp/fws-build/releng
NEW_ISO="$(ls -1 "$STAGE/"*.iso 2>/dev/null | head -1)"
[ -n "$NEW_ISO" ] || { echo -e "\e[31mAucune ISO produite par mkarchiso.\e[0m" >&2; exit 1; }
if { chmod -f 666 "$ISO_FILE" 2>/dev/null; rm -f "$ISO_FILE" 2>/dev/null; } && mv -f "$NEW_ISO" "$ISO_FILE" 2>/dev/null; then
FINAL="$ISO_FILE"
else
FINAL="${ISO_FILE%.iso}-$(date +%H%M%S).iso"
echo -e "\e[33m[!] '$ISO_FILE' est verrouillée (une VM l'utilise ?). Écriture sous un nom unique.\e[0m" >&2
mv -f "$NEW_ISO" "$FINAL"
fi
rm -rf "$STAGE" 2>/dev/null || true
echo "==> Nettoyage..."
rm -rf /tmp/fws-build/work
echo "==> Build officiel terminé ! ISO : $FINAL"
echo -e "\e[32mScript de build officiel (fws-${VERSION}-${ISO_TAG}-${BUILD_DATE}) terminé avec succès.\e[0m"