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
+[](https://github.com/mayukh4/linux-android/actions/workflows/shellcheck.yml)
[](LICENSE)
[](https://termux.dev)
[](https://f-droid.org/en/packages/com.termux/)
[](https://github.com/mayukh4/linux-android/stargazers)
+[](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)**
+[](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 "$@"