From d2162a0b040634079f5d13ae6f69b406e45dc5a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 20:52:50 +0000 Subject: [PATCH] Add one-line install, live install progress, and hardened apt calls - One-line install: both scripts now work piped from curl. Prompts read from /dev/tty (stdin is the script itself when piped), and new flags (--de, --wine/--no-wine, --hacs/--no-hacs, -y, --help) allow fully unattended installs. - Spinner shows elapsed time and the latest apt log line, and warns once if nothing moves for 5 minutes, so a long download no longer looks like a hang (refs #7). - Every apt call gets network retries/timeouts, a dpkg lock timeout and confdef/confold, and stdin from /dev/null so nothing can block on a prompt. - Wine step only removes wine-stable when it is installed, via apt-get instead of the pkg wrapper. - Desktop script warns on non-arm64 and low storage; both scripts refuse to run outside Termux; wake lock is released on any exit or Ctrl+C. - HA script: real install-status check (dpkg -s matches removed packages), network check that does not depend on pinging google.com, stop script uses $PREFIX and only kills the hass process. - CI: shellcheck at warning level plus a CLI smoke test. - README: quick-start one-liner, unattended flags, "install looks stuck" and Android 12+ phantom process killer troubleshooting, FAQ, unshittify.dev badge (#8). Chinese README updated to match. - Bug template asks which step it stopped at and whether Wine was chosen. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01H15noZPfLdvcWEpDJ1sSrQ --- .github/ISSUE_TEMPLATE/bug_report.yml | 7 + .github/workflows/shellcheck.yml | 19 +- CONTRIBUTING.md | 9 +- README.md | 91 ++++++- README.zh.md | 35 ++- setup-homeassistant.sh | 244 ++++++++++++++++--- termux-linux-setup.sh | 327 +++++++++++++++++++++----- 7 files changed, 616 insertions(+), 116 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 569e677..8506b65 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -30,6 +30,13 @@ body: validations: required: true + - type: input + id: step + attributes: + label: Where did it stop? + description: The step number and the exact line on screen, e.g. "Step 9/11 — Installing Hangover Wine... [12:40]". Did you choose Wine? + placeholder: "Step 9/11 — Installing Hangover Wine... (Wine: yes)" + - type: textarea id: device attributes: diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index f3f4da5..a4951a7 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -24,8 +24,23 @@ jobs: bash -n "$f" done - # shellcheck runs at error severity only. These scripts intentionally + # shellcheck runs at warning severity. These scripts intentionally # avoid `set -e`/`set -u` (see the header comment in termux-linux-setup.sh), # which trips a lot of shellcheck's style and info-level advice. - name: shellcheck - run: shellcheck --severity=error --shell=bash *.sh + run: shellcheck --severity=warning --shell=bash *.sh + + # The flag parser runs before anything Termux-specific, so it can be + # exercised on a normal runner. Also checks the scripts refuse to run + # outside Termux instead of half-installing. + - name: CLI smoke test + run: | + bash termux-linux-setup.sh --help | grep -q -- "--de" + bash setup-homeassistant.sh --help | grep -q -- "--no-hacs" + ! bash termux-linux-setup.sh --de nope 2>/dev/null + ! bash termux-linux-setup.sh --bogus 2>/dev/null + ! bash setup-homeassistant.sh --bogus 2>/dev/null + out=$(HOME=$(mktemp -d) PREFIX=/nonexistent bash termux-linux-setup.sh -y 2>&1 < /dev/null || true) + echo "$out" | grep -q "must be run inside Termux" + out=$(HOME=$(mktemp -d) PREFIX=/nonexistent bash setup-homeassistant.sh -y 2>&1 < /dev/null || true) + echo "$out" | grep -q "must be run inside Termux" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aa73fe7..acb741e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,9 @@ There is no build system and no test suite. Before opening a PR: 1. `bash -n termux-linux-setup.sh && bash -n setup-homeassistant.sh` — a syntax error here strands someone half-way through a 30-minute install. -2. `shellcheck --severity=error --shell=bash *.sh` — same check CI runs. +2. `shellcheck --severity=warning --shell=bash *.sh` — same check CI runs. + CI also runs `--help` and a few bad flags through both scripts, so keep + `parse_args` free of anything Termux-specific. 3. Run the changed script end to end on a real device, and say which device and Android version you tested on in the PR description. @@ -38,6 +40,11 @@ There is no build system and no test suite. Before opening a PR: - **Desktop-specific logic branches on `$DE_CHOICE`** (1=XFCE4, 2=LXQt, 3=MATE, 4=KDE). If you add a desktop, update every `case` that switches on it — packages, kill command, start command, terminal shortcut. +- **Never `read` from stdin directly.** Use the `ask` helper. With + `curl ... | bash` stdin *is* the script, so a bare `read` eats the rest of it. + Every new choice also needs a flag, so `-y` installs stay fully unattended. +- **Redirect `< /dev/null` on anything that might prompt** (apt, pkg) for the + same reason. - **Colors:** GREEN success, RED error, YELLOW warning, CYAN info, GRAY skipped. ## Adding or changing a package diff --git a/README.md b/README.md index d44c7a9..989d86d 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,25 @@ -# Repurpose Your Old Android Phone +# Linux on Android — Repurpose Your Old Phone +[![shellcheck](https://github.com/mayukh4/linux-android/actions/workflows/shellcheck.yml/badge.svg)](https://github.com/mayukh4/linux-android/actions/workflows/shellcheck.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Platform: Android](https://img.shields.io/badge/platform-Android%207%2B-3ddc84.svg)](https://termux.dev) [![Termux](https://img.shields.io/badge/Termux-F--Droid-000000.svg)](https://f-droid.org/en/packages/com.termux/) [![Stars](https://img.shields.io/github/stars/mayukh4/linux-android?style=flat)](https://github.com/mayukh4/linux-android/stargazers) +[![listed on unshittify.dev](https://unshittify.dev/badge.svg)](https://unshittify.dev/p/linux-android/) -Turn any old Android phone into a **Linux desktop** or a **smart home server** — no PC, no root, no cloud. Just [Termux](https://termux.dev). +Turn any old Android phone or tablet into a **Linux desktop** or a **smart home server** — no PC, no root, no cloud. Just [Termux](https://termux.dev). + +One script installs a **GPU-accelerated desktop** (XFCE4, LXQt, MATE or KDE Plasma) using native Termux packages and Termux-X11 — with Turnip/Zink hardware acceleration on Snapdragon, Firefox, VLC, Python, SSH, and optional Wine for Windows apps. A second script turns the phone into an always-on **Home Assistant** server with HACS. + +### Quick start + +In [Termux from F-Droid](https://f-droid.org/en/packages/com.termux/), with the [Termux-X11 app](https://github.com/termux/termux-x11/releases) installed: + +```bash +pkg upgrade -y && curl -fsSL https://raw.githubusercontent.com/mayukh4/linux-android/main/termux-linux-setup.sh | bash +``` + +Then `bash ~/start-linux.sh` and open Termux-X11. Home Assistant instead? Swap in `setup-homeassistant.sh`. Full walkthrough [below](#installation). > Created to accompany a YouTube video walkthrough. Timestamps are referenced in the video description. @@ -85,17 +99,42 @@ pkg upgrade -y The `termux-wake-lock` command keeps Termux alive when your screen turns off — without it, Android can kill the process mid-install. The `pkg upgrade` brings your base system up to date before the script runs, preventing a known crash involving `libpcre` and `libandroid-selinux`. -### Step 3 — Download and run the script +### Step 3 — Run the script + +```bash +curl -fsSL https://raw.githubusercontent.com/mayukh4/linux-android/main/termux-linux-setup.sh | bash +``` + +The script will ask you to choose a desktop environment and whether you want Wine. Everything else is automatic. + +Prefer to read it before running it? Download first: ```bash curl -O https://raw.githubusercontent.com/mayukh4/linux-android/main/termux-linux-setup.sh -chmod +x termux-linux-setup.sh +less termux-linux-setup.sh bash termux-linux-setup.sh ``` -The script will ask you to choose a desktop environment and whether you want Wine. Everything else is automatic. +Installation takes **10–30 minutes** depending on your internet speed. Each package shows a live timer and the latest download line, so you can tell a big download from a real hang. A full log is saved to `~/termux-setup.log`. Re-running the script is safe — anything already installed is skipped. + +
+Unattended install (no questions) + +Pass choices as flags, and `-y` to accept defaults for anything not given: + +```bash +curl -fsSL https://raw.githubusercontent.com/mayukh4/linux-android/main/termux-linux-setup.sh | bash -s -- --de kde --no-wine -y +``` -Installation takes **10–30 minutes** depending on your internet speed. A full log is saved to `~/termux-setup.log` if anything goes wrong. +| Flag | Meaning | +|---|---| +| `--de xfce\|lxqt\|mate\|kde` | Desktop to install (default `xfce`) | +| `--wine` / `--no-wine` | Windows app support (default: no) | +| `-y`, `--yes` | Don't ask anything | +| `-h`, `--help` | Show all options | + +`setup-homeassistant.sh` takes `--hacs` / `--no-hacs` and `-y` the same way. +
### Step 4 — Start your desktop @@ -269,10 +308,11 @@ Home Assistant Core runs inside a lightweight Ubuntu container (via proot-distro ### Installation ```bash -curl -O https://raw.githubusercontent.com/mayukh4/linux-android/main/setup-homeassistant.sh -bash setup-homeassistant.sh +curl -fsSL https://raw.githubusercontent.com/mayukh4/linux-android/main/setup-homeassistant.sh | bash ``` +Unattended: `... | bash -s -- --hacs -y`. Run `bash setup-homeassistant.sh --help` for all options. + Installation takes **15–45 minutes** depending on your phone. The longest step is compiling Home Assistant's Python dependencies (numpy, cryptography, etc.) inside the Ubuntu container. ### Starting and stopping @@ -418,6 +458,21 @@ Ideas for what you can do with your old Android phone running this setup: ## Troubleshooting +**The install looks stuck on one step** +Big packages (KDE Plasma, Firefox, Wine) are hundreds of MB and can take 10+ minutes on a slow connection or phone. The spinner shows the elapsed time and the latest line from apt — if that line keeps changing, it is working. If nothing moves for 5 minutes the script prints a warning. Then: + +- Keep Termux in the foreground with the screen on, and make sure `termux-wake-lock` is active. +- Check the live log from a second Termux session (swipe from the left edge → New session): `tail -f ~/termux-setup.log` +- Press `Ctrl+C` and re-run the script. Already-installed packages are skipped, so you lose nothing. +- If one mirror is slow, pick another with `termux-change-repo`. + +**Termux closes with "[Process completed (signal 9)]" (Android 12+)** +Android 12 added a "phantom process killer" that kills background processes Termux starts. On **Android 14+**, turn on *Developer options → Disable child process restrictions*. On Android 12–13, run this once from a PC with [ADB](https://developer.android.com/tools/adb): + +```bash +adb shell "/system/bin/device_config set_sync_disabled_for_tests persistent; /system/bin/device_config put activity_manager max_phantom_processes 2147483647" +``` + **Script exits mid-install without a clear error** Check `~/termux-setup.log`. The script logs every package install result. The last line will tell you exactly which package triggered the failure. @@ -476,6 +531,22 @@ This is expected on Android. The `/proc/net/dev` restriction on Android 10+ prev --- +## FAQ + +**Do I need root?** No. Everything runs as a normal Android app. Root is not used by either script. + +**Is this a proot / chroot distro like Andronix or UserLAnd?** The desktop is not. It uses native Termux packages and the Termux-X11 display server, so there is no emulation layer between the apps and the phone — that is what makes GPU acceleration through Turnip/Zink possible. Home Assistant does run in a proot-distro Ubuntu container, because it needs a glibc Python environment. + +**Will it work on my phone?** Any arm64 (64-bit) Android 7+ phone or tablet with 3 GB+ RAM should work. Qualcomm Snapdragon (Adreno GPU) gets full hardware acceleration; Mali, PowerVR and others fall back to software Vulkan, so pick XFCE4 or LXQt there. + +**Can I use the Play Store version of Termux?** No — it has been outdated for years and cannot install current packages. Use F-Droid or the [Termux GitHub releases](https://github.com/termux/termux-app/releases). + +**Can I use a keyboard, mouse and external monitor?** Yes. Bluetooth or USB keyboards and mice work in Termux-X11, and on phones with DisplayPort-over-USB-C (or Samsung DeX) you can put the desktop on a monitor. + +**How do I uninstall?** Run `bash ~/stop-linux.sh`, then remove the packages with `pkg uninstall `, or simply clear Termux's app data to start fresh. For Home Assistant: `proot-distro remove ubuntu` and delete `~/hass-config`. + +--- + ## Advanced Notes
@@ -549,7 +620,9 @@ All hardcoded `/data/data/com.termux/...` paths have been replaced with `$PREFIX ## Contributing -PRs and issues are welcome. If a package name has changed, a DE has a better startup command, or you've found a new conflict to handle, open an issue with your device model and Android version. +PRs and issues are welcome. If a package name has changed, a DE has a better startup command, or you've found a new conflict to handle, open an issue with your device model and Android version. See [CONTRIBUTING.md](CONTRIBUTING.md). + +**Tested it on your device?** Even a one-line "works on Pixel 6 / Android 15 / XFCE4" report helps the next person — open an issue or comment on an existing one. --- diff --git a/README.zh.md b/README.zh.md index 1063bef..8be8588 100644 --- a/README.zh.md +++ b/README.zh.md @@ -2,6 +2,8 @@ **[English](README.md)** +[![listed on unshittify.dev](https://unshittify.dev/badge.svg)](https://unshittify.dev/p/linux-android/) + > 本翻译可能落后于英文 README。以 [README.md](README.md) 为准。 将任意旧安卓手机变成 **Linux 桌面**或**智能家居服务器** — 无需电脑、无需 Root、无需云服务,只需 [Termux](https://termux.dev)。 @@ -84,17 +86,23 @@ pkg upgrade -y `termux-wake-lock` 命令可保持 Termux 在屏幕关闭时继续运行 — 没有它,安卓可能会在安装过程中杀死进程。`pkg upgrade` 会在脚本运行前更新基础系统,避免 `libpcre` 和 `libandroid-selinux` 相关的已知崩溃问题。 -### 步骤 3 — 下载并运行脚本 +### 步骤 3 — 运行脚本 ```bash -curl -O https://raw.githubusercontent.com/mayukh4/linux-android/main/termux-linux-setup.sh -chmod +x termux-linux-setup.sh -bash termux-linux-setup.sh +curl -fsSL https://raw.githubusercontent.com/mayukh4/linux-android/main/termux-linux-setup.sh | bash ``` -脚本会询问你选择哪个桌面环境以及是否安装 Wine,其余全部自动完成。 +脚本会询问你选择哪个桌面环境以及是否安装 Wine,其余全部自动完成。想先阅读脚本再运行,可以用 `curl -O` 下载后执行 `bash termux-linux-setup.sh`。 + +安装耗时 **10–30 分钟**,取决于网络速度。每个软件包都会显示计时和 apt 的最新输出行,方便区分"大文件下载中"和"真的卡住"。完整的日志保存在 `~/termux-setup.log` 中。重复运行脚本是安全的 — 已安装的包会被跳过。 + +**无人值守安装**(不提问): + +```bash +curl -fsSL https://raw.githubusercontent.com/mayukh4/linux-android/main/termux-linux-setup.sh | bash -s -- --de kde --no-wine -y +``` -安装耗时 **10–30 分钟**,取决于网络速度。完整的日志保存在 `~/termux-setup.log` 中,方便排查问题。 +`--de` 可选 `xfce`(默认)、`lxqt`、`mate`、`kde`;`--wine` / `--no-wine`;`-y` 表示全部使用默认值;`--help` 查看全部选项。 ### 步骤 4 — 启动桌面 @@ -255,10 +263,11 @@ Home Assistant Core 运行在手机上的轻量级 Ubuntu 容器中(通过 pro ### 安装 ```bash -curl -O https://raw.githubusercontent.com/mayukh4/linux-android/main/setup-homeassistant.sh -bash setup-homeassistant.sh +curl -fsSL https://raw.githubusercontent.com/mayukh4/linux-android/main/setup-homeassistant.sh | bash ``` +无人值守:`... | bash -s -- --hacs -y`。运行 `bash setup-homeassistant.sh --help` 查看全部选项。 + 安装耗时 **15–45 分钟**,取决于手机性能。最耗时的步骤是在 Ubuntu 容器中编译 Home Assistant 的 Python 依赖(numpy、cryptography 等)。 ### 启动和停止 @@ -379,6 +388,16 @@ echo 'termux-wake-lock && nohup bash ~/start-homeassistant.sh > ~/hass.log 2>&1 ## 故障排除 +**安装似乎卡在某一步** +KDE Plasma、Firefox、Wine 等大包有几百 MB,网络或手机较慢时可能需要 10 分钟以上。只要计时后面的 apt 输出行还在变化,就说明仍在进行。如果 5 分钟没有任何进展,脚本会打印警告。此时:保持 Termux 在前台并亮屏;在第二个 Termux 会话中运行 `tail -f ~/termux-setup.log` 查看日志;或按 `Ctrl+C` 后重新运行脚本(已安装的包会被跳过);镜像太慢可用 `termux-change-repo` 切换镜像。 + +**Termux 退出并显示 "[Process completed (signal 9)]"(Android 12+)** +这是 Android 12 引入的"幻影进程终止"机制。**Android 14+**:开启 *开发者选项 → 停用子进程限制*。Android 12–13:在电脑上通过 ADB 执行一次: + +```bash +adb shell "/system/bin/device_config set_sync_disabled_for_tests persistent; /system/bin/device_config put activity_manager max_phantom_processes 2147483647" +``` + **脚本安装中途退出且无明确错误** 查看 `~/termux-setup.log`。脚本会记录每个包的安装结果,最后一行会告诉你具体是哪个包导致了失败。 diff --git a/setup-homeassistant.sh b/setup-homeassistant.sh index 629e568..dfbb39d 100644 --- a/setup-homeassistant.sh +++ b/setup-homeassistant.sh @@ -25,8 +25,83 @@ CURRENT_STEP=0 TOTAL_STEPS=7 INSTALL_HACS="no" +HACS_FROM_FLAG="" +ASSUME_YES="no" + LOG_FILE="$TERMUX_HOME/termux-setup.log" +# Same apt options as termux-linux-setup.sh: never prompt about config files, +# retry/time out on a stalled mirror instead of hanging, wait for the dpkg lock. +APT_OPTS=( + -o Dpkg::Options::=--force-confdef + -o Dpkg::Options::=--force-confold + -o Acquire::Retries=3 + -o Acquire::http::Timeout=30 + -o Acquire::https::Timeout=30 + -o DPkg::Lock::Timeout=300 +) +# The same, as a string for apt inside the Ubuntu container. +APT_OPTS_STR="${APT_OPTS[*]}" + +# ============== COMMAND-LINE FLAGS ============== +usage() { + cat << 'USAGE' +Usage: bash setup-homeassistant.sh [options] + +Install Home Assistant Core on Android (Termux + proot-distro Ubuntu). +Run with no options for the interactive installer. + +Options: + --hacs Also install HACS, the community store (default) + --no-hacs Skip HACS + -y, --yes Don't ask anything; use the flags above or the defaults + -h, --help Show this help + +One-line install: + curl -fsSL https://raw.githubusercontent.com/mayukh4/linux-android/main/setup-homeassistant.sh | bash + +Re-running is safe: finished steps are skipped. Install log: ~/termux-setup.log +USAGE +} + +parse_args() { + while [ $# -gt 0 ]; do + case "$1" in + --hacs) HACS_FROM_FLAG="yes" ;; + --no-hacs) HACS_FROM_FLAG="no" ;; + -y|--yes) ASSUME_YES="yes" ;; + -h|--help) usage; exit 0 ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac + shift + done +} + +# Where to read answers from. With `curl ... | bash` stdin is the script +# itself, so a plain `read` would swallow the rest of it; read from the +# terminal instead. No terminal at all (CI, nohup) means use the defaults. +TTY_IN="" +if [ -t 0 ]; then + TTY_IN=/dev/stdin +elif { : < /dev/tty; } 2>/dev/null; then + TTY_IN=/dev/tty +fi + +# ask — read one answer, or leave it empty (= default) when +# running unattended. +ask() { + local __reply="" + if [ "$ASSUME_YES" != "yes" ] && [ -n "$TTY_IN" ]; then + read -rp "$2" __reply < "$TTY_IN" || __reply="" + fi + printf -v "$1" '%s' "$__reply" +} + + # ============== COLORS ============== RED='\033[0;31m' GREEN='\033[0;32m' @@ -36,7 +111,6 @@ CYAN='\033[0;36m' WHITE='\033[1;37m' GRAY='\033[0;90m' NC='\033[0m' -BOLD='\033[1m' # ============== LOGGING ============== log() { @@ -64,38 +138,104 @@ update_progress() { } # ============== SPINNER ============== -spinner() { +# +# Big packages (KDE, Firefox, Wine) can take many minutes on a slow phone or +# connection, and a bare spinner looks exactly like a hang (issue #7). So the +# spinner shows the elapsed time and, after a few seconds, the latest line apt +# wrote to the log ("Get:12 ... firefox [74.1 MB]", "Unpacking ..."). +# If the log stops moving for several minutes it says so, once. + +# Latest non-empty log line, stripped of control characters. +last_log_line() { + tail -n 3 "$LOG_FILE" 2>/dev/null \ + | tr -d '\r' \ + | sed 's/\x1b\[[0-9;]*[A-Za-z]//g' \ + | grep -v '^[[:space:]]*$' \ + | tail -n 1 +} + +_spin() { local pid=$1 local message=$2 + local optional=$3 local spin=('⠋' '⠙' '⠸' '⠴' '⠦' '⠇') - local i=0 + local i=0 tick=0 + local start=$SECONDS elapsed + local cols detail="" line + local size last_size=-1 last_change=$SECONDS warned=0 + + cols=$(tput cols 2>/dev/null || echo 80) + [ "$cols" -gt 20 ] 2>/dev/null || cols=80 while kill -0 "$pid" 2>/dev/null; do - printf "\r ${CYAN}${spin[$i]}${NC} %s " "$message" + elapsed=$((SECONDS - start)) + + # Once a second: refresh the detail line and check for a stall. + if [ $((tick % 10)) -eq 0 ]; then + size=$(wc -c < "$LOG_FILE" 2>/dev/null || echo 0) + if [ "$size" != "$last_size" ]; then + last_size=$size + last_change=$SECONDS + fi + if [ "$elapsed" -ge 4 ]; then + detail=$(last_log_line) + fi + if [ "$warned" -eq 0 ] && [ $((SECONDS - last_change)) -ge 300 ]; then + warned=1 + printf "\r\033[K ${YELLOW}⚠${NC} No progress for 5 minutes. Check your connection and keep\n" + printf " Termux in the foreground. If it never moves, press Ctrl+C and\n" + printf " re-run the script — installed packages are skipped.\n" + log "STALL: no log output for 300s during: $message" + fi + fi + + line=$(printf "%s [%d:%02d]" "$message" $((elapsed / 60)) $((elapsed % 60))) + if [ -n "$detail" ]; then + line="$line · $detail" + fi + # Keep it on one line so \r can redraw it. + line=${line:0:$((cols - 6))} + printf "\r\033[K ${CYAN}%s${NC} %s" "${spin[$i]}" "$line" + i=$(( (i + 1) % 6 )) + tick=$((tick + 1)) sleep 0.1 done wait "$pid" local exit_code=$? + elapsed=$((SECONDS - start)) + local took="" + [ "$elapsed" -ge 10 ] && took=$(printf " (%d:%02d)" $((elapsed / 60)) $((elapsed % 60))) if [ "$exit_code" -eq 0 ]; then - printf "\r ${GREEN}✔${NC} %-55s\n" "$message" + printf "\r\033[K ${GREEN}✔${NC} %-55s${GRAY}%s${NC}\n" "$message" "$took" log "OK: $message" + elif [ "$optional" = "optional" ]; then + printf "\r\033[K ${YELLOW}⚠${NC} %-55s ${YELLOW}(optional — skipped)${NC}\n" "$message" + log "OPTIONAL FAILED: $message" + return 0 else - printf "\r ${RED}✘${NC} %-55s ${RED}(failed — see $LOG_FILE)${NC}\n" "$message" + printf "\r\033[K ${RED}✘${NC} %-55s ${RED}(failed — see $LOG_FILE)${NC}\n" "$message" log "FAILED: $message" fi return "$exit_code" } +spinner() { _spin "$1" "$2" ""; } + +# Same as spinner, but a failure is reported as a warning rather than an error. +# For packages the desktop still works without. +spinner_optional() { _spin "$1" "$2" "optional"; } + # ============== TERMUX PACKAGE INSTALLER ============== safe_install_pkg() { local pkg=$1 local name=${2:-$pkg} - if dpkg -s "$pkg" &>/dev/null; then + # dpkg -s also succeeds for removed-but-not-purged packages. + if [ "$(dpkg-query -W -f='${db:Status-Status}' "$pkg" 2>/dev/null)" = "installed" ]; then printf " ${GRAY}~${NC} %-55s ${GRAY}(already installed)${NC}\n" "$name" log "SKIP (already installed): $pkg" return 0 @@ -110,7 +250,7 @@ safe_install_pkg() { for conflict in $conflicts; do [ -z "$conflict" ] && continue - if dpkg -s "$conflict" &>/dev/null; then + if [ "$(dpkg-query -W -f='${db:Status-Status}' "$conflict" 2>/dev/null)" = "installed" ]; then printf " ${YELLOW}⚠${NC} %-55s ${YELLOW}(skipped — conflicts with: %s)${NC}\n" \ "$name" "$conflict" log "SKIP (conflict with $conflict): $pkg" @@ -118,9 +258,8 @@ safe_install_pkg() { fi done - (DEBIAN_FRONTEND=noninteractive apt-get install -y \ - -o Dpkg::Options::="--force-confold" \ - "$pkg" >> "$LOG_FILE" 2>&1) & + (DEBIAN_FRONTEND=noninteractive apt-get install -y "${APT_OPTS[@]}" \ + "$pkg" < /dev/null >> "$LOG_FILE" 2>&1) & spinner $! "Installing ${name}..." } @@ -137,8 +276,8 @@ proot_install_pkg() { fi (proot-distro login ubuntu -- bash -c \ - "DEBIAN_FRONTEND=noninteractive apt-get install -y '$pkg'" \ - >> "$LOG_FILE" 2>&1) & + "DEBIAN_FRONTEND=noninteractive apt-get install -y ${APT_OPTS_STR} '$pkg'" \ + < /dev/null >> "$LOG_FILE" 2>&1) & spinner $! "Installing ${name} (Ubuntu)..." } @@ -184,8 +323,8 @@ preflight_checks() { echo "" echo -e " ${WHITE}You might run out of space during installation.${NC}" echo -e " ${WHITE}Continue anyway? (y/n)${NC}" - read -rp " " CONTINUE_INPUT - if [[ ! "$CONTINUE_INPUT" =~ ^[Yy] ]]; then + ask CONTINUE_INPUT " [default: y]: " + if [[ ! "${CONTINUE_INPUT:-y}" =~ ^[Yy] ]]; then echo -e " ${RED}Aborted.${NC}" exit 1 fi @@ -194,12 +333,24 @@ preflight_checks() { fi # Check network connectivity - if ping -c 1 -W 3 google.com &>/dev/null; then + # Don't rely on pinging google.com: ICMP is filtered on many networks and + # Google is unreachable in some countries. Try the hosts the install + # actually needs, over HTTPS, and only warn if none answer. + local net_ok="no" host + for host in https://packages-cf.termux.dev https://pypi.org https://github.com; do + if curl -fsS -o /dev/null --max-time 8 "$host" 2>/dev/null; then + net_ok="yes"; break + fi + done + if [ "$net_ok" = "no" ] && ping -c 1 -W 3 1.1.1.1 &>/dev/null; then + net_ok="yes" + fi + if [ "$net_ok" = "yes" ]; then echo -e " ${GREEN}✔${NC} Network: ${WHITE}Connected${NC}" else - echo -e " ${RED}✘ No internet connection detected.${NC}" - echo -e " ${WHITE}This script requires an internet connection to download packages.${NC}" - exit 1 + echo -e " ${YELLOW}⚠${NC} Could not confirm an internet connection." + echo -e " ${WHITE}The install downloads ~1 GB. It will fail without a connection.${NC}" + log "WARNING: network check failed" fi # Detect phone IP for later @@ -215,15 +366,24 @@ preflight_checks() { echo -e " ${GRAY}HACS is the add-on store for community integrations and themes.${NC}" echo -e " ${GRAY}It must be installed BEFORE any other custom integration.${NC}" echo "" - while true; do - read -rp " Install HACS? (y/n) [default: y]: " HACS_INPUT - HACS_INPUT=${HACS_INPUT:-y} - case "$HACS_INPUT" in - [Yy]*) INSTALL_HACS="yes"; echo -e " ${GREEN}✔ HACS will be installed.${NC}"; break;; - [Nn]*) INSTALL_HACS="no"; echo -e " ${GRAY} HACS skipped.${NC}"; break;; - *) echo -e " ${RED}Please enter y or n.${NC}";; - esac - done + if [ -n "$HACS_FROM_FLAG" ]; then + INSTALL_HACS="$HACS_FROM_FLAG" + else + while true; do + ask HACS_INPUT " Install HACS? (y/n) [default: y]: " + HACS_INPUT=${HACS_INPUT:-y} + case "$HACS_INPUT" in + [Yy]*) INSTALL_HACS="yes"; break;; + [Nn]*) INSTALL_HACS="no"; break;; + *) echo -e " ${RED}Please enter y or n.${NC}";; + esac + done + fi + if [ "$INSTALL_HACS" == "yes" ]; then + echo -e " ${GREEN}✔ HACS will be installed.${NC}" + else + echo -e " ${GRAY} HACS skipped.${NC}" + fi [ "$INSTALL_HACS" == "yes" ] && TOTAL_STEPS=8 echo "" @@ -257,8 +417,8 @@ step_ubuntu() { # Update package lists inside Ubuntu (proot-distro login ubuntu -- bash -c \ - "DEBIAN_FRONTEND=noninteractive apt-get update -y" \ - >> "$LOG_FILE" 2>&1) & + "DEBIAN_FRONTEND=noninteractive apt-get update -y ${APT_OPTS_STR}" \ + < /dev/null >> "$LOG_FILE" 2>&1) & spinner $! "Updating Ubuntu package lists..." } @@ -400,11 +560,13 @@ STARTEOF echo -e " ${GREEN}✔ Created ~/start-homeassistant.sh${NC}" # stop-homeassistant.sh - cat > "$TERMUX_HOME/stop-homeassistant.sh" << 'STOPEOF' -#!/data/data/com.termux/files/usr/bin/bash + cat > "$TERMUX_HOME/stop-homeassistant.sh" << STOPEOF +#!${TERMUX_PREFIX}/bin/bash echo "[*] Stopping Home Assistant..." -pkill -f "hass" 2>/dev/null || true +# Match the hass process itself, not anything with "hass" in its command line +# (an editor open on ~/hass-config, a log tail, ...). +pkill -f "hass-venv/bin/hass" 2>/dev/null || pkill -f "hass -c" 2>/dev/null || true if command -v termux-wake-unlock &>/dev/null; then termux-wake-unlock @@ -428,7 +590,7 @@ echo "[*] Upgrading Home Assistant Core..." echo "" echo "[*] Stopping any running instance..." -pkill -f "hass -c" 2>/dev/null || true +pkill -f "hass-venv/bin/hass" 2>/dev/null || pkill -f "hass -c" 2>/dev/null || true sleep 2 BEFORE=\$(proot-distro login ubuntu -- ${HASS_BIN} --version 2>/dev/null) @@ -657,12 +819,21 @@ COMPLETE # ============== MAIN ============== main() { - log "setup-homeassistant.sh started" + log "setup-homeassistant.sh started (args: $*)" + + if [ ! -d "$TERMUX_PREFIX" ] || ! command -v pkg >/dev/null 2>&1; then + echo -e "${RED}This script must be run inside Termux on Android.${NC}" + echo -e "Get Termux from F-Droid: https://f-droid.org/en/packages/com.termux/" + exit 1 + fi # Acquire wake lock if command -v termux-wake-lock &>/dev/null; then termux-wake-lock log "Wake lock acquired." + # Release it however we exit: success, an early `exit 1`, or Ctrl+C. + trap 'termux-wake-unlock 2>/dev/null' EXIT + trap 'echo; echo "Interrupted. Re-run the script to continue — finished steps are skipped."; exit 130' INT TERM else echo -e "${YELLOW} [!] termux-wake-lock unavailable — keep your screen on during install.${NC}" echo "" @@ -687,4 +858,5 @@ main() { fi } -main +parse_args "$@" +main "$@" diff --git a/termux-linux-setup.sh b/termux-linux-setup.sh index 7ac80f1..0ced321 100644 --- a/termux-linux-setup.sh +++ b/termux-linux-setup.sh @@ -10,6 +10,7 @@ # - Productivity & Media tools (VLC, Firefox) # - Python 3 + pip # - Optional: Windows App Support (Wine/Hangover + Box64) +# - One-line install (curl | bash) and unattended flags: see --help # # Tested on: LineageOS (Android 9+), arm64 devices ####################################################### @@ -33,8 +34,120 @@ DE_NAME="XFCE4" CURRENT_STEP=0 # TOTAL_STEPS is set after user choices are made +# Set from command-line flags (see usage below). +DE_FROM_FLAG="" +WINE_FROM_FLAG="" +ASSUME_YES="no" + LOG_FILE="$TERMUX_HOME/termux-setup.log" +# Options for every apt call. +# - confold/confdef: never stop to ask about a changed config file. +# - Retries/Timeout: a stalled mirror fails and retries instead of hanging the +# install forever with the spinner still going (issue #7). +# - Lock::Timeout: wait for another apt/dpkg (e.g. a second Termux session) +# instead of failing at once. +APT_OPTS=( + -o Dpkg::Options::=--force-confdef + -o Dpkg::Options::=--force-confold + -o Acquire::Retries=3 + -o Acquire::http::Timeout=30 + -o Acquire::https::Timeout=30 + -o DPkg::Lock::Timeout=300 +) + +# ============== COMMAND-LINE FLAGS ============== +usage() { + cat << 'USAGE' +Usage: bash termux-linux-setup.sh [options] + +Turn an Android phone into a Linux desktop with Termux + Termux-X11. +Run with no options for the interactive installer. + +Options: + --de Desktop to install: xfce (default), lxqt, mate, kde + --wine Also install Windows app support (Hangover Wine + Box64) + --no-wine Skip Wine (default) + -y, --yes Don't ask anything; use the flags above or the defaults + -h, --help Show this help + +One-line install: + curl -fsSL https://raw.githubusercontent.com/mayukh4/linux-android/main/termux-linux-setup.sh | bash + +One-line, unattended, KDE without Wine: + curl -fsSL https://raw.githubusercontent.com/mayukh4/linux-android/main/termux-linux-setup.sh | bash -s -- --de kde --no-wine -y + +Re-running is safe: installed packages are skipped and the launchers are +regenerated. Install log: ~/termux-setup.log +USAGE +} + +# Map a desktop name or number to DE_CHOICE. +de_from_name() { + case "$(echo "$1" | tr '[:upper:]' '[:lower:]')" in + 1|xfce|xfce4) echo 1 ;; + 2|lxqt) echo 2 ;; + 3|mate) echo 3 ;; + 4|kde|plasma|kde-plasma) echo 4 ;; + *) return 1 ;; + esac +} + +parse_args() { + local arg val + while [ $# -gt 0 ]; do + arg=$1 + case "$arg" in + --de|--desktop) + val=${2:-} + [ $# -ge 2 ] && shift + ;; + --de=*|--desktop=*) + val=${arg#*=} + arg=--de + ;; + esac + case "$arg" in + --de|--desktop) + if ! DE_FROM_FLAG=$(de_from_name "$val"); then + echo "Unknown desktop: '${val}'. Use xfce, lxqt, mate or kde." >&2 + exit 2 + fi + ;; + --wine) WINE_FROM_FLAG="yes" ;; + --no-wine) WINE_FROM_FLAG="no" ;; + -y|--yes) ASSUME_YES="yes" ;; + -h|--help) usage; exit 0 ;; + *) + echo "Unknown option: $arg" >&2 + usage >&2 + exit 2 + ;; + esac + shift + done +} + +# Where to read answers from. With `curl ... | bash` stdin is the script +# itself, so a plain `read` would swallow the rest of it; read from the +# terminal instead. No terminal at all (CI, nohup) means use the defaults. +TTY_IN="" +if [ -t 0 ]; then + TTY_IN=/dev/stdin +elif { : < /dev/tty; } 2>/dev/null; then + TTY_IN=/dev/tty +fi + +# ask — read one answer, or leave it empty (= default) when +# running unattended. +ask() { + local __reply="" + if [ "$ASSUME_YES" != "yes" ] && [ -n "$TTY_IN" ]; then + read -rp "$2" __reply < "$TTY_IN" || __reply="" + fi + printf -v "$1" '%s' "$__reply" +} + # ============== COLORS ============== RED='\033[0;31m' GREEN='\033[0;32m' @@ -72,59 +185,96 @@ update_progress() { } # ============== SPINNER ============== -spinner() { +# +# Big packages (KDE, Firefox, Wine) can take many minutes on a slow phone or +# connection, and a bare spinner looks exactly like a hang (issue #7). So the +# spinner shows the elapsed time and, after a few seconds, the latest line apt +# wrote to the log ("Get:12 ... firefox [74.1 MB]", "Unpacking ..."). +# If the log stops moving for several minutes it says so, once. + +# Latest non-empty log line, stripped of control characters. +last_log_line() { + tail -n 3 "$LOG_FILE" 2>/dev/null \ + | tr -d '\r' \ + | sed 's/\x1b\[[0-9;]*[A-Za-z]//g' \ + | grep -v '^[[:space:]]*$' \ + | tail -n 1 +} + +_spin() { local pid=$1 local message=$2 + local optional=$3 local spin=('⠋' '⠙' '⠸' '⠴' '⠦' '⠇') - local i=0 + local i=0 tick=0 + local start=$SECONDS elapsed + local cols detail="" line + local size last_size=-1 last_change=$SECONDS warned=0 + + cols=$(tput cols 2>/dev/null || echo 80) + [ "$cols" -gt 20 ] 2>/dev/null || cols=80 while kill -0 "$pid" 2>/dev/null; do - printf "\r ${CYAN}${spin[$i]}${NC} %s " "$message" + elapsed=$((SECONDS - start)) + + # Once a second: refresh the detail line and check for a stall. + if [ $((tick % 10)) -eq 0 ]; then + size=$(wc -c < "$LOG_FILE" 2>/dev/null || echo 0) + if [ "$size" != "$last_size" ]; then + last_size=$size + last_change=$SECONDS + fi + if [ "$elapsed" -ge 4 ]; then + detail=$(last_log_line) + fi + if [ "$warned" -eq 0 ] && [ $((SECONDS - last_change)) -ge 300 ]; then + warned=1 + printf "\r\033[K ${YELLOW}⚠${NC} No progress for 5 minutes. Check your connection and keep\n" + printf " Termux in the foreground. If it never moves, press Ctrl+C and\n" + printf " re-run the script — installed packages are skipped.\n" + log "STALL: no log output for 300s during: $message" + fi + fi + + line=$(printf "%s [%d:%02d]" "$message" $((elapsed / 60)) $((elapsed % 60))) + if [ -n "$detail" ]; then + line="$line · $detail" + fi + # Keep it on one line so \r can redraw it. + line=${line:0:$((cols - 6))} + printf "\r\033[K ${CYAN}%s${NC} %s" "${spin[$i]}" "$line" + i=$(( (i + 1) % 6 )) + tick=$((tick + 1)) sleep 0.1 done wait "$pid" local exit_code=$? + elapsed=$((SECONDS - start)) + local took="" + [ "$elapsed" -ge 10 ] && took=$(printf " (%d:%02d)" $((elapsed / 60)) $((elapsed % 60))) if [ "$exit_code" -eq 0 ]; then - printf "\r ${GREEN}✔${NC} %-55s\n" "$message" + printf "\r\033[K ${GREEN}✔${NC} %-55s${GRAY}%s${NC}\n" "$message" "$took" log "OK: $message" + elif [ "$optional" = "optional" ]; then + printf "\r\033[K ${YELLOW}⚠${NC} %-55s ${YELLOW}(optional — skipped)${NC}\n" "$message" + log "OPTIONAL FAILED: $message" + return 0 else - printf "\r ${RED}✘${NC} %-55s ${RED}(failed — see $LOG_FILE)${NC}\n" "$message" + printf "\r\033[K ${RED}✘${NC} %-55s ${RED}(failed — see $LOG_FILE)${NC}\n" "$message" log "FAILED: $message" fi return "$exit_code" } +spinner() { _spin "$1" "$2" ""; } + # Same as spinner, but a failure is reported as a warning rather than an error. # For packages the desktop still works without. -spinner_optional() { - local pid=$1 - local message=$2 - local spin=('⠋' '⠙' '⠸' '⠴' '⠦' '⠇') - local i=0 - - while kill -0 "$pid" 2>/dev/null; do - printf "\r ${CYAN}${spin[$i]}${NC} %s " "$message" - i=$(( (i + 1) % 6 )) - sleep 0.1 - done - - wait "$pid" - local exit_code=$? - - if [ "$exit_code" -eq 0 ]; then - printf "\r ${GREEN}✔${NC} %-55s\n" "$message" - log "OK: $message" - else - printf "\r ${YELLOW}⚠${NC} %-55s ${YELLOW}(optional — skipped)${NC}\n" "$message" - log "OPTIONAL FAILED: $message" - fi - - return 0 -} +spinner_optional() { _spin "$1" "$2" "optional"; } # ============== DPKG / APT HELPERS ============== @@ -276,9 +426,9 @@ $(apt_field "$pkg" Provides)") fi done <<< "$(apt_field "$pkg" Conflicts | tr ',' '\n')" - (DEBIAN_FRONTEND=noninteractive apt-get install -y \ - -o Dpkg::Options::="--force-confold" \ - "$pkg" >> "$LOG_FILE" 2>&1) & + log "INSTALL: $pkg" + (DEBIAN_FRONTEND=noninteractive apt-get install -y "${APT_OPTS[@]}" \ + "$pkg" < /dev/null >> "$LOG_FILE" 2>&1) & if [ "$optional" = "optional" ]; then spinner_optional $! "Installing ${name}..." @@ -336,6 +486,24 @@ setup_environment() { echo -e " ${CYAN}Android:${NC} ${WHITE}Android ${ANDROID_VERSION}${NC}" echo -e " ${CYAN}CPU ABI:${NC} ${WHITE}${CPU_ABI}${NC}" + # Termux-X11, Turnip and Hangover are only built for 64-bit ARM. Warn + # rather than abort: some 32-bit userlands on 64-bit chips still get far. + case "$(uname -m)" in + aarch64|arm64) ;; + *) + echo -e " ${YELLOW}⚠ $(uname -m) detected. This script targets arm64 (aarch64);${NC}" + echo -e " ${YELLOW} GPU acceleration and Wine will likely be unavailable.${NC}" + log "WARNING: non-arm64 arch $(uname -m)" + ;; + esac + + local free_mb + free_mb=$(df -m "$TERMUX_HOME" 2>/dev/null | awk 'NR==2 {print $4}') + if [ -n "$free_mb" ] && [ "$free_mb" -lt 4000 ] 2>/dev/null; then + echo -e " ${YELLOW}⚠ Only ${free_mb} MB free. A desktop needs 4–6 GB (more with Wine/KDE).${NC}" + log "WARNING: low storage ${free_mb}MB" + fi + if [[ "$GPU_RENDERER" == *"adreno"* ]] || \ [[ "$GPU_RENDERER" == *"Adreno"* ]] || \ [[ "$GPU_VENDOR_PROP" == *"adreno"* ]] || \ @@ -359,15 +527,19 @@ setup_environment() { echo -e " ${WHITE}4) KDE Plasma${NC} — Modern Windows-style UI. Requires 4 GB+ RAM." echo "" - while true; do - read -rp " Enter number (1-4) [default: 1]: " DE_INPUT - DE_INPUT=${DE_INPUT:-1} - if [[ "$DE_INPUT" =~ ^[1-4]$ ]]; then - DE_CHOICE="$DE_INPUT"; break - else - echo -e " ${RED}Invalid — enter 1, 2, 3, or 4.${NC}" - fi - done + if [ -n "$DE_FROM_FLAG" ]; then + DE_CHOICE="$DE_FROM_FLAG" + else + while true; do + ask DE_INPUT " Enter number (1-4) [default: 1]: " + DE_INPUT=${DE_INPUT:-1} + if [[ "$DE_INPUT" =~ ^[1-4]$ ]]; then + DE_CHOICE="$DE_INPUT"; break + else + echo -e " ${RED}Invalid — enter 1, 2, 3, or 4.${NC}" + fi + done + fi case $DE_CHOICE in 1) DE_NAME="XFCE4";; @@ -382,15 +554,24 @@ setup_environment() { echo -e "${CYAN}Optional: Install Windows app support (Wine + Box64/Hangover)?${NC}" echo -e " ${GRAY}Adds ~500 MB. Runs some Windows x86 apps on ARM.${NC}" echo "" - while true; do - read -rp " Install Wine? (y/n) [default: n]: " WINE_INPUT - WINE_INPUT=${WINE_INPUT:-n} - case "$WINE_INPUT" in - [Yy]*) INSTALL_WINE="yes"; echo -e " ${GREEN}✔ Wine will be installed.${NC}"; break;; - [Nn]*) INSTALL_WINE="no"; echo -e " ${GRAY} Wine skipped.${NC}"; break;; - *) echo -e " ${RED}Please enter y or n.${NC}";; - esac - done + if [ -n "$WINE_FROM_FLAG" ]; then + INSTALL_WINE="$WINE_FROM_FLAG" + else + while true; do + ask WINE_INPUT " Install Wine? (y/n) [default: n]: " + WINE_INPUT=${WINE_INPUT:-n} + case "$WINE_INPUT" in + [Yy]*) INSTALL_WINE="yes"; break;; + [Nn]*) INSTALL_WINE="no"; break;; + *) echo -e " ${RED}Please enter y or n.${NC}";; + esac + done + fi + if [ "$INSTALL_WINE" == "yes" ]; then + echo -e " ${GREEN}✔ Wine will be installed.${NC}" + else + echo -e " ${GRAY} Wine skipped.${NC}" + fi TOTAL_STEPS=10 [ "$INSTALL_WINE" == "yes" ] && TOTAL_STEPS=11 @@ -405,7 +586,8 @@ step_update() { echo -e "${PURPLE}[Step ${CURRENT_STEP}/${TOTAL_STEPS}] Updating system packages...${NC}" echo "" - (DEBIAN_FRONTEND=noninteractive apt-get update -y >> "$LOG_FILE" 2>&1) & + (DEBIAN_FRONTEND=noninteractive apt-get update -y "${APT_OPTS[@]}" \ + < /dev/null >> "$LOG_FILE" 2>&1) & spinner $! "Updating package lists..." # Run pkg upgrade in FOREGROUND (not backgrounded via &) so we can catch @@ -413,8 +595,8 @@ step_update() { # mid-session when core libraries are replaced. If it fails, we tell the # user exactly what to do rather than dying silently. echo -e " ${CYAN}⠿${NC} Upgrading packages (may take a few minutes)..." - if ! DEBIAN_FRONTEND=noninteractive pkg upgrade -y \ - -o Dpkg::Options::="--force-confold" >> "$LOG_FILE" 2>&1; then + if ! DEBIAN_FRONTEND=noninteractive pkg upgrade -y "${APT_OPTS[@]}" \ + < /dev/null >> "$LOG_FILE" 2>&1; then echo "" echo -e " ${YELLOW}⚠ Upgrade hit a library conflict — this is common on first run.${NC}" echo "" @@ -440,7 +622,8 @@ step_repos() { # MUST refresh after adding repos — otherwise packages from x11-repo # and tur-repo (termux-x11-nightly, firefox, etc.) won't be found. echo "" - (DEBIAN_FRONTEND=noninteractive apt-get update -y >> "$LOG_FILE" 2>&1) & + (DEBIAN_FRONTEND=noninteractive apt-get update -y "${APT_OPTS[@]}" \ + < /dev/null >> "$LOG_FILE" 2>&1) & spinner $! "Refreshing package lists (post-repo)..." } @@ -600,8 +783,16 @@ step_wine() { echo -e "${PURPLE}[Step ${CURRENT_STEP}/${TOTAL_STEPS}] Installing Windows support (Wine + Box64)...${NC}" echo "" - (pkg remove wine-stable -y >> "$LOG_FILE" 2>&1 || true) & - spinner $! "Removing old Wine versions (if any)..." + # Only touch apt when there is actually something to remove. The old + # unconditional `pkg remove` went through the pkg wrapper (mirror checks, + # possible prompts) for nothing on almost every install. + if pkg_installed "wine-stable"; then + (DEBIAN_FRONTEND=noninteractive apt-get remove -y "${APT_OPTS[@]}" \ + wine-stable < /dev/null >> "$LOG_FILE" 2>&1) & + spinner_optional $! "Removing old wine-stable..." + fi + + echo -e " ${GRAY}Wine is a large download (~500 MB) — this step can take a while.${NC}" safe_install_pkg "hangover-wine" "Hangover Wine" safe_install_pkg "hangover-wowbox64" "Box64 Wrapper" @@ -886,20 +1077,35 @@ COMPLETE # ============== MAIN ============== main() { - echo "" > "$LOG_FILE" - log "termux-linux-setup.sh started" + : > "$LOG_FILE" + log "termux-linux-setup.sh started (args: $*)" + + # This script is for Termux. Bail out early and clearly anywhere else, + # instead of failing ten steps in. + if [ ! -d "$TERMUX_PREFIX" ] || ! command -v pkg >/dev/null 2>&1; then + echo -e "${RED}This script must be run inside Termux on Android.${NC}" + echo -e "Get Termux from F-Droid: https://f-droid.org/en/packages/com.termux/" + exit 1 + fi # Prevent Android from suspending Termux mid-install when screen turns off. # A suspended Termux kills background apt processes, causing random failures. if command -v termux-wake-lock &>/dev/null; then termux-wake-lock log "Wake lock acquired." + # Release it however we exit: success, an early `exit 1`, or Ctrl+C. + trap 'termux-wake-unlock 2>/dev/null' EXIT + trap 'echo; echo "Interrupted. Re-run the script to continue — finished steps are skipped."; exit 130' INT TERM else echo -e "${YELLOW} [!] termux-wake-lock unavailable — keep your screen on during install.${NC}" echo "" fi show_banner + if [ "$ASSUME_YES" == "yes" ] || [ -z "$TTY_IN" ]; then + echo -e "${GRAY} Running unattended — using flags/defaults for every choice.${NC}" + echo "" + fi setup_environment step_update @@ -923,4 +1129,5 @@ main() { fi } -main +parse_args "$@" +main "$@"