#!/usr/bin/env bash set -euo pipefail source "$stpRoot/lib/log.sh" source "$stpRoot/lib/utils.sh" source "$stpRoot/lib/yaml.sh" readonly registryConfig="$stpRoot/config/registry.yaml" fieldOf() { local entryId="$1" field="$2" default="${3:-}" local value value="$(yq ".registry[] | select(.id == \"$entryId\") | .${field}" "$registryConfig" 2>/dev/null)" if util::isYamlNull "$value"; then echo "$default" else echo "$value" fi } arrayFieldOf() { local entryId="$1" field="$2" yq -r ".registry[] | select(.id == \"$entryId\") | .${field}[]" "$registryConfig" 2>/dev/null || true } # ---- PPA ---- ppaOwnerFrom() { local ppaAddress="$1" local owner="${ppaAddress#ppa:}" echo "${owner%%/*}" } ppaIsAlreadyAdded() { local ppaAddress="$1" find /etc/apt/sources.list.d/ -name "*$(ppaOwnerFrom "$ppaAddress")*" 2>/dev/null | grep -q . } applyPpa() { local entryId="$1" local ppaAddress ppaAddress="$(fieldOf "$entryId" "address")" if ppaIsAlreadyAdded "$ppaAddress"; then log::info "PPA ya existe: $ppaAddress" return fi util::cmdExists add-apt-repository \ || sudo apt-get install -y software-properties-common sudo add-apt-repository -y "$ppaAddress" sudo apt-get update -qq log::ok "PPA agregado: $ppaAddress" } # ---- APT ---- installedVersionOf() { local packageName="$1" dpkg-query -W -f='${Version}' "$packageName" 2>/dev/null } aptVersionIsInRange() { local installedVersion="$1" minVersion="$2" maxVersion="$3" if [[ -n "$minVersion" ]] && dpkg --compare-versions "$installedVersion" lt "$minVersion"; then return 1 fi if [[ -n "$maxVersion" ]] && dpkg --compare-versions "$installedVersion" gt "$maxVersion"; then return 1 fi return 0 } applyAptPackage() { local entryId="$1" local packageName minVersion maxVersion packageName="$(fieldOf "$entryId" "package" "$entryId")" minVersion="$(fieldOf "$entryId" "minVersion")" maxVersion="$(fieldOf "$entryId" "maxVersion")" if util::isAptInstalled "$packageName"; then local installedVersion installedVersion="$(installedVersionOf "$packageName")" if aptVersionIsInRange "$installedVersion" "$minVersion" "$maxVersion"; then log::info "Ya instalado (apt): $packageName ($installedVersion)" return fi log::warn "$packageName: versión $installedVersion instalada, fuera del rango requerido" fi util::aptUpdateOnce if [[ -n "$maxVersion" ]]; then sudo apt-get install -y --allow-downgrades "${packageName}=${maxVersion}" log::ok "Instalado (apt): ${packageName}=${maxVersion}" else sudo apt-get install -y "$packageName" log::ok "Instalado (apt): $packageName" fi } # ---- SNAP ---- applySnapPackage() { local entryId="$1" local packageName classicFlag installFlags packageName="$(fieldOf "$entryId" "package" "$entryId")" classicFlag="$(fieldOf "$entryId" "classic" "false")" installFlags=() [[ "$classicFlag" == "true" ]] && installFlags+=(--classic) if util::isSnapInstalled "$packageName"; then log::info "Ya instalado (snap): $packageName" return fi if ! util::cmdExists snap; then log::warn "snap no disponible, salteando: $entryId" return fi sudo snap install "$packageName" "${installFlags[@]}" log::ok "Instalado (snap): $packageName" } # ---- FLATPAK ---- applyFlatpakApp() { local entryId="$1" local appId remote appId="$(fieldOf "$entryId" "appId" "$entryId")" remote="$(fieldOf "$entryId" "remote" "flathub")" if util::isFlatpakInstalled "$appId"; then log::info "Ya instalado (flatpak): $appId" return fi if ! util::cmdExists flatpak; then log::warn "flatpak no disponible, salteando: $entryId" return fi flatpak install -y "$remote" "$appId" log::ok "Instalado (flatpak): $appId" } # ---- DOTFILE ---- dotfileSymlinkAlreadyExists() { local destination="$1" [[ -L "$destination" ]] } backupExistingFile() { local destination="$1" [[ -e "$destination" ]] || return 0 mv "$destination" "${destination}.stpbackup" log::info "Copia de seguridad: ~/${destination#${HOME}/}" } deployFileFromElement() { local sourceFile="$1" elementSourceDirectory="$2" local relativePath destination relativePath="${sourceFile#${elementSourceDirectory}/}" destination="$HOME/$relativePath" if dotfileSymlinkAlreadyExists "$destination"; then log::info "Enlace ya existe: ~/$relativePath" return fi backupExistingFile "$destination" mkdir -p "$(dirname "$destination")" ln -sfn "$sourceFile" "$destination" log::ok "Enlazado: ~/$relativePath" } applyDotfile() { local entryId="$1" local elementSourceDirectory="$stpRoot/dotfiles/$entryId" if [[ ! -d "$elementSourceDirectory" ]]; then log::warn "Carpeta no encontrada: dotfiles/$entryId/" return fi while IFS= read -r sourceFile; do deployFileFromElement "$sourceFile" "$elementSourceDirectory" done < <(find "$elementSourceDirectory" -not -type d | sort) } # ---- SERVICE ---- applyService() { local entryId="$1" local serviceName scope state serviceName="$(fieldOf "$entryId" "name" "$entryId")" scope="$(fieldOf "$entryId" "scope" "system")" state="$(fieldOf "$entryId" "state" "enable")" local scopeFlags=() [[ "$scope" == "user" ]] && scopeFlags+=(--user) case "$state" in enable) systemctl "${scopeFlags[@]}" enable --now "$serviceName" 2>/dev/null \ || log::warn "No se pudo habilitar: $serviceName" ;; disable) systemctl "${scopeFlags[@]}" disable --now "$serviceName" 2>/dev/null || true ;; mask) systemctl "${scopeFlags[@]}" mask "$serviceName" 2>/dev/null || true ;; esac log::ok "Servicio [$scope] $state: $serviceName" } # ---- PIPEWIRE ---- allPipewirePackagesAreInstalled() { local entryId="$1" while IFS= read -r packageName; do util::isYamlNull "$packageName" && continue util::isAptInstalled "$packageName" || return 1 done < <(arrayFieldOf "$entryId" "packages") } installMissingPipewirePackages() { local entryId="$1" local missingPackages=() while IFS= read -r packageName; do util::isYamlNull "$packageName" && continue util::isAptInstalled "$packageName" || missingPackages+=("$packageName") done < <(arrayFieldOf "$entryId" "packages") if [[ ${#missingPackages[@]} -gt 0 ]]; then util::aptUpdateOnce sudo apt-get install -y "${missingPackages[@]}" fi } disablePulseaudio() { systemctl --user --now disable pulseaudio.service pulseaudio.socket 2>/dev/null || true systemctl --user mask pulseaudio 2>/dev/null || true } enablePipewireUserServices() { local entryId="$1" while IFS= read -r serviceName; do util::isYamlNull "$serviceName" && continue systemctl --user --now enable "$serviceName" 2>/dev/null \ || log::warn "No se pudo habilitar servicio de usuario: $serviceName" done < <(arrayFieldOf "$entryId" "userServices") } applyPipewire() { local entryId="$1" allPipewirePackagesAreInstalled "$entryId" \ || installMissingPipewirePackages "$entryId" local shouldReplace shouldReplace="$(fieldOf "$entryId" "replacePulseaudio" "false")" [[ "$shouldReplace" == "true" ]] && disablePulseaudio enablePipewireUserServices "$entryId" log::ok "PipeWire configurado" } # ---- VIDEO ---- detectGpuVendor() { if lspci 2>/dev/null | grep -qi nvidia; then echo "nvidia" elif lspci 2>/dev/null | grep -qi "amd\|radeon"; then echo "amd" elif lspci 2>/dev/null | grep -qi intel; then echo "intel" else echo "unknown" fi } applyVideoDrivers() { local entryId="$1" local gpuVendor gpuVendor="$(detectGpuVendor)" log::info "GPU detectada: $gpuVendor" if [[ "$gpuVendor" == "unknown" ]]; then log::warn "No se pudo identificar la GPU, salteando drivers" return fi local missingPackages=() while IFS= read -r packageName; do util::isYamlNull "$packageName" && continue util::isAptInstalled "$packageName" || missingPackages+=("$packageName") done < <(arrayFieldOf "$entryId" "drivers.${gpuVendor}.packages") if [[ ${#missingPackages[@]} -eq 0 ]]; then log::info "Drivers $gpuVendor ya instalados" return fi util::aptUpdateOnce sudo apt-get install -y "${missingPackages[@]}" log::ok "Drivers $gpuVendor instalados" } # ---- DOCKER ---- dockerIsInstalled() { util::cmdExists docker } findDockerComposeFile() { local destinationDirectory="$1" local composeFile for composeFile in "compose.yaml" "compose.yml" "docker-compose.yaml" "docker-compose.yml"; do [[ -f "$destinationDirectory/$composeFile" ]] && echo "$destinationDirectory/$composeFile" && return 0 done return 1 } deployDockerConfigFile() { local sourceFile="$1" sourceDirectory="$2" destinationDirectory="$3" local relativePath destination relativePath="${sourceFile#${sourceDirectory}/}" destination="$destinationDirectory/$relativePath" if [[ -L "$destination" ]]; then log::info "Enlace ya existe: $relativePath" return fi [[ -e "$destination" ]] && mv "$destination" "${destination}.stpbackup" mkdir -p "$(dirname "$destination")" ln -sfn "$sourceFile" "$destination" log::ok "Enlazado: $relativePath" } applyDocker() { local entryId="$1" local rawDestination destination autostart rawDestination="$(fieldOf "$entryId" "destination" "~/docker/$entryId")" destination="${rawDestination/#\~/$HOME}" autostart="$(fieldOf "$entryId" "autostart" "false")" local sourceDirectory="$stpRoot/docker/$entryId" if [[ ! -d "$sourceDirectory" ]]; then log::warn "Carpeta no encontrada: docker/$entryId/" return fi if ! dockerIsInstalled; then log::warn "Docker no instalado, salteando: $entryId" return fi mkdir -p "$destination" while IFS= read -r sourceFile; do deployDockerConfigFile "$sourceFile" "$sourceDirectory" "$destination" done < <(find "$sourceDirectory" -not -type d | sort) if [[ "$autostart" == "true" ]]; then local composeFile if composeFile="$(findDockerComposeFile "$destination")"; then docker compose -f "$composeFile" up -d \ || log::warn "No se pudo iniciar el servicio: $entryId" log::ok "Servicio Docker iniciado: $entryId" else log::warn "No se encontró archivo compose en: $destination" fi fi log::ok "Docker configurado: $entryId" } # ---- Dispatcher ---- dispatchByType() { local entryId="$1" entryType="$2" case "$entryType" in ppa) applyPpa "$entryId" ;; apt) applyAptPackage "$entryId" ;; snap) applySnapPackage "$entryId" ;; flatpak) applyFlatpakApp "$entryId" ;; dotfile) applyDotfile "$entryId" ;; service) applyService "$entryId" ;; pipewire) applyPipewire "$entryId" ;; video) applyVideoDrivers "$entryId" ;; docker) applyDocker "$entryId" ;; *) log::warn "Tipo no reconocido '$entryType' en entrada: $entryId" ;; esac } processRegistry() { local totalProcessed=0 while IFS=$'\t' read -r entryId entryType; do util::isYamlNull "$entryId" && continue util::isYamlNull "$entryType" && continue log::info "[$entryType] $entryId" dispatchByType "$entryId" "$entryType" ((++totalProcessed)) done < <(yq -r '.registry[] | [.id, .type] | @tsv' "$registryConfig") log::ok "$totalProcessed entrada(s) del registro procesadas" } if ! yaml::has "$registryConfig" ".registry[0]"; then log::info "Registro vacío, salteando" exit 0 fi processRegistry