diff --git a/docs/superpowers/contracts/linux-config-batch8-2026-08-16.json b/docs/superpowers/contracts/linux-config-batch8-2026-08-16.json new file mode 100644 index 00000000..ec4ed59e --- /dev/null +++ b/docs/superpowers/contracts/linux-config-batch8-2026-08-16.json @@ -0,0 +1,19 @@ +{ + "title": "Split three more linux_configuration scripts and harden the split verifier", + "objective": "Bring transcribe.sh (473), setup_activitywatch.sh (457) and setup_wireguard_ssh.sh (464) under the 250-line cap, and make the verifier catch a split that lands inside a heredoc.", + "acceptance_criteria": [ + "All three entry scripts and all nine new libs are at most 250 lines.", + "The pre-commit shellcheck hook passes on every touched file, with no new suppression and no disable directive.", + "No lib sources a sibling.", + "meta/scripts/verify_shell_split.sh parses every lib with bash -n BEFORE linting the entry script, and reports the lib by name when it fails. A heredoc cut otherwise surfaces as a confusing SC1094 on the caller.", + "That new check is proven in both directions: it fails on a deliberately unterminated heredoc and passes once restored.", + "No script is executed. These install CUDA runtimes, start services and rewrite sshd and nftables config.", + "enforce_vbox_hosts.sh is fully restored, leaving no partial split and no orphan lib directory." + ], + "out_of_scope": [ + "Executing any install or firewall step.", + "enforce_vbox_hosts.sh, whose seam fell inside a heredoc.", + "Changing what any script does when run, beyond the cd guard." + ], + "verifier": "meta/scripts/verify_shell_split.sh per script, plus a deliberate heredoc-corruption test of the verifier itself, plus pre-commit run --files and check_file_length.sh." +} diff --git a/docs/superpowers/evidence/linux-config-batch8-2026-08-16.json b/docs/superpowers/evidence/linux-config-batch8-2026-08-16.json new file mode 100644 index 00000000..de65cf6d --- /dev/null +++ b/docs/superpowers/evidence/linux-config-batch8-2026-08-16.json @@ -0,0 +1,49 @@ +{ + "intent": "Split three more linux_configuration scripts, and harden the verifier after a split silently corrupted a file.", + "scope": [ + "misc/testsAndMisc-bash/transcribe.sh (473 -> 39) + transcribe_pkgmgr, transcribe_deps, transcribe_venv", + "single_use/features/setup_activitywatch.sh (457 -> 102) + aw_install, aw_service, aw_autostart", + "single_use/features/setup_wireguard_ssh.sh (464 -> 136) + wg_keys, wg_firewall, wg_sshd", + "meta/scripts/verify_shell_split.sh (lib parse check moved ahead of the entry lint)", + "Deferred and fully restored: virtualbox/enforce_vbox_hosts.sh (443)", + "Non-goal: no script executed" + ], + "changes": [ + "Nine libs across three scripts, each needing a second carve after the first pass.", + "enforce_vbox_hosts.sh was reverted because the split landed INSIDE a heredoc: the resulting lib had an unterminated here-document and bash -n rejected it outright. This is the first split in the whole effort to produce a file that does not parse, and it is the most dangerous failure mode yet - shellcheck reported it only as SC1094 'parsing of sourced file failed' on the caller, which reads like a linting nicety rather than a broken file.", + "The verifier now runs bash -n over every lib BEFORE linting the entry, and names the offending lib with a hint about heredocs. Proven in both directions on a deliberately corrupted copy.", + "Fixed another unguarded cd, this one before `git clone ... .` - a failed cd would have cloned an AUR repo into the caller's working directory.", + "ALLOW_WEB and ALLOW_DNS are declared in the entry, set by the arg parser in one lib and read by the ruleset builder in another; exported, which is what SC2034 asks for and makes the three-way contract explicit." + ], + "verification": [ + { + "command": "meta/scripts/verify_shell_split.sh per script", + "result": "fail then pass", + "evidence": "All three now report 'libs sourced, functions defined'. Four real failures on the way: chained libs in all three, the unguarded cd, the exported flags, and the vbox heredoc corruption." + }, + { + "command": "append an unterminated heredoc to a lib, re-run the verifier, restore", + "result": "pass", + "evidence": "Reports the here-document parse failure and exits non-zero; passes again once restored. Tested in both directions, because a check that never fires is worse than none." + }, + { + "command": "git checkout HEAD -- enforce_vbox_hosts.sh, rm its lib dir, bash -n and shellcheck -x", + "result": "pass", + "evidence": "Restored to 443 lines, parses and lints clean." + }, + { + "command": "pre-commit run --files <13 changed files>", + "result": "pass", + "evidence": "Clean, including shellcheck over each lib in isolation." + } + ], + "risks": [ + "None of the three was run: they install CUDA runtimes and AUR packages, start user services, and rewrite sshd and nftables configuration.", + "The heredoc failure means the function-boundary heuristic the splitter uses is not safe on files with heredocs at top level. bash -n now catches it, but only after the fact - the splitter itself still has no awareness of heredocs.", + "enforce_vbox_hosts.sh stays at 443 lines and needs a seam chosen by hand, outside any heredoc." + ], + "rollback": [ + "git revert the commit; the three scripts return to their previous sizes, the nine libs disappear, and the verifier loses its parse check.", + "After rollback, re-run bash -n and shellcheck on the restored scripts." + ] +} diff --git a/linux_configuration/scripts/single_use/features/lib/aw_autostart.sh b/linux_configuration/scripts/single_use/features/lib/aw_autostart.sh new file mode 100755 index 00000000..fdec4eba --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/aw_autostart.sh @@ -0,0 +1,241 @@ +#!/bin/bash +# Autostart unit and the installed/running checks. +# +# Sourced by setup_activitywatch.sh; split out to keep aw_service.sh under the +# 250-line cap. Sourced rather than run, so it inherits the caller's +# strict mode and the variables defined above the source line. + +# Function to setup autostart +setup_autostart() { + echo "" + echo "5. Setting Up Autostart..." + echo "=========================" + + local autostart_dir="$USER_HOME/.config/autostart" + local desktop_file="$autostart_dir/activitywatch.desktop" + local i3_config="$USER_HOME/.config/i3/config" + + # Method 1: XDG Autostart (works with most desktop environments) + if [[ $EUID -eq 0 ]]; then + sudo -u "$ACTUAL_USER" mkdir -p "$autostart_dir" + else + mkdir -p "$autostart_dir" + fi + + # Create desktop file for autostart + cat >"$desktop_file" <> '$i3_config' + +# Auto-start ActivityWatch +exec --no-startup-id aw-qt +EOF" + else + { + printf '\n' + printf '# Auto-start ActivityWatch\n' + printf 'exec --no-startup-id aw-qt\n' + } >>"$i3_config" + fi + + echo "✓ Added ActivityWatch to i3 config autostart" + else + echo "✓ ActivityWatch autostart already exists in i3 config" + fi + else + echo "! i3 config not found at $i3_config" + fi +} + +# Function to create i3blocks status script +create_i3blocks_status() { + echo "" + echo "6. Creating i3blocks Status Script..." + echo "====================================" + + local i3blocks_dir="$USER_HOME/.config/i3blocks" + local status_script="$i3blocks_dir/activitywatch_status.sh" + + # Create i3blocks directory if it doesn't exist + if [[ $EUID -eq 0 ]]; then + sudo -u "$ACTUAL_USER" mkdir -p "$i3blocks_dir" + else + mkdir -p "$i3blocks_dir" + fi + + # Create the status script + cat >"$status_script" <<'EOF' +#!/bin/bash +# ActivityWatch status script for i3blocks +# Shows ActivityWatch installation and running status + +# Check if ActivityWatch is installed +check_installed() { + # Check if activitywatch-bin package is installed + if pacman -Qi activitywatch-bin &>/dev/null; then + return 0 + fi + + # Check if aw-qt binary exists + if command -v aw-qt &>/dev/null; then + return 0 + fi + + return 1 +} + +# Check if ActivityWatch is running +check_running() { + # Check for aw-qt process + if pgrep -f "aw-qt" >/dev/null 2>&1; then + return 0 + fi + + # Check for aw-server process + if pgrep -f "aw-server" >/dev/null 2>&1; then + return 0 + fi + + return 1 +} + +# Main logic +if ! check_installed; then + echo "AW uninstalled" + echo + echo "#FF0000" # Red +elif check_running; then + echo "AW on" + echo + echo "#00FF00" # Green +else + echo "AW off" + echo + echo "#FF0000" # Red +fi +EOF + + chmod +x "$status_script" + + # Set proper ownership if running as root + if [[ $EUID -eq 0 ]]; then + chown "$ACTUAL_USER:$ACTUAL_USER" "$status_script" + fi + + echo "✓ Created i3blocks status script: $status_script" + + # Show configuration instructions + echo "" + echo "To add to your i3blocks config, add this block:" + echo "" + echo "[activitywatch]" + echo "command=~/.config/i3blocks/activitywatch_status.sh" + echo "interval=10" + echo "color=#FFFFFF" + echo "" +} + +# Function to test the setup +test_setup() { + echo "" + echo "7. Testing Setup..." + echo "==================" + + echo "Installation status:" + if check_activitywatch_installed >/dev/null 2>&1; then + echo "✓ ActivityWatch is installed" + else + echo "✗ ActivityWatch is not installed" + fi + + echo "Running status:" + if check_activitywatch_running >/dev/null 2>&1; then + echo "✓ ActivityWatch is running" + else + echo "✗ ActivityWatch is not running" + fi + + echo "Autostart files:" + if [[ -f "$USER_HOME/.config/autostart/activitywatch.desktop" ]]; then + echo "✓ XDG autostart file exists" + else + echo "✗ XDG autostart file missing" + fi + + if [[ -f "$USER_HOME/.config/i3/config" ]] && grep -q "aw-qt" "$USER_HOME/.config/i3/config"; then + echo "✓ i3 autostart configured" + else + echo "! i3 autostart may not be configured" + fi + + echo "i3blocks status script:" + if [[ -x "$USER_HOME/.config/i3blocks/activitywatch_status.sh" ]]; then + echo "✓ i3blocks status script created" + echo "Testing status script:" + if [[ $EUID -eq 0 ]]; then + sudo -u "$ACTUAL_USER" "$USER_HOME/.config/i3blocks/activitywatch_status.sh" + else + "$USER_HOME/.config/i3blocks/activitywatch_status.sh" + fi + else + echo "✗ i3blocks status script missing" + fi +} + +# Function to show final instructions +show_instructions() { + echo "" + echo "==========================================" + echo "ActivityWatch Setup Complete" + echo "==========================================" + echo "Summary:" + echo "✓ ActivityWatch installation checked/completed" + echo "✓ ActivityWatch startup configured" + echo "✓ Autostart configured (XDG + i3)" + echo "✓ i3blocks status script created" + echo "" + echo "Next steps:" + echo "1. Add the i3blocks configuration to your config file:" + echo " ~/.config/i3blocks/config" + echo "" + echo "2. Reload i3 configuration:" + echo " Super+Shift+R" + echo "" + echo "3. ActivityWatch web interface should be available at:" + echo " http://localhost:5600" + echo "" + echo "4. Check system tray for ActivityWatch icon" + echo "" + echo "Files created:" + echo " ~/.config/autostart/activitywatch.desktop" + echo " ~/.config/i3blocks/activitywatch_status.sh" + echo " ~/.config/i3/config (modified)" + echo "" +} diff --git a/linux_configuration/scripts/single_use/features/lib/aw_install.sh b/linux_configuration/scripts/single_use/features/lib/aw_install.sh new file mode 100755 index 00000000..587c37d7 --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/aw_install.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# ActivityWatch install, both packaged and manual. +# +# Sourced by setup_activitywatch.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +# Function to install ActivityWatch +install_activitywatch() { + echo "" + echo "2. Installing ActivityWatch..." + echo "=============================" + + # Check if we need sudo for installation + check_sudo "install" + + echo "Installing activitywatch-bin from AUR..." + + # Check if an AUR helper is available + local aur_helpers=("yay" "paru" "makepkg") + local helper_found="" + + for helper in "${aur_helpers[@]}"; do + if command -v "$helper" &>/dev/null; then + helper_found="$helper" + break + fi + done + + if [[ -n $helper_found && $helper_found != "makepkg" ]]; then + echo "Using AUR helper: $helper_found" + if [[ $EUID -eq 0 ]]; then + # Running as root, need to install as user + sudo -u "$ACTUAL_USER" "$helper_found" -S --noconfirm activitywatch-bin + else + "$helper_found" -S --noconfirm activitywatch-bin + fi + else + echo "No AUR helper found. Installing manually with makepkg..." + install_activitywatch_manual + fi + + echo "✓ ActivityWatch installation completed" +} + +# Function to manually install ActivityWatch via makepkg +install_activitywatch_manual() { + local temp_dir="/tmp/activitywatch-install" + local original_user="$ACTUAL_USER" + + # Create temp directory + mkdir -p "$temp_dir" + # Guarded: the next command is `git clone ... .`, which would otherwise + # clone into whatever directory the caller happened to be in. + cd "$temp_dir" || { + print_error "Cannot enter $temp_dir" + return 1 + } + + # Download PKGBUILD + if command -v git &>/dev/null; then + sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git . + else + echo "Installing git..." + pacman -S --noconfirm git + sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git . + fi + + # Build and install package + sudo -u "$original_user" makepkg -si --noconfirm + + # Cleanup + cd / + rm -rf "$temp_dir" +} diff --git a/linux_configuration/scripts/single_use/features/lib/aw_service.sh b/linux_configuration/scripts/single_use/features/lib/aw_service.sh new file mode 100755 index 00000000..2e36970d --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/aw_service.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# Service checks, startup and the autostart unit. +# +# Sourced by setup_activitywatch.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +# Function to check if ActivityWatch is running +check_activitywatch_running() { + echo "" + echo "3. Checking ActivityWatch Status..." + echo "==================================" + + # Check for aw-qt process + if pgrep -f "aw-qt" >/dev/null; then + echo "✓ ActivityWatch (aw-qt) is running" + return 0 + fi + + # Check for aw-server process + if pgrep -f "aw-server" >/dev/null; then + echo "✓ ActivityWatch server is running" + return 0 + fi + + echo "✗ ActivityWatch is not running" + return 1 +} + +# Function to start ActivityWatch +start_activitywatch() { + echo "" + echo "4. Starting ActivityWatch..." + echo "===========================" + + # Find aw-qt executable + local aw_qt_path="" + + if command -v aw-qt &>/dev/null; then + aw_qt_path="$(which aw-qt)" + elif [[ -x "/usr/bin/aw-qt" ]]; then + aw_qt_path="/usr/bin/aw-qt" + else + echo "✗ Could not find aw-qt executable" + return 1 + fi + + echo "Starting ActivityWatch as user: $ACTUAL_USER" + echo "Using aw-qt from: $aw_qt_path" + + # Start as the actual user in the background + if [[ $EUID -eq 0 ]]; then + # Running as root, start as user + sudo -u "$ACTUAL_USER" env DISPLAY=:0 "$aw_qt_path" & + else + # Running as user + "$aw_qt_path" & + fi + + # Give it time to start + sleep 3 + + if check_activitywatch_running >/dev/null 2>&1; then + echo "✓ ActivityWatch started successfully" + else + echo "! ActivityWatch may be starting (check system tray)" + fi +} diff --git a/linux_configuration/scripts/single_use/features/lib/wg_firewall.sh b/linux_configuration/scripts/single_use/features/lib/wg_firewall.sh new file mode 100755 index 00000000..7ab65db1 --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/wg_firewall.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# nftables ruleset, its verification and sshd hardening. +# +# Sourced by setup_wireguard_ssh.sh; split out to keep wg_keys.sh under the +# 250-line cap. Sourced rather than run, so it inherits the caller's +# strict mode and the variables defined above the source line. + +write_nftables_ruleset() { + detect_lan_subnet + if [[ -f $NFT_CONF ]]; then + cp "$NFT_CONF" "${NFT_CONF}.bak.$(date +%s)" + log_warn "Backed up existing ${NFT_CONF} before overwriting." + fi + local web_rule="" + if [[ $ALLOW_WEB == "true" ]]; then + web_rule=$'\n\t\ttcp dport { 80, 443 } accept' + fi + # DNS blocker (setup_dns_blocker.sh): serve DNS (53) -- and DHCP (67) when + # this PC is the LAN DHCP server -- to LAN clients only. Restricted to the + # LAN subnet so nothing is ever exposed to the internet. + local dns_rule="" dhcp_rule="" + if [[ $ALLOW_DNS == "true" ]]; then + dns_rule=$'\n\t\tip saddr '"${LAN_SUBNET}"$' udp dport 53 accept' + dns_rule+=$'\n\t\tip saddr '"${LAN_SUBNET}"$' tcp dport 53 accept' + # DHCP clients have no IP yet (saddr 0.0.0.0 -> 255.255.255.255) and the + # broadcast is classified INVALID by conntrack -- so this rule MUST sit + # BEFORE 'ct state invalid drop' and match the client source port, not a + # saddr. Link-local only: :67 broadcasts never route in from the internet. + dhcp_rule=$'\n\t\tudp sport 68 udp dport 67 accept' + fi + cat >"${NFT_CONF}.new" <"${WG_DIR}/server_public.key" + log_ok "Generated server keypair." +} + +write_wg0_conf() { + if [[ -f $WG_CONF ]]; then + log_info "${WG_CONF} already exists -- leaving peers intact, not regenerating." + return 0 + fi + local server_private_key + server_private_key=$(<"${WG_DIR}/server_private.key") + umask 077 + cat >"$WG_CONF" <"$SSHD_DROPIN" <<'EOF' +# Managed by setup_wireguard_ssh.sh -- drop-in, does not touch sshd_config. +PasswordAuthentication no +PubkeyAuthentication yes +PermitRootLogin no +EOF + sshd -t || { + rm -f "$SSHD_DROPIN" + die "sshd config invalid after adding drop-in -- removed it." + } + systemctl reload sshd + log_ok "sshd hardened: key-only auth, no root login." +} + +duckdns_already_updated() { + local domain="$1" actual_user line script + actual_user=$(get_actual_user) + while IFS= read -r line; do + [[ $line =~ ^[[:space:]]*(#|$) ]] && continue + for script in $line; do + [[ -f $script ]] || continue + if grep -q "duckdns.org/update?domains=${domain}" "$script" 2>/dev/null; then + return 0 + fi + done + done < <(crontab -u "$actual_user" -l 2>/dev/null || true) + return 1 +} + +setup_duckdns() { + if [[ -z $DUCKDNS_DOMAIN || -z $DUCKDNS_TOKEN ]]; then + read -r -p "DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN + read -r -p "DuckDNS token: " DUCKDNS_TOKEN + [[ -n $DUCKDNS_DOMAIN && -n $DUCKDNS_TOKEN ]] || die "Both fields are required." + save_config + fi + if duckdns_already_updated "$DUCKDNS_DOMAIN"; then + log_info "An existing cron job already keeps ${DUCKDNS_DOMAIN}.duckdns.org updated -- not adding a duplicate." + return 0 + fi + ensure_dir "$DUCKDNS_DIR" + cat >"${DUCKDNS_DIR}/duck.sh" </dev/null || true + ) | grep -vF "${DUCKDNS_DIR}/duck.sh" | { + cat + echo "*/5 * * * * ${DUCKDNS_DIR}/duck.sh >/dev/null 2>&1" + } | crontab - + log_ok "DuckDNS configured: ${DUCKDNS_DOMAIN}.duckdns.org (refreshed every 5 min via cron)." +} + +next_free_wg_ip() { + local used octet + used=$(grep -oP 'AllowedIPs\s*=\s*10\.8\.0\.\K[0-9]+' "$WG_CONF" 2>/dev/null || true) + for ((octet = 2; octet <= 254; octet++)); do + if ! grep -qx "$octet" <<<"$used"; then + echo "10.8.0.${octet}" + return 0 + fi + done + die "No free IPs left in ${WG_SUBNET}." +} + +add_phone_peer() { + local name="${1:?usage: add-peer }" + [[ -f $WG_CONF ]] || die "Run 'setup' first -- ${WG_CONF} does not exist yet." + [[ -n $DUCKDNS_DOMAIN ]] || die "DuckDNS domain not configured -- run 'setup' first." + if grep -q "# peer:${name}\$" "$WG_CONF"; then + die "A peer named '${name}' already exists in ${WG_CONF}. Use 'revoke ${name}' first to replace it." + fi + + ensure_dir "$WG_CLIENTS_DIR" + chmod 700 "$WG_CLIENTS_DIR" + + local tmpdir + tmpdir=$(mktemp -d) + trap 'rm -rf "$tmpdir"; trap - RETURN' RETURN + + umask 077 + wg genkey | tee "${tmpdir}/priv" | wg pubkey >"${tmpdir}/pub" + local client_priv client_pub server_pub peer_ip + client_priv=$(<"${tmpdir}/priv") + client_pub=$(<"${tmpdir}/pub") + server_pub=$(<"${WG_DIR}/server_public.key") + peer_ip=$(next_free_wg_ip) + + cat >>"$WG_CONF" <"$client_conf" <"$tmp" + cat "$tmp" >"$WG_CONF" + chmod 600 "$WG_CONF" + + if is_service_active "wg-quick@${WG_IFACE}"; then + wg syncconf "$WG_IFACE" <(wg-quick strip "$WG_IFACE") + fi + rm -f "${WG_CLIENTS_DIR}/${name}.conf" + log_ok "Revoked peer '${name}'." +} + +print_router_instructions() { + local lan_ip + lan_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}') + cat < ${lan_ip} (UDP only -- do NOT forward TCP/22). +4. Save (and reboot the router if it requires it). +5. Confirm your ISP gives you a real public IPv4 (not CGNAT): + curl -s https://api.ipify.org + getent hosts ${DUCKDNS_DOMAIN:-}.duckdns.org + These two must match and must NOT be in 100.64.0.0/10 (CGNAT range). +EOF +} + +print_android_instructions() { + cat < "+" -> "Scan from QR code" -> scan the code printed above. +5. Toggle the tunnel on. +6. From Termux/ConnectBot: ssh $(get_actual_user)@${WG_SERVER_IP} +EOF +} + +allow_web() { + export ALLOW_WEB=true + save_config + write_nftables_ruleset + verify_nftables_then_apply + log_ok "Opened tcp/80 and tcp/443 in the input chain (persisted -- future 'setup' re-runs keep this rule)." +} + +allow_dns() { + export ALLOW_DNS=true + save_config + write_nftables_ruleset + verify_nftables_then_apply + log_ok "Opened udp/tcp 53 from ${LAN_SUBNET:-the LAN} in the input chain (persisted -- future 'setup' re-runs keep this rule)." +} + +status_cmd() { + echo "=== WireGuard ===" + wg show 2>/dev/null || echo "(interface not up)" + echo + echo "=== wg-quick@${WG_IFACE} service ===" + systemctl status "wg-quick@${WG_IFACE}" --no-pager 2>/dev/null || echo "(not installed)" + echo + echo "=== nftables (input chain) ===" + nft list ruleset 2>/dev/null | sed -n '/chain input/,/}/p' || echo "(nftables not active)" + echo + echo "=== sshd password auth ===" + sshd -T 2>/dev/null | grep -i passwordauthentication || echo "(could not query sshd)" +} + +usage() { + cat < [args] + +Commands: + setup Full first-time setup (WireGuard, firewall, sshd, DuckDNS). + add-peer Provision a new phone/laptop and print its QR code. + allow-web Open tcp/80 and tcp/443 in the firewall (for a web server on this host). + allow-dns Open udp/tcp 53 from the LAN in the firewall (for the DNS blocker on this host). + status Show WireGuard/firewall/sshd status. + revoke Remove a peer. + help Show this message. +EOF +} diff --git a/linux_configuration/scripts/single_use/features/setup_activitywatch.sh b/linux_configuration/scripts/single_use/features/setup_activitywatch.sh index c341db9f..fe45b202 100755 --- a/linux_configuration/scripts/single_use/features/setup_activitywatch.sh +++ b/linux_configuration/scripts/single_use/features/setup_activitywatch.sh @@ -60,366 +60,13 @@ check_activitywatch_installed() { return 1 } -# Function to install ActivityWatch -install_activitywatch() { - echo "" - echo "2. Installing ActivityWatch..." - echo "=============================" +# shellcheck source=lib/aw_install.sh +source "$SCRIPT_DIR/lib/aw_install.sh" +# shellcheck source=lib/aw_service.sh +source "$SCRIPT_DIR/lib/aw_service.sh" +# shellcheck source=lib/aw_autostart.sh +source "$SCRIPT_DIR/lib/aw_autostart.sh" - # Check if we need sudo for installation - check_sudo "install" - - echo "Installing activitywatch-bin from AUR..." - - # Check if an AUR helper is available - local aur_helpers=("yay" "paru" "makepkg") - local helper_found="" - - for helper in "${aur_helpers[@]}"; do - if command -v "$helper" &>/dev/null; then - helper_found="$helper" - break - fi - done - - if [[ -n $helper_found && $helper_found != "makepkg" ]]; then - echo "Using AUR helper: $helper_found" - if [[ $EUID -eq 0 ]]; then - # Running as root, need to install as user - sudo -u "$ACTUAL_USER" "$helper_found" -S --noconfirm activitywatch-bin - else - "$helper_found" -S --noconfirm activitywatch-bin - fi - else - echo "No AUR helper found. Installing manually with makepkg..." - install_activitywatch_manual - fi - - echo "✓ ActivityWatch installation completed" -} - -# Function to manually install ActivityWatch via makepkg -install_activitywatch_manual() { - local temp_dir="/tmp/activitywatch-install" - local original_user="$ACTUAL_USER" - - # Create temp directory - mkdir -p "$temp_dir" - cd "$temp_dir" - - # Download PKGBUILD - if command -v git &>/dev/null; then - sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git . - else - echo "Installing git..." - pacman -S --noconfirm git - sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git . - fi - - # Build and install package - sudo -u "$original_user" makepkg -si --noconfirm - - # Cleanup - cd / - rm -rf "$temp_dir" -} - -# Function to check if ActivityWatch is running -check_activitywatch_running() { - echo "" - echo "3. Checking ActivityWatch Status..." - echo "==================================" - - # Check for aw-qt process - if pgrep -f "aw-qt" >/dev/null; then - echo "✓ ActivityWatch (aw-qt) is running" - return 0 - fi - - # Check for aw-server process - if pgrep -f "aw-server" >/dev/null; then - echo "✓ ActivityWatch server is running" - return 0 - fi - - echo "✗ ActivityWatch is not running" - return 1 -} - -# Function to start ActivityWatch -start_activitywatch() { - echo "" - echo "4. Starting ActivityWatch..." - echo "===========================" - - # Find aw-qt executable - local aw_qt_path="" - - if command -v aw-qt &>/dev/null; then - aw_qt_path="$(which aw-qt)" - elif [[ -x "/usr/bin/aw-qt" ]]; then - aw_qt_path="/usr/bin/aw-qt" - else - echo "✗ Could not find aw-qt executable" - return 1 - fi - - echo "Starting ActivityWatch as user: $ACTUAL_USER" - echo "Using aw-qt from: $aw_qt_path" - - # Start as the actual user in the background - if [[ $EUID -eq 0 ]]; then - # Running as root, start as user - sudo -u "$ACTUAL_USER" env DISPLAY=:0 "$aw_qt_path" & - else - # Running as user - "$aw_qt_path" & - fi - - # Give it time to start - sleep 3 - - if check_activitywatch_running >/dev/null 2>&1; then - echo "✓ ActivityWatch started successfully" - else - echo "! ActivityWatch may be starting (check system tray)" - fi -} - -# Function to setup autostart -setup_autostart() { - echo "" - echo "5. Setting Up Autostart..." - echo "=========================" - - local autostart_dir="$USER_HOME/.config/autostart" - local desktop_file="$autostart_dir/activitywatch.desktop" - local i3_config="$USER_HOME/.config/i3/config" - - # Method 1: XDG Autostart (works with most desktop environments) - if [[ $EUID -eq 0 ]]; then - sudo -u "$ACTUAL_USER" mkdir -p "$autostart_dir" - else - mkdir -p "$autostart_dir" - fi - - # Create desktop file for autostart - cat >"$desktop_file" <> '$i3_config' - -# Auto-start ActivityWatch -exec --no-startup-id aw-qt -EOF" - else - { - printf '\n' - printf '# Auto-start ActivityWatch\n' - printf 'exec --no-startup-id aw-qt\n' - } >>"$i3_config" - fi - - echo "✓ Added ActivityWatch to i3 config autostart" - else - echo "✓ ActivityWatch autostart already exists in i3 config" - fi - else - echo "! i3 config not found at $i3_config" - fi -} - -# Function to create i3blocks status script -create_i3blocks_status() { - echo "" - echo "6. Creating i3blocks Status Script..." - echo "====================================" - - local i3blocks_dir="$USER_HOME/.config/i3blocks" - local status_script="$i3blocks_dir/activitywatch_status.sh" - - # Create i3blocks directory if it doesn't exist - if [[ $EUID -eq 0 ]]; then - sudo -u "$ACTUAL_USER" mkdir -p "$i3blocks_dir" - else - mkdir -p "$i3blocks_dir" - fi - - # Create the status script - cat >"$status_script" <<'EOF' -#!/bin/bash -# ActivityWatch status script for i3blocks -# Shows ActivityWatch installation and running status - -# Check if ActivityWatch is installed -check_installed() { - # Check if activitywatch-bin package is installed - if pacman -Qi activitywatch-bin &>/dev/null; then - return 0 - fi - - # Check if aw-qt binary exists - if command -v aw-qt &>/dev/null; then - return 0 - fi - - return 1 -} - -# Check if ActivityWatch is running -check_running() { - # Check for aw-qt process - if pgrep -f "aw-qt" >/dev/null 2>&1; then - return 0 - fi - - # Check for aw-server process - if pgrep -f "aw-server" >/dev/null 2>&1; then - return 0 - fi - - return 1 -} - -# Main logic -if ! check_installed; then - echo "AW uninstalled" - echo - echo "#FF0000" # Red -elif check_running; then - echo "AW on" - echo - echo "#00FF00" # Green -else - echo "AW off" - echo - echo "#FF0000" # Red -fi -EOF - - chmod +x "$status_script" - - # Set proper ownership if running as root - if [[ $EUID -eq 0 ]]; then - chown "$ACTUAL_USER:$ACTUAL_USER" "$status_script" - fi - - echo "✓ Created i3blocks status script: $status_script" - - # Show configuration instructions - echo "" - echo "To add to your i3blocks config, add this block:" - echo "" - echo "[activitywatch]" - echo "command=~/.config/i3blocks/activitywatch_status.sh" - echo "interval=10" - echo "color=#FFFFFF" - echo "" -} - -# Function to test the setup -test_setup() { - echo "" - echo "7. Testing Setup..." - echo "==================" - - echo "Installation status:" - if check_activitywatch_installed >/dev/null 2>&1; then - echo "✓ ActivityWatch is installed" - else - echo "✗ ActivityWatch is not installed" - fi - - echo "Running status:" - if check_activitywatch_running >/dev/null 2>&1; then - echo "✓ ActivityWatch is running" - else - echo "✗ ActivityWatch is not running" - fi - - echo "Autostart files:" - if [[ -f "$USER_HOME/.config/autostart/activitywatch.desktop" ]]; then - echo "✓ XDG autostart file exists" - else - echo "✗ XDG autostart file missing" - fi - - if [[ -f "$USER_HOME/.config/i3/config" ]] && grep -q "aw-qt" "$USER_HOME/.config/i3/config"; then - echo "✓ i3 autostart configured" - else - echo "! i3 autostart may not be configured" - fi - - echo "i3blocks status script:" - if [[ -x "$USER_HOME/.config/i3blocks/activitywatch_status.sh" ]]; then - echo "✓ i3blocks status script created" - echo "Testing status script:" - if [[ $EUID -eq 0 ]]; then - sudo -u "$ACTUAL_USER" "$USER_HOME/.config/i3blocks/activitywatch_status.sh" - else - "$USER_HOME/.config/i3blocks/activitywatch_status.sh" - fi - else - echo "✗ i3blocks status script missing" - fi -} - -# Function to show final instructions -show_instructions() { - echo "" - echo "==========================================" - echo "ActivityWatch Setup Complete" - echo "==========================================" - echo "Summary:" - echo "✓ ActivityWatch installation checked/completed" - echo "✓ ActivityWatch startup configured" - echo "✓ Autostart configured (XDG + i3)" - echo "✓ i3blocks status script created" - echo "" - echo "Next steps:" - echo "1. Add the i3blocks configuration to your config file:" - echo " ~/.config/i3blocks/config" - echo "" - echo "2. Reload i3 configuration:" - echo " Super+Shift+R" - echo "" - echo "3. ActivityWatch web interface should be available at:" - echo " http://localhost:5600" - echo "" - echo "4. Check system tray for ActivityWatch icon" - echo "" - echo "Files created:" - echo " ~/.config/autostart/activitywatch.desktop" - echo " ~/.config/i3blocks/activitywatch_status.sh" - echo " ~/.config/i3/config (modified)" - echo "" -} # Main execution flow main() { diff --git a/linux_configuration/scripts/single_use/features/setup_wireguard_ssh.sh b/linux_configuration/scripts/single_use/features/setup_wireguard_ssh.sh index 42f6ee0e..b78b30c5 100755 --- a/linux_configuration/scripts/single_use/features/setup_wireguard_ssh.sh +++ b/linux_configuration/scripts/single_use/features/setup_wireguard_ssh.sh @@ -37,8 +37,8 @@ fi DUCKDNS_DOMAIN="${DUCKDNS_DOMAIN:-}" DUCKDNS_TOKEN="${DUCKDNS_TOKEN:-}" LAN_SUBNET="${LAN_SUBNET:-}" -ALLOW_WEB="${ALLOW_WEB:-false}" -ALLOW_DNS="${ALLOW_DNS:-false}" +export ALLOW_WEB="${ALLOW_WEB:-false}" +export ALLOW_DNS="${ALLOW_DNS:-false}" die() { log_error "$1" @@ -75,337 +75,13 @@ install_dependencies() { install_missing_pacman_packages wireguard-tools qrencode nftables openssh } -generate_server_keys() { - ensure_dir "$WG_DIR" - chmod 700 "$WG_DIR" - if [[ -f "${WG_DIR}/server_private.key" ]]; then - log_info "Server keypair already exists -- not rotating (would break existing peer configs)." - return 0 - fi - umask 077 - wg genkey | tee "${WG_DIR}/server_private.key" | wg pubkey >"${WG_DIR}/server_public.key" - log_ok "Generated server keypair." -} +# shellcheck source=lib/wg_keys.sh +source "$SCRIPT_DIR/lib/wg_keys.sh" +# shellcheck source=lib/wg_firewall.sh +source "$SCRIPT_DIR/lib/wg_firewall.sh" +# shellcheck source=lib/wg_sshd.sh +source "$SCRIPT_DIR/lib/wg_sshd.sh" -write_wg0_conf() { - if [[ -f $WG_CONF ]]; then - log_info "${WG_CONF} already exists -- leaving peers intact, not regenerating." - return 0 - fi - local server_private_key - server_private_key=$(<"${WG_DIR}/server_private.key") - umask 077 - cat >"$WG_CONF" < 255.255.255.255) and the - # broadcast is classified INVALID by conntrack -- so this rule MUST sit - # BEFORE 'ct state invalid drop' and match the client source port, not a - # saddr. Link-local only: :67 broadcasts never route in from the internet. - dhcp_rule=$'\n\t\tudp sport 68 udp dport 67 accept' - fi - cat >"${NFT_CONF}.new" <"$SSHD_DROPIN" <<'EOF' -# Managed by setup_wireguard_ssh.sh -- drop-in, does not touch sshd_config. -PasswordAuthentication no -PubkeyAuthentication yes -PermitRootLogin no -EOF - sshd -t || { - rm -f "$SSHD_DROPIN" - die "sshd config invalid after adding drop-in -- removed it." - } - systemctl reload sshd - log_ok "sshd hardened: key-only auth, no root login." -} - -duckdns_already_updated() { - local domain="$1" actual_user line script - actual_user=$(get_actual_user) - while IFS= read -r line; do - [[ $line =~ ^[[:space:]]*(#|$) ]] && continue - for script in $line; do - [[ -f $script ]] || continue - if grep -q "duckdns.org/update?domains=${domain}" "$script" 2>/dev/null; then - return 0 - fi - done - done < <(crontab -u "$actual_user" -l 2>/dev/null || true) - return 1 -} - -setup_duckdns() { - if [[ -z $DUCKDNS_DOMAIN || -z $DUCKDNS_TOKEN ]]; then - read -r -p "DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN - read -r -p "DuckDNS token: " DUCKDNS_TOKEN - [[ -n $DUCKDNS_DOMAIN && -n $DUCKDNS_TOKEN ]] || die "Both fields are required." - save_config - fi - if duckdns_already_updated "$DUCKDNS_DOMAIN"; then - log_info "An existing cron job already keeps ${DUCKDNS_DOMAIN}.duckdns.org updated -- not adding a duplicate." - return 0 - fi - ensure_dir "$DUCKDNS_DIR" - cat >"${DUCKDNS_DIR}/duck.sh" </dev/null || true - ) | grep -vF "${DUCKDNS_DIR}/duck.sh" | { - cat - echo "*/5 * * * * ${DUCKDNS_DIR}/duck.sh >/dev/null 2>&1" - } | crontab - - log_ok "DuckDNS configured: ${DUCKDNS_DOMAIN}.duckdns.org (refreshed every 5 min via cron)." -} - -next_free_wg_ip() { - local used octet - used=$(grep -oP 'AllowedIPs\s*=\s*10\.8\.0\.\K[0-9]+' "$WG_CONF" 2>/dev/null || true) - for ((octet = 2; octet <= 254; octet++)); do - if ! grep -qx "$octet" <<<"$used"; then - echo "10.8.0.${octet}" - return 0 - fi - done - die "No free IPs left in ${WG_SUBNET}." -} - -add_phone_peer() { - local name="${1:?usage: add-peer }" - [[ -f $WG_CONF ]] || die "Run 'setup' first -- ${WG_CONF} does not exist yet." - [[ -n $DUCKDNS_DOMAIN ]] || die "DuckDNS domain not configured -- run 'setup' first." - if grep -q "# peer:${name}\$" "$WG_CONF"; then - die "A peer named '${name}' already exists in ${WG_CONF}. Use 'revoke ${name}' first to replace it." - fi - - ensure_dir "$WG_CLIENTS_DIR" - chmod 700 "$WG_CLIENTS_DIR" - - local tmpdir - tmpdir=$(mktemp -d) - trap 'rm -rf "$tmpdir"; trap - RETURN' RETURN - - umask 077 - wg genkey | tee "${tmpdir}/priv" | wg pubkey >"${tmpdir}/pub" - local client_priv client_pub server_pub peer_ip - client_priv=$(<"${tmpdir}/priv") - client_pub=$(<"${tmpdir}/pub") - server_pub=$(<"${WG_DIR}/server_public.key") - peer_ip=$(next_free_wg_ip) - - cat >>"$WG_CONF" <"$client_conf" <"$tmp" - cat "$tmp" >"$WG_CONF" - chmod 600 "$WG_CONF" - - if is_service_active "wg-quick@${WG_IFACE}"; then - wg syncconf "$WG_IFACE" <(wg-quick strip "$WG_IFACE") - fi - rm -f "${WG_CLIENTS_DIR}/${name}.conf" - log_ok "Revoked peer '${name}'." -} - -print_router_instructions() { - local lan_ip - lan_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}') - cat < ${lan_ip} (UDP only -- do NOT forward TCP/22). -4. Save (and reboot the router if it requires it). -5. Confirm your ISP gives you a real public IPv4 (not CGNAT): - curl -s https://api.ipify.org - getent hosts ${DUCKDNS_DOMAIN:-}.duckdns.org - These two must match and must NOT be in 100.64.0.0/10 (CGNAT range). -EOF -} - -print_android_instructions() { - cat < "+" -> "Scan from QR code" -> scan the code printed above. -5. Toggle the tunnel on. -6. From Termux/ConnectBot: ssh $(get_actual_user)@${WG_SERVER_IP} -EOF -} - -allow_web() { - ALLOW_WEB=true - save_config - write_nftables_ruleset - verify_nftables_then_apply - log_ok "Opened tcp/80 and tcp/443 in the input chain (persisted -- future 'setup' re-runs keep this rule)." -} - -allow_dns() { - ALLOW_DNS=true - save_config - write_nftables_ruleset - verify_nftables_then_apply - log_ok "Opened udp/tcp 53 from ${LAN_SUBNET:-the LAN} in the input chain (persisted -- future 'setup' re-runs keep this rule)." -} - -status_cmd() { - echo "=== WireGuard ===" - wg show 2>/dev/null || echo "(interface not up)" - echo - echo "=== wg-quick@${WG_IFACE} service ===" - systemctl status "wg-quick@${WG_IFACE}" --no-pager 2>/dev/null || echo "(not installed)" - echo - echo "=== nftables (input chain) ===" - nft list ruleset 2>/dev/null | sed -n '/chain input/,/}/p' || echo "(nftables not active)" - echo - echo "=== sshd password auth ===" - sshd -T 2>/dev/null | grep -i passwordauthentication || echo "(could not query sshd)" -} - -usage() { - cat < [args] - -Commands: - setup Full first-time setup (WireGuard, firewall, sshd, DuckDNS). - add-peer Provision a new phone/laptop and print its QR code. - allow-web Open tcp/80 and tcp/443 in the firewall (for a web server on this host). - allow-dns Open udp/tcp 53 from the LAN in the firewall (for the DNS blocker on this host). - status Show WireGuard/firewall/sshd status. - revoke Remove a peer. - help Show this message. -EOF -} main() { local cmd="${1:-help}" diff --git a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/lib/transcribe_deps.sh b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/lib/transcribe_deps.sh new file mode 100755 index 00000000..65a9e921 --- /dev/null +++ b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/lib/transcribe_deps.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# System dependency and CUDA runtime installation. +# +# Sourced by transcribe.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +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 + + # 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 [[ $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))" + + 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 +} diff --git a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/lib/transcribe_pkgmgr.sh b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/lib/transcribe_pkgmgr.sh new file mode 100755 index 00000000..2a24879b --- /dev/null +++ b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/lib/transcribe_pkgmgr.sh @@ -0,0 +1,101 @@ +#!/bin/bash +# Package manager detection and CUDA runtime checks. +# +# Sourced by transcribe.sh; split out to keep it under the 250-line cap. +# Sourced rather than run, so it inherits the caller's strict mode and +# the helper functions and variables defined above the source line. + +detect_pkg_mgr() { + 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 + 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 "$PY_HELPERS" python-version 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 +} diff --git a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/lib/transcribe_venv.sh b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/lib/transcribe_venv.sh new file mode 100755 index 00000000..485b741a --- /dev/null +++ b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/lib/transcribe_venv.sh @@ -0,0 +1,101 @@ +#!/bin/bash +# Virtualenv setup and the model download. +# +# Sourced by transcribe.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +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 +} + +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 "$PY_HELPERS" check-faster-whisper; then + exit 7 + fi + # If diarization requested offline, check for its deps too (warn-only) + if [[ ${FW_DIARIZE:-} == "1" ]]; then + python "$PY_HELPERS" check-diarization || true + fi + return 0 + fi + if [[ $has_nvidia_flag -eq 1 ]]; then + # If ctranslate2 is not installed, attempt CUDA-enabled wheel (with fallback) + if ! "$VENV_DIR/bin/python" "$PY_HELPERS" check-ctranslate2 2> /dev/null; then + log "Installing CUDA-enabled CTranslate2 (cu12 wheel)" + python -m pip install --progress-bar on --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 --progress-bar on --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 --progress-bar on --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 --progress-bar on --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 --progress-bar on --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_HELPERS" deps-installed +} + +ensure_runner() { + 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 "$PY_HELPERS" generate-wav --file "$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_HELPERS" prepare-model --model "$name" --model-dir "$MODEL_DIR" +} diff --git a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/transcribe.sh b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/transcribe.sh index 26b401fb..6656cd68 100755 --- a/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/transcribe.sh +++ b/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/transcribe.sh @@ -34,281 +34,12 @@ log() { echo "[$(date +'%H:%M:%S')]" "$@" } -detect_pkg_mgr() { - 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 - 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 "$PY_HELPERS" python-version 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 -} - -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 - - # 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 [[ $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))" - - 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 -} - -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 -} - -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 "$PY_HELPERS" check-faster-whisper; then - exit 7 - fi - # If diarization requested offline, check for its deps too (warn-only) - if [[ ${FW_DIARIZE:-} == "1" ]]; then - python "$PY_HELPERS" check-diarization || true - fi - return 0 - fi - if [[ $has_nvidia_flag -eq 1 ]]; then - # If ctranslate2 is not installed, attempt CUDA-enabled wheel (with fallback) - if ! "$VENV_DIR/bin/python" "$PY_HELPERS" check-ctranslate2 2> /dev/null; then - log "Installing CUDA-enabled CTranslate2 (cu12 wheel)" - python -m pip install --progress-bar on --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 --progress-bar on --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 --progress-bar on --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 --progress-bar on --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 --progress-bar on --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_HELPERS" deps-installed -} - -ensure_runner() { - 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 "$PY_HELPERS" generate-wav --file "$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_HELPERS" prepare-model --model "$name" --model-dir "$MODEL_DIR" -} +# shellcheck source=lib/transcribe_pkgmgr.sh +source "$SCRIPT_DIR/lib/transcribe_pkgmgr.sh" +# shellcheck source=lib/transcribe_deps.sh +source "$SCRIPT_DIR/lib/transcribe_deps.sh" +# shellcheck source=lib/transcribe_venv.sh +source "$SCRIPT_DIR/lib/transcribe_venv.sh" main() { # Defaults diff --git a/meta/scripts/verify_shell_split.sh b/meta/scripts/verify_shell_split.sh index 775b23fc..23eb21a1 100755 --- a/meta/scripts/verify_shell_split.sh +++ b/meta/scripts/verify_shell_split.sh @@ -27,13 +27,18 @@ tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT echo "--- $(basename "$script")" -bash -n "$script" || exit 1 -shellcheck -x "$script" || exit 1 for lib in "$dir"/lib/*.sh; do [[ -e $lib ]] || continue - bash -n "$lib" || exit 1 + # bash -n first: a split landing inside a heredoc produces a file the + # linter only warns about (SC1094 in the caller) but which bash rejects. + bash -n "$lib" || { + echo " $lib does not parse -- the split probably cut inside a heredoc" >&2 + exit 1 + } shellcheck "$lib" || exit 1 done +bash -n "$script" || exit 1 +shellcheck -x "$script" || exit 1 dollar='$' grep -q "source \"${dollar}SCRIPT_DIR/" "$script" || {