#!/usr/bin/env bash set -Eeuo pipefail readonly REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" DO_PACKAGES=0 DO_CONFIGS=0 DO_TOOLS=0 DO_SERVICES=0 DRY_RUN=0 BACKUP_DIR='' TEMP_DIR='' BENEFICIARY='' MACHINE_NAME='' DISPLAY_USER='' CURRENT_STAGE='précontrôles' CURRENT_ACTION='' COMPLETED=() APPLIED=() OFFICIAL_PACKAGES=() FOREIGN_PACKAGES=() SYSTEM_SERVICES=() SYSTEM_TIMERS=() ON_DEMAND_SERVICES=() readonly CONFIG_SOURCES=( rootfs/etc/pacman.conf rootfs/etc/makepkg.conf rootfs/etc/mkinitcpio.conf rootfs/etc/nftables.conf rootfs/etc/limine-snapper-sync.conf rootfs/etc/tlp.d/10-archasp.conf rootfs/etc/systemd/zram-generator.conf rootfs/etc/snapper/configs/root ) readonly CONFIG_TARGETS=( /etc/pacman.conf /etc/makepkg.conf /etc/mkinitcpio.conf /etc/nftables.conf /etc/limine-snapper-sync.conf /etc/tlp.d/10-archasp.conf /etc/systemd/zram-generator.conf /etc/snapper/configs/root ) readonly HELPER_NAMES=(batlimit-set vmware-stack) readonly SUDOERS_NAMES=(batlimit vmware-stack) # Le fragment privilégié est fixe : aucun argument n'est interprété comme du # code. find termine avec 0 et émet un état même si le composant est absent ; # toute erreur d'inspection fait échouer la commande au lieu de signifier ABSENT. readonly INSPECT_CODE=' target="$1" [[ "$target" == /* && "$target" != / ]] || exit 2 rest="${target#/}" cursor=/ while [[ -n "$rest" ]]; do name="${rest%%/*}" [[ -n "$name" && "$name" != . && "$name" != .. ]] || exit 2 if [[ "$rest" == */* ]]; then rest="${rest#*/}"; last=0 else rest=""; last=1; fi kind="$(find "$cursor" -mindepth 1 -maxdepth 1 -name "$name" -printf "%y\n" -quit)" || exit 3 case "$kind" in "") printf "ABSENT\n"; exit 0 ;; l) printf "LINK\n"; exit 0 ;; d) if (( last )); then printf "INVALID\n"; exit 0; fi ;; f) if (( last )); then printf "EXISTING\n"; else printf "INVALID\n"; fi; exit 0 ;; *) printf "INVALID\n"; exit 0 ;; esac if [[ "$cursor" == / ]]; then cursor="/$name"; else cursor="$cursor/$name"; fi done exit 2 ' usage() { cat <<'HELP' Usage : deploy.sh [--dry-run] (--packages|--configs|--tools|--services|--all) ... --packages Installe les listes de paquets natifs et étrangers (yay requis) --configs Restaure les configurations système suivies --tools Installe les helpers root et les règles sudoers --services Active les services et timers système voulus --all Exécute les quatre étapes dans cet ordre --dry-run Affiche un plan statique, sans privilèges ni application -h, --help Affiche cette aide Codes : 0 = plan valide, annulation ou application terminée ; 1 = précondition/application échouée ; 2 = usage invalide ; 130/143 = interruption INT/TERM. HELP } fatal() { printf '[!] Échec (%s) : %s\n' "$CURRENT_STAGE" "$1" >&2 if (( ${#COMPLETED[@]} )); then printf '[!] Étapes achevées : %s\n' "${COMPLETED[*]}" >&2 else printf '[!] Aucune étape achevée.\n' >&2 fi if (( ${#APPLIED[@]} )); then printf '[!] Actions déjà appliquées : %s\n' "${APPLIED[*]}" >&2 fi [[ -z "$CURRENT_ACTION" ]] || printf '[!] Action échouée, effets à vérifier : %s\n' "$CURRENT_ACTION" >&2 [[ -z "$BACKUP_DIR" ]] || printf '[!] Sauvegardes et marqueurs de cibles absentes : %s\n' "$BACKUP_DIR" >&2 printf '[!] Pas de rollback global : inspecter les changements avant reprise.\n' >&2 exit 1 } run() { local label="$1" shift CURRENT_ACTION="$label" "$@" || fatal "$label" CURRENT_ACTION='' } interrupt() { local signal="$1" status="$2" trap - INT TERM printf '[!] Interruption %s (%s), phase : %s\n' "$signal" "$status" "$CURRENT_STAGE" >&2 if (( ${#COMPLETED[@]} )); then printf '[!] Étapes achevées : %s\n' "${COMPLETED[*]}" >&2 else printf '[!] Aucune étape achevée.\n' >&2 fi if (( ${#APPLIED[@]} )); then printf '[!] Actions confirmées : %s\n' "${APPLIED[*]}" >&2 else printf '[!] Aucune action confirmée.\n' >&2 fi [[ -z "$CURRENT_ACTION" ]] || printf '[!] Action interrompue, effets à vérifier : %s\n' "$CURRENT_ACTION" >&2 [[ -z "$BACKUP_DIR" ]] || printf '[!] Sauvegardes et marqueurs conservés : %s\n' "$BACKUP_DIR" >&2 exit "$status" } cleanup() { local status="$1" if [[ -n "$TEMP_DIR" ]]; then # Supprimer uniquement les deux fichiers créés par cette invocation. rm -f -- "$TEMP_DIR/batlimit" "$TEMP_DIR/vmware-stack" || printf '[!] Nettoyage des sudoers temporaires incomplet : %s\n' "$TEMP_DIR" >&2 rmdir -- "$TEMP_DIR" 2>/dev/null || true fi exit "$status" } trap 'cleanup $?' EXIT trap 'interrupt INT 130' INT trap 'interrupt TERM 143' TERM while (( $# > 0 )); do case "$1" in --packages) DO_PACKAGES=1 ;; --configs) DO_CONFIGS=1 ;; --tools) DO_TOOLS=1 ;; --services) DO_SERVICES=1 ;; --all) DO_PACKAGES=1; DO_CONFIGS=1; DO_TOOLS=1; DO_SERVICES=1 ;; --dry-run) DRY_RUN=1 ;; -h|--help) usage; exit 0 ;; *) printf 'Option inconnue : %s\n' "$1" >&2; usage; exit 2 ;; esac shift done if (( DO_PACKAGES + DO_CONFIGS + DO_TOOLS + DO_SERVICES == 0 )); then usage exit 2 fi require_source() { [[ -f "$1" && -r "$1" && -s "$1" && ! -L "$1" ]] || fatal "source obligatoire absente, vide, illisible ou liée : $1" } # Une liste présente et vide est volontairement vide. Une substitution de # processus cacherait une erreur de lecture : lire directement et vérifier. read_list() { local file="$1" kind="$2" output_name="$3" line item local -n output="$output_name" local -A seen=() [[ -f "$file" && -r "$file" && ! -L "$file" ]] || fatal "liste/manifest absent, illisible ou lié : $file" while IFS= read -r line || [[ -n "$line" ]]; do item="${line#"${line%%[![:space:]]*}"}" item="${item%"${item##*[![:space:]]}"}" [[ -z "$item" || "$item" == \#* ]] && continue case "$kind" in package) [[ "$item" =~ ^[a-zA-Z0-9@][a-zA-Z0-9@._+-]*$ ]] || fatal "nom de paquet invalide dans $file : $item" ;; service) [[ "$item" =~ ^[a-zA-Z0-9@_.-]+\.service$ ]] || fatal "unité service invalide dans $file : $item" ;; timer) [[ "$item" =~ ^[a-zA-Z0-9@_.-]+\.timer$ ]] || fatal "unité timer invalide dans $file : $item" ;; esac [[ ! -v "seen[$item]" ]] || fatal "doublon dans $file : $item" seen["$item"]=1 output+=("$item") done < "$file" || fatal "lecture échouée : $file" } require_command() { command -v "$1" >/dev/null 2>&1 || fatal "commande requise absente : $1" } read_machine_name() { [[ -r /etc/hostname ]] || fatal 'nom de machine indisponible : /etc/hostname illisible ou absent' IFS= read -r MACHINE_NAME < /etc/hostname || [[ -n "$MACHINE_NAME" ]] || fatal 'lecture du nom de machine échouée' [[ "$MACHINE_NAME" =~ ^[a-zA-Z0-9][a-zA-Z0-9.-]*$ ]] || fatal 'nom de machine invalide dans /etc/hostname' } check_beneficiary() { local uid # Ne jamais utiliser USER/SUDO_USER comme autorité pour sudoers ou AUR. (( EUID != 0 )) || fatal 'compte root refusé pour cette invocation' BENEFICIARY="$(id -un)" || fatal 'identité du compte indisponible' uid="$(id -u "$BENEFICIARY")" || fatal 'compte bénéficiaire inexistant' [[ "$BENEFICIARY" =~ ^[a-z_][a-z0-9_-]*\$?$ && "$uid" =~ ^[0-9]+$ ]] || fatal 'nom de compte bénéficiaire invalide' (( uid > 0 )) || fatal 'compte bénéficiaire root refusé' } check_target_local() { local parent="${1%/*}" while [[ "$parent" != / ]]; do [[ ! -L "$parent" ]] || fatal "répertoire cible lié refusé : $parent" [[ ! -e "$parent" || -d "$parent" ]] || fatal "parent non répertoire refusé : $parent" parent="${parent%/*}" [[ -n "$parent" ]] || parent=/ done [[ ! -L "$1" ]] || fatal "cible liée refusée : $1" [[ ! -e "$1" || -f "$1" ]] || fatal "cible non régulière refusée : $1" } check_unit() { systemctl cat "$1" >/dev/null 2>&1 || fatal "unité requise absente : $1" } check_units() { local unit if (( DO_SERVICES )); then for unit in "${SYSTEM_SERVICES[@]}" "${SYSTEM_TIMERS[@]}"; do check_unit "$unit" done fi if (( DO_TOOLS || DO_SERVICES )); then for unit in "${ON_DEMAND_SERVICES[@]}"; do check_unit "$unit" done fi } preflight_static() { local i name if (( DO_PACKAGES )); then read_list "$REPO_DIR/pkglist/pacman.txt" package OFFICIAL_PACKAGES read_list "$REPO_DIR/pkglist/aur.txt" package FOREIGN_PACKAGES require_command pacman if (( ${#FOREIGN_PACKAGES[@]} )); then require_command yay check_beneficiary fi fi if (( DO_CONFIGS )); then for i in "${!CONFIG_SOURCES[@]}"; do require_source "$REPO_DIR/${CONFIG_SOURCES[i]}" check_target_local "${CONFIG_TARGETS[i]}" done if (( ! DO_PACKAGES )); then require_command nft; fi require_command systemctl fi if (( DO_TOOLS )); then check_beneficiary for name in "${HELPER_NAMES[@]}"; do require_source "$REPO_DIR/rootfs/usr/local/sbin/$name" check_target_local "/usr/local/sbin/$name" done for name in "${SUDOERS_NAMES[@]}"; do require_source "$REPO_DIR/templates/sudoers.d/$name" grep -Fq '@USER@' "$REPO_DIR/templates/sudoers.d/$name" || fatal "placeholder @USER@ absent : $name" check_target_local "/etc/sudoers.d/$name" done require_command visudo fi if (( DO_SERVICES )); then read_list "$REPO_DIR/manifests/system-services.txt" service SYSTEM_SERVICES read_list "$REPO_DIR/manifests/system-timers.txt" timer SYSTEM_TIMERS fi if (( DO_TOOLS || DO_SERVICES )); then read_list "$REPO_DIR/manifests/on-demand-system-services.txt" service ON_DEMAND_SERVICES require_command systemctl fi if (( ! DO_PACKAGES )); then check_units fi if (( ! DRY_RUN )); then require_command sudo require_command install require_command mktemp require_command find read_machine_name DISPLAY_USER="$(id -un)" || fatal 'identité du compte indisponible' fi } show_plan() { local i name unit printf '[PLAN] Aucun changement appliqué ; validations privilégiées non exécutées.\n' if (( DO_PACKAGES )); then printf '[PLAN] %s -> pacman -Syu --needed (%s entrées)\n' \ "$REPO_DIR/pkglist/pacman.txt" "${#OFFICIAL_PACKAGES[@]}" printf '[PLAN] %s -> yay -S --needed (%s entrées étrangères)\n' \ "$REPO_DIR/pkglist/aur.txt" "${#FOREIGN_PACKAGES[@]}" fi if (( DO_CONFIGS )); then printf '[PLAN] Validation nftables privilégiée différée : %s\n' \ "$REPO_DIR/rootfs/etc/nftables.conf" for i in "${!CONFIG_SOURCES[@]}"; do printf '[PLAN] %s -> %s\n' \ "$REPO_DIR/${CONFIG_SOURCES[i]}" "${CONFIG_TARGETS[i]}" done printf '[PLAN] systemctl daemon-reload ; tlp start si présent.\n' fi if (( DO_TOOLS )); then for name in "${HELPER_NAMES[@]}"; do printf '[PLAN] %s -> /usr/local/sbin/%s\n' \ "$REPO_DIR/rootfs/usr/local/sbin/$name" "$name" done for name in "${SUDOERS_NAMES[@]}"; do printf '[PLAN] %s -> /etc/sudoers.d/%s (visudo différé, compte %s)\n' \ "$REPO_DIR/templates/sudoers.d/$name" "$name" "$BENEFICIARY" done fi if (( DO_SERVICES )); then for unit in "${SYSTEM_SERVICES[@]}" "${SYSTEM_TIMERS[@]}"; do printf '[PLAN] Activer : %s\n' "$unit" done fi if (( DO_TOOLS || DO_SERVICES )); then for unit in "${ON_DEMAND_SERVICES[@]}"; do printf '[PLAN] Désactiver et arrêter si nécessaire : %s\n' "$unit" done fi if (( DO_PACKAGES )); then printf '[PLAN] Unités fournies par les paquets : contrôle différé après installation.\n' fi printf '[PLAN] Plan statique valide, déploiement non effectué.\n' } preflight_privileged() { local name run 'autorisation sudo' sudo -v if (( DO_CONFIGS )); then if command -v nft >/dev/null 2>&1; then run 'validation nftables' sudo nft -c -f "$REPO_DIR/rootfs/etc/nftables.conf" elif (( ! DO_PACKAGES )); then fatal 'nft absent avant la phase configurations' fi fi if (( DO_TOOLS )); then TEMP_DIR="$(mktemp -d)" || fatal 'création du répertoire temporaire sudoers' for name in "${SUDOERS_NAMES[@]}"; do sed "s/@USER@/$BENEFICIARY/g" "$REPO_DIR/templates/sudoers.d/$name" \ > "$TEMP_DIR/$name" || fatal "rendu sudoers $name" run "validation sudoers $name" sudo visudo -cf "$TEMP_DIR/$name" done fi } # Seul un résultat strict, produit avec un code de succès, décrit la cible. # Un échec sudo/find, y compris avec une sortie plausible, reste indéterminé. check_target_privileged() { local target="$1" state if state="$(sudo /usr/bin/bash -c "$INSPECT_CODE" -- "$target")"; then : else fatal "contrôle privilégié impossible : $target" fi case "$state" in EXISTING) return 0 ;; ABSENT) return 1 ;; LINK) fatal "cible ou parent lié refusé : $target" ;; INVALID) fatal "cible ou parent non régulier refusé : $target" ;; *) fatal "réponse de contrôle invalide pour $target" ;; esac } ensure_backup_dir() { [[ -n "$BACKUP_DIR" ]] && return 0 # mktemp crée atomiquement un répertoire root privé, sans collision # entre deux invocations commencées dans la même seconde. BACKUP_DIR="$(sudo mktemp -d /root/arch-system-restore.XXXXXXXX)" || fatal 'création du répertoire privé de sauvegarde' [[ "$BACKUP_DIR" == /root/arch-system-restore.* ]] || fatal 'chemin de sauvegarde inattendu' } backup_target() { local target="$1" local exists=0 if check_target_privileged "$target"; then exists=1; fi ensure_backup_dir if (( exists )); then run "répertoire de sauvegarde $target" sudo mkdir -p \ "$BACKUP_DIR/existing$(dirname "$target")" run "sauvegarde $target" sudo cp -a -- "$target" \ "$BACKUP_DIR/existing$target" else # Un marqueur distinct d'une copie permet de retirer manuellement # une cible créée par ce déploiement sans inventer un ancien contenu. run "marqueur de cible absente $target" sudo install -D -m 0600 \ /dev/null "$BACKUP_DIR/absent$target" fi } install_target() { local source="$1" target="$2" mode="$3" backup_target "$target" # Réexaminer la cible après la copie et avant le remplacement ; un # changement concurrent ne doit pas transformer la destination en lien. if check_target_privileged "$target"; then :; fi run "installation $target" sudo install -D -m "$mode" "$source" "$target" APPLIED+=("installation:$target") } unit_state() { local unit="$1" property="$2" result if [[ "$property" == enabled ]]; then result="$(systemctl is-enabled "$unit" 2>/dev/null)" || : else result="$(systemctl is-active "$unit" 2>/dev/null)" || : fi [[ -n "$result" ]] || fatal "état systemd indéterminé : $unit" printf '%s' "$result" } disable_on_demand() { local unit enabled active for unit in "${ON_DEMAND_SERVICES[@]}"; do check_unit "$unit" enabled="$(unit_state "$unit" enabled)" active="$(unit_state "$unit" active)" if [[ "$enabled" == disabled && "$active" == inactive ]]; then printf '[*] Déjà désactivé/inactif : %s\n' "$unit" continue fi [[ "$enabled" =~ ^(enabled|enabled-runtime|disabled|masked)$ && "$active" =~ ^(active|activating|inactive|failed)$ ]] || fatal "état systemd non pris en charge : $unit ($enabled/$active)" run "désactivation $unit" sudo systemctl disable --now "$unit" APPLIED+=("désactivation:$unit") done } enable_units() { local unit for unit in "${SYSTEM_SERVICES[@]}" "${SYSTEM_TIMERS[@]}"; do check_unit "$unit" run "activation $unit" sudo systemctl enable "$unit" APPLIED+=("activation:$unit") done } preflight_static if (( DRY_RUN )); then show_plan exit 0 fi read -rp "Déployer arch-system sur ${MACHINE_NAME} pour ${DISPLAY_USER} ? [y/N] " confirm || fatal 'confirmation indisponible' [[ "$confirm" =~ ^[yY]$ ]] || { printf '[*] Déploiement annulé.\n'; exit 0; } # Les validations privilégiées précèdent tout changement durable ; les unités # fournies par les paquets demandés restent nécessairement vérifiées après eux. preflight_privileged if (( DO_PACKAGES )); then CURRENT_STAGE='paquets' if (( ${#OFFICIAL_PACKAGES[@]} )); then run 'installation des paquets natifs' sudo pacman -Syu --needed \ "${OFFICIAL_PACKAGES[@]}" APPLIED+=(paquets-natifs) else printf '[*] Liste native présente mais vide.\n' fi if (( ${#FOREIGN_PACKAGES[@]} )); then run 'installation des paquets étrangers' yay -S --needed \ "${FOREIGN_PACKAGES[@]}" APPLIED+=(paquets-étrangers) else printf '[*] Liste étrangère présente mais vide.\n' fi COMPLETED+=(paquets) if (( DO_CONFIGS )) && ! command -v nft >/dev/null 2>&1; then fatal 'nft reste absent après installation des paquets' fi if (( DO_CONFIGS )); then run 'validation nftables différée' sudo nft -c -f \ "$REPO_DIR/rootfs/etc/nftables.conf" fi if (( DO_TOOLS || DO_SERVICES )); then CURRENT_STAGE='contrôle différé des unités' check_units fi fi if (( DO_CONFIGS )); then CURRENT_STAGE='configurations' for i in "${!CONFIG_SOURCES[@]}"; do install_target "$REPO_DIR/${CONFIG_SOURCES[i]}" "${CONFIG_TARGETS[i]}" 0644 done run 'rechargement systemd' sudo systemctl daemon-reload APPLIED+=(rechargement-systemd) if command -v tlp >/dev/null 2>&1; then run 'démarrage TLP' sudo tlp start APPLIED+=(démarrage-tlp) fi COMPLETED+=(configurations) printf '[!] fstab, Limine et Secure Boot restent à finaliser manuellement.\n' fi if (( DO_TOOLS )); then CURRENT_STAGE='outils root' for name in "${HELPER_NAMES[@]}"; do install_target "$REPO_DIR/rootfs/usr/local/sbin/$name" \ "/usr/local/sbin/$name" 0755 done for name in "${SUDOERS_NAMES[@]}"; do install_target "$TEMP_DIR/$name" "/etc/sudoers.d/$name" 0440 done disable_on_demand COMPLETED+=(outils) fi if (( DO_SERVICES )); then CURRENT_STAGE='services système' enable_units disable_on_demand COMPLETED+=(services) fi printf '[+] Déploiement sélectionné terminé : %s\n' "${COMPLETED[*]}" [[ -z "$BACKUP_DIR" ]] || printf '[+] Sauvegardes et marqueurs : %s\n' "$BACKUP_DIR" printf '[!] Aucune transaction globale ni rollback automatique.\n' printf '[!] Vérifier fstab, Limine, mkinitcpio, Snapper, nftables et sbctl avant reboot.\n'