diff --git a/fresh-install/install_amd_driver.sh b/fresh-install/install_amd_driver.sh index 6cd3711..083403d 100755 --- a/fresh-install/install_amd_driver.sh +++ b/fresh-install/install_amd_driver.sh @@ -13,8 +13,8 @@ set -e [ "${GPU_VENDOR}" = "amd" ] || { - echo "AMD installer invoked but GPU_VENDOR=${GPU_VENDOR}" - exit 0 + echo "AMD installer invoked but GPU_VENDOR=${GPU_VENDOR}" + exit 0 } AMD_INSTALL_XF86=${AMD_INSTALL_XF86:-0} @@ -26,15 +26,15 @@ AMD_ENABLE_SI_CIK=${AMD_ENABLE_SI_CIK:-auto} AMD_SKIP_INITRAMFS=${AMD_SKIP_INITRAMFS:-0} AMD_VERBOSE=${AMD_VERBOSE:-0} -vlog() { [ "$AMD_VERBOSE" = 1 ] && echo "[amd] $*" || true; } +vlog() { if [ "$AMD_VERBOSE" = 1 ]; then echo "[amd] $*"; fi; } info() { echo "[amd] $*"; } warn() { echo "[amd][warn] $*" >&2; } # Detect multilib enabled if grep -q '^\[multilib\]' /etc/pacman.conf; then - MULTILIB_ENABLED=1 + MULTILIB_ENABLED=1 else - MULTILIB_ENABLED=0 + MULTILIB_ENABLED=0 fi # Basic packages @@ -58,49 +58,49 @@ LIB32_AMDVLK_PKG="lib32-amdvlk" # Simple AUR builder (reused from NVIDIA script style) _build_aur_pkg() { - local pkg="$1" - local url="https://aur.archlinux.org/${pkg}.git" - mkdir -p "$HOME/aur" - cd "$HOME/aur" - if [ ! -d "$pkg" ]; then git clone "$url"; else (cd "$pkg" && git fetch -q --all && git reset -q --hard origin/HEAD || git pull --ff-only || true); fi - cd "$pkg" - rm -f -- *.pkg.tar.* 2> /dev/null || true - yes | makepkg -s -c -C --noconfirm --needed - local built=(*.pkg.tar.zst) - yes | sudo pacman -U --noconfirm "${built[@]}" + local pkg="$1" + local url="https://aur.archlinux.org/${pkg}.git" + mkdir -p "$HOME/aur" + cd "$HOME/aur" + if [ ! -d "$pkg" ]; then git clone "$url"; else (cd "$pkg" && git fetch -q --all && git reset -q --hard origin/HEAD || git pull --ff-only || true); fi + cd "$pkg" + rm -f -- *.pkg.tar.* 2>/dev/null || true + yes | makepkg -s -c -C --noconfirm --needed + local built=(*.pkg.tar.zst) + yes | sudo pacman -U --noconfirm "${built[@]}" } _install_repo_or_aur() { - local pkg="$1" - if pacman -Si "$pkg" > /dev/null 2>&1; then - if pacman -Qi "$pkg" > /dev/null 2>&1; then - vlog "$pkg already installed" - else - yes | sudo pacman -Sy --noconfirm "$pkg" - fi - else - info "Building AUR package: $pkg" - _build_aur_pkg "$pkg" - fi + local pkg="$1" + if pacman -Si "$pkg" >/dev/null 2>&1; then + if pacman -Qi "$pkg" >/dev/null 2>&1; then + vlog "$pkg already installed" + else + yes | sudo pacman -Sy --noconfirm "$pkg" + fi + else + info "Building AUR package: $pkg" + _build_aur_pkg "$pkg" + fi } info "Installing AMD GPU stack" for p in "${BASE_PKGS[@]}" "$VULKAN_PKG"; do _install_repo_or_aur "$p"; done if [ "$AMD_INSTALL_XF86" = 1 ]; then - _install_repo_or_aur "$XF86_PKG" + _install_repo_or_aur "$XF86_PKG" fi # AMDVLK optional (install after vulkan-radeon if requested) if [ "$AMD_INSTALL_AMDVLK" = 1 ]; then - _install_repo_or_aur "$AMDVLK_PKG" + _install_repo_or_aur "$AMDVLK_PKG" fi if [ $MULTILIB_ENABLED = 1 ] || [ "$AMD_INSTALL_LIB32" = 1 ]; then - for p in "${LIB32_BASE[@]}" "$LIB32_VULKAN_PKG"; do _install_repo_or_aur "$p"; done - if [ "$AMD_INSTALL_AMDVLK" = 1 ]; then _install_repo_or_aur "$LIB32_AMDVLK_PKG"; fi + for p in "${LIB32_BASE[@]}" "$LIB32_VULKAN_PKG"; do _install_repo_or_aur "$p"; done + if [ "$AMD_INSTALL_AMDVLK" = 1 ]; then _install_repo_or_aur "$LIB32_AMDVLK_PKG"; fi else - vlog "Skipping 32-bit packages (multilib disabled)" + vlog "Skipping 32-bit packages (multilib disabled)" fi # Detect SI / CIK codename presence for optional amdgpu enablement @@ -113,41 +113,41 @@ for n in "${SI_NAMES[@]}"; do echo "$GPU_LINES" | grep -q "$n" && IS_SI=1 && bre for n in "${CIK_NAMES[@]}"; do echo "$GPU_LINES" | grep -q "$n" && IS_CIK=1 && break; done if [ "$AMD_ENABLE_SI_CIK" = "1" ] || { [ "$AMD_ENABLE_SI_CIK" = "auto" ] && { [ $IS_SI = 1 ] || [ $IS_CIK = 1 ]; }; }; then - info "Configuring amdgpu for SI/CIK (IS_SI=$IS_SI IS_CIK=$IS_CIK)" - TMP_CONF=$(mktemp) - printf 'options amdgpu si_support=1\noptions amdgpu cik_support=1\n' > "$TMP_CONF" - printf 'options radeon si_support=0\noptions radeon cik_support=0\n' >> "$TMP_CONF" - sudo mkdir -p /etc/modprobe.d - sudo cp "$TMP_CONF" /etc/modprobe.d/10-amdgpu-si-cik.conf - rm -f "$TMP_CONF" - # Ensure amdgpu early in MODULES - if [ -f /etc/mkinitcpio.conf ]; then - if ! grep -q '^MODULES=.*amdgpu' /etc/mkinitcpio.conf; then - sudo sed -i 's/^MODULES=\(.*\)/MODULES=(amdgpu radeon)/' /etc/mkinitcpio.conf || true - fi - if ! grep -q 'modconf' /etc/mkinitcpio.conf; then - warn "modconf hook not found in mkinitcpio.conf (needed for module options)" - fi - if [ "$AMD_SKIP_INITRAMFS" != 1 ]; then - info "Regenerating initramfs (mkinitcpio -P)" - sudo mkinitcpio -P || warn "mkinitcpio failed; review manually" - else - info "Skipping initramfs regeneration per AMD_SKIP_INITRAMFS=1" - fi - else - warn "/etc/mkinitcpio.conf not found; skipping MODULES update" - fi + info "Configuring amdgpu for SI/CIK (IS_SI=$IS_SI IS_CIK=$IS_CIK)" + TMP_CONF=$(mktemp) + printf 'options amdgpu si_support=1\noptions amdgpu cik_support=1\n' >"$TMP_CONF" + printf 'options radeon si_support=0\noptions radeon cik_support=0\n' >>"$TMP_CONF" + sudo mkdir -p /etc/modprobe.d + sudo cp "$TMP_CONF" /etc/modprobe.d/10-amdgpu-si-cik.conf + rm -f "$TMP_CONF" + # Ensure amdgpu early in MODULES + if [ -f /etc/mkinitcpio.conf ]; then + if ! grep -q '^MODULES=.*amdgpu' /etc/mkinitcpio.conf; then + sudo sed -i 's/^MODULES=\(.*\)/MODULES=(amdgpu radeon)/' /etc/mkinitcpio.conf || true + fi + if ! grep -q 'modconf' /etc/mkinitcpio.conf; then + warn "modconf hook not found in mkinitcpio.conf (needed for module options)" + fi + if [ "$AMD_SKIP_INITRAMFS" != 1 ]; then + info "Regenerating initramfs (mkinitcpio -P)" + sudo mkinitcpio -P || warn "mkinitcpio failed; review manually" + else + info "Skipping initramfs regeneration per AMD_SKIP_INITRAMFS=1" + fi + else + warn "/etc/mkinitcpio.conf not found; skipping MODULES update" + fi else - vlog "SI/CIK enablement not required (AMD_ENABLE_SI_CIK=$AMD_ENABLE_SI_CIK IS_SI=$IS_SI IS_CIK=$IS_CIK)" + vlog "SI/CIK enablement not required (AMD_ENABLE_SI_CIK=$AMD_ENABLE_SI_CIK IS_SI=$IS_SI IS_CIK=$IS_CIK)" fi # Check active kernel driver -KDRV=$(lspci -k -d ::0300 2> /dev/null | awk '/Kernel driver in use:/ {print $5; exit}') +KDRV=$(lspci -k -d ::0300 2>/dev/null | awk '/Kernel driver in use:/ {print $5; exit}') [ -z "$KDRV" ] && KDRV=$(lsmod | grep -E 'amdgpu|radeon' | head -n1 | awk '{print $1}') info "Kernel driver in use: ${KDRV:-unknown}" if [ "$KDRV" = "radeon" ] && { [ $IS_SI = 1 ] || [ $IS_CIK = 1 ]; }; then - warn "radeon driver still active for SI/CIK; reboot may be required to switch to amdgpu" + warn "radeon driver still active for SI/CIK; reboot may be required to switch to amdgpu" fi export AMD_STACK_DONE=1 diff --git a/fresh-install/install_intel_driver.sh b/fresh-install/install_intel_driver.sh index f37c0e8..4dab607 100755 --- a/fresh-install/install_intel_driver.sh +++ b/fresh-install/install_intel_driver.sh @@ -13,8 +13,8 @@ set -e [ "$GPU_VENDOR" = "intel" ] || { - echo "Intel installer invoked but GPU_VENDOR=$GPU_VENDOR" - exit 0 + echo "Intel installer invoked but GPU_VENDOR=$GPU_VENDOR" + exit 0 } INTEL_USE_AMBER=${INTEL_USE_AMBER:-0} @@ -26,7 +26,7 @@ INTEL_ENABLE_GUC=${INTEL_ENABLE_GUC:-} INTEL_SKIP_INITRAMFS=${INTEL_SKIP_INITRAMFS:-0} INTEL_VERBOSE=${INTEL_VERBOSE:-1} -vlog() { [ "$INTEL_VERBOSE" = 1 ] && echo "[intel] $*" || true; } +vlog() { if [ "$INTEL_VERBOSE" = 1 ]; then echo "[intel] $*"; fi; } info() { echo "[intel] $*"; } warn() { echo "[intel][warn] $*" >&2; } @@ -35,24 +35,24 @@ if grep -q '^\[multilib\]' /etc/pacman.conf; then MULTILIB=1; else MULTILIB=0; f # Base mesa package if [ "$INTEL_USE_AMBER" = 1 ]; then - BASE_MESA=mesa-amber - LIB32_BASE=lib32-mesa-amber + BASE_MESA=mesa-amber + LIB32_BASE=lib32-mesa-amber else - BASE_MESA=mesa - LIB32_BASE=lib32-mesa + BASE_MESA=mesa + LIB32_BASE=lib32-mesa fi install_pkg() { - local pkg="$1" - if pacman -Qi "$pkg" > /dev/null 2>&1; then - vlog "$pkg already installed" - else - if pacman -Si "$pkg" > /dev/null 2>&1; then - yes | sudo pacman -Sy --noconfirm "$pkg" - else - warn "Package $pkg not found in repos (not handling AUR here)" - fi - fi + local pkg="$1" + if pacman -Qi "$pkg" >/dev/null 2>&1; then + vlog "$pkg already installed" + else + if pacman -Si "$pkg" >/dev/null 2>&1; then + yes | sudo pacman -Sy --noconfirm "$pkg" + else + warn "Package $pkg not found in repos (not handling AUR here)" + fi + fi } info "Installing Intel GPU stack" @@ -60,47 +60,47 @@ install_pkg "$BASE_MESA" # 32-bit mesa if { [ "$INTEL_INSTALL_LIB32" = auto ] && [ $MULTILIB = 1 ]; } || [ "$INTEL_INSTALL_LIB32" = 1 ]; then - install_pkg "$LIB32_BASE" + install_pkg "$LIB32_BASE" else - vlog "Skipping 32-bit mesa (INTEL_INSTALL_LIB32=$INTEL_INSTALL_LIB32 MULTILIB=$MULTILIB)" + vlog "Skipping 32-bit mesa (INTEL_INSTALL_LIB32=$INTEL_INSTALL_LIB32 MULTILIB=$MULTILIB)" fi # Vulkan if [ "$INTEL_INSTALL_VULKAN" = 1 ]; then - install_pkg vulkan-intel - if { [ "$INTEL_INSTALL_LIB32_VK" = auto ] && [ $MULTILIB = 1 ]; } || [ "$INTEL_INSTALL_LIB32_VK" = 1 ]; then - install_pkg lib32-vulkan-intel - else - vlog "Skipping 32-bit vulkan (INTEL_INSTALL_LIB32_VK=$INTEL_INSTALL_LIB32_VK MULTILIB=$MULTILIB)" - fi + install_pkg vulkan-intel + if { [ "$INTEL_INSTALL_LIB32_VK" = auto ] && [ $MULTILIB = 1 ]; } || [ "$INTEL_INSTALL_LIB32_VK" = 1 ]; then + install_pkg lib32-vulkan-intel + else + vlog "Skipping 32-bit vulkan (INTEL_INSTALL_LIB32_VK=$INTEL_INSTALL_LIB32_VK MULTILIB=$MULTILIB)" + fi fi # Legacy xf86-video-intel (not recommended) if [ "$INTEL_INSTALL_XF86" = 1 ]; then - install_pkg xf86-video-intel + install_pkg xf86-video-intel else - vlog "Not installing xf86-video-intel (INTEL_INSTALL_XF86=$INTEL_INSTALL_XF86)" + vlog "Not installing xf86-video-intel (INTEL_INSTALL_XF86=$INTEL_INSTALL_XF86)" fi # GuC / HuC enablement if [ -n "$INTEL_ENABLE_GUC" ]; then - if ! echo "$INTEL_ENABLE_GUC" | grep -Eq '^[0-3]$'; then - warn "INTEL_ENABLE_GUC must be 0..3; ignoring" - else - info "Configuring enable_guc=$INTEL_ENABLE_GUC" - sudo mkdir -p /etc/modprobe.d - echo "options i915 enable_guc=$INTEL_ENABLE_GUC" | sudo tee /etc/modprobe.d/i915-guc.conf > /dev/null - if [ "$INTEL_SKIP_INITRAMFS" != 1 ] && [ -f /etc/mkinitcpio.conf ]; then - info "Regenerating initramfs (mkinitcpio -P) for GuC/HuC change" - sudo mkinitcpio -P || warn "mkinitcpio failed; continue manually" - else - info "Skipping initramfs regeneration (INTEL_SKIP_INITRAMFS=$INTEL_SKIP_INITRAMFS)" - fi - fi + if ! echo "$INTEL_ENABLE_GUC" | grep -Eq '^[0-3]$'; then + warn "INTEL_ENABLE_GUC must be 0..3; ignoring" + else + info "Configuring enable_guc=$INTEL_ENABLE_GUC" + sudo mkdir -p /etc/modprobe.d + echo "options i915 enable_guc=$INTEL_ENABLE_GUC" | sudo tee /etc/modprobe.d/i915-guc.conf >/dev/null + if [ "$INTEL_SKIP_INITRAMFS" != 1 ] && [ -f /etc/mkinitcpio.conf ]; then + info "Regenerating initramfs (mkinitcpio -P) for GuC/HuC change" + sudo mkinitcpio -P || warn "mkinitcpio failed; continue manually" + else + info "Skipping initramfs regeneration (INTEL_SKIP_INITRAMFS=$INTEL_SKIP_INITRAMFS)" + fi + fi fi # Report kernel driver -KDRV=$(lspci -k -d ::0300 2> /dev/null | awk '/Kernel driver in use:/ {print $5; exit}') +KDRV=$(lspci -k -d ::0300 2>/dev/null | awk '/Kernel driver in use:/ {print $5; exit}') [ -z "$KDRV" ] && KDRV=$(lsmod | grep -E 'i915|xe' | head -n1 | awk '{print $1}') info "Kernel driver in use: ${KDRV:-unknown}" diff --git a/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh b/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh index 308738f..e407961 100644 --- a/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh +++ b/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh @@ -7,59 +7,63 @@ TARGET=/etc/hosts LOGTAG=hosts-guard-hook stop_units_if_present() { - local units=(hosts-bind-mount.service hosts-guard.path) - for u in "${units[@]}"; do - if command -v systemctl > /dev/null 2>&1; then - if systemctl list-unit-files 2> /dev/null | grep -q "^$u"; then - systemctl stop "$u" > /dev/null 2>&1 || true - fi - fi - done + local units=(hosts-bind-mount.service hosts-guard.path) + for u in "${units[@]}"; do + if command -v systemctl >/dev/null 2>&1; then + if systemctl list-unit-files 2>/dev/null | grep -q "^$u"; then + systemctl stop "$u" >/dev/null 2>&1 || true + fi + fi + done } -is_ro_mount() { findmnt -no OPTIONS -T "$TARGET" 2> /dev/null | grep -qw ro; } +is_ro_mount() { findmnt -no OPTIONS -T "$TARGET" 2>/dev/null | grep -qw ro; } -mount_layers_count() { awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2> /dev/null || echo 0; } +mount_layers_count() { awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0; } cleanup_mount_stacks() { - local i=0 - # Unmount bind layers until /etc/hosts is no longer a mountpoint - if command -v mountpoint > /dev/null 2>&1; then - while mountpoint -q "$TARGET"; do - umount -l "$TARGET" > /dev/null 2>&1 || break - i=$((i + 1)) - ((i > 20)) && break - done - else - # Fallback to best-effort using mountinfo count - local cnt - cnt=$(mount_layers_count) - while ((cnt > 1)); do - umount -l "$TARGET" > /dev/null 2>&1 || break - i=$((i + 1)) - ((i > 20)) && break - cnt=$(mount_layers_count) - done - fi + local i=0 + # Unmount bind layers until /etc/hosts is no longer a mountpoint + if command -v mountpoint >/dev/null 2>&1; then + while mountpoint -q "$TARGET"; do + umount -l "$TARGET" >/dev/null 2>&1 || break + i=$((i + 1)) + ((i > 20)) && break + done + else + # Fallback to best-effort using mountinfo count + local cnt + cnt=$(mount_layers_count) + while ((cnt > 1)); do + umount -l "$TARGET" >/dev/null 2>&1 || break + i=$((i + 1)) + ((i > 20)) && break + cnt=$(mount_layers_count) + done + fi } # Drop protective attributes if present -if command -v lsattr > /dev/null 2>&1; then - attrs=$(lsattr -d "$TARGET" 2> /dev/null || true) - echo "$attrs" | grep -q " i " && chattr -i "$TARGET" > /dev/null 2>&1 || true - echo "$attrs" | grep -q " a " && chattr -a "$TARGET" > /dev/null 2>&1 || true +if command -v lsattr >/dev/null 2>&1; then + attrs=$(lsattr -d "$TARGET" 2>/dev/null || true) + if echo "$attrs" | grep -q " i "; then + chattr -i "$TARGET" >/dev/null 2>&1 || true + fi + if echo "$attrs" | grep -q " a "; then + chattr -a "$TARGET" >/dev/null 2>&1 || true + fi fi stop_units_if_present logger -t "$LOGTAG" "pre: unlocking /etc/hosts (starting)" -echo "$(date -Is) pre-unlock" >> /run/hosts-guard-hook.log 2> /dev/null || true +echo "$(date -Is) pre-unlock" >>/run/hosts-guard-hook.log 2>/dev/null || true # Always collapse any existing layers; we'll operate on the plain file cleanup_mount_stacks # If someone managed a ro single-layer mount, ensure rw by remounting or collapsing again if is_ro_mount; then - mount -o remount,rw "$TARGET" > /dev/null 2>&1 || cleanup_mount_stacks + mount -o remount,rw "$TARGET" >/dev/null 2>&1 || cleanup_mount_stacks fi logger -t "$LOGTAG" "pre: unlocking /etc/hosts (done)" diff --git a/scripts/digital_wellbeing/pacman/pacman_wrapper.sh b/scripts/digital_wellbeing/pacman/pacman_wrapper.sh index a1f339b..7c15f5e 100755 --- a/scripts/digital_wellbeing/pacman/pacman_wrapper.sh +++ b/scripts/digital_wellbeing/pacman/pacman_wrapper.sh @@ -18,648 +18,670 @@ declare -a WHITELISTED_NAMES_LIST=() POLICY_LISTS_LOADED=0 load_policy_lists() { - if [[ $POLICY_LISTS_LOADED -eq 1 ]]; then - return - fi + if [[ $POLICY_LISTS_LOADED -eq 1 ]]; then + return + fi - local script_dir - script_dir="$(dirname "$(readlink -f "$0")")" - local blocked_file="$script_dir/pacman_blocked_keywords.txt" - local whitelist_file="$script_dir/pacman_whitelist.txt" + local script_dir + script_dir="$(dirname "$(readlink -f "$0")")" + local blocked_file="$script_dir/pacman_blocked_keywords.txt" + local whitelist_file="$script_dir/pacman_whitelist.txt" - if [[ -f $blocked_file ]]; then - mapfile -t BLOCKED_KEYWORDS_LIST < <(sed 's/\r$//' "$blocked_file" | grep -Ev '^[[:space:]]*(#|$)' || true) - else - BLOCKED_KEYWORDS_LIST=() - echo -e "${YELLOW}Warning:${NC} Missing blocked keywords file at $blocked_file" >&2 - fi + if [[ -f $blocked_file ]]; then + mapfile -t BLOCKED_KEYWORDS_LIST < <(sed 's/\r$//' "$blocked_file" | grep -Ev '^[[:space:]]*(#|$)' || true) + else + BLOCKED_KEYWORDS_LIST=() + echo -e "${YELLOW}Warning:${NC} Missing blocked keywords file at $blocked_file" >&2 + fi - if [[ -f $whitelist_file ]]; then - mapfile -t WHITELISTED_NAMES_LIST < <(sed 's/\r$//' "$whitelist_file" | grep -Ev '^[[:space:]]*(#|$)' || true) - else - WHITELISTED_NAMES_LIST=() - fi + if [[ -f $whitelist_file ]]; then + mapfile -t WHITELISTED_NAMES_LIST < <(sed 's/\r$//' "$whitelist_file" | grep -Ev '^[[:space:]]*(#|$)' || true) + else + WHITELISTED_NAMES_LIST=() + fi - for i in "${!BLOCKED_KEYWORDS_LIST[@]}"; do - BLOCKED_KEYWORDS_LIST[i]="${BLOCKED_KEYWORDS_LIST[i],,}" - done + for i in "${!BLOCKED_KEYWORDS_LIST[@]}"; do + BLOCKED_KEYWORDS_LIST[i]="${BLOCKED_KEYWORDS_LIST[i],,}" + done - for i in "${!WHITELISTED_NAMES_LIST[@]}"; do - WHITELISTED_NAMES_LIST[i]="${WHITELISTED_NAMES_LIST[i],,}" - done + for i in "${!WHITELISTED_NAMES_LIST[@]}"; do + WHITELISTED_NAMES_LIST[i]="${WHITELISTED_NAMES_LIST[i],,}" + done - POLICY_LISTS_LOADED=1 + POLICY_LISTS_LOADED=1 } # Determine if this invocation may perform a transaction (upgrade/install/remove) needs_unlock() { - # If args include -S (install/upgrade), -U (local install), or -R (remove), we unlock - # Also include -Su/-Syu/-Syuu when -S is part of the combined flag - for arg in "$@"; do - case "$arg" in - -S* | -U | -R | --sync | --upgrade | --remove) - return 0 - ;; - esac - done - return 1 + # If args include -S (install/upgrade), -U (local install), or -R (remove), we unlock + # Also include -Su/-Syu/-Syuu when -S is part of the combined flag + for arg in "$@"; do + case "$arg" in + -S* | -U | -R | --sync | --upgrade | --remove) + return 0 + ;; + esac + done + return 1 } # Run pre/post hooks for /etc/hosts guard if present pre_unlock_hosts() { - local pre="/usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh" - if [[ -x $pre ]]; then - echo -e "${CYAN}[hosts-guard] Preparing /etc/hosts for transaction...${NC}" >&2 - /bin/bash "$pre" || true - fi + local pre="/usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh" + if [[ -x $pre ]]; then + echo -e "${CYAN}[hosts-guard] Preparing /etc/hosts for transaction...${NC}" >&2 + /bin/bash "$pre" || true + fi } post_relock_hosts() { - local post="/usr/local/share/hosts-guard/pacman-post-relock-hosts.sh" - if [[ -x $post ]]; then - /bin/bash "$post" || true - echo -e "${CYAN}[hosts-guard] Protections re-applied to /etc/hosts.${NC}" >&2 - fi + local post="/usr/local/share/hosts-guard/pacman-post-relock-hosts.sh" + if [[ -x $post ]]; then + /bin/bash "$post" || true + echo -e "${CYAN}[hosts-guard] Protections re-applied to /etc/hosts.${NC}" >&2 + fi } # Ensure periodic system services (timer/monitor) are set up; if not, trigger setup ensure_periodic_maintenance() { - # Only proceed if systemd/systemctl is available - if ! command -v systemctl > /dev/null 2>&1; then - return 0 - fi + # Only proceed if systemd/systemctl is available + if ! command -v systemctl >/dev/null 2>&1; then + return 0 + fi - local timer_unit="periodic-system-maintenance.timer" - local startup_unit="periodic-system-startup.service" - local monitor_unit="hosts-file-monitor.service" - local needs_setup=0 + local timer_unit="periodic-system-maintenance.timer" + local startup_unit="periodic-system-startup.service" + local monitor_unit="hosts-file-monitor.service" + local needs_setup=0 - # Timer should be enabled and active - systemctl --quiet is-enabled "$timer_unit" || needs_setup=1 - systemctl --quiet is-active "$timer_unit" || needs_setup=1 + # Timer should be enabled and active + systemctl --quiet is-enabled "$timer_unit" || needs_setup=1 + systemctl --quiet is-active "$timer_unit" || needs_setup=1 - # Monitor should be enabled and active - systemctl --quiet is-enabled "$monitor_unit" || needs_setup=1 - systemctl --quiet is-active "$monitor_unit" || needs_setup=1 + # Monitor should be enabled and active + systemctl --quiet is-enabled "$monitor_unit" || needs_setup=1 + systemctl --quiet is-active "$monitor_unit" || needs_setup=1 - # Startup service should be enabled (it’s oneshot and may not be active except at boot) - systemctl --quiet is-enabled "$startup_unit" || needs_setup=1 + # Startup service should be enabled (it’s oneshot and may not be active except at boot) + systemctl --quiet is-enabled "$startup_unit" || needs_setup=1 - if [[ $needs_setup -eq 0 ]]; then - return 0 - fi + if [[ $needs_setup -eq 0 ]]; then + return 0 + fi - echo -e "${YELLOW}Periodic maintenance services missing or inactive. Running setup...${NC}" >&2 + echo -e "${YELLOW}Periodic maintenance services missing or inactive. Running setup...${NC}" >&2 - # Try to locate setup_periodic_system.sh - local setup_script="" - local self_dir - self_dir="$(dirname "$(readlink -f "$0")")" - if [[ -f "$self_dir/setup_periodic_system.sh" ]]; then - setup_script="$self_dir/setup_periodic_system.sh" - elif [[ -f "$HOME/linux-configuration/scripts/setup_periodic_system.sh" ]]; then - setup_script="$HOME/linux-configuration/scripts/setup_periodic_system.sh" - fi + # Try to locate setup_periodic_system.sh + local setup_script="" + local self_dir + self_dir="$(dirname "$(readlink -f "$0")")" + if [[ -f "$self_dir/setup_periodic_system.sh" ]]; then + setup_script="$self_dir/setup_periodic_system.sh" + elif [[ -f "$HOME/linux-configuration/scripts/setup_periodic_system.sh" ]]; then + setup_script="$HOME/linux-configuration/scripts/setup_periodic_system.sh" + fi - if [[ -n $setup_script ]]; then - if [[ $EUID -ne 0 ]]; then - sudo bash "$setup_script" - else - bash "$setup_script" - fi - echo -e "${CYAN}Tip:${NC} To disable these later:" >&2 - echo " sudo systemctl disable periodic-system-maintenance.timer" >&2 - echo " sudo systemctl disable periodic-system-startup.service" >&2 - echo " sudo systemctl disable hosts-file-monitor.service" >&2 - else - echo -e "${RED}Could not locate setup_periodic_system.sh to configure services automatically.${NC}" >&2 - fi + if [[ -n $setup_script ]]; then + if [[ $EUID -ne 0 ]]; then + sudo bash "$setup_script" + else + bash "$setup_script" + fi + echo -e "${CYAN}Tip:${NC} To disable these later:" >&2 + echo " sudo systemctl disable periodic-system-maintenance.timer" >&2 + echo " sudo systemctl disable periodic-system-startup.service" >&2 + echo " sudo systemctl disable hosts-file-monitor.service" >&2 + else + echo -e "${RED}Could not locate setup_periodic_system.sh to configure services automatically.${NC}" >&2 + fi } # Function to display help function show_help() { - echo -e "${BOLD}Pacman Wrapper Help${NC}" - echo "This wrapper adds helpful features while preserving all pacman functionality." - echo "" - echo "Additional commands:" - echo " --help-wrapper Show this help message" + echo -e "${BOLD}Pacman Wrapper Help${NC}" + echo "This wrapper adds helpful features while preserving all pacman functionality." + echo "" + echo "Additional commands:" + echo " --help-wrapper Show this help message" } # Function to display a message before executing function display_operation() { - case "$1" in - -S | -Sy | -S\ *) - echo -e "${BLUE}Installing packages...${NC}" >&2 - ;; - -Syu | -Syyu) - echo -e "${BLUE}Updating system...${NC}" >&2 - ;; - -R | -Rs | -Rns | -R\ *) - echo -e "${YELLOW}Removing packages...${NC}" >&2 - ;; - -Ss | -Ss\ *) - echo -e "${CYAN}Searching for packages...${NC}" >&2 - ;; - -Q | -Qs | -Qi | -Ql | -Q\ *) - echo -e "${CYAN}Querying package database...${NC}" >&2 - ;; - -U | -U\ *) - echo -e "${BLUE}Installing local packages...${NC}" >&2 - ;; - -Scc) - echo -e "${YELLOW}Cleaning package cache...${NC}" >&2 - ;; - *) - echo -e "${CYAN}Executing pacman command...${NC}" >&2 - ;; - esac + case "$1" in + -S | -Sy | -S\ *) + echo -e "${BLUE}Installing packages...${NC}" >&2 + ;; + -Syu | -Syyu) + echo -e "${BLUE}Updating system...${NC}" >&2 + ;; + -R | -Rs | -Rns | -R\ *) + echo -e "${YELLOW}Removing packages...${NC}" >&2 + ;; + -Ss | -Ss\ *) + echo -e "${CYAN}Searching for packages...${NC}" >&2 + ;; + -Q | -Qs | -Qi | -Ql | -Q\ *) + echo -e "${CYAN}Querying package database...${NC}" >&2 + ;; + -U | -U\ *) + echo -e "${BLUE}Installing local packages...${NC}" >&2 + ;; + -Scc) + echo -e "${YELLOW}Cleaning package cache...${NC}" >&2 + ;; + *) + echo -e "${CYAN}Executing pacman command...${NC}" >&2 + ;; + esac } # Helper: return 0 if the given package name is blocked by policy function is_blocked_package_name() { - load_policy_lists - local normalized="${1,,}" + load_policy_lists + local normalized="${1,,}" - for allowed in "${WHITELISTED_NAMES_LIST[@]}"; do - if [[ $normalized == "$allowed" ]]; then - return 1 - fi - done + for allowed in "${WHITELISTED_NAMES_LIST[@]}"; do + if [[ $normalized == "$allowed" ]]; then + return 1 + fi + done - for keyword in "${BLOCKED_KEYWORDS_LIST[@]}"; do - if [[ -n $keyword && $normalized == *"$keyword"* ]]; then - return 0 - fi - done + for keyword in "${BLOCKED_KEYWORDS_LIST[@]}"; do + if [[ -n $keyword && $normalized == *"$keyword"* ]]; then + return 0 + fi + done - return 1 + return 1 } # Helper: detect if current invocation includes --noconfirm function has_noconfirm_flag() { - for arg in "$@"; do - if [[ $arg == "--noconfirm" ]]; then - return 0 - fi - done - return 1 + for arg in "$@"; do + if [[ $arg == "--noconfirm" ]]; then + return 0 + fi + done + return 1 } # Handle stale pacman database lock if present and no package managers are running check_and_handle_db_lock() { - local lock_file="/var/lib/pacman/db.lck" - # Quick exit if no lock - if [[ ! -e $lock_file ]]; then - return 0 - fi + local lock_file="/var/lib/pacman/db.lck" + # Quick exit if no lock + if [[ ! -e $lock_file ]]; then + return 0 + fi - # Determine which processes actually have the lock open - local -a holders=() - if command -v fuser > /dev/null 2>&1; then - mapfile -t holders < <(fuser "$lock_file" 2> /dev/null | tr ' ' '\n' | grep -E '^[0-9]+$' || true) - elif command -v lsof > /dev/null 2>&1; then - mapfile -t holders < <(lsof -t "$lock_file" 2> /dev/null | grep -E '^[0-9]+$' || true) - else - holders=() - fi + # Determine which processes actually have the lock open + local -a holders=() + if command -v fuser >/dev/null 2>&1; then + mapfile -t holders < <(fuser "$lock_file" 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+$' || true) + elif command -v lsof >/dev/null 2>&1; then + mapfile -t holders < <(lsof -t "$lock_file" 2>/dev/null | grep -E '^[0-9]+$' || true) + else + holders=() + fi - # Filter out our own PID if it somehow appears - if [[ ${#holders[@]} -gt 0 ]]; then - local -a filtered=() - for pid in "${holders[@]}"; do - [[ $pid -eq $$ ]] && continue - filtered+=("$pid") - done - holders=("${filtered[@]}") - fi + # Filter out our own PID if it somehow appears + if [[ ${#holders[@]} -gt 0 ]]; then + local -a filtered=() + for pid in "${holders[@]}"; do + [[ $pid -eq $$ ]] && continue + filtered+=("$pid") + done + holders=("${filtered[@]}") + fi - if [[ ${#holders[@]} -gt 0 ]]; then - local pac_holder=0 - local gui_holder=0 - for pid in "${holders[@]}"; do - local comm args lower - comm=$(ps -p "$pid" -o comm= 2> /dev/null || true) - args=$(ps -p "$pid" -o args= 2> /dev/null || true) - lower="${comm,,} ${args,,}" - if [[ $lower == *" pacman"* || $lower == pacman* || $lower == *"/pacman "* || $lower == *" pamac"* ]]; then - pac_holder=1 - elif [[ $lower == *packagekit* || $lower == *gnome-software* || $lower == *discover* ]]; then - gui_holder=1 - fi - done + if [[ ${#holders[@]} -gt 0 ]]; then + local pac_holder=0 + local gui_holder=0 + for pid in "${holders[@]}"; do + local comm args lower + comm=$(ps -p "$pid" -o comm= 2>/dev/null || true) + args=$(ps -p "$pid" -o args= 2>/dev/null || true) + lower="${comm,,} ${args,,}" + if [[ $lower == *" pacman"* || $lower == pacman* || $lower == *"/pacman "* || $lower == *" pamac"* ]]; then + pac_holder=1 + elif [[ $lower == *packagekit* || $lower == *gnome-software* || $lower == *discover* ]]; then + gui_holder=1 + fi + done - if [[ $pac_holder -eq 1 ]]; then - echo -e "${RED}Another pacman/pamac transaction is holding the database lock. Try again later.${NC}" >&2 - return 1 - fi + if [[ $pac_holder -eq 1 ]]; then + echo -e "${RED}Another pacman/pamac transaction is holding the database lock. Try again later.${NC}" >&2 + return 1 + fi - if [[ $gui_holder -eq 1 ]]; then - echo -e "${YELLOW}A background software updater is holding the pacman lock. Attempting to stop it...${NC}" >&2 - if command -v systemctl > /dev/null 2>&1; then - systemctl --quiet stop packagekit.service 2> /dev/null || true - systemctl --quiet stop packagekit 2> /dev/null || true - fi - pkill -x packagekitd 2> /dev/null || true - pkill -f gnome-software 2> /dev/null || true - pkill -f discover 2> /dev/null || true - sleep 1 + if [[ $gui_holder -eq 1 ]]; then + echo -e "${YELLOW}A background software updater is holding the pacman lock. Attempting to stop it...${NC}" >&2 + if command -v systemctl >/dev/null 2>&1; then + systemctl --quiet stop packagekit.service 2>/dev/null || true + systemctl --quiet stop packagekit 2>/dev/null || true + fi + pkill -x packagekitd 2>/dev/null || true + pkill -f gnome-software 2>/dev/null || true + pkill -f discover 2>/dev/null || true + sleep 1 - # Re-check holders - holders=() - if command -v fuser > /dev/null 2>&1; then - mapfile -t holders < <(fuser "$lock_file" 2> /dev/null | tr ' ' '\n' | grep -E '^[0-9]+$' || true) - elif command -v lsof > /dev/null 2>&1; then - mapfile -t holders < <(lsof -t "$lock_file" 2> /dev/null | grep -E '^[0-9]+$' || true) - fi - if [[ ${#holders[@]} -gt 0 ]]; then - echo -e "${RED}Cannot free the pacman lock; another process still holds it. Try again later.${NC}" >&2 - return 1 - fi - fi - fi + # Re-check holders + holders=() + if command -v fuser >/dev/null 2>&1; then + mapfile -t holders < <(fuser "$lock_file" 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+$' || true) + elif command -v lsof >/dev/null 2>&1; then + mapfile -t holders < <(lsof -t "$lock_file" 2>/dev/null | grep -E '^[0-9]+$' || true) + fi + if [[ ${#holders[@]} -gt 0 ]]; then + echo -e "${RED}Cannot free the pacman lock; another process still holds it. Try again later.${NC}" >&2 + return 1 + fi + fi + fi - # Decide whether to remove the lock - local now epoch age - if epoch=$(stat -c %Y "$lock_file" 2> /dev/null); then - now=$(date +%s) - age=$((now - epoch)) - else - age=999999 - fi + # Decide whether to remove the lock + local now epoch age + if epoch=$(stat -c %Y "$lock_file" 2>/dev/null); then + now=$(date +%s) + age=$((now - epoch)) + else + age=999999 + fi - # Auto-remove in non-interactive mode (--noconfirm) or if the lock is older than 10 minutes - if has_noconfirm_flag "$@" || [[ $age -ge 600 ]]; then - echo -e "${YELLOW}Stale pacman lock detected (age: ${age}s). Removing it automatically...${NC}" >&2 - if [[ $EUID -ne 0 ]]; then - sudo rm -f "$lock_file" || return 1 - else - rm -f "$lock_file" || return 1 - fi - return 0 - fi + # Auto-remove in non-interactive mode (--noconfirm) or if the lock is older than 10 minutes + if has_noconfirm_flag "$@" || [[ $age -ge 600 ]]; then + echo -e "${YELLOW}Stale pacman lock detected (age: ${age}s). Removing it automatically...${NC}" >&2 + if [[ $EUID -ne 0 ]]; then + sudo rm -f "$lock_file" || return 1 + else + rm -f "$lock_file" || return 1 + fi + return 0 + fi - # Interactive prompt (15s timeout) - echo -e "${YELLOW}A pacman lock exists but no active pacman is running.${NC}" >&2 - echo -e "${CYAN}Lock path:${NC} $lock_file (age: ${age}s)" >&2 - read -r -t 15 -p $'Remove stale lock and continue? [y/N]: ' reply || reply="n" - if [[ ${reply,,} == "y" || ${reply,,} == "yes" ]]; then - if [[ $EUID -ne 0 ]]; then - sudo rm -f "$lock_file" || return 1 - else - rm -f "$lock_file" || return 1 - fi - return 0 - fi - echo -e "${RED}Aborting due to existing pacman lock. Close other updaters and retry, or run with --noconfirm to auto-clear stale locks.${NC}" >&2 - return 1 + # Interactive prompt (15s timeout) + echo -e "${YELLOW}A pacman lock exists but no active pacman is running.${NC}" >&2 + echo -e "${CYAN}Lock path:${NC} $lock_file (age: ${age}s)" >&2 + read -r -t 15 -p $'Remove stale lock and continue? [y/N]: ' reply || reply="n" + if [[ ${reply,,} == "y" || ${reply,,} == "yes" ]]; then + if [[ $EUID -ne 0 ]]; then + sudo rm -f "$lock_file" || return 1 + else + rm -f "$lock_file" || return 1 + fi + return 0 + fi + echo -e "${RED}Aborting due to existing pacman lock. Close other updaters and retry, or run with --noconfirm to auto-clear stale locks.${NC}" >&2 + return 1 } # Cleanup: remove any installed blocked packages (in addition to the queued operation) function remove_installed_blocked_packages() { - # args not used; kept for future policy extension - # List installed package names - mapfile -t installed_names < <("$PACMAN_BIN" -Qq 2> /dev/null) - local to_remove=() - for name in "${installed_names[@]}"; do - if is_blocked_package_name "$name"; then - to_remove+=("$name") - fi - done + # args not used; kept for future policy extension + # List installed package names + mapfile -t installed_names < <("$PACMAN_BIN" -Qq 2>/dev/null) + local to_remove=() + for name in "${installed_names[@]}"; do + if is_blocked_package_name "$name"; then + to_remove+=("$name") + fi + done - if [[ ${#to_remove[@]} -eq 0 ]]; then - return 0 - fi + if [[ ${#to_remove[@]} -eq 0 ]]; then + return 0 + fi - echo -e "${YELLOW}Policy cleanup:${NC} Removing blocked installed packages: ${BOLD}${to_remove[*]}${NC}" >&2 - local remove_cmd=("$PACMAN_BIN" -Rns --noconfirm) - "${remove_cmd[@]}" "${to_remove[@]}" - local rc=$? - if [[ $rc -ne 0 ]]; then - echo -e "${RED}Cleanup removal failed with exit code ${rc}.${NC}" >&2 - else - echo -e "${GREEN}Cleanup removal completed for: ${to_remove[*]}${NC}" >&2 - fi - return $rc + echo -e "${YELLOW}Policy cleanup:${NC} Removing blocked installed packages: ${BOLD}${to_remove[*]}${NC}" >&2 + local remove_cmd=("$PACMAN_BIN" -Rns --noconfirm) + "${remove_cmd[@]}" "${to_remove[@]}" + local rc=$? + if [[ $rc -ne 0 ]]; then + echo -e "${RED}Cleanup removal failed with exit code ${rc}.${NC}" >&2 + else + echo -e "${GREEN}Cleanup removal completed for: ${to_remove[*]}${NC}" >&2 + fi + return $rc } # Function to check if user is trying to install packages that are always blocked function check_for_always_blocked() { - # Check if the command is an installation command - if [[ $1 == "-S" || $1 == "-Sy" || $1 == "-Syu" || $1 == "-Syyu" || $1 == "-U" ]]; then - # Check all arguments - for arg in "$@"; do - # Strip repository prefix if present (like extra/ or community/) - local package_name="${arg##*/}" - if is_blocked_package_name "$package_name"; then - return 0 # Always blocked package found - fi - done - fi - return 1 # No always blocked package found + # Check if the command is an installation command + if [[ $1 == "-S" || $1 == "-Sy" || $1 == "-Syu" || $1 == "-Syyu" || $1 == "-U" ]]; then + # Check all arguments + for arg in "$@"; do + # Strip repository prefix if present (like extra/ or community/) + local package_name="${arg##*/}" + if is_blocked_package_name "$package_name"; then + return 0 # Always blocked package found + fi + done + fi + return 1 # No always blocked package found } # Function to check if user is trying to install steam (challenge-eligible package) function check_for_steam() { - # List of packages that require challenge (only steam in this case) - local steam_packages=("steam") + # List of packages that require challenge (only steam in this case) + local steam_packages=("steam") - # Check if the command is an installation command - if [[ $1 == "-S" || $1 == "-Sy" || $1 == "-Syu" || $1 == "-Syyu" || $1 == "-U" ]]; then - # Check all arguments - for arg in "$@"; do - # Strip repository prefix if present (like extra/ or community/) - local package_name="${arg##*/}" + # Check if the command is an installation command + if [[ $1 == "-S" || $1 == "-Sy" || $1 == "-Syu" || $1 == "-Syyu" || $1 == "-U" ]]; then + # Check all arguments + for arg in "$@"; do + # Strip repository prefix if present (like extra/ or community/) + local package_name="${arg##*/}" - # Check if argument matches steam - for package in "${steam_packages[@]}"; do - if [[ $arg == "$package" || $arg == *"/$package-"* || $arg == *"/$package/"* || - $arg == *"/$package" || $package_name == "$package" ]]; then - return 0 # Steam package found - fi - done - done - fi - return 1 # No steam package found + # Check if argument matches steam + for package in "${steam_packages[@]}"; do + if [[ $arg == "$package" || $arg == *"/$package-"* || $arg == *"/$package/"* || + $arg == *"/$package" || $package_name == "$package" ]]; then + return 0 # Steam package found + fi + done + done + fi + return 1 # No steam package found } # Function to check if current day is a weekday (after 4PM Friday until midnight Sunday) function is_weekday() { - local day_of_week - day_of_week=$(date +%u) # %u gives 1-7 (Monday is 1, Sunday is 7) - local hour - hour=$(date +%H) # %H gives hour in 24-hour format (00-23) + local day_of_week + day_of_week=$(date +%u) # %u gives 1-7 (Monday is 1, Sunday is 7) + local hour + hour=$(date +%H) # %H gives hour in 24-hour format (00-23) - # Monday through Thursday are always weekdays - if [[ $day_of_week -ge 1 && $day_of_week -le 4 ]]; then - return 0 # Is weekday - # Friday before 4PM is weekday, after 4PM is weekend - elif [[ $day_of_week -eq 5 ]]; then - if [[ $hour -lt 14 ]]; then - return 0 # Is weekday (Friday before 4PM) - else - return 1 # Is weekend (Friday after 4PM) - fi - # Saturday and Sunday are weekend - else - return 1 # Is weekend - fi + # Monday through Thursday are always weekdays + if [[ $day_of_week -ge 1 && $day_of_week -le 4 ]]; then + return 0 # Is weekday + # Friday before 4PM is weekday, after 4PM is weekend + elif [[ $day_of_week -eq 5 ]]; then + if [[ $hour -lt 14 ]]; then + return 0 # Is weekday (Friday before 4PM) + else + return 1 # Is weekend (Friday after 4PM) + fi + # Saturday and Sunday are weekend + else + return 1 # Is weekend + fi } # Function to prompt for solving a word unscrambling challenge (only for steam) function prompt_for_steam_challenge() { - echo -e "${YELLOW}WARNING: You are trying to install Steam.${NC}" + echo -e "${YELLOW}WARNING: You are trying to install Steam.${NC}" - # Check if it's a weekday and block completely - if is_weekday; then - local day_name - day_name=$(date +%A) - echo -e "${RED}Steam installation BLOCKED: Steam cannot be installed on weekdays.${NC}" - echo -e "${RED}Today is $day_name. Please try again on the weekend (Saturday or Sunday).${NC}" - return 1 - fi + # Check if it's a weekday and block completely + if is_weekday; then + local day_name + day_name=$(date +%A) + echo -e "${RED}Steam installation BLOCKED: Steam cannot be installed on weekdays.${NC}" + echo -e "${RED}Today is $day_name. Please try again on the weekend (Saturday or Sunday).${NC}" + return 1 + fi - echo -e "${YELLOW}Weekend Steam challenge will begin shortly...${NC}" + echo -e "${YELLOW}Weekend Steam challenge will begin shortly...${NC}" - # Sleep for random 20-40 seconds - # sleep_duration=$((RANDOM % 20 + 20)) - sleep_duration=$((RANDOM % 20)) - sleep "$sleep_duration" + # Sleep for random 20-40 seconds + # sleep_duration=$((RANDOM % 20 + 20)) + sleep_duration=$((RANDOM % 20)) + sleep "$sleep_duration" - # Define path to words.txt (in the same directory as the script) - script_dir="$(dirname "$(readlink -f "$0")")" - words_file="$script_dir/words.txt" + # Define path to words.txt (in the same directory as the script) + script_dir="$(dirname "$(readlink -f "$0")")" + words_file="$script_dir/words.txt" - # Check if words.txt exists - if [[ ! -f $words_file ]]; then - echo -e "${RED}Error: words.txt file not found at $words_file${NC}" - return 1 - fi + # Check if words.txt exists + if [[ ! -f $words_file ]]; then + echo -e "${RED}Error: words.txt file not found at $words_file${NC}" + return 1 + fi - # Choose a specific word length (5, 6, 7, or 8 characters) - # - word_length=5 - echo -e "${CYAN}Today's challenge: Words with ${word_length} letters${NC}" + # Choose a specific word length (5, 6, 7, or 8 characters) + # + word_length=5 + echo -e "${CYAN}Today's challenge: Words with ${word_length} letters${NC}" - # Filter words by the specific chosen length and load random words - words_count=160 - mapfile -t selected_words < <(grep -E "^[a-zA-Z]{$word_length}$" "$words_file" | shuf -n "$words_count") + # Filter words by the specific chosen length and load random words + words_count=160 + mapfile -t selected_words < <(grep -E "^[a-zA-Z]{$word_length}$" "$words_file" | shuf -n "$words_count") - # If we couldn't get enough words of the right length - if [[ ${#selected_words[@]} -lt $words_count ]]; then - echo -e "${RED}Warning: Could only find ${#selected_words[@]} words of length $word_length.${NC}" - words_count=${#selected_words[@]} - if [[ $words_count -eq 0 ]]; then - echo -e "${RED}Error: No words of length $word_length found in $words_file${NC}" - return 1 - fi - fi + # If we couldn't get enough words of the right length + if [[ ${#selected_words[@]} -lt $words_count ]]; then + echo -e "${RED}Warning: Could only find ${#selected_words[@]} words of length $word_length.${NC}" + words_count=${#selected_words[@]} + if [[ $words_count -eq 0 ]]; then + echo -e "${RED}Error: No words of length $word_length found in $words_file${NC}" + return 1 + fi + fi - # Convert all words to uppercase - for i in "${!selected_words[@]}"; do - selected_words[i]=$(echo "${selected_words[i]}" | tr '[:lower:]' '[:upper:]') - done + # Convert all words to uppercase + for i in "${!selected_words[@]}"; do + selected_words[i]=$(echo "${selected_words[i]}" | tr '[:lower:]' '[:upper:]') + done - echo -e "${CYAN}Here are ${words_count} random words. Remember them:${NC}" + echo -e "${CYAN}Here are ${words_count} random words. Remember them:${NC}" - # Display the words in a grid (4 columns) - for ((i = 0; i < words_count; i++)); do - printf "${BLUE}%-15s${NC}" "${selected_words[i]}" - if (((i + 1) % 4 == 0)); then - echo "" - fi - done + # Display the words in a grid (4 columns) + for ((i = 0; i < words_count; i++)); do + printf "${BLUE}%-15s${NC}" "${selected_words[i]}" + if (((i + 1) % 4 == 0)); then + echo "" + fi + done - # Select a random word to scramble (already in uppercase) - target_index=$((RANDOM % words_count)) - target_word="${selected_words[target_index]}" + # Select a random word to scramble (already in uppercase) + target_index=$((RANDOM % words_count)) + target_word="${selected_words[target_index]}" - # Scramble the word - scrambled_word=$(echo "$target_word" | fold -w1 | shuf | tr -d '\n') + # Scramble the word + scrambled_word=$(echo "$target_word" | fold -w1 | shuf | tr -d '\n') - # Ensure scrambled word is different from original - if [[ $scrambled_word == "$target_word" ]]; then - # Use simple reversal as fallback - scrambled_word=$(echo "$target_word" | rev) - fi + # Ensure scrambled word is different from original + if [[ $scrambled_word == "$target_word" ]]; then + # Use simple reversal as fallback + scrambled_word=$(echo "$target_word" | rev) + fi - echo -e "\n${YELLOW}One of those words has been scrambled to:${NC} ${CYAN}$scrambled_word${NC}" - echo -e "${YELLOW}Unscramble the word to proceed with installation (you have 2 minutes):${NC}" + echo -e "\n${YELLOW}One of those words has been scrambled to:${NC} ${CYAN}$scrambled_word${NC}" + echo -e "${YELLOW}Unscramble the word to proceed with installation (you have 2 minutes):${NC}" - # Set up a background process to display the timer - ( - start_time=$(date +%s) - while true; do - current_time=$(date +%s) - elapsed=$((current_time - start_time)) - remaining=$((60 - elapsed)) + # Set up a background process to display the timer + ( + start_time=$(date +%s) + while true; do + current_time=$(date +%s) + elapsed=$((current_time - start_time)) + remaining=$((60 - elapsed)) - if [[ $remaining -le 0 ]]; then - echo -ne "\r${YELLOW}Time remaining: 0 seconds${NC} " - break - fi + if [[ $remaining -le 0 ]]; then + echo -ne "\r${YELLOW}Time remaining: 0 seconds${NC} " + break + fi - echo -ne "\r${YELLOW}Time remaining: ${remaining} seconds${NC} " - sleep 1 - done - ) & - display_pid=$! + echo -ne "\r${YELLOW}Time remaining: ${remaining} seconds${NC} " + sleep 1 + done + ) & + display_pid=$! - # Read user input with timeout - read -t 60 -r user_input - read_status=$? + # Read user input with timeout + read -t 60 -r user_input + read_status=$? - # Kill the timer display - kill "$display_pid" 2> /dev/null - wait "$display_pid" 2> /dev/null - echo # Add a newline after the timer + # Kill the timer display + kill "$display_pid" 2>/dev/null + wait "$display_pid" 2>/dev/null + echo # Add a newline after the timer - # Check if read timed out - if [[ $read_status -ne 0 ]]; then - echo -e "${RED}Time's up! Challenge failed. The correct word was '$target_word'.${NC}" - return 1 - fi + # Check if read timed out + if [[ $read_status -ne 0 ]]; then + echo -e "${RED}Time's up! Challenge failed. The correct word was '$target_word'.${NC}" + return 1 + fi - # Convert user input to uppercase and trim whitespaces - user_input=$(echo "$user_input" | tr '[:lower:]' '[:upper:]' | xargs) + # Convert user input to uppercase and trim whitespaces + user_input=$(echo "$user_input" | tr '[:lower:]' '[:upper:]' | xargs) - if [[ $user_input == "$target_word" ]]; then - echo -e "${GREEN}Correct! Proceeding with installation...${NC}" + if [[ $user_input == "$target_word" ]]; then + echo -e "${GREEN}Correct! Proceeding with installation...${NC}" - # Add sleep after successful challenge completion (20-40 seconds) - # post_challenge_sleep=$((RANDOM % 20 + 20)) - post_challenge_sleep=$((RANDOM % 20)) - sleep "$post_challenge_sleep" + # Add sleep after successful challenge completion (20-40 seconds) + # post_challenge_sleep=$((RANDOM % 20 + 20)) + post_challenge_sleep=$((RANDOM % 20)) + sleep "$post_challenge_sleep" - return 0 - else - echo -e "${RED}Incorrect answer. Installation aborted. The correct word was '$target_word'.${NC}" - return 1 - fi + return 0 + else + echo -e "${RED}Incorrect answer. Installation aborted. The correct word was '$target_word'.${NC}" + return 1 + fi +} + +function check_for_virtualbox() { + # List of VirtualBox-related packages + local vbox_packages=("virtualbox" "virtualbox-host-modules-arch" "virtualbox-guest-iso" "virtualbox-ext-oracle") + + # Check if the command is an installation command + if [[ $1 == "-S" || $1 == "-Sy" || $1 == "-Syu" || $1 == "-Syyu" || $1 == "-U" ]]; then + # Check all arguments + for arg in "$@"; do + # Strip repository prefix if present + local package_name="${arg##*/}" + + # Check if argument matches any VirtualBox package + for package in "${vbox_packages[@]}"; do + if [[ $arg == "$package" || $arg == *"/$package-"* || $arg == *"/$package/"* || + $arg == *"/$package" || $package_name == "$package" ]]; then + return 0 # VirtualBox package found + fi + done + done + fi + return 1 # No VirtualBox package found } # Function to prompt for solving a word unscrambling challenge (for virtualbox - always active) -# shellcheck disable=SC2329 # Invoked dynamically when matching VirtualBox packages function prompt_for_virtualbox_challenge() { - echo -e "${YELLOW}WARNING: You are trying to install VirtualBox.${NC}" - echo -e "${YELLOW}VirtualBox challenge will begin shortly...${NC}" + echo -e "${YELLOW}WARNING: You are trying to install VirtualBox.${NC}" + echo -e "${YELLOW}VirtualBox challenge will begin shortly...${NC}" - # Sleep for random 10-30 seconds - sleep_duration=$((RANDOM % 20 + 10)) - sleep "$sleep_duration" + # Sleep for random 10-30 seconds + sleep_duration=$((RANDOM % 20 + 10)) + sleep "$sleep_duration" - # Define path to words.txt (in the same directory as the script) - script_dir="$(dirname "$(readlink -f "$0")")" - words_file="$script_dir/words.txt" + # Define path to words.txt (in the same directory as the script) + script_dir="$(dirname "$(readlink -f "$0")")" + words_file="$script_dir/words.txt" - # Check if words.txt exists - if [[ ! -f $words_file ]]; then - echo -e "${RED}Error: words.txt file not found at $words_file${NC}" - return 1 - fi + # Check if words.txt exists + if [[ ! -f $words_file ]]; then + echo -e "${RED}Error: words.txt file not found at $words_file${NC}" + return 1 + fi - # Choose a specific word length (6, 7, or 8 characters for VirtualBox) - word_length=6 - echo -e "${CYAN}VirtualBox challenge: Words with ${word_length} letters${NC}" + # Choose a specific word length (6, 7, or 8 characters for VirtualBox) + word_length=6 + echo -e "${CYAN}VirtualBox challenge: Words with ${word_length} letters${NC}" - # Filter words by the specific chosen length and load random words - words_count=120 - mapfile -t selected_words < <(grep -E "^[a-zA-Z]{$word_length}$" "$words_file" | shuf -n "$words_count") + # Filter words by the specific chosen length and load random words + words_count=120 + mapfile -t selected_words < <(grep -E "^[a-zA-Z]{$word_length}$" "$words_file" | shuf -n "$words_count") - # If we couldn't get enough words of the right length - if [[ ${#selected_words[@]} -lt $words_count ]]; then - echo -e "${RED}Warning: Could only find ${#selected_words[@]} words of length $word_length.${NC}" - words_count=${#selected_words[@]} - if [[ $words_count -eq 0 ]]; then - echo -e "${RED}Error: No words of length $word_length found in $words_file${NC}" - return 1 - fi - fi + # If we couldn't get enough words of the right length + if [[ ${#selected_words[@]} -lt $words_count ]]; then + echo -e "${RED}Warning: Could only find ${#selected_words[@]} words of length $word_length.${NC}" + words_count=${#selected_words[@]} + if [[ $words_count -eq 0 ]]; then + echo -e "${RED}Error: No words of length $word_length found in $words_file${NC}" + return 1 + fi + fi - # Convert all words to uppercase - for i in "${!selected_words[@]}"; do - selected_words[i]=$(echo "${selected_words[i]}" | tr '[:lower:]' '[:upper:]') - done + # Convert all words to uppercase + for i in "${!selected_words[@]}"; do + selected_words[i]=$(echo "${selected_words[i]}" | tr '[:lower:]' '[:upper:]') + done - echo -e "${CYAN}Here are ${words_count} random words. Remember them:${NC}" + echo -e "${CYAN}Here are ${words_count} random words. Remember them:${NC}" - # Display the words in a grid (4 columns) - for ((i = 0; i < words_count; i++)); do - printf "${BLUE}%-15s${NC}" "${selected_words[i]}" - if (((i + 1) % 4 == 0)); then - echo "" - fi - done + # Display the words in a grid (4 columns) + for ((i = 0; i < words_count; i++)); do + printf "${BLUE}%-15s${NC}" "${selected_words[i]}" + if (((i + 1) % 4 == 0)); then + echo "" + fi + done - # Select a random word to scramble (already in uppercase) - target_index=$((RANDOM % words_count)) - target_word="${selected_words[target_index]}" + # Select a random word to scramble (already in uppercase) + target_index=$((RANDOM % words_count)) + target_word="${selected_words[target_index]}" - # Scramble the word - scrambled_word=$(echo "$target_word" | fold -w1 | shuf | tr -d '\n') + # Scramble the word + scrambled_word=$(echo "$target_word" | fold -w1 | shuf | tr -d '\n') - # Ensure scrambled word is different from original - if [[ $scrambled_word == "$target_word" ]]; then - # Use simple reversal as fallback - scrambled_word=$(echo "$target_word" | rev) - fi + # Ensure scrambled word is different from original + if [[ $scrambled_word == "$target_word" ]]; then + # Use simple reversal as fallback + scrambled_word=$(echo "$target_word" | rev) + fi - echo -e "\n${YELLOW}One of those words has been scrambled to:${NC} ${CYAN}$scrambled_word${NC}" - echo -e "${YELLOW}Unscramble the word to proceed with VirtualBox installation (you have 90 seconds):${NC}" + echo -e "\n${YELLOW}One of those words has been scrambled to:${NC} ${CYAN}$scrambled_word${NC}" + echo -e "${YELLOW}Unscramble the word to proceed with VirtualBox installation (you have 90 seconds):${NC}" - # Set up a background process to display the timer - ( - start_time=$(date +%s) - while true; do - current_time=$(date +%s) - elapsed=$((current_time - start_time)) - remaining=$((90 - elapsed)) + # Set up a background process to display the timer + ( + start_time=$(date +%s) + while true; do + current_time=$(date +%s) + elapsed=$((current_time - start_time)) + remaining=$((90 - elapsed)) - if [[ $remaining -le 0 ]]; then - echo -ne "\r${YELLOW}Time remaining: 0 seconds${NC} " - break - fi + if [[ $remaining -le 0 ]]; then + echo -ne "\r${YELLOW}Time remaining: 0 seconds${NC} " + break + fi - echo -ne "\r${YELLOW}Time remaining: ${remaining} seconds${NC} " - sleep 1 - done - ) & - display_pid=$! + echo -ne "\r${YELLOW}Time remaining: ${remaining} seconds${NC} " + sleep 1 + done + ) & + display_pid=$! - # Read user input with timeout (90 seconds for VirtualBox) - read -t 90 -r user_input - read_status=$? + # Read user input with timeout (90 seconds for VirtualBox) + read -t 90 -r user_input + read_status=$? - # Kill the timer display - kill "$display_pid" 2> /dev/null - wait "$display_pid" 2> /dev/null - echo # Add a newline after the timer + # Kill the timer display + kill "$display_pid" 2>/dev/null + wait "$display_pid" 2>/dev/null + echo # Add a newline after the timer - # Check if read timed out - if [[ $read_status -ne 0 ]]; then - echo -e "${RED}Time's up! VirtualBox challenge failed. The correct word was '$target_word'.${NC}" - return 1 - fi + # Check if read timed out + if [[ $read_status -ne 0 ]]; then + echo -e "${RED}Time's up! VirtualBox challenge failed. The correct word was '$target_word'.${NC}" + return 1 + fi - # Convert user input to uppercase and trim whitespaces - user_input=$(echo "$user_input" | tr '[:lower:]' '[:upper:]' | xargs) + # Convert user input to uppercase and trim whitespaces + user_input=$(echo "$user_input" | tr '[:lower:]' '[:upper:]' | xargs) - if [[ $user_input == "$target_word" ]]; then - echo -e "${GREEN}Correct! Proceeding with VirtualBox installation...${NC}" + if [[ $user_input == "$target_word" ]]; then + echo -e "${GREEN}Correct! Proceeding with VirtualBox installation...${NC}" - # Add sleep after successful challenge completion (15-35 seconds) - post_challenge_sleep=$((RANDOM % 20 + 15)) - sleep "$post_challenge_sleep" + # Add sleep after successful challenge completion (15-35 seconds) + post_challenge_sleep=$((RANDOM % 20 + 15)) + sleep "$post_challenge_sleep" - return 0 - else - echo -e "${RED}Incorrect answer. VirtualBox installation aborted. The correct word was '$target_word'.${NC}" - return 1 - fi + return 0 + else + echo -e "${RED}Incorrect answer. VirtualBox installation aborted. The correct word was '$target_word'.${NC}" + return 1 + fi } # Check for wrapper-specific commands if [[ $1 == "--help-wrapper" ]]; then - show_help - exit 0 + show_help + exit 0 fi # Before any pacman action, ensure maintenance services exist @@ -667,18 +689,25 @@ ensure_periodic_maintenance # Check for always blocked packages first (highest priority) if check_for_always_blocked "$@"; then - echo -e "${RED}Installation BLOCKED: This package is permanently restricted and cannot be installed.${NC}" - echo -e "${RED}Package installation has been denied by system policy.${NC}" - # Regardless of the attempted action, enforce cleanup of any installed blocked packages - remove_installed_blocked_packages "$@" - exit 1 + echo -e "${RED}Installation BLOCKED: This package is permanently restricted and cannot be installed.${NC}" + echo -e "${RED}Package installation has been denied by system policy.${NC}" + # Regardless of the attempted action, enforce cleanup of any installed blocked packages + remove_installed_blocked_packages "$@" + exit 1 fi # Check for steam (challenge-eligible package) if check_for_steam "$@"; then - if ! prompt_for_steam_challenge; then - exit 1 - fi + if ! prompt_for_steam_challenge; then + exit 1 + fi +fi + +# Check for VirtualBox (challenge-eligible package) +if check_for_virtualbox "$@"; then + if ! prompt_for_virtualbox_challenge; then + exit 1 + fi fi # Display operation @@ -692,19 +721,19 @@ start_time=$(date +%s) # Execute the real pacman command (with /etc/hosts guard handling) if needs_unlock "$@"; then - pre_unlock_hosts + pre_unlock_hosts fi # Handle a possible stale DB lock before executing if ! check_and_handle_db_lock "$@"; then - exit 1 + exit 1 fi "$PACMAN_BIN" "$@" exit_code=$? if needs_unlock "$@"; then - post_relock_hosts + post_relock_hosts fi # Record end time for statistics @@ -713,9 +742,9 @@ duration=$((end_time - start_time)) # Display results if [ $exit_code -eq 0 ]; then - echo -e "${GREEN}Command completed successfully in ${duration}s.${NC}" >&2 + echo -e "${GREEN}Command completed successfully in ${duration}s.${NC}" >&2 else - echo -e "${RED}Command failed with exit code ${exit_code}.${NC}" >&2 + echo -e "${RED}Command failed with exit code ${exit_code}.${NC}" >&2 fi # After any operation, remove installed blocked packages as part of policy enforcement @@ -723,11 +752,11 @@ remove_installed_blocked_packages "$@" # Display some helpful tips depending on the operation if [[ $1 == "-S" || $1 == "-S "* ]] && [ $exit_code -eq 0 ]; then - echo -e "${CYAN}Tip:${NC} You may need to log out or restart to use some newly installed software." + echo -e "${CYAN}Tip:${NC} You may need to log out or restart to use some newly installed software." fi if [[ $1 == "-Syu" || $1 == "-Syyu" ]] && [ $exit_code -eq 0 ]; then - echo -e "${CYAN}Tip:${NC} Consider restarting after major updates." + echo -e "${CYAN}Tip:${NC} Consider restarting after major updates." fi exit $exit_code diff --git a/scripts/features/install_unreal_mcp.sh b/scripts/features/install_unreal_mcp.sh index 699b6ab..b87bc7b 100755 --- a/scripts/features/install_unreal_mcp.sh +++ b/scripts/features/install_unreal_mcp.sh @@ -12,11 +12,11 @@ SCRIPT_NAME="$(basename "$0")" # ---------- User/paths ---------- if [[ -n ${SUDO_USER:-} ]]; then - ACTUAL_USER="$SUDO_USER" - USER_HOME="/home/$SUDO_USER" + ACTUAL_USER="$SUDO_USER" + USER_HOME="/home/$SUDO_USER" else - ACTUAL_USER="$USER" - USER_HOME="$HOME" + ACTUAL_USER="$USER" + USER_HOME="$HOME" fi INSTALL_ROOT_DEFAULT="$USER_HOME/.local/share/unreal-mcp" @@ -32,12 +32,12 @@ FORCE_UPDATE=false log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; } fail() { - echo "[ERROR] $*" >&2 - exit 1 + echo "[ERROR] $*" >&2 + exit 1 } usage() { - cat << EOF + cat < /dev/null 2>&1; } +require_cmd() { command -v "$1" >/dev/null 2>&1; } ensure_packages_arch() { - # Install with pacman using sudo when needed; keep idempotent with --needed - local pkgs=(git jq uv python rsync) - local to_install=() - for p in "${pkgs[@]}"; do - if ! pacman -Qi "$p" > /dev/null 2>&1; then - to_install+=("$p") - fi - done - if [[ ${#to_install[@]} -gt 0 ]]; then - log "Installing packages: ${to_install[*]}" - if [[ $EUID -eq 0 ]]; then - pacman -S --noconfirm --needed "${to_install[@]}" - else - sudo pacman -S --noconfirm --needed "${to_install[@]}" - fi - else - log "All required packages already installed" - fi + # Install with pacman using sudo when needed; keep idempotent with --needed + local pkgs=(git jq uv python rsync) + local to_install=() + for p in "${pkgs[@]}"; do + if ! pacman -Qi "$p" >/dev/null 2>&1; then + to_install+=("$p") + fi + done + if [[ ${#to_install[@]} -gt 0 ]]; then + log "Installing packages: ${to_install[*]}" + if [[ $EUID -eq 0 ]]; then + pacman -S --noconfirm --needed "${to_install[@]}" + else + sudo pacman -S --noconfirm --needed "${to_install[@]}" + fi + else + log "All required packages already installed" + fi } check_python_version() { - if require_cmd python; then - local v - v=$(python -V 2>&1 | awk '{print $2}') - elif require_cmd python3; then - local v - v=$(python3 -V 2>&1 | awk '{print $2}') - else - log "python not found; pacman install will provide it" - return 0 - fi - # Require >= 3.12 (Unreal MCP docs) - local major minor - major=$(echo "$v" | cut -d. -f1) - minor=$(echo "$v" | cut -d. -f2) - if ((major < 3 || (major == 3 && minor < 12))); then - log "Python $v detected; installing newer python via pacman" - if [[ $EUID -eq 0 ]]; then - pacman -S --noconfirm --needed python - else - sudo pacman -S --noconfirm --needed python - fi - fi + if require_cmd python; then + local v + v=$(python -V 2>&1 | awk '{print $2}') + elif require_cmd python3; then + local v + v=$(python3 -V 2>&1 | awk '{print $2}') + else + log "python not found; pacman install will provide it" + return 0 + fi + # Require >= 3.12 (Unreal MCP docs) + local major minor + major=$(echo "$v" | cut -d. -f1) + minor=$(echo "$v" | cut -d. -f2) + if ((major < 3 || (major == 3 && minor < 12))); then + log "Python $v detected; installing newer python via pacman" + if [[ $EUID -eq 0 ]]; then + pacman -S --noconfirm --needed python + else + sudo pacman -S --noconfirm --needed python + fi + fi } # ---------- Git clone/update ---------- setup_repo() { - mkdir -p "$INSTALL_ROOT" - if [[ ! -d "$REPO_DIR/.git" ]]; then - log "Cloning unreal-mcp into $REPO_DIR" - if require_cmd git; then - git clone "$REPO_URL" "$REPO_DIR" - else - fail "git is required but not found after install" - fi - else - log "Repo exists at $REPO_DIR" - if [[ $FORCE_UPDATE == true ]]; then - log "Updating repo with --force-update" - git -C "$REPO_DIR" fetch origin - git -C "$REPO_DIR" reset --hard origin/main - git -C "$REPO_DIR" pull --rebase --autostash - else - log "Pulling latest changes" - git -C "$REPO_DIR" pull --rebase --autostash - fi - fi + mkdir -p "$INSTALL_ROOT" + if [[ ! -d "$REPO_DIR/.git" ]]; then + log "Cloning unreal-mcp into $REPO_DIR" + if require_cmd git; then + git clone "$REPO_URL" "$REPO_DIR" + else + fail "git is required but not found after install" + fi + else + log "Repo exists at $REPO_DIR" + if [[ $FORCE_UPDATE == true ]]; then + log "Updating repo with --force-update" + git -C "$REPO_DIR" fetch origin + git -C "$REPO_DIR" reset --hard origin/main + git -C "$REPO_DIR" pull --rebase --autostash + else + log "Pulling latest changes" + git -C "$REPO_DIR" pull --rebase --autostash + fi + fi - # Ensure ownership for the real user when script ran via sudo - if [[ $EUID -eq 0 ]]; then - chown -R "$ACTUAL_USER:$ACTUAL_USER" "$INSTALL_ROOT" - fi + # Ensure ownership for the real user when script ran via sudo + if [[ $EUID -eq 0 ]]; then + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$INSTALL_ROOT" + fi } # ---------- Launcher ---------- install_launcher() { - local bin_dir="$USER_HOME/.local/bin" - local python_dir="$REPO_DIR/Python" - local launcher="$bin_dir/unreal-mcp-server" - mkdir -p "$bin_dir" - cat > "$launcher" << EOF + local bin_dir="$USER_HOME/.local/bin" + local python_dir="$REPO_DIR/Python" + local launcher="$bin_dir/unreal-mcp-server" + mkdir -p "$bin_dir" + cat >"$launcher" < "$tmp_file" << JSON + # Base JSON when no config exists + local tmp_file + tmp_file="$(mktemp)" + if [[ ! -f $cont_cfg ]]; then + cat >"$tmp_file" < "$tmp_file" && mv "$tmp_file" "$cont_cfg" - fi + ' "$cont_cfg" >"$tmp_file" && mv "$tmp_file" "$cont_cfg" + fi - if [[ $EUID -eq 0 ]]; then chown "$ACTUAL_USER:$ACTUAL_USER" "$cont_cfg"; fi - log "Configured Continue MCP at: $cont_cfg" + if [[ $EUID -eq 0 ]]; then chown "$ACTUAL_USER:$ACTUAL_USER" "$cont_cfg"; fi + log "Configured Continue MCP at: $cont_cfg" } # ---------- VS Code user MCP (native) ---------- configure_vscode_user_mcp() { - if [[ $CONFIGURE_VSCODE_USER != true ]]; then - log "Skipping VS Code user MCP config (--no-vscode)" - return 0 - fi + if [[ $CONFIGURE_VSCODE_USER != true ]]; then + log "Skipping VS Code user MCP config (--no-vscode)" + return 0 + fi - if ! require_cmd jq; then - fail "jq is required to compose VS Code --add-mcp JSON and to parse profiles" - fi + if ! require_cmd jq; then + fail "jq is required to compose VS Code --add-mcp JSON and to parse profiles" + fi - local python_dir="$REPO_DIR/Python" - local json - json=$(jq -n --arg dir "$python_dir" '{name:"unrealMCP", command:"uv", args:["--directory", $dir, "run", "unreal_mcp_server.py"]}') + local python_dir="$REPO_DIR/Python" + local json + json=$(jq -n --arg dir "$python_dir" '{name:"unrealMCP", command:"uv", args:["--directory", $dir, "run", "unreal_mcp_server.py"]}') - # Handle multiple VS Code variants if present - local candidates=(code code-insiders codium) - local found_any=false - for cli in "${candidates[@]}"; do - if ! command -v "$cli" > /dev/null 2>&1; then - continue - fi - found_any=true - log "Registering MCP server in VS Code user profile via: $cli --add-mcp" - if "$cli" --add-mcp "$json" > "/tmp/${cli}-add-mcp.log" 2>&1; then - log "[$cli] user profile: unrealMCP added/updated" - else - sed -n '1,200p' "/tmp/${cli}-add-mcp.log" || true - fail "[$cli] --add-mcp failed for user profile. Ensure your VS Code supports MCP or rerun with --no-vscode." - fi + # Handle multiple VS Code variants if present + local candidates=(code code-insiders codium) + local found_any=false + for cli in "${candidates[@]}"; do + if ! command -v "$cli" >/dev/null 2>&1; then + continue + fi + found_any=true + log "Registering MCP server in VS Code user profile via: $cli --add-mcp" + if "$cli" --add-mcp "$json" >"/tmp/${cli}-add-mcp.log" 2>&1; then + log "[$cli] user profile: unrealMCP added/updated" + else + sed -n '1,200p' "/tmp/${cli}-add-mcp.log" || true + fail "[$cli] --add-mcp failed for user profile. Ensure your VS Code supports MCP or rerun with --no-vscode." + fi - # Detect profiles with 'unreal' (case-insensitive) and add there too - local data_dir="" - case "$cli" in - code) - data_dir="$USER_HOME/.config/Code" - ;; - code-insiders) - data_dir="$USER_HOME/.config/Code - Insiders" - ;; - codium) - data_dir="$USER_HOME/.config/VSCodium" - ;; - esac - local profiles_json="$data_dir/User/profiles/profiles.json" - if [[ -f $profiles_json ]]; then - # Extract profile names matching /unreal/i - mapfile -t unreal_profiles < <(jq -r '.profiles // [] | .[] | .name // empty | select(test("unreal"; "i"))' "$profiles_json") - if [[ ${#unreal_profiles[@]} -gt 0 ]]; then - log "[$cli] Found profiles with 'unreal': ${unreal_profiles[*]}" - local name - for name in "${unreal_profiles[@]}"; do - log "[$cli] Adding unrealMCP to profile: $name" - if "$cli" --profile "$name" --add-mcp "$json" > "/tmp/${cli}-add-mcp-${name// /_}.log" 2>&1; then - log "[$cli] profile '$name': unrealMCP added/updated" - else - sed -n '1,200p' "/tmp/${cli}-add-mcp-${name// /_}.log" || true - fail "[$cli] --add-mcp failed for profile '$name'." - fi - done - else - log "[$cli] No VS Code profiles with 'unreal' in name" - fi - else - log "[$cli] Profiles file not found: $profiles_json (skipping profile-specific adds)" - fi - done + # Detect profiles with 'unreal' (case-insensitive) and add there too + local data_dir="" + case "$cli" in + code) + data_dir="$USER_HOME/.config/Code" + ;; + code-insiders) + data_dir="$USER_HOME/.config/Code - Insiders" + ;; + codium) + data_dir="$USER_HOME/.config/VSCodium" + ;; + esac + local profiles_json="$data_dir/User/profiles/profiles.json" + if [[ -f $profiles_json ]]; then + # Extract profile names matching /unreal/i + mapfile -t unreal_profiles < <(jq -r '.profiles // [] | .[] | .name // empty | select(test("unreal"; "i"))' "$profiles_json") + if [[ ${#unreal_profiles[@]} -gt 0 ]]; then + log "[$cli] Found profiles with 'unreal': ${unreal_profiles[*]}" + local name + for name in "${unreal_profiles[@]}"; do + log "[$cli] Adding unrealMCP to profile: $name" + if "$cli" --profile "$name" --add-mcp "$json" >"/tmp/${cli}-add-mcp-${name// /_}.log" 2>&1; then + log "[$cli] profile '$name': unrealMCP added/updated" + else + sed -n '1,200p' "/tmp/${cli}-add-mcp-${name// /_}.log" || true + fail "[$cli] --add-mcp failed for profile '$name'." + fi + done + else + log "[$cli] No VS Code profiles with 'unreal' in name" + fi + else + log "[$cli] Profiles file not found: $profiles_json (skipping profile-specific adds)" + fi + done - if [[ $found_any == false ]]; then - fail "VS Code CLI not found (code/code-insiders/codium). Install VS Code and ensure 'code' CLI is available, or run with --no-vscode to skip." - fi + if [[ $found_any == false ]]; then + fail "VS Code CLI not found (code/code-insiders/codium). Install VS Code and ensure 'code' CLI is available, or run with --no-vscode to skip." + fi } # ---------- Unreal Plugin copy (optional) ---------- install_plugin_into_project() { - [[ -n $PROJECT_UPROJECT ]] || return 0 - local upath="$PROJECT_UPROJECT" - if [[ -d $upath ]]; then - # Resolve .uproject in the provided directory - mapfile -t _uprojects < <(find "$upath" -maxdepth 1 -type f -name "*.uproject" 2> /dev/null || true) - if [[ ${#_uprojects[@]} -eq 0 ]]; then - fail "--project directory '$upath' contains no .uproject files" - elif [[ ${#_uprojects[@]} -gt 1 ]]; then - printf '[ERROR] Multiple .uproject files found in %s:\n' "$upath" >&2 - printf ' - %s\n' "${_uprojects[@]}" >&2 - fail "Please pass the specific .uproject path to --project" - else - upath="${_uprojects[0]}" - log "Resolved .uproject: $upath" - fi - elif [[ -f $upath ]]; then - true - else - fail "--project path does not exist: $upath" - fi - if [[ ${upath##*.} != "uproject" ]]; then - fail "--project must point to a .uproject file (got: $upath)" - fi - local proj_dir - proj_dir="$(cd "$(dirname "$upath")" && pwd)" - RESOLVED_PROJECT_DIR="$proj_dir" - local src_plugin="$REPO_DIR/MCPGameProject/Plugins/UnrealMCP" - local dst_plugin="$proj_dir/Plugins/UnrealMCP" - if [[ ! -d $src_plugin ]]; then - fail "Source plugin not found at $src_plugin (did repo layout change?)" - fi - mkdir -p "$proj_dir/Plugins" - log "Copying UnrealMCP plugin to project: $dst_plugin" - rsync -a --delete "$src_plugin/" "$dst_plugin/" - # Set ownership back to actual user if run as root - if [[ $EUID -eq 0 ]]; then chown -R "$ACTUAL_USER:$ACTUAL_USER" "$proj_dir/Plugins"; fi - log "Plugin installed. Enable it from Unreal Editor (Edit > Plugins) if needed." + [[ -n $PROJECT_UPROJECT ]] || return 0 + local upath="$PROJECT_UPROJECT" + if [[ -d $upath ]]; then + # Resolve .uproject in the provided directory + mapfile -t _uprojects < <(find "$upath" -maxdepth 1 -type f -name "*.uproject" 2>/dev/null || true) + if [[ ${#_uprojects[@]} -eq 0 ]]; then + fail "--project directory '$upath' contains no .uproject files" + elif [[ ${#_uprojects[@]} -gt 1 ]]; then + printf '[ERROR] Multiple .uproject files found in %s:\n' "$upath" >&2 + printf ' - %s\n' "${_uprojects[@]}" >&2 + fail "Please pass the specific .uproject path to --project" + else + upath="${_uprojects[0]}" + log "Resolved .uproject: $upath" + fi + elif [[ -f $upath ]]; then + true + else + fail "--project path does not exist: $upath" + fi + if [[ ${upath##*.} != "uproject" ]]; then + fail "--project must point to a .uproject file (got: $upath)" + fi + local proj_dir + proj_dir="$(cd "$(dirname "$upath")" && pwd)" + RESOLVED_PROJECT_DIR="$proj_dir" + local src_plugin="$REPO_DIR/MCPGameProject/Plugins/UnrealMCP" + local dst_plugin="$proj_dir/Plugins/UnrealMCP" + if [[ ! -d $src_plugin ]]; then + fail "Source plugin not found at $src_plugin (did repo layout change?)" + fi + mkdir -p "$proj_dir/Plugins" + log "Copying UnrealMCP plugin to project: $dst_plugin" + rsync -a --delete "$src_plugin/" "$dst_plugin/" + # Set ownership back to actual user if run as root + if [[ $EUID -eq 0 ]]; then chown -R "$ACTUAL_USER:$ACTUAL_USER" "$proj_dir/Plugins"; fi + log "Plugin installed. Enable it from Unreal Editor (Edit > Plugins) if needed." } # ---------- Summary ---------- print_summary() { - local python_dir="$REPO_DIR/Python" - local plugin_dest="N/A" - if [[ -n $RESOLVED_PROJECT_DIR ]]; then - plugin_dest="$RESOLVED_PROJECT_DIR/Plugins/UnrealMCP" - fi - cat << EOF + local python_dir="$REPO_DIR/Python" + local plugin_dest="N/A" + if [[ -n $RESOLVED_PROJECT_DIR ]]; then + plugin_dest="$RESOLVED_PROJECT_DIR/Plugins/UnrealMCP" + fi + cat <&2; } success() { echo -e "${GREEN}[OK ]${RESET} $*"; } usage() { - cat << EOF + cat < /dev/null 2>&1; then - key=$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 40 || true) - fi - fi - if [[ -z $key || ${#key} -lt 20 ]]; then - # Last resort static warning key (should not happen) - key="LT$(date +%s)$$RANDOM" - fi - printf '%s' "$key" + # Avoid SIGPIPE issues under set -o pipefail by capturing output first + local key + key=$(head -c 256 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c 40 || true) + if [[ -z $key || ${#key} -lt 40 ]]; then + # Fallback using openssl if available + if command -v openssl >/dev/null 2>&1; then + key=$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 40 || true) + fi + fi + if [[ -z $key || ${#key} -lt 20 ]]; then + # Last resort static warning key (should not happen) + key="LT$(date +%s)$$RANDOM" + fi + printf '%s' "$key" } need_cmd() { - command -v "$1" > /dev/null 2>&1 || { - err "Required command '$1' not found" - return 1 - } + command -v "$1" >/dev/null 2>&1 || { + err "Required command '$1' not found" + return 1 + } } parse_args() { - while [[ $# -gt 0 ]]; do - case "$1" in - --image) - IMAGE="$2" - shift 2 - ;; - --tag) - TAG="$2" - shift 2 - ;; - --port) - PORT="$2" - shift 2 - ;; - --host) - HOST="$2" - shift 2 - ;; - --data-dir) - DATA_DIR="$2" - CACHE_DIR="${DATA_DIR}/cache" - shift 2 - ;; - --cache-dir) - CACHE_DIR="$2" - shift 2 - ;; - --no-docker-install) - DOCKER_INSTALL=0 - shift - ;; - --keep-alive) - KEEP_ALIVE=1 - shift - ;; - --) - shift - RUN_COMMAND=("$@") - break - ;; - --api-key) - API_KEY="$2" - GENERATE_API_KEY=0 - shift 2 - ;; - --generate-api-key) - GENERATE_API_KEY=1 - shift - ;; - --disable-api-key) - DISABLE_API_KEY=1 - shift - ;; - --preload-langs) - PRELOAD_LANGS="$2" - shift 2 - ;; - --env) - EXTRA_ENV+=("$2") - shift 2 - ;; - --pull-only) - PULL_ONLY=1 - shift - ;; - --uninstall) - UNINSTALL=1 - shift - ;; - --purge) - UNINSTALL=1 - KEEP_DATA=0 - shift - ;; - --keep-data) - KEEP_DATA=1 - shift - ;; - --health-timeout) - HEALTH_TIMEOUT="$2" - shift 2 - ;; - --no-color) - NO_COLOR=1 - shift - ;; - --debug) - DEBUG=1 - shift - ;; - -h | --help) - usage - exit 0 - ;; - -v | --version) - echo "${VERSION}" - exit 0 - ;; - *) - err "Unknown argument: $1" - usage - exit 1 - ;; - esac - done + while [[ $# -gt 0 ]]; do + case "$1" in + --image) + IMAGE="$2" + shift 2 + ;; + --tag) + TAG="$2" + shift 2 + ;; + --port) + PORT="$2" + shift 2 + ;; + --host) + HOST="$2" + shift 2 + ;; + --data-dir) + DATA_DIR="$2" + CACHE_DIR="${DATA_DIR}/cache" + shift 2 + ;; + --cache-dir) + CACHE_DIR="$2" + shift 2 + ;; + --no-docker-install) + DOCKER_INSTALL=0 + shift + ;; + --keep-alive) + KEEP_ALIVE=1 + shift + ;; + --) + shift + RUN_COMMAND=("$@") + break + ;; + --api-key) + API_KEY="$2" + GENERATE_API_KEY=0 + shift 2 + ;; + --generate-api-key) + GENERATE_API_KEY=1 + shift + ;; + --disable-api-key) + DISABLE_API_KEY=1 + shift + ;; + --preload-langs) + PRELOAD_LANGS="$2" + shift 2 + ;; + --env) + EXTRA_ENV+=("$2") + shift 2 + ;; + --pull-only) + PULL_ONLY=1 + shift + ;; + --uninstall) + UNINSTALL=1 + shift + ;; + --purge) + UNINSTALL=1 + KEEP_DATA=0 + shift + ;; + --keep-data) + KEEP_DATA=1 + shift + ;; + --health-timeout) + HEALTH_TIMEOUT="$2" + shift 2 + ;; + --no-color) + NO_COLOR=1 + shift + ;; + --debug) + DEBUG=1 + shift + ;; + -h | --help) + usage + exit 0 + ;; + -v | --version) + echo "${VERSION}" + exit 0 + ;; + *) + err "Unknown argument: $1" + usage + exit 1 + ;; + esac + done } ensure_root() { - if [[ $EUID -ne 0 ]]; then - err "This script must run as root (or via sudo)." - exit 1 - fi + if [[ $EUID -ne 0 ]]; then + err "This script must run as root (or via sudo)." + exit 1 + fi } install_docker() { - if command -v docker > /dev/null 2>&1; then - log "Docker already installed" - return 0 - fi - if [[ ${DOCKER_INSTALL} -eq 0 ]]; then - err "Docker is not installed and --no-docker-install specified." - exit 1 - fi - log "Installing Docker..." - if command -v apt-get > /dev/null 2>&1; then - apt-get update -y - apt-get install -y ca-certificates curl gnupg - install -d -m 0755 /etc/apt/keyrings - curl -fsSL "https://download.docker.com/linux/$( - . /etc/os-release - echo "$ID" - )/gpg" | gpg --dearmor -o /etc/apt/keyrings/docker.gpg - chmod a+r /etc/apt/keyrings/docker.gpg - echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$( - . /etc/os-release - echo "$ID" - ) $( - . /etc/os-release - echo "$VERSION_CODENAME" - ) stable" \ - > /etc/apt/sources.list.d/docker.list - apt-get update -y - apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - else - err "Unsupported package manager. Please install Docker manually." - exit 1 - fi - # Attempt to start docker daemon if dockerd exists and systemctl available; otherwise rely on user - if command -v systemctl > /dev/null 2>&1; then - (systemctl enable --now docker 2> /dev/null && success "Docker installed and started") || warn "Docker installed; ensure dockerd is running" - else - warn "Docker installed; please ensure docker daemon is running" - fi + if command -v docker >/dev/null 2>&1; then + log "Docker already installed" + return 0 + fi + if [[ ${DOCKER_INSTALL} -eq 0 ]]; then + err "Docker is not installed and --no-docker-install specified." + exit 1 + fi + log "Installing Docker..." + if command -v apt-get >/dev/null 2>&1; then + apt-get update -y + apt-get install -y ca-certificates curl gnupg + install -d -m 0755 /etc/apt/keyrings + curl -fsSL "https://download.docker.com/linux/$( + . /etc/os-release + echo "$ID" + )/gpg" | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$( + . /etc/os-release + echo "$ID" + ) $( + . /etc/os-release + echo "$VERSION_CODENAME" + ) stable" \ + >/etc/apt/sources.list.d/docker.list + apt-get update -y + apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + else + err "Unsupported package manager. Please install Docker manually." + exit 1 + fi + # Attempt to start docker daemon if dockerd exists and systemctl available; otherwise rely on user + if command -v systemctl >/dev/null 2>&1; then + (systemctl enable --now docker 2>/dev/null && success "Docker installed and started") || warn "Docker installed; ensure dockerd is running" + else + warn "Docker installed; please ensure docker daemon is running" + fi } pull_image() { - log "Pulling image ${IMAGE}:${TAG}" - docker pull "${IMAGE}:${TAG}" - success "Image pulled" + log "Pulling image ${IMAGE}:${TAG}" + docker pull "${IMAGE}:${TAG}" + success "Image pulled" } detect_container_user() { - # Determine uid/gid of configured user inside image so host dirs can be chowned - if ! command -v docker > /dev/null 2>&1; then - return 0 - fi - local uid gid - uid=$(docker run --rm --entrypoint /usr/bin/id "${IMAGE}:${TAG}" -u 2> /dev/null || echo "") - gid=$(docker run --rm --entrypoint /usr/bin/id "${IMAGE}:${TAG}" -g 2> /dev/null || echo "") - if [[ -n $uid && -n $gid ]]; then - CONTAINER_UID=$uid - CONTAINER_GID=$gid - fi + # Determine uid/gid of configured user inside image so host dirs can be chowned + if ! command -v docker >/dev/null 2>&1; then + return 0 + fi + local uid gid + uid=$(docker run --rm --entrypoint /usr/bin/id "${IMAGE}:${TAG}" -u 2>/dev/null || echo "") + gid=$(docker run --rm --entrypoint /usr/bin/id "${IMAGE}:${TAG}" -g 2>/dev/null || echo "") + if [[ -n $uid && -n $gid ]]; then + CONTAINER_UID=$uid + CONTAINER_GID=$gid + fi } write_env_file() { - mkdir -p "${CONFIG_DIR}" "${DATA_DIR}" "${CACHE_DIR}" - detect_container_user - if [[ -n ${CONTAINER_UID:-} && -n ${CONTAINER_GID:-} ]]; then - if command -v stat > /dev/null 2>&1; then - for d in "${DATA_DIR}" "${CACHE_DIR}"; do - if [[ -d $d ]]; then - CUR_UID=$(stat -c %u "$d" 2> /dev/null || echo -1) - if [[ ${CUR_UID} -ne ${CONTAINER_UID} ]]; then - chown "${CONTAINER_UID}":"${CONTAINER_GID}" "$d" 2> /dev/null || warn "Unable to chown $d to ${CONTAINER_UID}:${CONTAINER_GID}" - fi - fi - done - fi - fi - if [[ ${DISABLE_API_KEY} -eq 1 ]]; then - API_KEY_LINE="LT_NO_API_KEY=true" - else - if [[ -z ${API_KEY} && ${GENERATE_API_KEY} -eq 1 ]]; then - API_KEY=$(gen_api_key) - GENERATED=1 - else - GENERATED=0 - fi - API_KEY_LINE="LT_API_KEYS=${API_KEY}" - fi + mkdir -p "${CONFIG_DIR}" "${DATA_DIR}" "${CACHE_DIR}" + detect_container_user + if [[ -n ${CONTAINER_UID:-} && -n ${CONTAINER_GID:-} ]]; then + if command -v stat >/dev/null 2>&1; then + for d in "${DATA_DIR}" "${CACHE_DIR}"; do + if [[ -d $d ]]; then + CUR_UID=$(stat -c %u "$d" 2>/dev/null || echo -1) + if [[ ${CUR_UID} -ne ${CONTAINER_UID} ]]; then + chown "${CONTAINER_UID}":"${CONTAINER_GID}" "$d" 2>/dev/null || warn "Unable to chown $d to ${CONTAINER_UID}:${CONTAINER_GID}" + fi + fi + done + fi + fi + if [[ ${DISABLE_API_KEY} -eq 1 ]]; then + API_KEY_LINE="LT_NO_API_KEY=true" + else + if [[ -z ${API_KEY} && ${GENERATE_API_KEY} -eq 1 ]]; then + API_KEY=$(gen_api_key) + GENERATED=1 + else + GENERATED=0 + fi + API_KEY_LINE="LT_API_KEYS=${API_KEY}" + fi - { - echo "# LibreTranslate environment file" - echo "# Generated $(date -u +%Y-%m-%dT%H:%M:%SZ)" - echo "${API_KEY_LINE}" - [[ -n ${PRELOAD_LANGS} ]] && echo "LT_PRELOAD_LANGS=${PRELOAD_LANGS}" - for kv in "${EXTRA_ENV[@]:-}"; do echo "$kv"; done - } > "${ENV_FILE}.tmp" - mv "${ENV_FILE}.tmp" "${ENV_FILE}" - chmod 600 "${ENV_FILE}" - success "Environment file written: ${ENV_FILE}" + { + echo "# LibreTranslate environment file" + echo "# Generated $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "${API_KEY_LINE}" + [[ -n ${PRELOAD_LANGS} ]] && echo "LT_PRELOAD_LANGS=${PRELOAD_LANGS}" + for kv in "${EXTRA_ENV[@]:-}"; do echo "$kv"; done + } >"${ENV_FILE}.tmp" + mv "${ENV_FILE}.tmp" "${ENV_FILE}" + chmod 600 "${ENV_FILE}" + success "Environment file written: ${ENV_FILE}" } start_container_ephemeral() { - docker rm -f "${SERVICE_NAME}" > /dev/null 2>&1 || true - docker run -d --name "${SERVICE_NAME}" \ - --env-file "${ENV_FILE}" \ - -v "${DATA_DIR}:/home/libretranslate/.local/share/argos-translate" \ - -v "${CACHE_DIR}:/app/cache" \ - -p "${PORT}:${PORT}" \ - "${IMAGE}:${TAG}" \ - --host 0.0.0.0 --port "${PORT}" - success "Container started (ephemeral)" - echo - echo "Endpoint (pending readiness): http://$(hostname -I | awk '{print $1}'):${PORT}" - echo "Waiting for health..." + docker rm -f "${SERVICE_NAME}" >/dev/null 2>&1 || true + docker run -d --name "${SERVICE_NAME}" \ + --env-file "${ENV_FILE}" \ + -v "${DATA_DIR}:/home/libretranslate/.local/share/argos-translate" \ + -v "${CACHE_DIR}:/app/cache" \ + -p "${PORT}:${PORT}" \ + "${IMAGE}:${TAG}" \ + --host 0.0.0.0 --port "${PORT}" + success "Container started (ephemeral)" + echo + echo "Endpoint (pending readiness): http://$(hostname -I | awk '{print $1}'):${PORT}" + echo "Waiting for health..." } health_check() { - local start - start=$(date +%s) - local url="http://127.0.0.1:${PORT}/languages" - local attempt=0 - while true; do - attempt=$((attempt + 1)) - if curl ${DEBUG:+-v} -fsS "$url" > /dev/null 2>&1; then - success "Service healthy (attempt $attempt)" - return 0 - else - [[ $DEBUG -eq 1 ]] && log "Health attempt $attempt failed" - fi - if (($(date +%s) - start > HEALTH_TIMEOUT)); then - err "Health check failed after ${HEALTH_TIMEOUT}s (attempts: $attempt)" - docker logs --tail 200 "${SERVICE_NAME}" || true - return 1 - fi - sleep 0.5 - done + local start + start=$(date +%s) + local url="http://127.0.0.1:${PORT}/languages" + local attempt=0 + while true; do + attempt=$((attempt + 1)) + if curl ${DEBUG:+-v} -fsS "$url" >/dev/null 2>&1; then + success "Service healthy (attempt $attempt)" + return 0 + else + [[ $DEBUG -eq 1 ]] && log "Health attempt $attempt failed" + fi + if (($(date +%s) - start > HEALTH_TIMEOUT)); then + err "Health check failed after ${HEALTH_TIMEOUT}s (attempts: $attempt)" + docker logs --tail 200 "${SERVICE_NAME}" || true + return 1 + fi + sleep 0.5 + done } sample_request() { - if [[ ${DISABLE_API_KEY} -eq 0 ]]; then - local key="${API_KEY}" - else - local key="" - fi - log "Performing sample translation (en->es)..." - local DATA='{"q":"Hello world","source":"en","target":"es","format":"text"}' - if [[ -n $key ]]; then - curl -fsS -H "Content-Type: application/json" -H "Authorization: ${key}" -d "$DATA" "http://127.0.0.1:${PORT}/translate" || warn "Sample request failed" - else - curl -fsS -H "Content-Type: application/json" -d "$DATA" "http://127.0.0.1:${PORT}/translate" || warn "Sample request failed" - fi - echo + if [[ ${DISABLE_API_KEY} -eq 0 ]]; then + local key="${API_KEY}" + else + local key="" + fi + log "Performing sample translation (en->es)..." + local DATA='{"q":"Hello world","source":"en","target":"es","format":"text"}' + if [[ -n $key ]]; then + curl -fsS -H "Content-Type: application/json" -H "Authorization: ${key}" -d "$DATA" "http://127.0.0.1:${PORT}/translate" || warn "Sample request failed" + else + curl -fsS -H "Content-Type: application/json" -d "$DATA" "http://127.0.0.1:${PORT}/translate" || warn "Sample request failed" + fi + echo } uninstall_all() { - log "Uninstalling LibreTranslate (ephemeral mode)..." - docker rm -f "${SERVICE_NAME}" 2> /dev/null || true - docker rmi "${IMAGE}:${TAG}" 2> /dev/null || true - if [[ ${KEEP_DATA} -eq 0 ]]; then - rm -rf "${DATA_DIR}" "${CONFIG_DIR}" || true - success "Data directories removed" - else - log "Data kept in ${DATA_DIR} and ${CONFIG_DIR}" - fi - success "Uninstall complete" - exit 0 + log "Uninstalling LibreTranslate (ephemeral mode)..." + docker rm -f "${SERVICE_NAME}" 2>/dev/null || true + docker rmi "${IMAGE}:${TAG}" 2>/dev/null || true + if [[ ${KEEP_DATA} -eq 0 ]]; then + rm -rf "${DATA_DIR}" "${CONFIG_DIR}" || true + success "Data directories removed" + else + log "Data kept in ${DATA_DIR} and ${CONFIG_DIR}" + fi + success "Uninstall complete" + exit 0 } main() { - parse_args "$@" - ensure_root + parse_args "$@" + ensure_root - if [[ ${UNINSTALL} -eq 1 ]]; then - uninstall_all - fi + if [[ ${UNINSTALL} -eq 1 ]]; then + uninstall_all + fi - install_docker - pull_image - if [[ ${PULL_ONLY} -eq 1 ]]; then - log "Pull-only requested, exiting." - exit 0 - fi + install_docker + pull_image + if [[ ${PULL_ONLY} -eq 1 ]]; then + log "Pull-only requested, exiting." + exit 0 + fi - write_env_file + write_env_file - # Always ephemeral now - start_container_ephemeral + # Always ephemeral now + start_container_ephemeral - health_check - sample_request || true + health_check + sample_request || true - # If a command is provided, run it and then shutdown container - if [[ ${#RUN_COMMAND[@]} -gt 0 ]]; then - log "Running user command: ${RUN_COMMAND[*]}" - set +e - "${RUN_COMMAND[@]}" - CMD_STATUS=$? - set -e - log "Command exited with status ${CMD_STATUS}; stopping container" - docker stop "${SERVICE_NAME}" > /dev/null 2>&1 || true - exit ${CMD_STATUS} - fi + # If a command is provided, run it and then shutdown container + if [[ ${#RUN_COMMAND[@]} -gt 0 ]]; then + log "Running user command: ${RUN_COMMAND[*]}" + set +e + "${RUN_COMMAND[@]}" + CMD_STATUS=$? + set -e + log "Command exited with status ${CMD_STATUS}; stopping container" + docker stop "${SERVICE_NAME}" >/dev/null 2>&1 || true + exit ${CMD_STATUS} + fi - if [[ ${KEEP_ALIVE} -eq 1 ]]; then - log "Tailing logs (Ctrl-C to stop and remove container)" - trap 'log "Stopping container"; docker stop "${SERVICE_NAME}" >/dev/null 2>&1 || true; exit 0' INT TERM - docker logs -f "${SERVICE_NAME}" - log "Logs ended; stopping container" - docker stop "${SERVICE_NAME}" > /dev/null 2>&1 || true - else - log "Ephemeral container left running in background (id: $(docker inspect --format '{{.Id}}' ${SERVICE_NAME} 2> /dev/null || echo unknown))" - log "Stop manually with: docker stop ${SERVICE_NAME}" - fi + if [[ ${KEEP_ALIVE} -eq 1 ]]; then + log "Tailing logs (Ctrl-C to stop and remove container)" + trap 'log "Stopping container"; docker stop "${SERVICE_NAME}" >/dev/null 2>&1 || true; exit 0' INT TERM + docker logs -f "${SERVICE_NAME}" + log "Logs ended; stopping container" + docker stop "${SERVICE_NAME}" >/dev/null 2>&1 || true + else + log "Ephemeral container left running in background (id: $(docker inspect --format '{{.Id}}' ${SERVICE_NAME} 2>/dev/null || echo unknown))" + log "Stop manually with: docker stop ${SERVICE_NAME}" + fi - echo - echo "${BOLD}LibreTranslate is ready.${RESET}" - echo "Endpoint: http://$(hostname -I | awk '{print $1}'):${PORT}" - if [[ ${DISABLE_API_KEY} -eq 0 ]]; then - if [[ ${GENERATED:-0} -eq 1 ]]; then - echo "Generated API key: ${API_KEY}" - else - echo "API key: ${API_KEY}" - fi - echo "Use header: Authorization: " - else - echo "API key authentication DISABLED (public instance)." - fi - [[ -n ${PRELOAD_LANGS} ]] && echo "Preloaded languages requested: ${PRELOAD_LANGS}" || true - echo "Environment file: ${ENV_FILE}" - echo "Manage: docker logs -f ${SERVICE_NAME} | docker stop ${SERVICE_NAME}" - echo "Uninstall: sudo ${SCRIPT_NAME} --uninstall" - echo + echo + echo "${BOLD}LibreTranslate is ready.${RESET}" + echo "Endpoint: http://$(hostname -I | awk '{print $1}'):${PORT}" + if [[ ${DISABLE_API_KEY} -eq 0 ]]; then + if [[ ${GENERATED:-0} -eq 1 ]]; then + echo "Generated API key: ${API_KEY}" + else + echo "API key: ${API_KEY}" + fi + echo "Use header: Authorization: " + else + echo "API key authentication DISABLED (public instance)." + fi + if [[ -n ${PRELOAD_LANGS} ]]; then + echo "Preloaded languages requested: ${PRELOAD_LANGS}" + fi + echo "Environment file: ${ENV_FILE}" + echo "Manage: docker logs -f ${SERVICE_NAME} | docker stop ${SERVICE_NAME}" + echo "Uninstall: sudo ${SCRIPT_NAME} --uninstall" + echo } main "$@" diff --git a/scripts/misc/testsAndMisc-bash/transcribe.sh b/scripts/misc/testsAndMisc-bash/transcribe.sh index deb0764..f7e0049 100755 --- a/scripts/misc/testsAndMisc-bash/transcribe.sh +++ b/scripts/misc/testsAndMisc-bash/transcribe.sh @@ -14,7 +14,7 @@ PY_RUNNER="$TOOLS_DIR/transcribe_fw.py" VENV_DIR="$PROJECT_DIR/.venv" usage() { - cat << USAGE + cat < /dev/null 2>&1; then - echo apt - return - fi - if command -v dnf > /dev/null 2>&1; then - echo dnf - return - fi - if command -v yum > /dev/null 2>&1; then - echo yum - return - fi - if command -v pacman > /dev/null 2>&1; then - echo pacman - return - fi - if command -v zypper > /dev/null 2>&1; then - echo zypper - return - fi - echo none + if command -v apt-get >/dev/null 2>&1; then + echo apt + return + fi + if command -v dnf >/dev/null 2>&1; then + echo dnf + return + fi + if command -v yum >/dev/null 2>&1; then + echo yum + return + fi + if command -v pacman >/dev/null 2>&1; then + echo pacman + return + fi + if command -v zypper >/dev/null 2>&1; then + echo zypper + return + fi + echo none } has_libcublas12() { - # Common system locations - for d in \ - /usr/lib \ - /usr/lib64 \ - /usr/local/cuda/lib64 \ - /usr/local/cuda-12*/lib64 \ - /opt/cuda/lib64 \ - /opt/cuda/targets/x86_64-linux/lib; do - [[ -e "$d/libcublas.so.12" ]] && return 0 || true - done - # venv-provided NVIDIA CUDA libs - if [[ -x "$VENV_DIR/bin/python" ]]; then - local pyver - pyver="$("$VENV_DIR"/bin/python -c 'import sys;print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2> /dev/null || true)" - if [[ -n $pyver ]]; then - for d in "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cublas/lib" \ - "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cudnn/lib" \ - "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib"; do - [[ -e "$d/libcublas.so.12" ]] && return 0 || true - done - fi - fi - return 1 + # Common system locations + for d in \ + /usr/lib \ + /usr/lib64 \ + /usr/local/cuda/lib64 \ + /usr/local/cuda-12*/lib64 \ + /opt/cuda/lib64 \ + /opt/cuda/targets/x86_64-linux/lib; do + if [[ -e "$d/libcublas.so.12" ]]; then + return 0 + fi + done + # venv-provided NVIDIA CUDA libs + if [[ -x "$VENV_DIR/bin/python" ]]; then + local pyver + pyver="$("$VENV_DIR"/bin/python -c 'import sys;print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || true)" + if [[ -n $pyver ]]; then + for d in "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cublas/lib" \ + "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cudnn/lib" \ + "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib"; do + if [[ -e "$d/libcublas.so.12" ]]; then + return 0 + fi + done + fi + fi + return 1 } ensure_cuda_runtime() { - local mgr - mgr="$(detect_pkg_mgr)" - if [[ $OFFLINE -eq 1 ]]; then - if has_libcublas12; then return 0; fi - echo "CUDA runtime (libcublas.so.12) not found and offline mode is enabled. Install CUDA 12 runtime or rerun with --online." >&2 - exit 6 - fi - if has_libcublas12; then - return 0 - fi - if ! command -v sudo > /dev/null 2>&1; then - log "sudo not found; skipping CUDA runtime install attempt." - else - log "CUDA cuBLAS 12 not found; attempting to install CUDA runtime (manager: $mgr)" - set +e - case "$mgr" in - pacman) - sudo pacman -Sy --noconfirm cuda cudnn || true - ;; - apt) - sudo apt-get update -y || true - sudo apt-get install -y nvidia-cuda-toolkit || true - ;; - dnf | yum) - sudo "$mgr" install -y cuda cudnn || true - ;; - zypper) - sudo zypper install -y cuda cudnn || true - ;; - *) log "Unknown package manager; cannot install CUDA automatically." ;; - esac - set -e - fi - # Re-check - if ! has_libcublas12; then - echo "CUDA runtime (libcublas.so.12) not found after attempted install. Please install CUDA 12 toolkit/runtime and re-run." >&2 - exit 6 - fi + local mgr + mgr="$(detect_pkg_mgr)" + if [[ $OFFLINE -eq 1 ]]; then + if has_libcublas12; then return 0; fi + echo "CUDA runtime (libcublas.so.12) not found and offline mode is enabled. Install CUDA 12 runtime or rerun with --online." >&2 + exit 6 + fi + if has_libcublas12; then + return 0 + fi + if ! command -v sudo >/dev/null 2>&1; then + log "sudo not found; skipping CUDA runtime install attempt." + else + log "CUDA cuBLAS 12 not found; attempting to install CUDA runtime (manager: $mgr)" + set +e + case "$mgr" in + pacman) + sudo pacman -Sy --noconfirm cuda cudnn || true + ;; + apt) + sudo apt-get update -y || true + sudo apt-get install -y nvidia-cuda-toolkit || true + ;; + dnf | yum) + sudo "$mgr" install -y cuda cudnn || true + ;; + zypper) + sudo zypper install -y cuda cudnn || true + ;; + *) log "Unknown package manager; cannot install CUDA automatically." ;; + esac + set -e + fi + # Re-check + if ! has_libcublas12; then + echo "CUDA runtime (libcublas.so.12) not found after attempted install. Please install CUDA 12 toolkit/runtime and re-run." >&2 + exit 6 + fi } install_system_deps() { - have_cmd() { command -v "$1" > /dev/null 2>&1; } - local need_ffmpeg=0 need_espeak=0 - have_cmd ffmpeg || need_ffmpeg=1 - have_cmd espeak-ng || need_espeak=1 + have_cmd() { command -v "$1" >/dev/null 2>&1; } + local need_ffmpeg=0 need_espeak=0 + have_cmd ffmpeg || need_ffmpeg=1 + have_cmd espeak-ng || need_espeak=1 - # If diarization requested and online, we may also try to ensure libsndfile - local need_libsndfile=0 - if [[ ${FW_DIARIZE:-} == "1" ]]; then - # Heuristic: check common library file - if [[ ! -e /usr/lib/x86_64-linux-gnu/libsndfile.so && ! -e /usr/lib/libsndfile.so && ! -e /usr/lib64/libsndfile.so ]]; then - need_libsndfile=1 - fi - fi + # If diarization requested and online, we may also try to ensure libsndfile + local need_libsndfile=0 + if [[ ${FW_DIARIZE:-} == "1" ]]; then + # Heuristic: check common library file + if [[ ! -e /usr/lib/x86_64-linux-gnu/libsndfile.so && ! -e /usr/lib/libsndfile.so && ! -e /usr/lib64/libsndfile.so ]]; then + need_libsndfile=1 + fi + fi - if [[ $need_ffmpeg -eq 0 && $need_espeak -eq 0 && $need_libsndfile -eq 0 ]]; then - log "System deps present: ffmpeg, espeak-ng${FW_DIARIZE:+, libsndfile}" - return 0 - fi + if [[ $need_ffmpeg -eq 0 && $need_espeak -eq 0 && $need_libsndfile -eq 0 ]]; then + log "System deps present: ffmpeg, espeak-ng${FW_DIARIZE:+, libsndfile}" + return 0 + fi - if [[ $OFFLINE -eq 1 ]]; then - echo "Missing system dependencies (ffmpeg/espeak-ng) but running in offline mode. Install them or rerun with --online." >&2 - exit 5 - fi + if [[ $OFFLINE -eq 1 ]]; then + echo "Missing system dependencies (ffmpeg/espeak-ng) but running in offline mode. Install them or rerun with --online." >&2 + exit 5 + fi - local mgr - mgr="$(detect_pkg_mgr)" - log "Detected package manager: $mgr (installing missing: $([[ $need_ffmpeg -eq 1 ]] && echo ffmpeg)$([[ $need_espeak -eq 1 ]] && echo espeak-ng)$([[ $need_libsndfile -eq 1 ]] && echo libsndfile))" + local mgr + mgr="$(detect_pkg_mgr)" + log "Detected package manager: $mgr (installing missing: $([[ $need_ffmpeg -eq 1 ]] && echo ffmpeg)$([[ $need_espeak -eq 1 ]] && echo espeak-ng)$([[ $need_libsndfile -eq 1 ]] && echo libsndfile))" - if ! command -v sudo > /dev/null 2>&1; then - log "sudo not found; skipping system package installation attempt." - return 0 - fi + if ! command -v sudo >/dev/null 2>&1; then + log "sudo not found; skipping system package installation attempt." + return 0 + fi - # Avoid exiting on install errors; continue best-effort - set +e - case "$mgr" in - apt) - sudo apt-get update -y || log "apt-get update failed; continuing" - pkgs=(python3-venv python3-pip) - [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) - [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) - if [[ $need_libsndfile -eq 1 ]]; then - # Try both names across releases - pkgs+=(libsndfile1) - sudo apt-get install -y libsndfile1 || true - # If that failed, try libsndfile2 (newer distros) - sudo apt-get install -y libsndfile2 || true - fi - sudo apt-get install -y "${pkgs[@]}" || log "apt-get install failed; continuing" - ;; - dnf) - pkgs=(python3-venv python3-pip) - [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) - [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) - [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) - sudo dnf install -y "${pkgs[@]}" || log "dnf install failed; continuing" - ;; - yum) - pkgs=(python3-venv python3-pip) - [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) - [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) - [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) - sudo yum install -y "${pkgs[@]}" || log "yum install failed; continuing" - ;; - pacman) - pkgs=(python-virtualenv python-pip) - [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) - [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) - [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) - sudo pacman -Sy --noconfirm "${pkgs[@]}" || log "pacman install failed; continuing" - ;; - zypper) - pkgs=(python311-virtualenv python311-pip) - [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) - [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) - [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile1) - sudo zypper install -y "${pkgs[@]}" || log "zypper install failed; continuing" - ;; - *) - log "Unknown package manager; please ensure ffmpeg and espeak-ng are installed." - ;; - esac - set -e + # Avoid exiting on install errors; continue best-effort + set +e + case "$mgr" in + apt) + sudo apt-get update -y || log "apt-get update failed; continuing" + pkgs=(python3-venv python3-pip) + [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) + [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) + if [[ $need_libsndfile -eq 1 ]]; then + # Try both names across releases + pkgs+=(libsndfile1) + sudo apt-get install -y libsndfile1 || true + # If that failed, try libsndfile2 (newer distros) + sudo apt-get install -y libsndfile2 || true + fi + sudo apt-get install -y "${pkgs[@]}" || log "apt-get install failed; continuing" + ;; + dnf) + pkgs=(python3-venv python3-pip) + [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) + [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) + [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) + sudo dnf install -y "${pkgs[@]}" || log "dnf install failed; continuing" + ;; + yum) + pkgs=(python3-venv python3-pip) + [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) + [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) + [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) + sudo yum install -y "${pkgs[@]}" || log "yum install failed; continuing" + ;; + pacman) + pkgs=(python-virtualenv python-pip) + [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) + [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) + [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) + sudo pacman -Sy --noconfirm "${pkgs[@]}" || log "pacman install failed; continuing" + ;; + zypper) + pkgs=(python311-virtualenv python311-pip) + [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) + [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) + [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile1) + sudo zypper install -y "${pkgs[@]}" || log "zypper install failed; continuing" + ;; + *) + log "Unknown package manager; please ensure ffmpeg and espeak-ng are installed." + ;; + esac + set -e } setup_venv() { - if [[ ! -d $VENV_DIR ]]; then - log "Creating venv at $VENV_DIR" - python3 -m venv "$VENV_DIR" - fi - # shellcheck disable=SC1091 - source "$VENV_DIR/bin/activate" - if [[ $OFFLINE -eq 0 ]]; then - python -m pip install --upgrade pip wheel setuptools - fi + if [[ ! -d $VENV_DIR ]]; then + log "Creating venv at $VENV_DIR" + python3 -m venv "$VENV_DIR" + fi + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" + if [[ $OFFLINE -eq 0 ]]; then + python -m pip install --upgrade pip wheel setuptools + fi } install_python_deps() { - # Install deps; if NVIDIA GPU is present, prefer CUDA-capable stack (cu12) - local has_nvidia_flag="${1:-0}" - log "Installing faster-whisper and dependencies" - export PIP_DISABLE_PIP_VERSION_CHECK=1 - export PIP_DEFAULT_TIMEOUT=${PIP_DEFAULT_TIMEOUT:-20} - if [[ $OFFLINE -eq 1 ]]; then - # Offline: do not install, just verify modules - if ! python -c 'import faster_whisper' > /dev/null 2>&1; then - echo "Python dependency 'faster_whisper' not found in offline mode. Run with --online to install." >&2 - exit 7 - fi - # If diarization requested offline, check for its deps too (warn-only) - if [[ ${FW_DIARIZE:-} == "1" ]]; then - python - << 'PY' || true + # Install deps; if NVIDIA GPU is present, prefer CUDA-capable stack (cu12) + local has_nvidia_flag="${1:-0}" + log "Installing faster-whisper and dependencies" + export PIP_DISABLE_PIP_VERSION_CHECK=1 + export PIP_DEFAULT_TIMEOUT=${PIP_DEFAULT_TIMEOUT:-20} + if [[ $OFFLINE -eq 1 ]]; then + # Offline: do not install, just verify modules + if ! python -c 'import faster_whisper' >/dev/null 2>&1; then + echo "Python dependency 'faster_whisper' not found in offline mode. Run with --online to install." >&2 + exit 7 + fi + # If diarization requested offline, check for its deps too (warn-only) + if [[ ${FW_DIARIZE:-} == "1" ]]; then + python - <<'PY' || true try: import soundfile, speechbrain, torch # noqa: F401 except Exception as e: print(f"[WARN] Diarization deps missing offline ({e}); speaker labels will be skipped.") PY - fi - return 0 - fi - if [[ $has_nvidia_flag -eq 1 ]]; then - # If ctranslate2 is not installed, attempt CUDA-enabled wheel (quiet, with fallback) - if ! "$VENV_DIR/bin/python" -c 'import ctranslate2' > /dev/null 2>&1; then - log "Installing CUDA-enabled CTranslate2 (cu12 wheel)" - python -m pip install -q --retries 1 --upgrade "ctranslate2<5,>=4.0" --extra-index-url https://download.opennmt.net/ctranslate2/cu12 || - log "Warning: could not reach cu12 wheel index; will proceed with available ctranslate2" - fi - # Ensure NVIDIA CUDA 12 runtime libs are available inside the venv - python -m pip install -q --retries 1 --upgrade nvidia-cublas-cu12 nvidia-cuda-runtime-cu12 nvidia-cudnn-cu12 || - log "Warning: failed to install NVIDIA cu12 runtime libs via pip" - fi - python -m pip install -q --retries 1 --upgrade faster-whisper ffmpeg-python + fi + return 0 + fi + if [[ $has_nvidia_flag -eq 1 ]]; then + # If ctranslate2 is not installed, attempt CUDA-enabled wheel (quiet, with fallback) + if ! "$VENV_DIR/bin/python" -c 'import ctranslate2' >/dev/null 2>&1; then + log "Installing CUDA-enabled CTranslate2 (cu12 wheel)" + python -m pip install -q --retries 1 --upgrade "ctranslate2<5,>=4.0" --extra-index-url https://download.opennmt.net/ctranslate2/cu12 || + log "Warning: could not reach cu12 wheel index; will proceed with available ctranslate2" + fi + # Ensure NVIDIA CUDA 12 runtime libs are available inside the venv + python -m pip install -q --retries 1 --upgrade nvidia-cublas-cu12 nvidia-cuda-runtime-cu12 nvidia-cudnn-cu12 || + log "Warning: failed to install NVIDIA cu12 runtime libs via pip" + fi + python -m pip install -q --retries 1 --upgrade faster-whisper ffmpeg-python - # If diarization requested and online, install its Python deps best-effort - if [[ ${FW_DIARIZE:-} == "1" ]]; then - python -m pip install -q --retries 1 --upgrade soundfile speechbrain || - log "Warning: failed to install soundfile/speechbrain" - # Torch and torchaudio CPU wheels (force to avoid mismatched CUDA builds) - python -m pip install -q --retries 1 --upgrade --force-reinstall --index-url https://download.pytorch.org/whl/cpu torch torchaudio || - log "Warning: failed to install torch/torchaudio CPU wheels" - fi - python - << 'PY' + # If diarization requested and online, install its Python deps best-effort + if [[ ${FW_DIARIZE:-} == "1" ]]; then + python -m pip install -q --retries 1 --upgrade soundfile speechbrain || + log "Warning: failed to install soundfile/speechbrain" + # Torch and torchaudio CPU wheels (force to avoid mismatched CUDA builds) + python -m pip install -q --retries 1 --upgrade --force-reinstall --index-url https://download.pytorch.org/whl/cpu torch torchaudio || + log "Warning: failed to install torch/torchaudio CPU wheels" + fi + python - <<'PY' import sys print(f"[PY] Python {sys.version.split()[0]} dependencies installed.") PY } ensure_runner() { - if [[ ! -f $PY_RUNNER ]]; then - echo "Runner not found: $PY_RUNNER" >&2 - exit 3 - fi + if [[ ! -f $PY_RUNNER ]]; then + echo "Runner not found: $PY_RUNNER" >&2 + exit 3 + fi } generate_test_audio() { - local tmpwav - tmpwav="${PROJECT_DIR}/test_fw.wav" - if command -v espeak-ng > /dev/null 2>&1; then - log "Generating test audio via espeak-ng -> $tmpwav" >&2 - espeak-ng -w "$tmpwav" "This is a quick test of faster whisper transcription." > /dev/null 2>&1 || true - fi - # If espeak-ng failed or not present, try espeak - if [[ ! -s $tmpwav ]] && command -v espeak > /dev/null 2>&1; then - log "espeak-ng unavailable or failed; trying espeak -> $tmpwav" >&2 - espeak -w "$tmpwav" "This is a quick test of faster whisper transcription." > /dev/null 2>&1 || true - fi - # Fallback: generate tone via Python stdlib (no external deps) - if [[ ! -s $tmpwav ]]; then - log "Generating 3s 1kHz WAV via Python stdlib -> $tmpwav" >&2 - python3 -c 'import sys,wave,math,array;outfile=sys.argv[1];fr=16000;dur=3;freq=1000.0;ampl=0.3;n=fr*dur;data=array.array("h",[int(max(-1.0,min(1.0,ampl*math.sin(2*math.pi*freq*(i/fr))))*32767) for i in range(n)]);wf=wave.open(outfile,"w");wf.setnchannels(1);wf.setsampwidth(2);wf.setframerate(fr);wf.writeframes(data.tobytes());wf.close()' "$tmpwav" || true - fi - # Final fallback: tone via ffmpeg - if [[ ! -s $tmpwav ]]; then - log "Creating a 3s sine tone WAV via ffmpeg -> $tmpwav" >&2 - ffmpeg -f lavfi -i sine=frequency=1000:duration=3 -ar 16000 -ac 1 -f wav -y "$tmpwav" > /dev/null 2>&1 || true - fi - echo "$tmpwav" + local tmpwav + tmpwav="${PROJECT_DIR}/test_fw.wav" + if command -v espeak-ng >/dev/null 2>&1; then + log "Generating test audio via espeak-ng -> $tmpwav" >&2 + espeak-ng -w "$tmpwav" "This is a quick test of faster whisper transcription." >/dev/null 2>&1 || true + fi + # If espeak-ng failed or not present, try espeak + if [[ ! -s $tmpwav ]] && command -v espeak >/dev/null 2>&1; then + log "espeak-ng unavailable or failed; trying espeak -> $tmpwav" >&2 + espeak -w "$tmpwav" "This is a quick test of faster whisper transcription." >/dev/null 2>&1 || true + fi + # Fallback: generate tone via Python stdlib (no external deps) + if [[ ! -s $tmpwav ]]; then + log "Generating 3s 1kHz WAV via Python stdlib -> $tmpwav" >&2 + python3 -c 'import sys,wave,math,array;outfile=sys.argv[1];fr=16000;dur=3;freq=1000.0;ampl=0.3;n=fr*dur;data=array.array("h",[int(max(-1.0,min(1.0,ampl*math.sin(2*math.pi*freq*(i/fr))))*32767) for i in range(n)]);wf=wave.open(outfile,"w");wf.setnchannels(1);wf.setsampwidth(2);wf.setframerate(fr);wf.writeframes(data.tobytes());wf.close()' "$tmpwav" || true + fi + # Final fallback: tone via ffmpeg + if [[ ! -s $tmpwav ]]; then + log "Creating a 3s sine tone WAV via ffmpeg -> $tmpwav" >&2 + ffmpeg -f lavfi -i sine=frequency=1000:duration=3 -ar 16000 -ac 1 -f wav -y "$tmpwav" >/dev/null 2>&1 || true + fi + echo "$tmpwav" } prepare_model() { - # Download a model for offline use into MODEL_DIR - local name="$1" - mkdir -p "$MODEL_DIR" - # shellcheck disable=SC1091 - source "$VENV_DIR/bin/activate" - log "Preparing model '$name' into $MODEL_DIR" - python - << PY + # Download a model for offline use into MODEL_DIR + local name="$1" + mkdir -p "$MODEL_DIR" + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" + log "Preparing model '$name' into $MODEL_DIR" + python - <&2 - exit 2 - fi - install_python_deps 0 - export FW_PREPARE_NAME="$PREPARE_MODEL" - export FW_MODEL_DIR="$MODEL_DIR" - prepare_model "$PREPARE_MODEL" - log "Model '$PREPARE_MODEL' downloaded to $MODEL_DIR" - exit 0 - fi + # If asked to prepare a model, do that and exit + if [[ -n $PREPARE_MODEL ]]; then + if [[ $OFFLINE -eq 1 ]]; then + echo "--prepare-model requires network; rerun with --online." >&2 + exit 2 + fi + install_python_deps 0 + export FW_PREPARE_NAME="$PREPARE_MODEL" + export FW_MODEL_DIR="$MODEL_DIR" + prepare_model "$PREPARE_MODEL" + log "Model '$PREPARE_MODEL' downloaded to $MODEL_DIR" + exit 0 + fi - # Detect NVIDIA GPU and enforce CUDA if present - has_nvidia=0 - if command -v nvidia-smi > /dev/null 2>&1 && nvidia-smi -L > /dev/null 2>&1; then - has_nvidia=1 - fi - install_python_deps "$has_nvidia" - ensure_runner + # Detect NVIDIA GPU and enforce CUDA if present + has_nvidia=0 + if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then + has_nvidia=1 + fi + install_python_deps "$has_nvidia" + ensure_runner - local input="$INPUT_FILE" - if [[ -z $input ]]; then - input="$(generate_test_audio)" - if [[ ! -s $input ]]; then - echo "Failed to generate test audio. Please provide an audio file." >&2 - exit 4 - fi - fi + local input="$INPUT_FILE" + if [[ -z $input ]]; then + input="$(generate_test_audio)" + if [[ ! -s $input ]]; then + echo "Failed to generate test audio. Please provide an audio file." >&2 + exit 4 + fi + fi - if [[ ! -f $input ]]; then - echo "Input file not found: $input" >&2 - exit 2 - fi + if [[ ! -f $input ]]; then + echo "Input file not found: $input" >&2 + exit 2 + fi - local args=("$input" "--model" "$MODEL") - [[ -n $LANGUAGE ]] && args+=("--language" "$LANGUAGE") - [[ -n $OUTDIR ]] && args+=("--outdir" "$OUTDIR") + local args=("$input" "--model" "$MODEL") + [[ -n $LANGUAGE ]] && args+=("--language" "$LANGUAGE") + [[ -n $OUTDIR ]] && args+=("--outdir" "$OUTDIR") - # Pass diarization via env if requested - if [[ ${FW_DIARIZE:-} == "1" ]]; then - args+=("--diarize") - if [[ -n ${FW_NUM_SPEAKERS:-} ]]; then - args+=("--num-speakers" "${FW_NUM_SPEAKERS}") - fi - fi + # Pass diarization via env if requested + if [[ ${FW_DIARIZE:-} == "1" ]]; then + args+=("--diarize") + if [[ -n ${FW_NUM_SPEAKERS:-} ]]; then + args+=("--num-speakers" "${FW_NUM_SPEAKERS}") + fi + fi - if [[ $has_nvidia -eq 1 ]]; then - ensure_cuda_runtime - # Export common CUDA paths in case the env lacks them - export CUDA_HOME="${CUDA_HOME:-/usr/local/cuda}" - # Include system and possible venv-provided CUDA libs - local pyver venv_cuda_paths="" - if [[ -x "$VENV_DIR/bin/python" ]]; then - pyver="$("$VENV_DIR"/bin/python -c 'import sys;print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2> /dev/null || true)" - if [[ -n $pyver ]]; then - venv_cuda_paths="$VENV_DIR/lib/python$pyver/site-packages/nvidia/cublas/lib:$VENV_DIR/lib/python$pyver/site-packages/nvidia/cudnn/lib:$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib" - fi - fi - export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}:${CUDA_HOME}/lib64:/usr/lib/x86_64-linux-gnu:/opt/cuda/lib64:/opt/cuda/targets/x86_64-linux/lib:${venv_cuda_paths}" - export PATH="${PATH}:${CUDA_HOME}/bin" - # shellcheck disable=SC1091 - source "$VENV_DIR/bin/activate" - python -c 'from faster_whisper import WhisperModel; WhisperModel("tiny", device="cuda", compute_type="float16"); print("[PY] CUDA test init succeeded.")' || { - echo "CUDA environment check failed. Aborting as requested." >&2 - exit 6 - } - args+=("--device" "cuda") - fi + if [[ $has_nvidia -eq 1 ]]; then + ensure_cuda_runtime + # Export common CUDA paths in case the env lacks them + export CUDA_HOME="${CUDA_HOME:-/usr/local/cuda}" + # Include system and possible venv-provided CUDA libs + local pyver venv_cuda_paths="" + if [[ -x "$VENV_DIR/bin/python" ]]; then + pyver="$("$VENV_DIR"/bin/python -c 'import sys;print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || true)" + if [[ -n $pyver ]]; then + venv_cuda_paths="$VENV_DIR/lib/python$pyver/site-packages/nvidia/cublas/lib:$VENV_DIR/lib/python$pyver/site-packages/nvidia/cudnn/lib:$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib" + fi + fi + export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}:${CUDA_HOME}/lib64:/usr/lib/x86_64-linux-gnu:/opt/cuda/lib64:/opt/cuda/targets/x86_64-linux/lib:${venv_cuda_paths}" + export PATH="${PATH}:${CUDA_HOME}/bin" + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" + python -c 'from faster_whisper import WhisperModel; WhisperModel("tiny", device="cuda", compute_type="float16"); print("[PY] CUDA test init succeeded.")' || { + echo "CUDA environment check failed. Aborting as requested." >&2 + exit 6 + } + args+=("--device" "cuda") + fi - log "Transcribing: $input" - # shellcheck disable=SC1091 - source "$VENV_DIR/bin/activate" - if [[ $has_nvidia -eq 1 ]]; then - if ! python "$PY_RUNNER" "${args[@]}"; then - echo "CUDA execution requested due to detected NVIDIA GPU, but it failed. Aborting as requested (no CPU fallback)." >&2 - exit 6 - fi - else - # Offline: prefer local directory if present; otherwise use cache without network - if [[ $OFFLINE -eq 1 ]]; then - local local_model_path="" - if [[ -d $MODEL ]]; then - local_model_path="$MODEL" - elif [[ -d "$MODEL_DIR/$MODEL" ]]; then - local_model_path="$MODEL_DIR/$MODEL" - fi - if [[ -n $local_model_path ]]; then - args=("$input" "--model" "$local_model_path") - [[ -n $LANGUAGE ]] && args+=("--language" "$LANGUAGE") - [[ -n $OUTDIR ]] && args+=("--outdir" "$OUTDIR") - fi - fi - python "$PY_RUNNER" "${args[@]}" - fi + log "Transcribing: $input" + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" + if [[ $has_nvidia -eq 1 ]]; then + if ! python "$PY_RUNNER" "${args[@]}"; then + echo "CUDA execution requested due to detected NVIDIA GPU, but it failed. Aborting as requested (no CPU fallback)." >&2 + exit 6 + fi + else + # Offline: prefer local directory if present; otherwise use cache without network + if [[ $OFFLINE -eq 1 ]]; then + local local_model_path="" + if [[ -d $MODEL ]]; then + local_model_path="$MODEL" + elif [[ -d "$MODEL_DIR/$MODEL" ]]; then + local_model_path="$MODEL_DIR/$MODEL" + fi + if [[ -n $local_model_path ]]; then + args=("$input" "--model" "$local_model_path") + [[ -n $LANGUAGE ]] && args+=("--language" "$LANGUAGE") + [[ -n $OUTDIR ]] && args+=("--outdir" "$OUTDIR") + fi + fi + python "$PY_RUNNER" "${args[@]}" + fi } main "$@"